@gmickel/gno 1.25.1 → 1.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 (94) hide show
  1. package/README.md +12 -5
  2. package/assets/skill/SKILL.md +37 -17
  3. package/browser-extension/artifacts/{gno-browser-clipper-v1.25.1.zip → gno-browser-clipper-v1.27.0.zip} +0 -0
  4. package/browser-extension/artifacts/gno-browser-clipper-v1.27.0.zip.sha256 +1 -0
  5. package/browser-extension/dist/manifest.json +1 -1
  6. package/package.json +1 -1
  7. package/spec/cli.md +161 -6
  8. package/spec/db/schema.sql +1 -1
  9. package/spec/evals-agentic.md +17 -0
  10. package/spec/mcp.md +25 -6
  11. package/spec/output-schemas/ask.schema.json +3 -0
  12. package/spec/output-schemas/project-profile-apply.schema.json +209 -0
  13. package/spec/output-schemas/project-profile-command.schema.json +160 -0
  14. package/spec/output-schemas/query-diagnose.schema.json +68 -5
  15. package/spec/output-schemas/search-results.schema.json +87 -1
  16. package/spec/output-schemas/setup-profile-result.schema.json +87 -0
  17. package/spec/output-schemas/status.schema.json +24 -0
  18. package/spec/project-profile.schema.json +303 -0
  19. package/src/app/context-runtime-contract.ts +4 -1
  20. package/src/app/context-runtime-types.ts +2 -0
  21. package/src/app/context-runtime.ts +26 -0
  22. package/src/app/verified-ask.ts +6 -1
  23. package/src/cli/commands/ask.ts +8 -1
  24. package/src/cli/commands/collection/add.ts +39 -45
  25. package/src/cli/commands/collection/remove.ts +28 -28
  26. package/src/cli/commands/collection/rename.ts +55 -73
  27. package/src/cli/commands/context/add.ts +37 -26
  28. package/src/cli/commands/context/rm.ts +47 -20
  29. package/src/cli/commands/init.ts +55 -125
  30. package/src/cli/commands/models/use.ts +43 -38
  31. package/src/cli/commands/profile-apply.ts +334 -0
  32. package/src/cli/commands/profile.ts +409 -0
  33. package/src/cli/commands/query.ts +6 -3
  34. package/src/cli/commands/search.ts +6 -1
  35. package/src/cli/commands/setup-activation.ts +205 -54
  36. package/src/cli/commands/setup-profile.ts +223 -0
  37. package/src/cli/commands/setup.ts +3 -0
  38. package/src/cli/commands/status.ts +43 -7
  39. package/src/cli/program.ts +112 -9
  40. package/src/config/content-types.ts +82 -0
  41. package/src/config/index.ts +11 -0
  42. package/src/config/project-profile.ts +374 -0
  43. package/src/config/saver.ts +16 -7
  44. package/src/config/types.ts +53 -2
  45. package/src/core/config-mutation.ts +138 -76
  46. package/src/core/config-write-lock.ts +89 -0
  47. package/src/core/context-compiler.ts +38 -1
  48. package/src/core/context-identity.ts +16 -0
  49. package/src/core/context-resolver.ts +2 -12
  50. package/src/core/folder-setup-planning.ts +6 -21
  51. package/src/core/folder-setup.ts +30 -2
  52. package/src/core/path-rules.ts +53 -0
  53. package/src/core/project-affinity-surface.ts +102 -7
  54. package/src/core/project-profile-apply-state.ts +268 -0
  55. package/src/core/project-profile-apply-validation.ts +95 -0
  56. package/src/core/project-profile-apply.ts +408 -0
  57. package/src/core/project-profile-canonical.ts +71 -0
  58. package/src/core/project-profile-diff.ts +302 -0
  59. package/src/core/project-profile-discovery.ts +519 -0
  60. package/src/core/project-profile-file.ts +37 -0
  61. package/src/core/project-profile-parser.ts +98 -0
  62. package/src/core/project-profile.ts +490 -0
  63. package/src/core/retrieval-replay-candidate.ts +6 -1
  64. package/src/ingestion/sync-options.ts +6 -2
  65. package/src/ingestion/sync.ts +21 -29
  66. package/src/ingestion/types.ts +1 -1
  67. package/src/ingestion/walker.ts +85 -44
  68. package/src/llm/cache.ts +21 -0
  69. package/src/mcp/tools/ask.ts +1 -0
  70. package/src/mcp/tools/index.ts +4 -0
  71. package/src/mcp/tools/query.ts +4 -2
  72. package/src/mcp/tools/search.ts +3 -0
  73. package/src/mcp/tools/status.ts +4 -0
  74. package/src/pipeline/content-type-boost.ts +264 -0
  75. package/src/pipeline/diagnose.ts +46 -19
  76. package/src/pipeline/explain.ts +15 -2
  77. package/src/pipeline/hybrid.ts +170 -74
  78. package/src/pipeline/rerank.ts +45 -15
  79. package/src/pipeline/search.ts +29 -11
  80. package/src/pipeline/types.ts +13 -4
  81. package/src/pipeline/vsearch.ts +30 -10
  82. package/src/sdk/client.ts +19 -3
  83. package/src/sdk/index.ts +1 -0
  84. package/src/sdk/types.ts +21 -5
  85. package/src/serve/config-sync.ts +2 -2
  86. package/src/serve/resident-runtime.ts +1 -0
  87. package/src/serve/routes/api.ts +18 -3
  88. package/src/serve/status-model.ts +2 -0
  89. package/src/serve/status.ts +4 -0
  90. package/src/store/migrations/021-multi-context-identity.ts +37 -0
  91. package/src/store/migrations/index.ts +2 -0
  92. package/src/store/sqlite/adapter.ts +86 -0
  93. package/src/store/types.ts +3 -2
  94. package/browser-extension/artifacts/gno-browser-clipper-v1.25.1.zip.sha256 +0 -1
@@ -15,6 +15,7 @@
15
15
  "recentErrors",
16
16
  "activePreset",
17
17
  "capabilities",
18
+ "contentTypeBoost",
18
19
  "activation",
19
20
  "onboarding",
20
21
  "health",
@@ -136,6 +137,29 @@
136
137
  }
137
138
  }
138
139
  },
140
+ "contentTypeBoost": {
141
+ "type": "object",
142
+ "additionalProperties": false,
143
+ "required": ["rulesFingerprint", "rules"],
144
+ "properties": {
145
+ "rulesFingerprint": {
146
+ "type": "string",
147
+ "pattern": "^[a-f0-9]{64}$"
148
+ },
149
+ "rules": {
150
+ "type": "array",
151
+ "items": {
152
+ "type": "object",
153
+ "additionalProperties": false,
154
+ "required": ["id", "searchBoost"],
155
+ "properties": {
156
+ "id": { "type": "string", "minLength": 1 },
157
+ "searchBoost": { "type": "number", "minimum": 0.5, "maximum": 2 }
158
+ }
159
+ }
160
+ }
161
+ }
162
+ },
139
163
  "activation": {
140
164
  "type": "object",
141
165
  "additionalProperties": false,
@@ -0,0 +1,303 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://gno.sh/schemas/project-profile-1.0.json",
4
+ "title": "GNO project retrieval profile",
5
+ "description": "Portable, declarative retrieval state for a single project collection.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["schemaVersion", "collection"],
9
+ "properties": {
10
+ "schemaVersion": {
11
+ "const": "1.0"
12
+ },
13
+ "collection": {
14
+ "$ref": "#/definitions/collection"
15
+ },
16
+ "contexts": {
17
+ "type": "array",
18
+ "maxItems": 64,
19
+ "uniqueItems": true,
20
+ "items": {
21
+ "$ref": "#/definitions/context"
22
+ },
23
+ "default": []
24
+ },
25
+ "contentTypes": {
26
+ "type": "object",
27
+ "maxProperties": 64,
28
+ "propertyNames": {
29
+ "pattern": "^[a-z0-9][a-z0-9_-]{0,63}$"
30
+ },
31
+ "additionalProperties": {
32
+ "$ref": "#/definitions/contentType"
33
+ },
34
+ "default": {}
35
+ },
36
+ "affinityDefaults": {
37
+ "$ref": "#/definitions/affinityDefaults"
38
+ },
39
+ "recommendedCapabilities": {
40
+ "type": "array",
41
+ "maxItems": 32,
42
+ "uniqueItems": true,
43
+ "items": {
44
+ "type": "string",
45
+ "pattern": "^[a-z][a-z0-9._-]{0,63}$"
46
+ },
47
+ "default": []
48
+ }
49
+ },
50
+ "definitions": {
51
+ "portableRule": {
52
+ "type": "string",
53
+ "minLength": 1,
54
+ "maxLength": 512,
55
+ "allOf": [
56
+ {
57
+ "not": {
58
+ "type": "string",
59
+ "pattern": "^(?:[/\\\\]|[A-Za-z]:|~)"
60
+ }
61
+ },
62
+ {
63
+ "not": {
64
+ "type": "string",
65
+ "pattern": "(?:^|[\\\\/])\\.\\.(?:[\\\\/]|$)"
66
+ }
67
+ },
68
+ {
69
+ "not": {
70
+ "type": "string",
71
+ "pattern": "[{}]"
72
+ }
73
+ },
74
+ {
75
+ "not": {
76
+ "type": "string",
77
+ "pattern": "^!"
78
+ }
79
+ },
80
+ {
81
+ "not": {
82
+ "type": "string",
83
+ "pattern": "[<>:\"|\\u0000-\\u001f]"
84
+ }
85
+ },
86
+ {
87
+ "not": {
88
+ "type": "string",
89
+ "pattern": "(?:^|[\\\\/])(?:[cC][oO][nN]|[pP][rR][nN]|[aA][uU][xX]|[nN][uU][lL]|[cC][oO][mM][1-9]|[lL][pP][tT][1-9])(?:\\.[^\\\\/]*)?(?:[\\\\/]|$)"
90
+ }
91
+ },
92
+ {
93
+ "not": {
94
+ "type": "string",
95
+ "pattern": "(?:^|[\\\\/])(?:[^.\\\\/][^\\\\/]*|\\.[^\\\\/]+)[. ](?:[\\\\/]|$)"
96
+ }
97
+ },
98
+ {
99
+ "not": {
100
+ "type": "string",
101
+ "pattern": "(?:\\$\\{|\\$[A-Za-z_]|%[A-Za-z_][A-Za-z0-9_]*%)"
102
+ }
103
+ },
104
+ {
105
+ "not": {
106
+ "type": "string",
107
+ "pattern": "\\u0000"
108
+ }
109
+ }
110
+ ]
111
+ },
112
+ "portablePath": {
113
+ "allOf": [
114
+ {
115
+ "$ref": "#/definitions/portableRule"
116
+ },
117
+ {
118
+ "not": {
119
+ "type": "string",
120
+ "pattern": "[*?\\[\\]{}]"
121
+ }
122
+ },
123
+ {
124
+ "not": {
125
+ "type": "string",
126
+ "pattern": "(?:^|[\\\\/])\\.[gG][nN][oO](?:[\\\\/]|$)"
127
+ }
128
+ },
129
+ {
130
+ "not": {
131
+ "type": "string",
132
+ "pattern": "\\.(?:[dD][bB]|[sS][qQ][lL][iI][tT][eE](?:3)?|[gG][gG][uU][fF]|[lL][oO][cC][kK])$"
133
+ }
134
+ }
135
+ ]
136
+ },
137
+ "portableInclude": {
138
+ "allOf": [
139
+ {
140
+ "$ref": "#/definitions/portableRule"
141
+ },
142
+ {
143
+ "not": {
144
+ "type": "string",
145
+ "pattern": "(?:^|[\\\\/])\\.[gG][nN][oO](?:[\\\\/]|$)|\\.(?:[dD][bB]|[sS][qQ][lL][iI][tT][eE](?:3)?|[gG][gG][uU][fF]|[lL][oO][cC][kK])$"
146
+ }
147
+ }
148
+ ]
149
+ },
150
+ "collection": {
151
+ "type": "object",
152
+ "additionalProperties": false,
153
+ "required": ["name"],
154
+ "properties": {
155
+ "name": {
156
+ "type": "string",
157
+ "pattern": "^[a-z0-9][a-z0-9_-]{0,63}$"
158
+ },
159
+ "root": {
160
+ "$ref": "#/definitions/portablePath",
161
+ "default": "."
162
+ },
163
+ "include": {
164
+ "type": "array",
165
+ "minItems": 1,
166
+ "maxItems": 128,
167
+ "uniqueItems": true,
168
+ "items": {
169
+ "$ref": "#/definitions/portableInclude"
170
+ },
171
+ "default": ["**/*"]
172
+ },
173
+ "exclude": {
174
+ "type": "array",
175
+ "maxItems": 128,
176
+ "uniqueItems": true,
177
+ "items": {
178
+ "$ref": "#/definitions/portableRule"
179
+ },
180
+ "default": []
181
+ },
182
+ "languageHint": {
183
+ "type": "string",
184
+ "pattern": "^[A-Za-z]{2,3}(?:-[A-Za-z]{2}|-[A-Za-z]{4})?$"
185
+ },
186
+ "modelPreset": {
187
+ "type": "string",
188
+ "pattern": "^[a-z0-9][a-z0-9_-]{0,63}$"
189
+ }
190
+ }
191
+ },
192
+ "inlineContext": {
193
+ "type": "object",
194
+ "additionalProperties": false,
195
+ "required": ["text"],
196
+ "properties": {
197
+ "text": {
198
+ "type": "string",
199
+ "minLength": 1,
200
+ "maxLength": 65536
201
+ }
202
+ }
203
+ },
204
+ "fileContext": {
205
+ "type": "object",
206
+ "additionalProperties": false,
207
+ "required": ["file"],
208
+ "properties": {
209
+ "file": {
210
+ "allOf": [
211
+ {
212
+ "$ref": "#/definitions/portablePath"
213
+ },
214
+ {
215
+ "not": {
216
+ "type": "string",
217
+ "pattern": "(?:^|[\\\\/])(?:\\.[eE][nN][vV](?:\\..*)?|[cC][rR][eE][dD][eE][nN][tT][iI][aA][lL][sS]?(?:\\..*)?|[iI][dD]_(?:[rR][sS][aA]|[dD][sS][aA]|[eE][cC][dD][sS][aA]|[eE][dD]25519)(?:\\..*)?|[sS][eE][cC][rR][eE][tT][sS]?(?:\\..*)?|[^\\\\/]+\\.(?:[kK][eE][yY]|[pP][eE][mM]|[pP]12|[pP][fF][xX]))$"
218
+ }
219
+ }
220
+ ]
221
+ }
222
+ }
223
+ },
224
+ "context": {
225
+ "oneOf": [
226
+ {
227
+ "$ref": "#/definitions/inlineContext"
228
+ },
229
+ {
230
+ "$ref": "#/definitions/fileContext"
231
+ }
232
+ ]
233
+ },
234
+ "contentType": {
235
+ "type": "object",
236
+ "additionalProperties": false,
237
+ "required": ["prefixes", "preset"],
238
+ "properties": {
239
+ "prefixes": {
240
+ "type": "array",
241
+ "minItems": 1,
242
+ "maxItems": 64,
243
+ "uniqueItems": true,
244
+ "items": {
245
+ "$ref": "#/definitions/portablePath"
246
+ }
247
+ },
248
+ "preset": {
249
+ "enum": [
250
+ "blank",
251
+ "project-note",
252
+ "research-note",
253
+ "decision-note",
254
+ "prompt-pattern",
255
+ "source-summary",
256
+ "idea-original",
257
+ "person",
258
+ "company-project",
259
+ "meeting"
260
+ ]
261
+ },
262
+ "graphHints": {
263
+ "type": "array",
264
+ "maxItems": 16,
265
+ "uniqueItems": true,
266
+ "items": {
267
+ "type": "string",
268
+ "minLength": 1,
269
+ "maxLength": 64
270
+ }
271
+ },
272
+ "searchBoost": {
273
+ "type": "number",
274
+ "minimum": 0.5,
275
+ "maximum": 2
276
+ },
277
+ "temporal": {
278
+ "type": "boolean"
279
+ }
280
+ }
281
+ },
282
+ "affinityDefaults": {
283
+ "type": "object",
284
+ "additionalProperties": false,
285
+ "properties": {
286
+ "enabled": {
287
+ "type": "boolean",
288
+ "default": true
289
+ },
290
+ "contribution": {
291
+ "type": "number",
292
+ "minimum": 0,
293
+ "maximum": 0.03,
294
+ "default": 0.03
295
+ }
296
+ },
297
+ "default": {
298
+ "enabled": true,
299
+ "contribution": 0.03
300
+ }
301
+ }
302
+ }
303
+ }
@@ -26,7 +26,10 @@ const fingerprint = (value: unknown): string =>
26
26
 
27
27
  export const contextRuntimeConfigFingerprint = (
28
28
  deps: Pick<ContextCapsuleRuntimeDeps, "config">
29
- ): string => fingerprint(deps.config);
29
+ ): string =>
30
+ fingerprint(
31
+ JSON.parse(JSON.stringify(deps.config)) as Record<string, unknown>
32
+ );
30
33
 
31
34
  const configuredContextFingerprint = (
32
35
  deps: ContextCapsuleRuntimeDeps
@@ -55,6 +55,8 @@ export interface ContextCapsuleRuntimeDeps {
55
55
  projectAffinity?: ProjectAffinityScoringInput;
56
56
  /** Optional non-canonical receipt session owned by the calling surface. */
57
57
  traceSession?: RetrievalTraceSession;
58
+ /** Internal non-canonical retrieval explanation request. */
59
+ explain?: boolean;
58
60
  }
59
61
 
60
62
  export type ContextRuntimeErrorCode =
@@ -4,6 +4,7 @@ import type {
4
4
  ContextCapsuleV1,
5
5
  ContextCapsuleVerification,
6
6
  } from "../core/context-capsule";
7
+ import type { SearchExplain } from "../pipeline/types";
7
8
  import type {
8
9
  ContextCapsuleBuildInput,
9
10
  ContextCapsuleRuntimeDeps,
@@ -36,6 +37,19 @@ export type {
36
37
  } from "./context-runtime-types";
37
38
  export { ContextRuntimeError } from "./context-runtime-types";
38
39
 
40
+ export const CONTEXT_CAPSULE_EXPLAIN_METADATA = Symbol(
41
+ "gno.contextCapsuleExplainMetadata"
42
+ );
43
+
44
+ export const getContextCapsuleExplain = (
45
+ capsule: ContextCapsuleV1
46
+ ): SearchExplain | undefined =>
47
+ (
48
+ capsule as ContextCapsuleV1 & {
49
+ [CONTEXT_CAPSULE_EXPLAIN_METADATA]?: SearchExplain;
50
+ }
51
+ )[CONTEXT_CAPSULE_EXPLAIN_METADATA];
52
+
39
53
  /** Build one strict Capsule through the shared compiler composition. */
40
54
  export const buildContextCapsule = async (
41
55
  input: ContextCapsuleBuildInput,
@@ -72,6 +86,7 @@ export const buildContextCapsule = async (
72
86
  limit: normalized.limit,
73
87
  candidateLimit: normalized.candidateLimit,
74
88
  temporalNow: now,
89
+ explain: deps.explain,
75
90
  limits: {
76
91
  requestedBytes: normalized.budgetBytes,
77
92
  requestedTokens: normalized.budgetTokens,
@@ -134,6 +149,17 @@ export const buildContextCapsule = async (
134
149
  traceResult.error.cause
135
150
  );
136
151
  }
152
+ if (plan.explain) {
153
+ Object.defineProperty(
154
+ plan.projection.value,
155
+ CONTEXT_CAPSULE_EXPLAIN_METADATA,
156
+ {
157
+ configurable: true,
158
+ enumerable: false,
159
+ value: plan.explain,
160
+ }
161
+ );
162
+ }
137
163
  return plan.projection.value;
138
164
  };
139
165
 
@@ -16,6 +16,7 @@ import { attachCitationTraceMetadata } from "../pipeline/trace-metadata";
16
16
  import { CITATION_TRACE_METADATA } from "../pipeline/types";
17
17
  import {
18
18
  buildContextCapsule,
19
+ getContextCapsuleExplain,
19
20
  verifyContextCapsuleRuntime,
20
21
  } from "./context-runtime";
21
22
  import { contextRuntimeConfigFingerprint } from "./context-runtime-contract";
@@ -199,7 +200,7 @@ export const buildVerifiedAsk = async (
199
200
  budgetBytes: options.contextBudgetBytes,
200
201
  depthPolicy: "balanced",
201
202
  },
202
- deps
203
+ { ...deps, explain: options.explain }
203
204
  );
204
205
  const freshness = await verifyContextCapsuleRuntime(capsule, deps);
205
206
  return synthesizeVerifiedAsk(query, options, capsule, freshness, deps);
@@ -258,6 +259,9 @@ export const synthesizeVerifiedAsk = async (
258
259
  citationEvidenceIds(verification, new Set(["supported", "contradicted"]))
259
260
  );
260
261
  await recordRetainedCitations(deps.traceSession, traceCitations);
262
+ const retrievalExplain = options.explain
263
+ ? getContextCapsuleExplain(capsule)
264
+ : undefined;
261
265
  return {
262
266
  query,
263
267
  mode: capsule.capabilities.semanticSearch ? "hybrid" : "bm25_only",
@@ -276,6 +280,7 @@ export const synthesizeVerifiedAsk = async (
276
280
  exclude: capsule.retrieval.request.exclude,
277
281
  answerGenerated: generated.ok,
278
282
  totalResults: capsule.evidence.length,
283
+ ...(retrievalExplain ? { explain: retrievalExplain } : {}),
279
284
  verificationRequested: true,
280
285
  abstained: verification.verification.abstained,
281
286
  },
@@ -48,7 +48,10 @@ export { formatAsk } from "./ask-format";
48
48
  // Types
49
49
  // ─────────────────────────────────────────────────────────────────────────────
50
50
 
51
- export type AskCommandOptions = Omit<AskOptions, "projectAffinity"> &
51
+ export type AskCommandOptions = Omit<
52
+ AskOptions,
53
+ "contentTypeRules" | "projectAffinity"
54
+ > &
52
55
  CliProjectAffinityRequest & {
53
56
  /** Override config path */
54
57
  configPath?: string;
@@ -318,6 +321,7 @@ export async function ask(
318
321
  noExpand: options.noExpand,
319
322
  noRerank: options.noRerank,
320
323
  candidateLimit: options.candidateLimit,
324
+ explain: options.explain,
321
325
  projectAffinity,
322
326
  traceSession,
323
327
  });
@@ -397,6 +401,9 @@ export async function ask(
397
401
  answerGenerated,
398
402
  totalResults: results.length,
399
403
  answerContext,
404
+ ...(options.explain && searchResult.value.meta.explain
405
+ ? { explain: searchResult.value.meta.explain }
406
+ : {}),
400
407
  },
401
408
  };
402
409
 
@@ -3,12 +3,8 @@
3
3
  */
4
4
 
5
5
  import { addCollection } from "../../../collection";
6
- import {
7
- loadConfig,
8
- pathExists,
9
- saveConfig,
10
- toAbsolutePath,
11
- } from "../../../config";
6
+ import { pathExists, toAbsolutePath } from "../../../config";
7
+ import { applyConfigFileChange } from "../../../core/config-mutation";
12
8
  import { CliError } from "../../errors";
13
9
 
14
10
  interface AddOptions {
@@ -18,6 +14,7 @@ interface AddOptions {
18
14
  include?: string;
19
15
  exclude?: string;
20
16
  update?: string;
17
+ configPath?: string;
21
18
  }
22
19
 
23
20
  export async function collectionAdd(
@@ -36,52 +33,49 @@ export async function collectionAdd(
36
33
  throw new CliError("VALIDATION", `Path does not exist: ${absolutePath}`);
37
34
  }
38
35
 
39
- // Load config
40
- const configResult = await loadConfig();
41
- if (!configResult.ok) {
42
- throw new CliError(
43
- "RUNTIME",
44
- `Failed to load config: ${configResult.error.message}`
45
- );
46
- }
47
-
48
- // Add collection using shared module
49
- const result = await addCollection(configResult.value, {
50
- path,
51
- name: options.name,
52
- pattern: options.pattern,
53
- include: options.include,
54
- exclude: options.exclude,
55
- models: options.embedModel
56
- ? {
57
- embed: options.embedModel,
58
- }
59
- : undefined,
60
- updateCmd: options.update,
61
- });
36
+ const mutation = await applyConfigFileChange(
37
+ { configPath: options.configPath },
38
+ async (config) => {
39
+ const result = await addCollection(config, {
40
+ path,
41
+ name: options.name!,
42
+ pattern: options.pattern,
43
+ include: options.include,
44
+ exclude: options.exclude,
45
+ models: options.embedModel
46
+ ? {
47
+ embed: options.embedModel,
48
+ }
49
+ : undefined,
50
+ updateCmd: options.update,
51
+ });
52
+ return result.ok
53
+ ? {
54
+ ok: true as const,
55
+ config: result.config,
56
+ value: result.collection,
57
+ }
58
+ : {
59
+ ok: false as const,
60
+ error: result.message,
61
+ code: result.code,
62
+ };
63
+ }
64
+ );
62
65
 
63
- if (!result.ok) {
66
+ if (!mutation.ok) {
64
67
  // Map collection error codes to CLI error codes
65
68
  const cliCode =
66
- result.code === "VALIDATION" ||
67
- result.code === "PATH_NOT_FOUND" ||
68
- result.code === "DUPLICATE"
69
+ mutation.code === "VALIDATION" ||
70
+ mutation.code === "PATH_NOT_FOUND" ||
71
+ mutation.code === "DUPLICATE"
69
72
  ? "VALIDATION"
70
73
  : "RUNTIME";
71
- throw new CliError(cliCode, result.message);
72
- }
73
-
74
- // Save config
75
- const saveResult = await saveConfig(result.config);
76
- if (!saveResult.ok) {
77
- throw new CliError(
78
- "RUNTIME",
79
- `Failed to save config: ${saveResult.error.message}`
80
- );
74
+ throw new CliError(cliCode, mutation.error);
81
75
  }
82
76
 
83
77
  process.stdout.write(
84
- `Collection "${result.collection.name}" added successfully\n`
78
+ `Collection "${mutation.value?.name}" added successfully\n`
85
79
  );
86
- process.stdout.write(`Path: ${result.collection.path}\n`);
80
+ process.stdout.write(`Path: ${mutation.value?.path}\n`);
87
81
  }
@@ -3,43 +3,43 @@
3
3
  */
4
4
 
5
5
  import { removeCollection } from "../../../collection";
6
- import { loadConfig, saveConfig } from "../../../config";
6
+ import { applyConfigFileChange } from "../../../core/config-mutation";
7
7
  import { CliError } from "../../errors";
8
8
 
9
- export async function collectionRemove(name: string): Promise<void> {
10
- // Load config
11
- const configResult = await loadConfig();
12
- if (!configResult.ok) {
13
- throw new CliError(
14
- "RUNTIME",
15
- `Failed to load config: ${configResult.error.message}`
16
- );
17
- }
18
-
19
- // Remove collection using shared module
20
- const result = removeCollection(configResult.value, { name });
9
+ export async function collectionRemove(
10
+ name: string,
11
+ options: { configPath?: string } = {}
12
+ ): Promise<void> {
13
+ const mutation = await applyConfigFileChange(
14
+ { configPath: options.configPath },
15
+ (config) => {
16
+ const result = removeCollection(config, { name });
17
+ return result.ok
18
+ ? {
19
+ ok: true as const,
20
+ config: result.config,
21
+ value: result.collection,
22
+ }
23
+ : {
24
+ ok: false as const,
25
+ error: result.message,
26
+ code: result.code,
27
+ };
28
+ }
29
+ );
21
30
 
22
- if (!result.ok) {
31
+ if (!mutation.ok) {
23
32
  // Map collection error codes to CLI error codes
24
33
  const cliCode =
25
- result.code === "VALIDATION" ||
26
- result.code === "NOT_FOUND" ||
27
- result.code === "HAS_REFERENCES"
34
+ mutation.code === "VALIDATION" ||
35
+ mutation.code === "NOT_FOUND" ||
36
+ mutation.code === "HAS_REFERENCES"
28
37
  ? "VALIDATION"
29
38
  : "RUNTIME";
30
- throw new CliError(cliCode, result.message);
31
- }
32
-
33
- // Save config
34
- const saveResult = await saveConfig(result.config);
35
- if (!saveResult.ok) {
36
- throw new CliError(
37
- "RUNTIME",
38
- `Failed to save config: ${saveResult.error.message}`
39
- );
39
+ throw new CliError(cliCode, mutation.error);
40
40
  }
41
41
 
42
42
  process.stdout.write(
43
- `Collection "${result.collection.name}" removed successfully\n`
43
+ `Collection "${mutation.value?.name}" removed successfully\n`
44
44
  );
45
45
  }