@gmickel/gno 1.22.0 → 1.24.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 (59) hide show
  1. package/README.md +33 -12
  2. package/assets/skill/SKILL.md +41 -19
  3. package/package.json +1 -1
  4. package/spec/cli.md +127 -20
  5. package/spec/evals-agentic.md +35 -0
  6. package/spec/evals.md +6 -0
  7. package/spec/mcp.md +18 -0
  8. package/spec/output-schemas/query-diagnose-v1.schema.json +123 -0
  9. package/spec/output-schemas/query-diagnose.schema.json +89 -2
  10. package/spec/output-schemas/setup-activation-result.schema.json +456 -0
  11. package/spec/output-schemas/setup-command-result.schema.json +93 -0
  12. package/spec/output-schemas/setup-receipt.schema.json +258 -0
  13. package/spec/output-schemas/setup-semantic-receipt.schema.json +195 -0
  14. package/src/app/context-runtime-types.ts +3 -0
  15. package/src/app/context-runtime.ts +1 -0
  16. package/src/app/context-surface.ts +4 -2
  17. package/src/cli/commands/ask.ts +31 -20
  18. package/src/cli/commands/completion/scripts.ts +2 -0
  19. package/src/cli/commands/context-build.ts +17 -7
  20. package/src/cli/commands/embed.ts +7 -2
  21. package/src/cli/commands/query.ts +58 -37
  22. package/src/cli/commands/search.ts +29 -19
  23. package/src/cli/commands/setup-activation.ts +324 -0
  24. package/src/cli/commands/setup-semantic.ts +591 -0
  25. package/src/cli/commands/setup.ts +410 -0
  26. package/src/cli/commands/vsearch.ts +31 -22
  27. package/src/cli/options.ts +39 -0
  28. package/src/cli/program.ts +112 -0
  29. package/src/cli/setup-semantic-worker.ts +177 -0
  30. package/src/config/defaults.ts +10 -1
  31. package/src/config/types.ts +71 -0
  32. package/src/core/config-mutation.ts +94 -64
  33. package/src/core/file-lock.ts +70 -31
  34. package/src/core/folder-setup-planning.ts +453 -0
  35. package/src/core/folder-setup.ts +490 -0
  36. package/src/core/project-affinity-surface.ts +114 -0
  37. package/src/core/project-affinity.ts +330 -0
  38. package/src/core/setup-activation.ts +309 -0
  39. package/src/core/setup-receipt.ts +321 -0
  40. package/src/core/validation.ts +20 -1
  41. package/src/mcp/tools/ask.ts +10 -1
  42. package/src/mcp/tools/context.ts +18 -0
  43. package/src/mcp/tools/index.ts +13 -2
  44. package/src/mcp/tools/query.ts +12 -0
  45. package/src/mcp/tools/search.ts +7 -0
  46. package/src/mcp/tools/vsearch.ts +7 -0
  47. package/src/pipeline/diagnose.ts +48 -3
  48. package/src/pipeline/explain.ts +54 -13
  49. package/src/pipeline/hybrid.ts +100 -59
  50. package/src/pipeline/project-affinity.ts +162 -0
  51. package/src/pipeline/search.ts +76 -10
  52. package/src/pipeline/types.ts +9 -0
  53. package/src/pipeline/vsearch.ts +117 -91
  54. package/src/sdk/client.ts +80 -20
  55. package/src/sdk/index.ts +2 -0
  56. package/src/sdk/types.ts +20 -7
  57. package/src/serve/connectors.ts +29 -2
  58. package/src/serve/context-capsule.ts +18 -1
  59. package/src/serve/routes/api.ts +69 -0
@@ -0,0 +1,410 @@
1
+ /**
2
+ * Direct verified-folder setup CLI composition.
3
+ *
4
+ * @module src/cli/commands/setup
5
+ */
6
+
7
+ // node:readline/promises is the platform line-input API; Bun has no equivalent
8
+ // for one default-No terminal confirmation.
9
+ import { createInterface } from "node:readline/promises";
10
+
11
+ import type {
12
+ FolderSetupError,
13
+ FolderSetupOptions,
14
+ FolderSetupResult,
15
+ } from "../../core/folder-setup";
16
+ import type {
17
+ FolderSetupReceipt,
18
+ SetupStageName,
19
+ } from "../../core/setup-receipt";
20
+ import type { SqliteAdapter } from "../../store/sqlite/adapter";
21
+
22
+ import { getIndexDbPath } from "../../app/constants";
23
+ import {
24
+ getConfigPaths,
25
+ isInitialized,
26
+ loadConfig,
27
+ toAbsolutePath,
28
+ } from "../../config";
29
+ import { setupFolder } from "../../core/folder-setup";
30
+ import { persistSetupReceipt } from "../../core/setup-receipt";
31
+ import { SqliteAdapter as DefaultSqliteAdapter } from "../../store/sqlite/adapter";
32
+ import { init } from "./init";
33
+ import {
34
+ scheduleSetupSemantic,
35
+ type SetupSemanticReceipt,
36
+ } from "./setup-semantic";
37
+
38
+ export const SETUP_COMMAND_SCHEMA_VERSION = "1.0" as const;
39
+
40
+ export interface SetupCommandError {
41
+ code: string;
42
+ message: string;
43
+ remediation: string;
44
+ }
45
+
46
+ export interface SetupCommandResult {
47
+ schemaVersion: typeof SETUP_COMMAND_SCHEMA_VERSION;
48
+ status: "completed" | "failed";
49
+ lexical: {
50
+ receipt: FolderSetupReceipt | null;
51
+ error: SetupCommandError | null;
52
+ };
53
+ semantic: SetupSemanticReceipt | null;
54
+ }
55
+
56
+ export interface SetupCommandOptions {
57
+ folder: string;
58
+ name?: string;
59
+ exclude?: string[];
60
+ authorizeSecretRisk?: boolean;
61
+ semantic?: boolean;
62
+ indexName?: string;
63
+ configPath?: string;
64
+ offline?: boolean;
65
+ yes?: boolean;
66
+ json?: boolean;
67
+ quiet?: boolean;
68
+ stdinIsTTY?: boolean;
69
+ stderrIsTTY?: boolean;
70
+ progress?: (stage: SetupStageName, receipt: FolderSetupReceipt) => void;
71
+ confirmSecretRisk?: (receipt: FolderSetupReceipt) => Promise<boolean>;
72
+ setupFolderFn?: (options: FolderSetupOptions) => Promise<FolderSetupResult>;
73
+ scheduleSemanticFn?: typeof scheduleSetupSemantic;
74
+ initFn?: typeof init;
75
+ isInitializedFn?: typeof isInitialized;
76
+ createStore?: () => SqliteAdapter;
77
+ }
78
+
79
+ export interface SetupCommandOutcome {
80
+ result: SetupCommandResult;
81
+ exitCode: 0 | 1 | 2;
82
+ }
83
+
84
+ const VALIDATION_ERROR_CODES = new Set([
85
+ "folder_not_found",
86
+ "folder_not_directory",
87
+ "folder_unreadable",
88
+ "dangerous_root",
89
+ "secret_risk",
90
+ "empty_folder",
91
+ "unsupported_only",
92
+ "no_indexable_lexical_corpus",
93
+ "invalid_collection_name",
94
+ "collection_name_conflict",
95
+ "collection_overlap",
96
+ "collection_filter_disagreement",
97
+ "store_index_mismatch",
98
+ "setup_path_overlap",
99
+ ]);
100
+
101
+ function commandError(
102
+ code: string,
103
+ message: string,
104
+ remediation: string
105
+ ): SetupCommandError {
106
+ return { code, message, remediation };
107
+ }
108
+
109
+ function failureOutcome(
110
+ error: SetupCommandError,
111
+ receipt: FolderSetupReceipt | null,
112
+ exitCode: 1 | 2
113
+ ): SetupCommandOutcome {
114
+ return {
115
+ result: {
116
+ schemaVersion: SETUP_COMMAND_SCHEMA_VERSION,
117
+ status: "failed",
118
+ lexical: { receipt, error },
119
+ semantic: null,
120
+ },
121
+ exitCode,
122
+ };
123
+ }
124
+
125
+ function exitCodeForSetupError(error: FolderSetupError): 1 | 2 {
126
+ return VALIDATION_ERROR_CODES.has(error.code) ? 1 : 2;
127
+ }
128
+
129
+ function firstActiveStage(receipt: FolderSetupReceipt): SetupStageName {
130
+ const stages = Object.entries(receipt.stages);
131
+ const active = stages.find(([, stage]) => stage.status === "in_progress");
132
+ if (active) {
133
+ return active[0] as SetupStageName;
134
+ }
135
+ const failed = stages.find(([, stage]) => stage.status === "failed");
136
+ if (failed) {
137
+ return failed[0] as SetupStageName;
138
+ }
139
+ const lastPassed = stages
140
+ .reverse()
141
+ .find(([, stage]) => stage.status === "passed");
142
+ return (lastPassed?.[0] as SetupStageName | undefined) ?? "preflight";
143
+ }
144
+
145
+ export async function terminalSecretConfirmation(
146
+ receipt: FolderSetupReceipt,
147
+ ask?: (question: string) => Promise<string>
148
+ ): Promise<boolean> {
149
+ process.stderr.write(
150
+ `Potential secret files detected in ${receipt.input.folder}\n`
151
+ );
152
+ process.stderr.write(
153
+ `Effective exclusions: ${receipt.input.excludes.join(", ") || "(none)"}\n`
154
+ );
155
+ const prompt = ask
156
+ ? null
157
+ : createInterface({
158
+ input: process.stdin,
159
+ output: process.stderr,
160
+ });
161
+ try {
162
+ try {
163
+ const answer = await (ask ?? prompt!.question.bind(prompt))(
164
+ "Index this folder despite the secret-file risk? [y/N] "
165
+ );
166
+ return /^(?:y|yes)$/i.test(answer.trim());
167
+ } catch {
168
+ return false;
169
+ }
170
+ } finally {
171
+ prompt?.close();
172
+ }
173
+ }
174
+
175
+ export function lexicalSuccessIsProven(receipt: FolderSetupReceipt): boolean {
176
+ const resultUri = receipt.activation?.evidence.resultUri;
177
+ return (
178
+ receipt.status === "completed" &&
179
+ receipt.activation?.ready === true &&
180
+ typeof resultUri === "string" &&
181
+ resultUri.length > 0
182
+ );
183
+ }
184
+
185
+ function validateSetupArguments(
186
+ options: SetupCommandOptions
187
+ ): SetupCommandError | null {
188
+ if (!options.folder.trim()) {
189
+ return commandError(
190
+ "invalid_folder",
191
+ "Folder is required",
192
+ "Pass a readable local folder to `gno setup`."
193
+ );
194
+ }
195
+ if (options.exclude?.some((value) => value.length === 0)) {
196
+ return commandError(
197
+ "invalid_exclusion",
198
+ "--exclude requires a non-empty literal pattern",
199
+ "Remove the empty occurrence or pass a literal exclusion pattern."
200
+ );
201
+ }
202
+ return null;
203
+ }
204
+
205
+ /**
206
+ * Execute the standalone setup transaction. This function returns classified
207
+ * outcomes; the Commander surface owns stdout/stderr rendering.
208
+ */
209
+ async function executeSetup(
210
+ options: SetupCommandOptions
211
+ ): Promise<SetupCommandOutcome> {
212
+ const argumentError = validateSetupArguments(options);
213
+ if (argumentError) {
214
+ return failureOutcome(argumentError, null, 1);
215
+ }
216
+
217
+ const paths = getConfigPaths();
218
+ const configPath = toAbsolutePath(options.configPath ?? paths.configFile);
219
+ const dataDir = paths.dataDir;
220
+ const indexName = options.indexName ?? "default";
221
+ const initialized = await (options.isInitializedFn ?? isInitialized)(
222
+ configPath
223
+ );
224
+ if (!initialized) {
225
+ const initializedResult = await (options.initFn ?? init)({
226
+ configPath,
227
+ yes: true,
228
+ });
229
+ if (!initializedResult.success) {
230
+ return failureOutcome(
231
+ commandError(
232
+ "bootstrap_failed",
233
+ initializedResult.error ?? "Failed to initialize GNO",
234
+ "Fix config/data-directory permissions and rerun setup."
235
+ ),
236
+ null,
237
+ 2
238
+ );
239
+ }
240
+ }
241
+
242
+ const configResult = await loadConfig(configPath);
243
+ if (!configResult.ok) {
244
+ return failureOutcome(
245
+ commandError(
246
+ "config_load_failed",
247
+ configResult.error.message,
248
+ "Repair the selected config and rerun setup."
249
+ ),
250
+ null,
251
+ 2
252
+ );
253
+ }
254
+
255
+ const store =
256
+ options.createStore?.() ?? (new DefaultSqliteAdapter() as SqliteAdapter);
257
+ store.setConfigPath(configPath);
258
+ const opened = await store.open(
259
+ getIndexDbPath(indexName),
260
+ configResult.value.ftsTokenizer
261
+ );
262
+ if (!opened.ok) {
263
+ await store.close();
264
+ return failureOutcome(
265
+ commandError(
266
+ "store_open_failed",
267
+ opened.error.message,
268
+ "Repair the selected index database and rerun setup."
269
+ ),
270
+ null,
271
+ 2
272
+ );
273
+ }
274
+
275
+ const setupFolderFn = options.setupFolderFn ?? setupFolder;
276
+ let lastProgress: string | null = null;
277
+ const receiptWriter = async (receipt: FolderSetupReceipt): Promise<void> => {
278
+ await persistSetupReceipt(receipt);
279
+ if (options.quiet || options.json) {
280
+ return;
281
+ }
282
+ const stage = firstActiveStage(receipt);
283
+ const key = stage;
284
+ if (lastProgress !== key) {
285
+ lastProgress = key;
286
+ options.progress?.(stage, receipt);
287
+ }
288
+ };
289
+
290
+ const runCore = (authorized: boolean): Promise<FolderSetupResult> =>
291
+ setupFolderFn({
292
+ folder: options.folder,
293
+ store,
294
+ configPath,
295
+ dataDir,
296
+ indexName,
297
+ name: options.name,
298
+ exclude: options.exclude,
299
+ secretRiskAuthorized: authorized,
300
+ receiptWriter,
301
+ });
302
+
303
+ try {
304
+ let lexicalResult = await runCore(options.authorizeSecretRisk === true);
305
+ if (
306
+ !lexicalResult.ok &&
307
+ lexicalResult.error.code === "secret_risk" &&
308
+ lexicalResult.receipt &&
309
+ options.authorizeSecretRisk !== true
310
+ ) {
311
+ const mayPrompt =
312
+ options.json !== true &&
313
+ options.yes !== true &&
314
+ (options.stdinIsTTY ?? process.stdin.isTTY ?? false) &&
315
+ (options.stderrIsTTY ?? process.stderr.isTTY ?? false);
316
+ if (mayPrompt) {
317
+ const confirmed = await (
318
+ options.confirmSecretRisk ?? terminalSecretConfirmation
319
+ )(lexicalResult.receipt);
320
+ if (confirmed) {
321
+ lexicalResult = await runCore(true);
322
+ }
323
+ }
324
+ }
325
+
326
+ if (!lexicalResult.ok) {
327
+ return failureOutcome(
328
+ lexicalResult.error,
329
+ lexicalResult.receipt,
330
+ exitCodeForSetupError(lexicalResult.error)
331
+ );
332
+ }
333
+ if (!lexicalSuccessIsProven(lexicalResult.receipt)) {
334
+ return failureOutcome(
335
+ commandError(
336
+ "lexical_success_invariant_failed",
337
+ "Setup completed without an exact lexical retrieval result",
338
+ "Rerun setup after repairing the selected index."
339
+ ),
340
+ lexicalResult.receipt,
341
+ 2
342
+ );
343
+ }
344
+
345
+ const semantic = await (
346
+ options.scheduleSemanticFn ?? scheduleSetupSemantic
347
+ )({
348
+ setupReceipt: lexicalResult.receipt,
349
+ dataDir,
350
+ configPath,
351
+ indexName,
352
+ offline: options.offline ?? false,
353
+ disabled: options.semantic === false,
354
+ });
355
+ return {
356
+ result: {
357
+ schemaVersion: SETUP_COMMAND_SCHEMA_VERSION,
358
+ status: "completed",
359
+ lexical: {
360
+ receipt: lexicalResult.receipt,
361
+ error: null,
362
+ },
363
+ semantic,
364
+ },
365
+ exitCode: 0,
366
+ };
367
+ } finally {
368
+ await store.close();
369
+ }
370
+ }
371
+
372
+ export async function setup(
373
+ options: SetupCommandOptions
374
+ ): Promise<SetupCommandOutcome> {
375
+ try {
376
+ return await executeSetup(options);
377
+ } catch (error) {
378
+ return failureOutcome(
379
+ commandError(
380
+ "setup_runtime_failed",
381
+ error instanceof Error ? error.message : String(error),
382
+ "Fix the reported local setup error and rerun setup."
383
+ ),
384
+ null,
385
+ 2
386
+ );
387
+ }
388
+ }
389
+
390
+ export function formatSetupResult(
391
+ result: SetupCommandResult,
392
+ options: { json: boolean }
393
+ ): string {
394
+ if (options.json) {
395
+ return JSON.stringify(result, null, 2);
396
+ }
397
+ const receipt = result.lexical.receipt;
398
+ if (result.status === "failed") {
399
+ const error = result.lexical.error;
400
+ return `${error?.code ?? "setup_failed"}: ${error?.message ?? "Setup failed"}. ${error?.remediation ?? ""}`.trim();
401
+ }
402
+ const semantic = result.semantic;
403
+ return [
404
+ `Setup ${receipt?.collection.disposition}: ${receipt?.collection.name}`,
405
+ `result=${receipt?.activation?.evidence.resultUri}`,
406
+ `receipt=${receipt?.paths.receipt}`,
407
+ `semantic=${semantic?.status ?? "pending"}`,
408
+ `resume=${semantic?.resumeCommand ?? "gno embed"}`,
409
+ ].join(" ");
410
+ }
@@ -5,6 +5,7 @@
5
5
  * @module src/cli/commands/vsearch
6
6
  */
7
7
 
8
+ import type { CliProjectAffinityRequest } from "../../core/project-affinity-surface";
8
9
  import type {
9
10
  RetrievalTraceSession,
10
11
  RetrievalTraceSurfaceMetadata,
@@ -12,6 +13,7 @@ import type {
12
13
  import type { EmbeddingPort } from "../../llm/types";
13
14
  import type { SearchOptions, SearchResults } from "../../pipeline/types";
14
15
 
16
+ import { resolveCliProjectAffinity } from "../../core/project-affinity-surface";
15
17
  import {
16
18
  finishRetrievalTraceAfterError,
17
19
  retrievalTraceFilters,
@@ -35,26 +37,27 @@ import { decorateSearchResultsForIndex, initStore } from "./shared";
35
37
  // Types
36
38
  // ─────────────────────────────────────────────────────────────────────────────
37
39
 
38
- export type VsearchCommandOptions = SearchOptions & {
39
- /** Override config path */
40
- configPath?: string;
41
- /** Index name */
42
- indexName?: string;
43
- /** Override model URI */
44
- model?: string;
45
- /** Output as JSON */
46
- json?: boolean;
47
- /** Output as Markdown */
48
- md?: boolean;
49
- /** Output as CSV */
50
- csv?: boolean;
51
- /** Output as XML */
52
- xml?: boolean;
53
- /** Output files only */
54
- files?: boolean;
55
- /** Terminal hyperlink policy */
56
- terminalLinks?: FormatOptions["terminalLinks"];
57
- };
40
+ export type VsearchCommandOptions = Omit<SearchOptions, "projectAffinity"> &
41
+ CliProjectAffinityRequest & {
42
+ /** Override config path */
43
+ configPath?: string;
44
+ /** Index name */
45
+ indexName?: string;
46
+ /** Override model URI */
47
+ model?: string;
48
+ /** Output as JSON */
49
+ json?: boolean;
50
+ /** Output as Markdown */
51
+ md?: boolean;
52
+ /** Output as CSV */
53
+ csv?: boolean;
54
+ /** Output as XML */
55
+ xml?: boolean;
56
+ /** Output files only */
57
+ files?: boolean;
58
+ /** Terminal hyperlink policy */
59
+ terminalLinks?: FormatOptions["terminalLinks"];
60
+ };
58
61
 
59
62
  export type VsearchResult =
60
63
  | {
@@ -96,6 +99,12 @@ export async function vsearch(
96
99
  let traceSession: RetrievalTraceSession | undefined;
97
100
 
98
101
  try {
102
+ const { projectAffinityDisabled, projectRoots, ...searchOptions } = options;
103
+ const projectAffinity = await resolveCliProjectAffinity(config, {
104
+ cwd: process.cwd(),
105
+ disabled: projectAffinityDisabled,
106
+ projectRoots,
107
+ });
99
108
  // Get model URI from preset
100
109
  const modelUri = resolveModelUri(
101
110
  config,
@@ -107,7 +116,7 @@ export async function vsearch(
107
116
  store,
108
117
  config,
109
118
  query,
110
- filters: retrievalTraceFilters({ ...options, limit }),
119
+ filters: retrievalTraceFilters({ ...searchOptions, limit }),
111
120
  pipeline: "vector",
112
121
  indexName: options.indexName,
113
122
  modelUris: [modelUri],
@@ -152,7 +161,7 @@ export async function vsearch(
152
161
  deps,
153
162
  query,
154
163
  queryEmbedding,
155
- { ...options, limit, traceSession }
164
+ { ...searchOptions, limit, projectAffinity, traceSession }
156
165
  );
157
166
  if (!result.ok) {
158
167
  await traceSession?.finish("failed");
@@ -5,6 +5,10 @@
5
5
  * @module src/cli/options
6
6
  */
7
7
 
8
+ import {
9
+ normalizeProjectAffinityValues,
10
+ ProjectAffinityInputError,
11
+ } from "../core/project-affinity-surface";
8
12
  import { CliError } from "./errors";
9
13
 
10
14
  // ─────────────────────────────────────────────────────────────────────────────
@@ -13,6 +17,41 @@ import { CliError } from "./errors";
13
17
 
14
18
  export type OutputFormat = "terminal" | "json" | "files" | "csv" | "md" | "xml";
15
19
 
20
+ export interface CliProjectAffinityOptions {
21
+ projectAffinityDisabled: boolean;
22
+ projectRoots: string[];
23
+ }
24
+
25
+ export const collectRepeatableValue = (
26
+ value: string,
27
+ previous: string[] = []
28
+ ): string[] => [...previous, value];
29
+
30
+ export const parseCliProjectAffinityOptions = (
31
+ options: Record<string, unknown>
32
+ ): CliProjectAffinityOptions => {
33
+ try {
34
+ const projectRoots = normalizeProjectAffinityValues(
35
+ Array.isArray(options.projectRoot)
36
+ ? (options.projectRoot as string[])
37
+ : undefined,
38
+ "project roots"
39
+ );
40
+ const projectAffinityDisabled = options.projectAffinity === false;
41
+ if (projectAffinityDisabled && projectRoots.length > 0) {
42
+ throw new ProjectAffinityInputError(
43
+ "--no-project-affinity cannot be combined with --project-root"
44
+ );
45
+ }
46
+ return { projectAffinityDisabled, projectRoots };
47
+ } catch (error) {
48
+ throw new CliError(
49
+ "VALIDATION",
50
+ error instanceof Error ? error.message : "Invalid project affinity input"
51
+ );
52
+ }
53
+ };
54
+
16
55
  // ─────────────────────────────────────────────────────────────────────────────
17
56
  // Format Support Matrix (per spec/cli.md)
18
57
  // ─────────────────────────────────────────────────────────────────────────────