Skip to content

Commit e26a1fb

Browse files
committed
feat: add document CLI commands
Add document management commands to the CLI, completing interface parity with the API and MCP tools: - compass document list [--entity-urn, --source] - compass document view <id> - compass document upsert [--entity-urn, --title, --body, --format, --source, --source-id] - compass document delete <id> - compass document entity <urn> Uses direct HTTP calls to the REST endpoints since documents are not part of the Connect RPC service definition.
1 parent 734729f commit e26a1fb

3 files changed

Lines changed: 252 additions & 0 deletions

File tree

cli/documents.go

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
package cli
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"time"
10+
11+
"github.com/MakeNowJust/heredoc"
12+
"github.com/raystack/compass/internal/config"
13+
"github.com/spf13/cobra"
14+
)
15+
16+
func documentsCommand(cfg *config.Config) *cobra.Command {
17+
cmd := &cobra.Command{
18+
Use: "document",
19+
Aliases: []string{"documents", "doc", "docs"},
20+
Short: "Manage documents in the knowledge graph",
21+
Annotations: map[string]string{
22+
"group": "core",
23+
},
24+
Example: heredoc.Doc(`
25+
$ compass document list
26+
$ compass document view <id>
27+
$ compass document upsert
28+
$ compass document delete <id>
29+
$ compass document entity <urn>
30+
`),
31+
}
32+
33+
cmd.AddCommand(
34+
listDocumentsCommand(cfg),
35+
viewDocumentCommand(cfg),
36+
upsertDocumentCommand(cfg),
37+
deleteDocumentCommand(cfg),
38+
documentsByEntityCommand(cfg),
39+
)
40+
41+
return cmd
42+
}
43+
44+
func listDocumentsCommand(cfg *config.Config) *cobra.Command {
45+
var entityURN, source string
46+
47+
cmd := &cobra.Command{
48+
Use: "list",
49+
Short: "List all documents",
50+
RunE: func(cmd *cobra.Command, args []string) error {
51+
url := fmt.Sprintf("http://%s/v1/documents", cfg.Client.Host)
52+
params := ""
53+
if entityURN != "" {
54+
params += "?entity_urn=" + entityURN
55+
}
56+
if source != "" {
57+
sep := "?"
58+
if params != "" {
59+
sep = "&"
60+
}
61+
params += sep + "source=" + source
62+
}
63+
64+
body, err := doDocumentRequest(cfg, "GET", url+params, nil)
65+
if err != nil {
66+
return err
67+
}
68+
69+
fmt.Println(string(body))
70+
return nil
71+
},
72+
}
73+
cmd.Flags().StringVar(&entityURN, "entity-urn", "", "Filter by entity URN")
74+
cmd.Flags().StringVar(&source, "source", "", "Filter by source")
75+
return cmd
76+
}
77+
78+
func viewDocumentCommand(cfg *config.Config) *cobra.Command {
79+
return &cobra.Command{
80+
Use: "view <id>",
81+
Short: "View document by ID",
82+
Args: cobra.ExactArgs(1),
83+
RunE: func(cmd *cobra.Command, args []string) error {
84+
url := fmt.Sprintf("http://%s/v1/documents/%s", cfg.Client.Host, args[0])
85+
86+
body, err := doDocumentRequest(cfg, "GET", url, nil)
87+
if err != nil {
88+
return err
89+
}
90+
91+
fmt.Println(string(body))
92+
return nil
93+
},
94+
}
95+
}
96+
97+
func upsertDocumentCommand(cfg *config.Config) *cobra.Command {
98+
var entityURN, title, docBody, format, source, sourceID string
99+
100+
cmd := &cobra.Command{
101+
Use: "upsert",
102+
Short: "Create or update a document",
103+
RunE: func(cmd *cobra.Command, args []string) error {
104+
payload := map[string]interface{}{
105+
"entity_urn": entityURN,
106+
"title": title,
107+
"body": docBody,
108+
}
109+
if format != "" {
110+
payload["format"] = format
111+
}
112+
if source != "" {
113+
payload["source"] = source
114+
}
115+
if sourceID != "" {
116+
payload["source_id"] = sourceID
117+
}
118+
119+
url := fmt.Sprintf("http://%s/v1/documents", cfg.Client.Host)
120+
121+
body, err := doDocumentRequest(cfg, "POST", url, payload)
122+
if err != nil {
123+
return err
124+
}
125+
126+
var res map[string]string
127+
if err := json.Unmarshal(body, &res); err == nil {
128+
if id, ok := res["id"]; ok {
129+
fmt.Println("Document upserted:", id)
130+
return nil
131+
}
132+
}
133+
fmt.Println(string(body))
134+
return nil
135+
},
136+
}
137+
cmd.Flags().StringVar(&entityURN, "entity-urn", "", "Entity URN (required)")
138+
cmd.Flags().StringVar(&title, "title", "", "Document title (required)")
139+
cmd.Flags().StringVar(&docBody, "body", "", "Document body (required)")
140+
cmd.Flags().StringVar(&format, "format", "", "Format: markdown, plaintext")
141+
cmd.Flags().StringVar(&source, "source", "", "Source system (e.g., confluence, github)")
142+
cmd.Flags().StringVar(&sourceID, "source-id", "", "ID in source system")
143+
_ = cmd.MarkFlagRequired("entity-urn")
144+
_ = cmd.MarkFlagRequired("title")
145+
_ = cmd.MarkFlagRequired("body")
146+
return cmd
147+
}
148+
149+
func deleteDocumentCommand(cfg *config.Config) *cobra.Command {
150+
return &cobra.Command{
151+
Use: "delete <id>",
152+
Short: "Delete a document by ID",
153+
Args: cobra.ExactArgs(1),
154+
RunE: func(cmd *cobra.Command, args []string) error {
155+
url := fmt.Sprintf("http://%s/v1/documents/%s", cfg.Client.Host, args[0])
156+
157+
if _, err := doDocumentRequest(cfg, "DELETE", url, nil); err != nil {
158+
return err
159+
}
160+
161+
fmt.Println("Document deleted:", args[0])
162+
return nil
163+
},
164+
}
165+
}
166+
167+
func documentsByEntityCommand(cfg *config.Config) *cobra.Command {
168+
return &cobra.Command{
169+
Use: "entity <urn>",
170+
Short: "List documents for an entity",
171+
Args: cobra.ExactArgs(1),
172+
RunE: func(cmd *cobra.Command, args []string) error {
173+
url := fmt.Sprintf("http://%s/v1/entities/%s/documents", cfg.Client.Host, args[0])
174+
175+
body, err := doDocumentRequest(cfg, "GET", url, nil)
176+
if err != nil {
177+
return err
178+
}
179+
180+
fmt.Println(string(body))
181+
return nil
182+
},
183+
}
184+
}
185+
186+
func doDocumentRequest(cfg *config.Config, method, url string, payload interface{}) ([]byte, error) {
187+
var reqBody io.Reader
188+
if payload != nil {
189+
data, err := json.Marshal(payload)
190+
if err != nil {
191+
return nil, fmt.Errorf("marshal request: %w", err)
192+
}
193+
reqBody = bytes.NewReader(data)
194+
}
195+
196+
req, err := http.NewRequest(method, url, reqBody)
197+
if err != nil {
198+
return nil, fmt.Errorf("create request: %w", err)
199+
}
200+
201+
req.Header.Set("Content-Type", "application/json")
202+
req.Header.Set(cfg.Client.ServerHeaderKeyUserUUID, cfg.Client.ServerHeaderValueUserUUID)
203+
204+
client := &http.Client{Timeout: 30 * time.Second}
205+
resp, err := client.Do(req)
206+
if err != nil {
207+
return nil, fmt.Errorf("request failed: %w", err)
208+
}
209+
defer resp.Body.Close()
210+
211+
body, err := io.ReadAll(resp.Body)
212+
if err != nil {
213+
return nil, fmt.Errorf("read response: %w", err)
214+
}
215+
216+
if resp.StatusCode >= 400 {
217+
return nil, fmt.Errorf("server error (%d): %s", resp.StatusCode, string(body))
218+
}
219+
220+
return body, nil
221+
}

cli/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ func New(cliConfig *config.Config) *cobra.Command {
5757
configCommand(cliConfig),
5858
namespacesCommand(cliConfig),
5959
entitiesCommand(cliConfig),
60+
documentsCommand(cliConfig),
6061
embedCommand(cliConfig),
6162
versionCmd(),
6263
)

docs/docs/guides/cli.mdx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,36 @@ Alias: `s`
109109
--down Rollback migration one step
110110
```
111111

112+
## `compass document`
113+
114+
Alias: `documents`, `doc`, `docs`
115+
116+
| Command | Description |
117+
|---------|-------------|
118+
| `document list` | List documents |
119+
| `document view <id>` | View document by ID |
120+
| `document upsert` | Create or update a document |
121+
| `document delete <id>` | Delete a document |
122+
| `document entity <urn>` | List documents for an entity |
123+
124+
### `document list [flags]`
125+
126+
```
127+
--entity-urn string Filter by entity URN
128+
--source string Filter by source
129+
```
130+
131+
### `document upsert [flags]`
132+
133+
```
134+
--entity-urn string Entity URN (required)
135+
--title string Document title (required)
136+
--body string Document body (required)
137+
--format string Format: markdown, plaintext
138+
--source string Source system (e.g., confluence, github)
139+
--source-id string ID in source system
140+
```
141+
112142
## `compass embed`
113143

114144
Backfill embeddings for existing data.

0 commit comments

Comments
 (0)