@gmickel/gno 0.25.2 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +5 -3
  2. package/assets/skill/SKILL.md +5 -0
  3. package/assets/skill/cli-reference.md +8 -6
  4. package/package.json +1 -1
  5. package/src/cli/commands/get.ts +21 -0
  6. package/src/cli/commands/skill/install.ts +2 -2
  7. package/src/cli/commands/skill/paths.ts +26 -4
  8. package/src/cli/commands/skill/uninstall.ts +2 -2
  9. package/src/cli/program.ts +18 -12
  10. package/src/core/document-capabilities.ts +113 -0
  11. package/src/mcp/tools/get.ts +10 -0
  12. package/src/mcp/tools/index.ts +434 -110
  13. package/src/sdk/documents.ts +12 -0
  14. package/src/serve/doc-events.ts +69 -0
  15. package/src/serve/public/app.tsx +81 -24
  16. package/src/serve/public/components/CaptureModal.tsx +138 -3
  17. package/src/serve/public/components/QuickSwitcher.tsx +248 -0
  18. package/src/serve/public/components/ShortcutHelpModal.tsx +1 -0
  19. package/src/serve/public/components/ai-elements/code-block.tsx +74 -26
  20. package/src/serve/public/components/editor/CodeMirrorEditor.tsx +51 -0
  21. package/src/serve/public/components/ui/command.tsx +2 -2
  22. package/src/serve/public/hooks/use-doc-events.ts +34 -0
  23. package/src/serve/public/hooks/useCaptureModal.tsx +12 -3
  24. package/src/serve/public/hooks/useKeyboardShortcuts.ts +2 -2
  25. package/src/serve/public/lib/deep-links.ts +68 -0
  26. package/src/serve/public/lib/document-availability.ts +22 -0
  27. package/src/serve/public/lib/local-history.ts +44 -0
  28. package/src/serve/public/lib/wiki-link.ts +36 -0
  29. package/src/serve/public/pages/Browse.tsx +11 -0
  30. package/src/serve/public/pages/Dashboard.tsx +2 -2
  31. package/src/serve/public/pages/DocView.tsx +241 -18
  32. package/src/serve/public/pages/DocumentEditor.tsx +399 -9
  33. package/src/serve/public/pages/Search.tsx +20 -1
  34. package/src/serve/routes/api.ts +359 -28
  35. package/src/serve/server.ts +48 -1
  36. package/src/serve/watch-service.ts +149 -0
@@ -51,92 +51,284 @@ export function normalizeTagFilters(tags?: string[]): string[] | undefined {
51
51
  // ─────────────────────────────────────────────────────────────────────────────
52
52
 
53
53
  const searchInputSchema = z.object({
54
- query: z.string().min(1, "Query cannot be empty"),
55
- collection: z.string().optional(),
56
- limit: z.number().int().min(1).max(100).default(5),
57
- minScore: z.number().min(0).max(1).optional(),
58
- lang: z.string().optional(),
59
- intent: z.string().optional(),
60
- exclude: z.array(z.string()).optional(),
61
- since: z.string().optional(),
62
- until: z.string().optional(),
63
- categories: z.array(z.string()).optional(),
64
- author: z.string().optional(),
65
- tagsAll: z.array(z.string()).optional(),
66
- tagsAny: z.array(z.string()).optional(),
54
+ query: z
55
+ .string()
56
+ .min(1, "Query cannot be empty")
57
+ .describe("Search query text"),
58
+ collection: z
59
+ .string()
60
+ .optional()
61
+ .describe("Filter to a single collection name"),
62
+ limit: z
63
+ .number()
64
+ .int()
65
+ .min(1)
66
+ .max(100)
67
+ .default(5)
68
+ .describe("Max results to return"),
69
+ minScore: z
70
+ .number()
71
+ .min(0)
72
+ .max(1)
73
+ .optional()
74
+ .describe("Minimum relevance score (0-1). Omit to return all matches"),
75
+ lang: z
76
+ .string()
77
+ .optional()
78
+ .describe(
79
+ "BCP-47 language hint for tokenization (e.g. 'en', 'de'). Auto-detected if omitted"
80
+ ),
81
+ intent: z
82
+ .string()
83
+ .optional()
84
+ .describe(
85
+ "Disambiguating context for ambiguous queries (e.g. 'programming language' when query is 'python')"
86
+ ),
87
+ exclude: z
88
+ .array(z.string())
89
+ .optional()
90
+ .describe("Exclude documents containing any of these terms"),
91
+ since: z
92
+ .string()
93
+ .optional()
94
+ .describe(
95
+ "Only docs modified after this date (ISO format: 2026-03-01 or 2026-03-01T00:00:00)"
96
+ ),
97
+ until: z
98
+ .string()
99
+ .optional()
100
+ .describe("Only docs modified before this date (ISO format)"),
101
+ categories: z
102
+ .array(z.string())
103
+ .optional()
104
+ .describe("Require category match (from document frontmatter)"),
105
+ author: z
106
+ .string()
107
+ .optional()
108
+ .describe("Filter by author (case-insensitive substring match)"),
109
+ tagsAll: z
110
+ .array(z.string())
111
+ .optional()
112
+ .describe("Require ALL of these tags (AND filter)"),
113
+ tagsAny: z
114
+ .array(z.string())
115
+ .optional()
116
+ .describe("Require ANY of these tags (OR filter)"),
67
117
  });
68
118
 
69
119
  const captureInputSchema = z.object({
70
- collection: z.string().min(1, "Collection cannot be empty"),
71
- content: z.string(),
72
- title: z.string().optional(),
73
- path: z.string().optional(),
74
- overwrite: z.boolean().default(false),
75
- tags: z.array(z.string()).optional(),
120
+ collection: z
121
+ .string()
122
+ .min(1, "Collection cannot be empty")
123
+ .describe("Target collection name (must already exist)"),
124
+ content: z.string().describe("Document content (markdown or plain text)"),
125
+ title: z
126
+ .string()
127
+ .optional()
128
+ .describe("Document title. Auto-derived from content if omitted"),
129
+ path: z
130
+ .string()
131
+ .optional()
132
+ .describe(
133
+ "Relative path within collection (e.g. 'notes/meeting.md'). Auto-generated from title if omitted"
134
+ ),
135
+ overwrite: z
136
+ .boolean()
137
+ .default(false)
138
+ .describe("Overwrite if file already exists at path"),
139
+ tags: z
140
+ .array(z.string())
141
+ .optional()
142
+ .describe("Tags to apply to the new document"),
76
143
  });
77
144
 
78
145
  const addCollectionInputSchema = z.object({
79
- path: z.string().min(1, "Path cannot be empty"),
80
- name: z.string().optional(),
81
- pattern: z.string().optional(),
82
- include: z.array(z.string()).optional(),
83
- exclude: z.array(z.string()).optional(),
84
- gitPull: z.boolean().default(false),
146
+ path: z
147
+ .string()
148
+ .min(1, "Path cannot be empty")
149
+ .describe("Absolute path to the directory to index"),
150
+ name: z
151
+ .string()
152
+ .optional()
153
+ .describe("Collection name. Auto-derived from directory name if omitted"),
154
+ pattern: z
155
+ .string()
156
+ .optional()
157
+ .describe(
158
+ "Glob pattern for files to include (default: '**/*'). E.g. '**/*.md' for markdown only"
159
+ ),
160
+ include: z
161
+ .array(z.string())
162
+ .optional()
163
+ .describe("Extension allowlist (e.g. ['.md', '.pdf', '.docx'])"),
164
+ exclude: z
165
+ .array(z.string())
166
+ .optional()
167
+ .describe("Glob patterns to exclude (default: ['.git', 'node_modules'])"),
168
+ gitPull: z
169
+ .boolean()
170
+ .default(false)
171
+ .describe("Run git pull before indexing (if collection is a git repo)"),
85
172
  });
86
173
 
87
174
  const syncInputSchema = z.object({
88
- collection: z.string().optional(),
89
- gitPull: z.boolean().default(false),
90
- runUpdateCmd: z.boolean().default(false),
175
+ collection: z
176
+ .string()
177
+ .optional()
178
+ .describe("Collection name to sync. Omit to sync all collections"),
179
+ gitPull: z.boolean().default(false).describe("Run git pull before syncing"),
180
+ runUpdateCmd: z
181
+ .boolean()
182
+ .default(false)
183
+ .describe("Run the collection's configured update command before syncing"),
91
184
  });
92
185
 
93
186
  const embedInputSchema = z.object({});
94
187
 
95
188
  const indexInputSchema = z.object({
96
- collection: z.string().optional(),
97
- gitPull: z.boolean().default(false),
189
+ collection: z
190
+ .string()
191
+ .optional()
192
+ .describe("Collection name to index. Omit to index all collections"),
193
+ gitPull: z.boolean().default(false).describe("Run git pull before indexing"),
98
194
  });
99
195
 
100
196
  const removeCollectionInputSchema = z.object({
101
- collection: z.string().min(1, "Collection cannot be empty"),
197
+ collection: z
198
+ .string()
199
+ .min(1, "Collection cannot be empty")
200
+ .describe("Collection name to remove"),
102
201
  });
103
202
 
104
203
  const vsearchInputSchema = z.object({
105
- query: z.string().min(1, "Query cannot be empty"),
106
- collection: z.string().optional(),
107
- limit: z.number().int().min(1).max(100).default(5),
108
- minScore: z.number().min(0).max(1).optional(),
109
- lang: z.string().optional(),
110
- intent: z.string().optional(),
111
- exclude: z.array(z.string()).optional(),
112
- since: z.string().optional(),
113
- until: z.string().optional(),
114
- categories: z.array(z.string()).optional(),
115
- author: z.string().optional(),
116
- tagsAll: z.array(z.string()).optional(),
117
- tagsAny: z.array(z.string()).optional(),
204
+ query: z
205
+ .string()
206
+ .min(1, "Query cannot be empty")
207
+ .describe("Search query text (matched by semantic meaning, not keywords)"),
208
+ collection: z
209
+ .string()
210
+ .optional()
211
+ .describe("Filter to a single collection name"),
212
+ limit: z
213
+ .number()
214
+ .int()
215
+ .min(1)
216
+ .max(100)
217
+ .default(5)
218
+ .describe("Max results to return"),
219
+ minScore: z
220
+ .number()
221
+ .min(0)
222
+ .max(1)
223
+ .optional()
224
+ .describe("Minimum similarity score (0-1)"),
225
+ lang: z
226
+ .string()
227
+ .optional()
228
+ .describe("BCP-47 language hint (e.g. 'en', 'de')"),
229
+ intent: z
230
+ .string()
231
+ .optional()
232
+ .describe("Disambiguating context for the query"),
233
+ exclude: z
234
+ .array(z.string())
235
+ .optional()
236
+ .describe("Exclude documents containing any of these terms"),
237
+ since: z
238
+ .string()
239
+ .optional()
240
+ .describe("Only docs modified after this date (ISO format)"),
241
+ until: z
242
+ .string()
243
+ .optional()
244
+ .describe("Only docs modified before this date (ISO format)"),
245
+ categories: z.array(z.string()).optional().describe("Require category match"),
246
+ author: z
247
+ .string()
248
+ .optional()
249
+ .describe("Filter by author (case-insensitive substring)"),
250
+ tagsAll: z.array(z.string()).optional().describe("Require ALL of these tags"),
251
+ tagsAny: z.array(z.string()).optional().describe("Require ANY of these tags"),
118
252
  });
119
253
 
120
254
  const queryModeInputSchema = z.object({
121
- mode: z.enum(["term", "intent", "hyde"]),
122
- text: z.string().trim().min(1, "Query mode text cannot be empty"),
255
+ mode: z
256
+ .enum(["term", "intent", "hyde"])
257
+ .describe(
258
+ "Retrieval strategy: 'term' (keyword match), 'intent' (disambiguation), 'hyde' (hypothetical document for semantic matching)"
259
+ ),
260
+ text: z
261
+ .string()
262
+ .trim()
263
+ .min(1, "Query mode text cannot be empty")
264
+ .describe("Text for this query mode"),
123
265
  });
124
266
 
125
267
  export const queryInputSchema = z.object({
126
- query: z.string().min(1, "Query cannot be empty"),
127
- collection: z.string().optional(),
128
- limit: z.number().int().min(1).max(100).default(5),
129
- minScore: z.number().min(0).max(1).optional(),
130
- lang: z.string().optional(),
131
- intent: z.string().optional(),
132
- candidateLimit: z.number().int().min(1).max(100).optional(),
133
- exclude: z.array(z.string()).optional(),
134
- since: z.string().optional(),
135
- until: z.string().optional(),
136
- categories: z.array(z.string()).optional(),
137
- author: z.string().optional(),
268
+ query: z
269
+ .string()
270
+ .min(1, "Query cannot be empty")
271
+ .describe("Search query text"),
272
+ collection: z
273
+ .string()
274
+ .optional()
275
+ .describe("Filter to a single collection name"),
276
+ limit: z
277
+ .number()
278
+ .int()
279
+ .min(1)
280
+ .max(100)
281
+ .default(5)
282
+ .describe("Max results to return"),
283
+ minScore: z
284
+ .number()
285
+ .min(0)
286
+ .max(1)
287
+ .optional()
288
+ .describe("Minimum relevance score (0-1)"),
289
+ lang: z
290
+ .string()
291
+ .optional()
292
+ .describe(
293
+ "BCP-47 language hint (e.g. 'en', 'de'). Auto-detected if omitted"
294
+ ),
295
+ intent: z
296
+ .string()
297
+ .optional()
298
+ .describe(
299
+ "Disambiguating context (e.g. 'programming language' when query is 'python')"
300
+ ),
301
+ candidateLimit: z
302
+ .number()
303
+ .int()
304
+ .min(1)
305
+ .max(100)
306
+ .optional()
307
+ .describe(
308
+ "Max candidates passed to reranking stage (higher = better recall, slower)"
309
+ ),
310
+ exclude: z
311
+ .array(z.string())
312
+ .optional()
313
+ .describe("Exclude documents containing any of these terms"),
314
+ since: z
315
+ .string()
316
+ .optional()
317
+ .describe("Only docs modified after this date (ISO format)"),
318
+ until: z
319
+ .string()
320
+ .optional()
321
+ .describe("Only docs modified before this date (ISO format)"),
322
+ categories: z.array(z.string()).optional().describe("Require category match"),
323
+ author: z
324
+ .string()
325
+ .optional()
326
+ .describe("Filter by author (case-insensitive substring)"),
138
327
  queryModes: z
139
328
  .array(queryModeInputSchema)
329
+ .describe(
330
+ "Structured query modes to combine multiple retrieval strategies. Max one 'hyde' entry."
331
+ )
140
332
  .superRefine((entries, ctx) => {
141
333
  const hydeCount = entries.filter((entry) => entry.mode === "hyde").length;
142
334
  if (hydeCount > 1) {
@@ -147,68 +339,200 @@ export const queryInputSchema = z.object({
147
339
  }
148
340
  })
149
341
  .optional(),
150
- fast: z.boolean().default(false),
151
- thorough: z.boolean().default(false),
152
- expand: z.boolean().optional(),
153
- rerank: z.boolean().optional(),
154
- tagsAll: z.array(z.string()).optional(),
155
- tagsAny: z.array(z.string()).optional(),
342
+ fast: z
343
+ .boolean()
344
+ .default(false)
345
+ .describe("Skip expansion and reranking (~0.7s). Use for quick lookups"),
346
+ thorough: z
347
+ .boolean()
348
+ .default(false)
349
+ .describe(
350
+ "Enable query expansion for best recall (~5-8s). Use when default returns no results"
351
+ ),
352
+ expand: z
353
+ .boolean()
354
+ .optional()
355
+ .describe("Override: enable/disable query expansion"),
356
+ rerank: z
357
+ .boolean()
358
+ .optional()
359
+ .describe("Override: enable/disable cross-encoder reranking"),
360
+ tagsAll: z.array(z.string()).optional().describe("Require ALL of these tags"),
361
+ tagsAny: z.array(z.string()).optional().describe("Require ANY of these tags"),
156
362
  });
157
363
 
158
364
  const getInputSchema = z.object({
159
- ref: z.string().min(1, "Reference cannot be empty"),
160
- fromLine: z.number().int().min(1).optional(),
161
- lineCount: z.number().int().min(1).optional(),
162
- lineNumbers: z.boolean().default(true),
365
+ ref: z
366
+ .string()
367
+ .min(1, "Reference cannot be empty")
368
+ .describe(
369
+ "Document reference: URI (gno://collection/path), docid (#abc123), or collection/path"
370
+ ),
371
+ fromLine: z
372
+ .number()
373
+ .int()
374
+ .min(1)
375
+ .optional()
376
+ .describe("Start reading from this line number"),
377
+ lineCount: z
378
+ .number()
379
+ .int()
380
+ .min(1)
381
+ .optional()
382
+ .describe("Number of lines to return (from fromLine)"),
383
+ lineNumbers: z
384
+ .boolean()
385
+ .default(true)
386
+ .describe("Include line numbers in output"),
163
387
  });
164
388
 
165
389
  const multiGetInputSchema = z.object({
166
- refs: z.array(z.string()).min(1).optional(),
167
- pattern: z.string().optional(),
168
- maxBytes: z.number().int().min(1).default(10_240),
169
- lineNumbers: z.boolean().default(true),
390
+ refs: z
391
+ .array(z.string())
392
+ .min(1)
393
+ .optional()
394
+ .describe("Array of document references (URIs or docids)"),
395
+ pattern: z
396
+ .string()
397
+ .optional()
398
+ .describe("Glob pattern to match documents (e.g. 'work/**/*.md')"),
399
+ maxBytes: z
400
+ .number()
401
+ .int()
402
+ .min(1)
403
+ .default(10_240)
404
+ .describe("Max bytes per document (truncates longer docs)"),
405
+ lineNumbers: z
406
+ .boolean()
407
+ .default(true)
408
+ .describe("Include line numbers in output"),
170
409
  });
171
410
 
172
411
  const statusInputSchema = z.object({});
173
412
 
174
413
  const jobStatusInputSchema = z.object({
175
- jobId: z.string().min(1, "Job ID cannot be empty"),
414
+ jobId: z
415
+ .string()
416
+ .min(1, "Job ID cannot be empty")
417
+ .describe("Job ID returned by async operations (embed, index)"),
176
418
  });
177
419
 
178
420
  const listJobsInputSchema = z.object({
179
- limit: z.number().int().min(1).max(100).default(10),
421
+ limit: z
422
+ .number()
423
+ .int()
424
+ .min(1)
425
+ .max(100)
426
+ .default(10)
427
+ .describe("Max jobs to return"),
180
428
  });
181
429
 
182
430
  const listTagsInputSchema = z.object({
183
- collection: z.string().optional(),
184
- prefix: z.string().optional(),
431
+ collection: z
432
+ .string()
433
+ .optional()
434
+ .describe("Filter tags to a single collection"),
435
+ prefix: z
436
+ .string()
437
+ .optional()
438
+ .describe("Filter tags by prefix (e.g. 'project/' for hierarchical tags)"),
185
439
  });
186
440
 
187
441
  const linksInputSchema = z.object({
188
- ref: z.string().trim().min(1, "Reference cannot be empty"),
189
- type: z.enum(["wiki", "markdown"]).optional(),
442
+ ref: z
443
+ .string()
444
+ .trim()
445
+ .min(1, "Reference cannot be empty")
446
+ .describe("Document reference (URI, docid, or collection/path)"),
447
+ type: z
448
+ .enum(["wiki", "markdown"])
449
+ .optional()
450
+ .describe(
451
+ "Filter by link type: 'wiki' ([[links]]) or 'markdown' ([links](url))"
452
+ ),
190
453
  });
191
454
 
192
455
  const backlinksInputSchema = z.object({
193
- ref: z.string().trim().min(1, "Reference cannot be empty"),
194
- collection: z.string().trim().optional(),
456
+ ref: z
457
+ .string()
458
+ .trim()
459
+ .min(1, "Reference cannot be empty")
460
+ .describe("Document reference to find backlinks for"),
461
+ collection: z
462
+ .string()
463
+ .trim()
464
+ .optional()
465
+ .describe("Filter backlinks to a single collection"),
195
466
  });
196
467
 
197
468
  const similarInputSchema = z.object({
198
- ref: z.string().trim().min(1, "Reference cannot be empty"),
199
- limit: z.number().int().min(1).max(50).default(5),
200
- threshold: z.number().min(0).max(1).optional(),
201
- crossCollection: z.boolean().default(false),
469
+ ref: z
470
+ .string()
471
+ .trim()
472
+ .min(1, "Reference cannot be empty")
473
+ .describe("Document reference to find similar docs for"),
474
+ limit: z
475
+ .number()
476
+ .int()
477
+ .min(1)
478
+ .max(50)
479
+ .default(5)
480
+ .describe("Max similar documents to return"),
481
+ threshold: z
482
+ .number()
483
+ .min(0)
484
+ .max(1)
485
+ .optional()
486
+ .describe("Minimum similarity score (0-1, default: 0.7)"),
487
+ crossCollection: z
488
+ .boolean()
489
+ .default(false)
490
+ .describe(
491
+ "Search across all collections (not just the document's own collection)"
492
+ ),
202
493
  });
203
494
 
204
495
  const graphInputSchema = z.object({
205
- collection: z.string().trim().optional(),
206
- limit: z.number().int().min(1).max(5000).default(2000),
207
- edgeLimit: z.number().int().min(1).max(50000).default(10000),
208
- includeSimilar: z.boolean().default(false),
209
- threshold: z.number().min(0).max(1).default(0.7),
210
- linkedOnly: z.boolean().default(true),
211
- similarTopK: z.number().int().min(1).max(20).default(5),
496
+ collection: z
497
+ .string()
498
+ .trim()
499
+ .optional()
500
+ .describe("Filter graph to a single collection"),
501
+ limit: z
502
+ .number()
503
+ .int()
504
+ .min(1)
505
+ .max(5000)
506
+ .default(2000)
507
+ .describe("Max nodes in graph"),
508
+ edgeLimit: z
509
+ .number()
510
+ .int()
511
+ .min(1)
512
+ .max(50000)
513
+ .default(10000)
514
+ .describe("Max edges in graph"),
515
+ includeSimilar: z
516
+ .boolean()
517
+ .default(false)
518
+ .describe("Include vector-similarity edges (not just wiki/markdown links)"),
519
+ threshold: z
520
+ .number()
521
+ .min(0)
522
+ .max(1)
523
+ .default(0.7)
524
+ .describe("Similarity threshold for similar edges (0-1)"),
525
+ linkedOnly: z
526
+ .boolean()
527
+ .default(true)
528
+ .describe("Exclude isolated nodes (no links)"),
529
+ similarTopK: z
530
+ .number()
531
+ .int()
532
+ .min(1)
533
+ .max(20)
534
+ .default(5)
535
+ .describe("Max similar docs per node when includeSimilar=true"),
212
536
  });
213
537
 
214
538
  // ─────────────────────────────────────────────────────────────────────────────
@@ -323,77 +647,77 @@ export function registerTools(server: McpServer, ctx: ToolContext): void {
323
647
  // Tool IDs use underscores (MCP pattern: ^[a-zA-Z0-9_-]{1,64}$)
324
648
  server.tool(
325
649
  "gno_search",
326
- "BM25 full-text search across indexed documents",
650
+ "BM25 keyword search. Instant, best for exact terms. Use gno_query for better quality.",
327
651
  searchInputSchema.shape,
328
652
  (args) => handleSearch(args, ctx)
329
653
  );
330
654
 
331
655
  server.tool(
332
656
  "gno_vsearch",
333
- "Vector/semantic similarity search",
657
+ "Vector semantic search. Finds conceptually similar docs even with different wording. Use gno_query for best results.",
334
658
  vsearchInputSchema.shape,
335
659
  (args) => handleVsearch(args, ctx)
336
660
  );
337
661
 
338
662
  server.tool(
339
663
  "gno_query",
340
- "Hybrid search with optional expansion and reranking",
664
+ "Hybrid search (BM25 + vector + reranking). Best quality, recommended default. Use fast=true for speed, thorough=true for best recall.",
341
665
  queryInputSchema.shape,
342
666
  (args) => handleQuery(args, ctx)
343
667
  );
344
668
 
345
669
  server.tool(
346
670
  "gno_get",
347
- "Retrieve a single document by URI, docid, or collection/path",
671
+ "Retrieve a single document's full content by URI (gno://collection/path), docid (#abc123), or collection/path.",
348
672
  getInputSchema.shape,
349
673
  (args) => handleGet(args, ctx)
350
674
  );
351
675
 
352
676
  server.tool(
353
677
  "gno_multi_get",
354
- "Retrieve multiple documents by refs or glob pattern",
678
+ "Retrieve multiple documents by refs array or glob pattern. Use maxBytes to control truncation.",
355
679
  multiGetInputSchema.shape,
356
680
  (args) => handleMultiGet(args, ctx)
357
681
  );
358
682
 
359
683
  server.tool(
360
684
  "gno_status",
361
- "Get index status and health information",
685
+ "Get index health: collection count, document count, chunk count, embedding backlog, and per-collection stats.",
362
686
  statusInputSchema.shape,
363
687
  (args) => handleStatus(args, ctx)
364
688
  );
365
689
 
366
690
  server.tool(
367
691
  "gno_list_tags",
368
- "List tags with document counts",
692
+ "List all tags with document counts. Use prefix to filter hierarchical tags (e.g. 'project/').",
369
693
  listTagsInputSchema.shape,
370
694
  (args) => handleListTags(args, ctx)
371
695
  );
372
696
 
373
697
  server.tool(
374
698
  "gno_links",
375
- "Get outgoing links from a document",
699
+ "Get outgoing wiki ([[links]]) and markdown links from a document.",
376
700
  linksInputSchema.shape,
377
701
  (args) => handleLinks(args, ctx)
378
702
  );
379
703
 
380
704
  server.tool(
381
705
  "gno_backlinks",
382
- "Get documents linking TO a document",
706
+ "Find all documents that link TO a given document (incoming references).",
383
707
  backlinksInputSchema.shape,
384
708
  (args) => handleBacklinks(args, ctx)
385
709
  );
386
710
 
387
711
  server.tool(
388
712
  "gno_similar",
389
- "Find semantically similar documents using vector embeddings",
713
+ "Find semantically similar documents using vector embeddings. Requires embeddings to exist for source document.",
390
714
  similarInputSchema.shape,
391
715
  (args) => handleSimilar(args, ctx)
392
716
  );
393
717
 
394
718
  server.tool(
395
719
  "gno_graph",
396
- "Get knowledge graph of document connections (nodes and edges)",
720
+ "Get knowledge graph of document connections (wiki links, markdown links, optional similarity edges).",
397
721
  graphInputSchema.shape,
398
722
  (args) => handleGraph(args, ctx)
399
723
  );
@@ -401,42 +725,42 @@ export function registerTools(server: McpServer, ctx: ToolContext): void {
401
725
  if (ctx.enableWrite) {
402
726
  server.tool(
403
727
  "gno_capture",
404
- "Create a new document",
728
+ "Create a new document in a collection. Writes to disk. Does NOT auto-embed; run gno_index after to make it searchable via vector search.",
405
729
  captureInputSchema.shape,
406
730
  (args) => handleCapture(args, ctx)
407
731
  );
408
732
 
409
733
  server.tool(
410
734
  "gno_add_collection",
411
- "Add a collection and start indexing",
735
+ "Add a directory as a new collection and start indexing. Returns a job ID for tracking.",
412
736
  addCollectionInputSchema.shape,
413
737
  (args) => handleAddCollection(args, ctx)
414
738
  );
415
739
 
416
740
  server.tool(
417
741
  "gno_sync",
418
- "Sync one or all collections",
742
+ "Sync files from disk into the index (FTS only, no embeddings). Does NOT auto-embed; run gno_embed after if vector search needed.",
419
743
  syncInputSchema.shape,
420
744
  (args) => handleSync(args, ctx)
421
745
  );
422
746
 
423
747
  server.tool(
424
748
  "gno_embed",
425
- "Generate embeddings for unembedded chunks",
749
+ "Generate vector embeddings for all unembedded chunks. Async: returns a job ID. Poll with gno_job_status.",
426
750
  embedInputSchema.shape,
427
751
  (args) => handleEmbed(args, ctx)
428
752
  );
429
753
 
430
754
  server.tool(
431
755
  "gno_index",
432
- "Full index: sync files + generate embeddings",
756
+ "Full index: sync files from disk + generate embeddings. Async: returns a job ID. Poll with gno_job_status.",
433
757
  indexInputSchema.shape,
434
758
  (args) => handleIndex(args, ctx)
435
759
  );
436
760
 
437
761
  server.tool(
438
762
  "gno_remove_collection",
439
- "Remove a collection from config",
763
+ "Remove a collection from config and delete its indexed data.",
440
764
  removeCollectionInputSchema.shape,
441
765
  (args) => handleRemoveCollection(args, ctx)
442
766
  );
@@ -444,14 +768,14 @@ export function registerTools(server: McpServer, ctx: ToolContext): void {
444
768
 
445
769
  server.tool(
446
770
  "gno_job_status",
447
- "Get status of an async job",
771
+ "Check status of an async job (embed, index). Returns progress percentage and completion state.",
448
772
  jobStatusInputSchema.shape,
449
773
  (args) => handleJobStatus(args, ctx)
450
774
  );
451
775
 
452
776
  server.tool(
453
777
  "gno_list_jobs",
454
- "List active and recent jobs",
778
+ "List active and recently completed async jobs with their status and progress.",
455
779
  listJobsInputSchema.shape,
456
780
  (args) => handleListJobs(args, ctx)
457
781
  );