@gmickel/gno 0.25.2 → 0.26.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.
package/README.md CHANGED
@@ -194,9 +194,11 @@ Check status: `gno mcp status`
194
194
  Skills integrate via CLI with no MCP overhead:
195
195
 
196
196
  ```bash
197
- gno skill install --scope user # User-wide
198
- gno skill install --target codex # Codex
199
- gno skill install --target all # Both Claude + Codex
197
+ gno skill install --scope user # User-wide
198
+ gno skill install --target codex # Codex
199
+ gno skill install --target opencode # OpenCode
200
+ gno skill install --target openclaw # OpenClaw
201
+ gno skill install --target all # All targets
200
202
  ```
201
203
 
202
204
  > **Full setup guide**: [MCP Integration](https://gno.sh/docs/MCP/) · [CLI Reference](https://gno.sh/docs/CLI/)
@@ -488,18 +488,20 @@ Install GNO skill for AI coding assistants.
488
488
  gno skill install [options]
489
489
  ```
490
490
 
491
- | Option | Default | Description |
492
- | -------------- | ------- | -------------------------------- |
493
- | `-t, --target` | claude | Target: `claude`, `codex`, `amp` |
494
- | `-s, --scope` | user | Scope: `user`, `project` |
495
- | `-f, --force` | false | Overwrite existing |
496
- | `--dry-run` | false | Preview changes |
491
+ | Option | Default | Description |
492
+ | -------------- | ------- | -------------------------------------------------------- |
493
+ | `-t, --target` | claude | Target: `claude`, `codex`, `opencode`, `openclaw`, `all` |
494
+ | `-s, --scope` | user | Scope: `user`, `project` |
495
+ | `-f, --force` | false | Overwrite existing |
496
+ | `--dry-run` | false | Preview changes |
497
497
 
498
498
  Examples:
499
499
 
500
500
  ```bash
501
501
  gno skill install --target claude --scope project
502
502
  gno skill install --target codex --scope user
503
+ gno skill install --target openclaw --scope user
504
+ gno skill install --target all --force # Install to all targets
503
505
  ```
504
506
 
505
507
  ### gno skill uninstall
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gmickel/gno",
3
- "version": "0.25.2",
3
+ "version": "0.26.0",
4
4
  "description": "Local semantic search for your documents. Index Markdown, PDF, and Office files with hybrid BM25 + vector search.",
5
5
  "keywords": [
6
6
  "embeddings",
@@ -13,6 +13,7 @@ import { CliError } from "../../errors.js";
13
13
  import { getGlobals } from "../../program.js";
14
14
  import {
15
15
  resolveSkillPaths,
16
+ SKILL_TARGETS,
16
17
  type SkillScope,
17
18
  type SkillTarget,
18
19
  validatePathForDeletion,
@@ -171,8 +172,7 @@ export async function installSkill(opts: InstallOptions = {}): Promise<void> {
171
172
  const yes = opts.yes ?? globals.yes;
172
173
  const quiet = opts.quiet ?? globals.quiet;
173
174
 
174
- const targets: SkillTarget[] =
175
- target === "all" ? ["claude", "codex"] : [target];
175
+ const targets: SkillTarget[] = target === "all" ? SKILL_TARGETS : [target];
176
176
 
177
177
  const results: InstallResult[] = [];
178
178
 
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * Path resolution for skill installation.
3
- * Supports Claude Code and Codex targets with project/user scopes.
4
- * Note: OpenCode and Amp use the same .claude path as Claude Code.
3
+ * Supports Claude Code, Codex, OpenCode, and OpenClaw targets with project/user scopes.
5
4
  *
6
5
  * @module src/cli/commands/skill/paths
7
6
  */
@@ -22,14 +21,25 @@ export const ENV_CLAUDE_SKILLS_DIR = "CLAUDE_SKILLS_DIR";
22
21
  /** Override Codex skills directory */
23
22
  export const ENV_CODEX_SKILLS_DIR = "CODEX_SKILLS_DIR";
24
23
 
24
+ /** Override OpenCode skills directory */
25
+ export const ENV_OPENCODE_SKILLS_DIR = "OPENCODE_SKILLS_DIR";
26
+
27
+ /** Override OpenClaw skills directory */
28
+ export const ENV_OPENCLAW_SKILLS_DIR = "OPENCLAW_SKILLS_DIR";
29
+
25
30
  // ─────────────────────────────────────────────────────────────────────────────
26
31
  // Types
27
32
  // ─────────────────────────────────────────────────────────────────────────────
28
33
 
29
34
  export type SkillScope = "project" | "user";
30
- export type SkillTarget = "claude" | "codex";
35
+ export type SkillTarget = "claude" | "codex" | "opencode" | "openclaw";
31
36
 
32
- export const SKILL_TARGETS: SkillTarget[] = ["claude", "codex"];
37
+ export const SKILL_TARGETS: SkillTarget[] = [
38
+ "claude",
39
+ "codex",
40
+ "opencode",
41
+ "openclaw",
42
+ ];
33
43
 
34
44
  export interface SkillPathOptions {
35
45
  scope: SkillScope;
@@ -77,6 +87,18 @@ const TARGET_CONFIGS: Record<SkillTarget, TargetPathConfig> = {
77
87
  skillsSubdir: "skills",
78
88
  envVar: ENV_CODEX_SKILLS_DIR,
79
89
  },
90
+ opencode: {
91
+ projectBase: ".opencode",
92
+ userBase: ".config/opencode",
93
+ skillsSubdir: "skills",
94
+ envVar: ENV_OPENCODE_SKILLS_DIR,
95
+ },
96
+ openclaw: {
97
+ projectBase: ".openclaw",
98
+ userBase: ".openclaw",
99
+ skillsSubdir: "skills",
100
+ envVar: ENV_OPENCLAW_SKILLS_DIR,
101
+ },
80
102
  };
81
103
 
82
104
  // ─────────────────────────────────────────────────────────────────────────────
@@ -12,6 +12,7 @@ import { CliError } from "../../errors.js";
12
12
  import { getGlobals } from "../../program.js";
13
13
  import {
14
14
  resolveSkillPaths,
15
+ SKILL_TARGETS,
15
16
  type SkillScope,
16
17
  type SkillTarget,
17
18
  validatePathForDeletion,
@@ -100,8 +101,7 @@ export async function uninstallSkill(
100
101
  const json = opts.json ?? globals.json;
101
102
  const quiet = opts.quiet ?? globals.quiet;
102
103
 
103
- const targets: SkillTarget[] =
104
- target === "all" ? ["claude", "codex"] : [target];
104
+ const targets: SkillTarget[] = target === "all" ? SKILL_TARGETS : [target];
105
105
 
106
106
  const results: UninstallResult[] = [];
107
107
  const notFound: string[] = [];
@@ -1578,7 +1578,7 @@ function wireSkillCommands(program: Command): void {
1578
1578
  )
1579
1579
  .option(
1580
1580
  "-t, --target <target>",
1581
- "target agent (claude, codex, all)",
1581
+ "target agent (claude, codex, opencode, openclaw, all)",
1582
1582
  "claude"
1583
1583
  )
1584
1584
  .option("-f, --force", "overwrite existing installation")
@@ -1595,17 +1595,19 @@ function wireSkillCommands(program: Command): void {
1595
1595
  );
1596
1596
  }
1597
1597
  // Validate target
1598
- if (!["claude", "codex", "all"].includes(target)) {
1598
+ if (
1599
+ !["claude", "codex", "opencode", "openclaw", "all"].includes(target)
1600
+ ) {
1599
1601
  throw new CliError(
1600
1602
  "VALIDATION",
1601
- `Invalid target: ${target}. Must be 'claude', 'codex', or 'all'.`
1603
+ `Invalid target: ${target}. Must be 'claude', 'codex', 'opencode', 'openclaw', or 'all'.`
1602
1604
  );
1603
1605
  }
1604
1606
 
1605
1607
  const { installSkill } = await import("./commands/skill/install.js");
1606
1608
  await installSkill({
1607
1609
  scope: scope as "project" | "user",
1608
- target: target as "claude" | "codex" | "all",
1610
+ target: target as "claude" | "codex" | "opencode" | "openclaw" | "all",
1609
1611
  force: Boolean(cmdOpts.force),
1610
1612
  json: Boolean(cmdOpts.json),
1611
1613
  });
@@ -1621,7 +1623,7 @@ function wireSkillCommands(program: Command): void {
1621
1623
  )
1622
1624
  .option(
1623
1625
  "-t, --target <target>",
1624
- "target agent (claude, codex, all)",
1626
+ "target agent (claude, codex, opencode, openclaw, all)",
1625
1627
  "claude"
1626
1628
  )
1627
1629
  .option("--json", "JSON output")
@@ -1637,17 +1639,19 @@ function wireSkillCommands(program: Command): void {
1637
1639
  );
1638
1640
  }
1639
1641
  // Validate target
1640
- if (!["claude", "codex", "all"].includes(target)) {
1642
+ if (
1643
+ !["claude", "codex", "opencode", "openclaw", "all"].includes(target)
1644
+ ) {
1641
1645
  throw new CliError(
1642
1646
  "VALIDATION",
1643
- `Invalid target: ${target}. Must be 'claude', 'codex', or 'all'.`
1647
+ `Invalid target: ${target}. Must be 'claude', 'codex', 'opencode', 'openclaw', or 'all'.`
1644
1648
  );
1645
1649
  }
1646
1650
 
1647
1651
  const { uninstallSkill } = await import("./commands/skill/uninstall.js");
1648
1652
  await uninstallSkill({
1649
1653
  scope: scope as "project" | "user",
1650
- target: target as "claude" | "codex" | "all",
1654
+ target: target as "claude" | "codex" | "opencode" | "openclaw" | "all",
1651
1655
  json: Boolean(cmdOpts.json),
1652
1656
  });
1653
1657
  });
@@ -1675,7 +1679,7 @@ function wireSkillCommands(program: Command): void {
1675
1679
  )
1676
1680
  .option(
1677
1681
  "-t, --target <target>",
1678
- "filter by target (claude, codex, all)",
1682
+ "filter by target (claude, codex, opencode, openclaw, all)",
1679
1683
  "all"
1680
1684
  )
1681
1685
  .option("--json", "JSON output")
@@ -1691,17 +1695,19 @@ function wireSkillCommands(program: Command): void {
1691
1695
  );
1692
1696
  }
1693
1697
  // Validate target
1694
- if (!["claude", "codex", "all"].includes(target)) {
1698
+ if (
1699
+ !["claude", "codex", "opencode", "openclaw", "all"].includes(target)
1700
+ ) {
1695
1701
  throw new CliError(
1696
1702
  "VALIDATION",
1697
- `Invalid target: ${target}. Must be 'claude', 'codex', or 'all'.`
1703
+ `Invalid target: ${target}. Must be 'claude', 'codex', 'opencode', 'openclaw', or 'all'.`
1698
1704
  );
1699
1705
  }
1700
1706
 
1701
1707
  const { showPaths } = await import("./commands/skill/paths-cmd.js");
1702
1708
  await showPaths({
1703
1709
  scope: scope as "project" | "user" | "all",
1704
- target: target as "claude" | "codex" | "all",
1710
+ target: target as "claude" | "codex" | "opencode" | "openclaw" | "all",
1705
1711
  json: Boolean(cmdOpts.json),
1706
1712
  });
1707
1713
  });
@@ -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
  );