@gmickel/gno 1.37.1 → 1.39.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 (47) hide show
  1. package/assets/skill/README.md +2 -0
  2. package/browser-extension/artifacts/{gno-browser-clipper-v1.37.1.zip → gno-browser-clipper-v1.39.0.zip} +0 -0
  3. package/browser-extension/artifacts/gno-browser-clipper-v1.39.0.zip.sha256 +1 -0
  4. package/browser-extension/dist/manifest.json +1 -1
  5. package/package.json +1 -1
  6. package/spec/cli.md +243 -26
  7. package/spec/output-schemas/agents-mutation.schema.json +108 -0
  8. package/spec/output-schemas/agents-verify.schema.json +89 -0
  9. package/src/cli/commands/agents/block.ts +164 -0
  10. package/src/cli/commands/agents/commands.ts +413 -0
  11. package/src/cli/commands/agents/engine.ts +417 -0
  12. package/src/cli/commands/agents/harnesses.ts +298 -0
  13. package/src/cli/commands/agents/index.ts +35 -0
  14. package/src/cli/commands/cleanup.ts +8 -2
  15. package/src/cli/commands/collection/clear-embeddings.ts +6 -1
  16. package/src/cli/commands/completion/scripts.ts +5 -0
  17. package/src/cli/commands/doctor-activation.ts +5 -1
  18. package/src/cli/commands/doctor.ts +72 -2
  19. package/src/cli/commands/embed.ts +227 -194
  20. package/src/cli/commands/index-cmd.ts +74 -50
  21. package/src/cli/commands/init.ts +5 -1
  22. package/src/cli/commands/profile-apply.ts +5 -1
  23. package/src/cli/commands/setup-activation.ts +2 -1
  24. package/src/cli/commands/setup.ts +2 -1
  25. package/src/cli/commands/shared.ts +5 -1
  26. package/src/cli/commands/status.ts +5 -1
  27. package/src/cli/commands/tags.ts +18 -3
  28. package/src/cli/commands/update.ts +34 -27
  29. package/src/cli/commands/vec.ts +13 -4
  30. package/src/cli/errors.ts +3 -2
  31. package/src/cli/program.ts +449 -194
  32. package/src/config/defaults.ts +2 -0
  33. package/src/config/index.ts +3 -0
  34. package/src/config/types.ts +32 -1
  35. package/src/core/file-lock.ts +16 -4
  36. package/src/core/write-lease.ts +354 -0
  37. package/src/embed/backlog.ts +9 -1
  38. package/src/embed/retry.ts +116 -3
  39. package/src/sdk/client.ts +3 -1
  40. package/src/sdk/embed.ts +8 -3
  41. package/src/sdk/types.ts +2 -0
  42. package/src/serve/embed-scheduler.ts +8 -0
  43. package/src/serve/resident-runtime.ts +5 -1
  44. package/src/store/sqlite/adapter.ts +28 -4
  45. package/src/store/sqlite/scoped-index.ts +5 -1
  46. package/src/store/vector/sqlite-vec.ts +2 -1
  47. package/browser-extension/artifacts/gno-browser-clipper-v1.37.1.zip.sha256 +0 -1
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Harness matrix for `gno agents`: which global (user-scope) instruction
3
+ * file each supported harness reads, how the harness is detected, and which
4
+ * import chains make a separate install redundant.
5
+ *
6
+ * Discovery is standard documented locations only — nonstandard/multi-
7
+ * instance layouts are served by the explicit `--extra-dir` flag, never by
8
+ * guessed discovery.
9
+ *
10
+ * @module src/cli/commands/agents/harnesses
11
+ */
12
+
13
+ // node:fs: detection needs synchronous stat/realpath (symlink-aware identity)
14
+ // with no Bun equivalent.
15
+ import { existsSync, realpathSync, statSync } from "node:fs";
16
+ import { homedir } from "node:os";
17
+ import {
18
+ basename,
19
+ dirname,
20
+ isAbsolute,
21
+ join,
22
+ normalize,
23
+ resolve,
24
+ } from "node:path";
25
+
26
+ import { CliError } from "../../errors.js";
27
+
28
+ // ─────────────────────────────────────────────────────────────────────────────
29
+ // Environment Variables
30
+ // ─────────────────────────────────────────────────────────────────────────────
31
+
32
+ /** Override home dir for testing / sandboxed live verification. */
33
+ export const ENV_AGENTS_HOME_OVERRIDE = "GNO_AGENTS_HOME_OVERRIDE";
34
+
35
+ // ─────────────────────────────────────────────────────────────────────────────
36
+ // Types
37
+ // ─────────────────────────────────────────────────────────────────────────────
38
+
39
+ export type HarnessId =
40
+ | "claude"
41
+ | "codex"
42
+ | "cursor"
43
+ | "opencode"
44
+ | "grok"
45
+ | "hermes"
46
+ | "openclaw";
47
+
48
+ export const HARNESS_IDS: HarnessId[] = [
49
+ "claude",
50
+ "codex",
51
+ "cursor",
52
+ "opencode",
53
+ "grok",
54
+ "hermes",
55
+ "openclaw",
56
+ ];
57
+
58
+ interface HarnessDef {
59
+ id: HarnessId;
60
+ label: string;
61
+ /** Harness config dir (detection root), relative to home. */
62
+ configDir: string;
63
+ /** Instruction file: inside the config dir, or directly under home. */
64
+ instructionFile: { dir: "config" | "home"; name: string };
65
+ /**
66
+ * Env var naming the harness's documented config-dir override
67
+ * (honored only when no explicit homeDir override is active).
68
+ */
69
+ configDirEnvVar?: string;
70
+ /**
71
+ * Import chain: this harness reads another harness's instruction file, so
72
+ * a separate install would double the block. Data-driven so future chains
73
+ * are matrix entries, not code changes.
74
+ */
75
+ coveredBy?: HarnessId;
76
+ }
77
+
78
+ const HARNESS_DEFS: Record<HarnessId, HarnessDef> = {
79
+ claude: {
80
+ id: "claude",
81
+ label: "Claude Code",
82
+ configDir: ".claude",
83
+ instructionFile: { dir: "config", name: "CLAUDE.md" },
84
+ configDirEnvVar: "CLAUDE_CONFIG_DIR",
85
+ },
86
+ codex: {
87
+ id: "codex",
88
+ label: "Codex",
89
+ configDir: ".codex",
90
+ instructionFile: { dir: "config", name: "AGENTS.md" },
91
+ configDirEnvVar: "CODEX_HOME",
92
+ },
93
+ cursor: {
94
+ id: "cursor",
95
+ label: "Cursor Agent",
96
+ configDir: ".cursor",
97
+ // Cursor Agent discovers AGENTS.md walking from the working directory
98
+ // towards the home directory; ~/AGENTS.md is its user-global surface.
99
+ instructionFile: { dir: "home", name: "AGENTS.md" },
100
+ },
101
+ opencode: {
102
+ id: "opencode",
103
+ label: "OpenCode",
104
+ configDir: ".config/opencode",
105
+ instructionFile: { dir: "config", name: "AGENTS.md" },
106
+ },
107
+ grok: {
108
+ id: "grok",
109
+ label: "Grok Build",
110
+ configDir: ".grok",
111
+ // Grok imports the Claude global instruction file — no file of its own;
112
+ // `coveredBy` makes resolution report Claude's file for it.
113
+ instructionFile: { dir: "config", name: "AGENTS.md" },
114
+ coveredBy: "claude",
115
+ },
116
+ hermes: {
117
+ id: "hermes",
118
+ label: "Hermes",
119
+ configDir: ".hermes",
120
+ instructionFile: { dir: "config", name: "SOUL.md" },
121
+ },
122
+ openclaw: {
123
+ id: "openclaw",
124
+ label: "OpenClaw",
125
+ configDir: ".openclaw/workspace",
126
+ instructionFile: { dir: "config", name: "AGENTS.md" },
127
+ },
128
+ };
129
+
130
+ export interface ResolvedTarget {
131
+ /** Harness id, or "extra-dir" for explicit --extra-dir paths. */
132
+ id: HarnessId | "extra-dir";
133
+ label: string;
134
+ /** Detection root (harness config dir, or the extra dir itself). */
135
+ configDir: string;
136
+ /** Absolute path to the instruction file the harness reads. */
137
+ file: string;
138
+ /** Real (symlink-resolved) identity of the file, for dedupe + writes. */
139
+ realFile: string;
140
+ /** Whether the harness is detected on this machine. */
141
+ detected: boolean;
142
+ /** Import chain target this harness is covered by, when applicable. */
143
+ coveredBy?: HarnessId;
144
+ }
145
+
146
+ export interface ResolveOptions {
147
+ /** Explicit home override (testing / sandboxed verification). When set,
148
+ * harness config-dir env overrides are ignored for determinism. */
149
+ homeDir?: string;
150
+ /** Explicit extra instruction dirs (nonstandard/multi-instance layouts). */
151
+ extraDirs?: string[];
152
+ }
153
+
154
+ // ─────────────────────────────────────────────────────────────────────────────
155
+ // Resolution
156
+ // ─────────────────────────────────────────────────────────────────────────────
157
+
158
+ /**
159
+ * Symlink-resolved identity of an instruction file. An existing file resolves
160
+ * fully (so a canonical file linked into several harnesses is written once,
161
+ * through the link); a file that does not exist yet resolves its parent dir,
162
+ * so two harnesses whose config dirs are links to one place still share an
163
+ * identity. Anything unresolvable falls back to the normalized path.
164
+ */
165
+ export function realIdentity(file: string): string {
166
+ try {
167
+ return realpathSync(file);
168
+ } catch {
169
+ const normalized = normalize(file);
170
+ try {
171
+ return join(realpathSync(dirname(normalized)), basename(normalized));
172
+ } catch {
173
+ return normalized;
174
+ }
175
+ }
176
+ }
177
+
178
+ function isDirectory(path: string): boolean {
179
+ try {
180
+ return statSync(path).isDirectory();
181
+ } catch {
182
+ return false;
183
+ }
184
+ }
185
+
186
+ function resolveHarness(
187
+ def: HarnessDef,
188
+ home: string,
189
+ explicitHome: boolean
190
+ ): ResolvedTarget {
191
+ let configDir = join(home, def.configDir);
192
+
193
+ if (!explicitHome && def.configDirEnvVar) {
194
+ const envOverride = process.env[def.configDirEnvVar];
195
+ if (envOverride) {
196
+ if (!isAbsolute(envOverride)) {
197
+ throw new CliError(
198
+ "VALIDATION",
199
+ `${def.configDirEnvVar} must be an absolute path`
200
+ );
201
+ }
202
+ configDir = normalize(envOverride);
203
+ }
204
+ }
205
+
206
+ // A covered harness (grok → claude) reads its covering harness's file and
207
+ // has none of its own, so that is the file its rows report.
208
+ const file = def.coveredBy
209
+ ? resolveHarness(HARNESS_DEFS[def.coveredBy], home, explicitHome).file
210
+ : def.instructionFile.dir === "home"
211
+ ? join(home, def.instructionFile.name)
212
+ : join(configDir, def.instructionFile.name);
213
+
214
+ return {
215
+ id: def.id,
216
+ label: def.label,
217
+ configDir,
218
+ file,
219
+ realFile: realIdentity(file),
220
+ detected: isDirectory(configDir),
221
+ coveredBy: def.coveredBy,
222
+ };
223
+ }
224
+
225
+ /** Instruction file candidates for --extra-dir, in priority order. */
226
+ const EXTRA_DIR_FILE_CANDIDATES = ["CLAUDE.md", "AGENTS.md", "SOUL.md"];
227
+ const DEFAULT_EXTRA_DIR_FILE = "AGENTS.md";
228
+
229
+ function resolveExtraDir(dir: string): ResolvedTarget {
230
+ const abs = resolve(dir);
231
+ if (!isDirectory(abs)) {
232
+ throw new CliError(
233
+ "VALIDATION",
234
+ `--extra-dir ${dir} does not exist or is not a directory. The installer never fabricates harness directories.`
235
+ );
236
+ }
237
+ const existing = EXTRA_DIR_FILE_CANDIDATES.find((name) =>
238
+ existsSync(join(abs, name))
239
+ );
240
+ const file = join(abs, existing ?? DEFAULT_EXTRA_DIR_FILE);
241
+ return {
242
+ id: "extra-dir",
243
+ label: `extra dir ${abs}`,
244
+ configDir: abs,
245
+ file,
246
+ realFile: realIdentity(file),
247
+ detected: true,
248
+ };
249
+ }
250
+
251
+ /** Covering chain for an explicit target (e.g. grok → claude), covering first. */
252
+ function coveringChain(id: HarnessId): HarnessId[] {
253
+ const chain: HarnessId[] = [];
254
+ let cursor = HARNESS_DEFS[id].coveredBy;
255
+ while (cursor && !chain.includes(cursor) && cursor !== id) {
256
+ chain.unshift(cursor);
257
+ cursor = HARNESS_DEFS[cursor].coveredBy;
258
+ }
259
+ return chain;
260
+ }
261
+
262
+ /**
263
+ * Resolve all requested targets to concrete instruction files.
264
+ * `target: "all"` = every supported harness (detection filters at plan time).
265
+ * An explicit covered target (e.g. `grok`) also resolves its covering
266
+ * target(s) so the file it actually reads is planned/verified — but only when
267
+ * the requested target itself is detected; an absent one is reported
268
+ * `not-detected` without touching the covering harness's file.
269
+ */
270
+ export function resolveTargets(
271
+ target: HarnessId | "all",
272
+ opts: ResolveOptions = {}
273
+ ): ResolvedTarget[] {
274
+ // Any home override (option or env) suppresses harness config-dir env
275
+ // overrides too — an overridden home is an isolation request.
276
+ const explicitHome =
277
+ opts.homeDir !== undefined ||
278
+ process.env[ENV_AGENTS_HOME_OVERRIDE] !== undefined;
279
+ const home =
280
+ opts.homeDir ?? process.env[ENV_AGENTS_HOME_OVERRIDE] ?? homedir();
281
+ const one = (id: HarnessId): ResolvedTarget =>
282
+ resolveHarness(HARNESS_DEFS[id], home, explicitHome);
283
+
284
+ let results: ResolvedTarget[];
285
+ if (target === "all") {
286
+ results = HARNESS_IDS.map(one);
287
+ } else {
288
+ const leaf = one(target);
289
+ results = leaf.detected
290
+ ? [...coveringChain(target).map(one), leaf]
291
+ : [leaf];
292
+ }
293
+
294
+ for (const dir of opts.extraDirs ?? []) {
295
+ results.push(resolveExtraDir(dir));
296
+ }
297
+ return results;
298
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Agents command exports.
3
+ *
4
+ * @module src/cli/commands/agents
5
+ */
6
+
7
+ export {
8
+ BEGIN_MARKER,
9
+ BLOCK_VERSION,
10
+ END_MARKER,
11
+ extractBlock,
12
+ hashBlockBody,
13
+ renderBlock,
14
+ renderBlockBody,
15
+ } from "./block.js";
16
+ export {
17
+ type AgentsOptions,
18
+ installAgents,
19
+ parseTargetOption,
20
+ uninstallAgents,
21
+ verifyAgents,
22
+ } from "./commands.js";
23
+ export {
24
+ applyPlan,
25
+ planTargets,
26
+ type TargetPlan,
27
+ unifiedDiff,
28
+ } from "./engine.js";
29
+ export {
30
+ ENV_AGENTS_HOME_OVERRIDE,
31
+ type HarnessId,
32
+ HARNESS_IDS,
33
+ type ResolvedTarget,
34
+ resolveTargets,
35
+ } from "./harnesses.js";
@@ -17,6 +17,8 @@ import { SqliteAdapter } from "../../store/sqlite/adapter";
17
17
  export interface CleanupOptions {
18
18
  /** Override config path */
19
19
  configPath?: string;
20
+ /** Index name */
21
+ indexName?: string;
20
22
  }
21
23
 
22
24
  /**
@@ -47,9 +49,13 @@ export async function cleanup(
47
49
 
48
50
  // Open database
49
51
  const store = new SqliteAdapter();
50
- const dbPath = getIndexDbPath();
52
+ const dbPath = getIndexDbPath(options.indexName);
51
53
 
52
- const openResult = await store.open(dbPath, config.ftsTokenizer);
54
+ const openResult = await store.open(
55
+ dbPath,
56
+ config.ftsTokenizer,
57
+ config.busyTimeoutMs
58
+ );
53
59
  if (!openResult.ok) {
54
60
  return { success: false, error: openResult.error.message };
55
61
  }
@@ -10,6 +10,7 @@ import { CliError } from "../../errors";
10
10
 
11
11
  interface ClearEmbeddingsOptions {
12
12
  all?: boolean;
13
+ indexName?: string;
13
14
  json?: boolean;
14
15
  }
15
16
 
@@ -39,7 +40,11 @@ export async function collectionClearEmbeddings(
39
40
  }
40
41
 
41
42
  const store = new SqliteAdapter();
42
- const openResult = await store.open(getIndexDbPath(), config.ftsTokenizer);
43
+ const openResult = await store.open(
44
+ getIndexDbPath(options.indexName),
45
+ config.ftsTokenizer,
46
+ config.busyTimeoutMs
47
+ );
43
48
  if (!openResult.ok) {
44
49
  throw new CliError("RUNTIME", openResult.error.message);
45
50
  }
@@ -57,6 +57,11 @@ const COMMANDS = [
57
57
  "skill uninstall",
58
58
  "skill show",
59
59
  "skill paths",
60
+ "agents",
61
+ "agents install",
62
+ "agents update",
63
+ "agents verify",
64
+ "agents uninstall",
60
65
  "completion",
61
66
  "completion output",
62
67
  "completion install",
@@ -43,7 +43,11 @@ export async function buildDoctorActivation(
43
43
 
44
44
  const store = new SqliteAdapter();
45
45
  store.setConfigPath(options.configPath ?? "");
46
- const opened = await store.open(dbPath, config.ftsTokenizer);
46
+ const opened = await store.open(
47
+ dbPath,
48
+ config.ftsTokenizer,
49
+ config.busyTimeoutMs
50
+ );
47
51
  if (!opened.ok) {
48
52
  return unavailableActivation(config);
49
53
  }
@@ -14,7 +14,12 @@ import type { Config } from "../../config/types";
14
14
  import type { ActivationStatus } from "../../core/activation-status";
15
15
 
16
16
  import { getIndexDbPath, getModelsCachePath } from "../../app/constants";
17
- import { getConfigPaths, isInitialized, loadConfig } from "../../config";
17
+ import {
18
+ DEFAULT_BUSY_TIMEOUT_MS,
19
+ getConfigPaths,
20
+ isInitialized,
21
+ loadConfig,
22
+ } from "../../config";
18
23
  import { isConnectorActivationComplete } from "../../core/activation-connector-health";
19
24
  import { getCodeChunkingStatus } from "../../ingestion/chunker";
20
25
  import { ModelCache } from "../../llm/cache";
@@ -140,6 +145,64 @@ async function checkDatabase(indexName?: string): Promise<DoctorCheck> {
140
145
  }
141
146
  }
142
147
 
148
+ /**
149
+ * Report the live SQLite busy_timeout (PRAGMA), not the config echo.
150
+ */
151
+ async function checkBusyTimeout(
152
+ config: Config,
153
+ indexName?: string
154
+ ): Promise<DoctorCheck> {
155
+ const dbPath = getIndexDbPath(indexName);
156
+
157
+ try {
158
+ await stat(dbPath);
159
+ } catch {
160
+ return {
161
+ name: "busy-timeout",
162
+ status: "warn",
163
+ message: "Database not found. Run: gno init",
164
+ };
165
+ }
166
+
167
+ const store = new SqliteAdapter();
168
+ const paths = getConfigPaths();
169
+ store.setConfigPath(paths.configFile);
170
+
171
+ const openResult = await store.open(
172
+ dbPath,
173
+ config.ftsTokenizer,
174
+ config.busyTimeoutMs ?? DEFAULT_BUSY_TIMEOUT_MS
175
+ );
176
+ if (!openResult.ok) {
177
+ return {
178
+ name: "busy-timeout",
179
+ status: "warn",
180
+ message: `busy_timeout unavailable: ${openResult.error.message}`,
181
+ };
182
+ }
183
+
184
+ try {
185
+ const timeout = store
186
+ .getRawDb()
187
+ .query<{ timeout: number }, []>("PRAGMA busy_timeout")
188
+ .get()?.timeout;
189
+ if (timeout === undefined) {
190
+ return {
191
+ name: "busy-timeout",
192
+ status: "warn",
193
+ message: "PRAGMA busy_timeout returned no value",
194
+ };
195
+ }
196
+ return {
197
+ name: "busy-timeout",
198
+ status: "ok",
199
+ message: `busy_timeout ${timeout}ms`,
200
+ };
201
+ } finally {
202
+ await store.close();
203
+ }
204
+ }
205
+
143
206
  async function checkModels(config: Config): Promise<DoctorCheck[]> {
144
207
  const checks: DoctorCheck[] = [];
145
208
  const cache = new ModelCache(getModelsCachePath());
@@ -209,7 +272,11 @@ async function checkEmbeddingFingerprints(
209
272
  const paths = getConfigPaths();
210
273
  store.setConfigPath(paths.configFile);
211
274
 
212
- const openResult = await store.open(dbPath, config.ftsTokenizer);
275
+ const openResult = await store.open(
276
+ dbPath,
277
+ config.ftsTokenizer,
278
+ config.busyTimeoutMs ?? DEFAULT_BUSY_TIMEOUT_MS
279
+ );
213
280
  if (!openResult.ok) {
214
281
  return {
215
282
  name: "embedding-fingerprint",
@@ -527,6 +594,9 @@ export async function doctor(
527
594
  const sqliteChecks = await checkSqliteExtensions();
528
595
  checks.push(...sqliteChecks);
529
596
 
597
+ // Live busy_timeout from the open index (not the config echo)
598
+ checks.push(await checkBusyTimeout(config, options.indexName));
599
+
530
600
  // Code chunking capability
531
601
  checks.push(checkCodeChunking());
532
602