@gmickel/gno 2.4.0 → 2.6.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 (136) hide show
  1. package/README.md +69 -6
  2. package/assets/skill/README.md +5 -1
  3. package/assets/skill/SKILL.md +98 -1
  4. package/assets/skill/cli-reference.md +140 -0
  5. package/assets/skill/examples.md +45 -0
  6. package/assets/skill/mcp-reference.md +62 -0
  7. package/assets/skill/recipes/capture-and-file.md +6 -0
  8. package/assets/skill/recipes/memory-file-decision.md +6 -0
  9. package/assets/skill/recipes/memory-supersede-fact.md +5 -0
  10. package/assets/skill/recipes/session-evidence-lookup.md +98 -0
  11. package/assets/spa-production.json.gz +0 -0
  12. package/browser-extension/artifacts/{gno-browser-clipper-v2.4.0.zip → gno-browser-clipper-v2.6.0.zip} +0 -0
  13. package/browser-extension/artifacts/gno-browser-clipper-v2.6.0.zip.sha256 +1 -0
  14. package/browser-extension/dist/manifest.json +1 -1
  15. package/package.json +2 -1
  16. package/spec/cli.md +358 -24
  17. package/spec/compiled-context.md +68 -0
  18. package/spec/mcp.md +223 -2
  19. package/spec/output-schemas/capture-receipt.schema.json +3 -0
  20. package/spec/output-schemas/compiled-context-check.schema.json +44 -0
  21. package/spec/output-schemas/compiled-context-file.schema.json +165 -0
  22. package/spec/output-schemas/compiled-context-preview.schema.json +142 -0
  23. package/spec/output-schemas/mcp-capture-result.schema.json +3 -0
  24. package/spec/output-schemas/memory-remember.schema.json +8 -2
  25. package/spec/output-schemas/request-status.schema.json +113 -0
  26. package/spec/output-schemas/sessions-automation-run.schema.json +46 -0
  27. package/spec/output-schemas/sessions-discovery.schema.json +38 -0
  28. package/spec/output-schemas/sessions-import-receipt.schema.json +156 -0
  29. package/spec/output-schemas/sessions-status.schema.json +432 -0
  30. package/src/app/compiled-context-files.ts +361 -0
  31. package/src/app/compiled-context.ts +240 -0
  32. package/src/app/context-surface.ts +7 -2
  33. package/src/cli/commands/ask.ts +14 -2
  34. package/src/cli/commands/audit.ts +4 -0
  35. package/src/cli/commands/capture.ts +55 -96
  36. package/src/cli/commands/context-compiled.ts +136 -0
  37. package/src/cli/commands/daemon.ts +41 -0
  38. package/src/cli/commands/ls.ts +3 -0
  39. package/src/cli/commands/memory.ts +12 -3
  40. package/src/cli/commands/request-status.ts +59 -0
  41. package/src/cli/commands/reset.ts +39 -5
  42. package/src/cli/commands/sessions.ts +713 -0
  43. package/src/cli/commands/shared.ts +14 -1
  44. package/src/cli/errors.ts +10 -3
  45. package/src/cli/program.ts +458 -1
  46. package/src/cli/session-binding.ts +49 -0
  47. package/src/config/types.ts +8 -0
  48. package/src/core/capture-publish.ts +239 -0
  49. package/src/core/capture-sync.ts +3 -0
  50. package/src/core/compiled-context.ts +254 -0
  51. package/src/core/context-budget.ts +2 -10
  52. package/src/core/file-lock.ts +22 -5
  53. package/src/core/folder-setup-planning.ts +2 -1
  54. package/src/core/memory-remember.ts +233 -122
  55. package/src/core/memory-types.ts +11 -0
  56. package/src/core/network-boundary-inventory.ts +16 -0
  57. package/src/core/request-receipts.ts +671 -0
  58. package/src/core/setup-receipt.ts +27 -20
  59. package/src/core/typed-metadata.ts +4 -0
  60. package/src/core/validation.ts +9 -2
  61. package/src/core/windows-private-path.ts +96 -0
  62. package/src/index.ts +11 -2
  63. package/src/ingestion/compiled-context.ts +15 -0
  64. package/src/ingestion/sync.ts +40 -8
  65. package/src/ingestion/walker.ts +5 -4
  66. package/src/llm/nodeLlamaCpp/simulator-install.ts +6 -2
  67. package/src/mcp/context.ts +8 -0
  68. package/src/mcp/http-egress.ts +15 -3
  69. package/src/mcp/http-transport.ts +2 -0
  70. package/src/mcp/retrieval-warnings.ts +24 -0
  71. package/src/mcp/tools/ask.ts +14 -1
  72. package/src/mcp/tools/capture.ts +87 -83
  73. package/src/mcp/tools/context.ts +46 -2
  74. package/src/mcp/tools/index.ts +119 -7
  75. package/src/mcp/tools/memory-remember.ts +7 -0
  76. package/src/mcp/tools/memory-shared.ts +7 -1
  77. package/src/mcp/tools/query.ts +3 -1
  78. package/src/mcp/tools/request-status.ts +73 -0
  79. package/src/mcp/tools/search.ts +3 -1
  80. package/src/mcp/tools/sessions.ts +208 -0
  81. package/src/mcp/tools/vsearch.ts +3 -1
  82. package/src/sdk/client.ts +252 -89
  83. package/src/sdk/index.ts +13 -0
  84. package/src/sdk/types.ts +82 -3
  85. package/src/serve/capture-service.ts +98 -32
  86. package/src/serve/compiled-context.ts +84 -0
  87. package/src/serve/config-sync.ts +3 -2
  88. package/src/serve/public/app.tsx +15 -1
  89. package/src/serve/public/components/CaptureModal.tsx +26 -8
  90. package/src/serve/public/components/sessions/AutomationPanel.tsx +800 -0
  91. package/src/serve/public/components/sessions/ImportReceipt.tsx +238 -0
  92. package/src/serve/public/components/sessions/SessionSearch.tsx +286 -0
  93. package/src/serve/public/components/sessions/SourcesPanel.tsx +541 -0
  94. package/src/serve/public/components/sessions/api.ts +40 -0
  95. package/src/serve/public/components/sessions/snippet.tsx +53 -0
  96. package/src/serve/public/globals.built.css +1 -1
  97. package/src/serve/public/hooks/use-api.ts +10 -2
  98. package/src/serve/public/lib/request-intent.ts +69 -0
  99. package/src/serve/public/lib/workspace-actions.ts +12 -1
  100. package/src/serve/public/lib/workspace-tabs.ts +4 -0
  101. package/src/serve/public/pages/CompiledContext.tsx +364 -0
  102. package/src/serve/public/pages/Dashboard.tsx +17 -0
  103. package/src/serve/public/pages/DocView.tsx +15 -1
  104. package/src/serve/public/pages/DocumentEditor.tsx +139 -96
  105. package/src/serve/public/pages/Sessions.tsx +350 -0
  106. package/src/serve/resident-runtime.ts +43 -3
  107. package/src/serve/routes/api.ts +476 -147
  108. package/src/serve/routes/sessions.ts +766 -0
  109. package/src/serve/security.ts +9 -0
  110. package/src/serve/server.ts +248 -3
  111. package/src/serve/session-automation.ts +146 -0
  112. package/src/serve/spa-production-build.ts +6 -5
  113. package/src/sessions/archive.ts +348 -0
  114. package/src/sessions/automation-state.ts +444 -0
  115. package/src/sessions/automation-status.ts +239 -0
  116. package/src/sessions/automation.ts +1169 -0
  117. package/src/sessions/binding.ts +105 -0
  118. package/src/sessions/claude-hook.ts +240 -0
  119. package/src/sessions/config.ts +176 -0
  120. package/src/sessions/format.ts +191 -0
  121. package/src/sessions/import-child-env.ts +8 -0
  122. package/src/sessions/import-child.ts +152 -0
  123. package/src/sessions/parsers/claude-code.ts +259 -0
  124. package/src/sessions/parsers/codex.ts +303 -0
  125. package/src/sessions/parsers/hermes.ts +248 -0
  126. package/src/sessions/parsers/openclaw.ts +496 -0
  127. package/src/sessions/parsers/shared.ts +184 -0
  128. package/src/sessions/sanitize.ts +222 -0
  129. package/src/sessions/service.ts +1533 -0
  130. package/src/sessions/setup.ts +477 -0
  131. package/src/sessions/sources.ts +518 -0
  132. package/src/sessions/state.ts +118 -0
  133. package/src/sessions/types.ts +457 -0
  134. package/src/store/sqlite/adapter.ts +68 -16
  135. package/src/store/sqlite/scoped-index.ts +9 -0
  136. package/browser-extension/artifacts/gno-browser-clipper-v2.4.0.zip.sha256 +0 -1
@@ -9,7 +9,11 @@ import type { Collection, Config } from "../../config/types";
9
9
  import type { SyncResult } from "../../ingestion";
10
10
  import type { SearchResults } from "../../pipeline/types";
11
11
 
12
- import { decorateUriForIndex, getIndexDbPath } from "../../app/constants";
12
+ import {
13
+ DEFAULT_INDEX_NAME,
14
+ decorateUriForIndex,
15
+ getIndexDbPath,
16
+ } from "../../app/constants";
13
17
  import {
14
18
  getConfigPaths,
15
19
  isInitialized,
@@ -17,6 +21,7 @@ import {
17
21
  writeConfigWarningsToStderr,
18
22
  } from "../../config";
19
23
  import { SqliteAdapter } from "../../store/sqlite/adapter";
24
+ import { assertCliSessionBinding } from "../session-binding";
20
25
 
21
26
  /**
22
27
  * Result of CLI store initialization.
@@ -90,6 +95,14 @@ export async function initStore(
90
95
  };
91
96
  }
92
97
 
98
+ // Every index a command opens (including one named by a `?index=` URI)
99
+ // honours the session-archive config/index binding.
100
+ await assertCliSessionBinding(
101
+ options.configPath,
102
+ options.indexName ?? DEFAULT_INDEX_NAME,
103
+ config
104
+ );
105
+
93
106
  // Ensure data directory exists (may have been deleted by reset)
94
107
  const { ensureDirectories } = await import("../../config");
95
108
  await ensureDirectories();
package/src/cli/errors.ts CHANGED
@@ -15,7 +15,9 @@ export type CliErrorCode =
15
15
  | "NOT_RUNNING"
16
16
  | "BUSY"
17
17
  | "AUDIT_FINDINGS"
18
- | "AUDIT_PARTIAL";
18
+ | "AUDIT_PARTIAL"
19
+ | "CONTEXT_STALE"
20
+ | "CONTEXT_CONFLICT";
19
21
 
20
22
  export interface CliErrorOptions {
21
23
  details?: Record<string, unknown>;
@@ -72,10 +74,15 @@ export function exitCodeFor(err: CliError): 1 | 2 | 3 | 4 | 5 {
72
74
  if (err.code === "VALIDATION") {
73
75
  return 1;
74
76
  }
75
- if (err.code === "NOT_RUNNING") {
77
+ if (err.code === "NOT_RUNNING" || err.code === "CONTEXT_STALE") {
76
78
  return 3;
77
79
  }
78
- if (err.code === "BUSY" || err.code === "AUDIT_FINDINGS") return 4;
80
+ if (
81
+ err.code === "BUSY" ||
82
+ err.code === "AUDIT_FINDINGS" ||
83
+ err.code === "CONTEXT_CONFLICT"
84
+ )
85
+ return 4;
79
86
  if (err.code === "AUDIT_PARTIAL") return 5;
80
87
  return 2;
81
88
  }
@@ -344,7 +344,7 @@ export function createProgram(): Command {
344
344
  .option("--no-pager", "disable automatic paging of long output");
345
345
 
346
346
  // Resolve globals ONCE before any command runs (ensures consistency)
347
- program.hook("preAction", (thisCommand) => {
347
+ program.hook("preAction", async (thisCommand) => {
348
348
  const rootOpts = thisCommand.optsWithGlobals();
349
349
  const globals = parseGlobalOptions(rootOpts);
350
350
  if (!isValidIndexName(globals.index)) {
@@ -355,6 +355,8 @@ export function createProgram(): Command {
355
355
  }
356
356
  applyGlobalOptions(globals);
357
357
  globalState.current = globals;
358
+ const { assertCliSessionBinding } = await import("./session-binding");
359
+ await assertCliSessionBinding(globals.config, globals.index);
358
360
  });
359
361
 
360
362
  // Wire command groups
@@ -362,6 +364,7 @@ export function createProgram(): Command {
362
364
  wireOnboardingCommands(program);
363
365
  wireCaptureCommand(program);
364
366
  wireMemoryCommands(program);
367
+ wireSessionsCommands(program);
365
368
  wireManagementCommands(program);
366
369
  wireTraceCommands(program);
367
370
  wirePublishCommand(program);
@@ -1874,6 +1877,10 @@ function wireCaptureCommand(program: Command): void {
1874
1877
  .option("--source-author <author>", "source author")
1875
1878
  .option("--source-date <date>", "source observed date/time")
1876
1879
  .option("--source-id <id>", "source external id")
1880
+ .option(
1881
+ "--request-id <id>",
1882
+ "retry identity: reuse it to retry this same capture after a lost response"
1883
+ )
1877
1884
  .option("--json", "JSON output")
1878
1885
  .action(
1879
1886
  async (contentParts: string[], cmdOpts: Record<string, unknown>) => {
@@ -1902,6 +1909,7 @@ function wireCaptureCommand(program: Command): void {
1902
1909
  sourceAuthor: cmdOpts.sourceAuthor as string | undefined,
1903
1910
  sourceDate: cmdOpts.sourceDate as string | undefined,
1904
1911
  sourceId: cmdOpts.sourceId as string | undefined,
1912
+ requestId: cmdOpts.requestId as string | undefined,
1905
1913
  });
1906
1914
  const output = formatCaptureReceipt(receipt, {
1907
1915
  json: format === "json",
@@ -1910,6 +1918,27 @@ function wireCaptureCommand(program: Command): void {
1910
1918
  await writeOutput(output, format);
1911
1919
  }
1912
1920
  );
1921
+
1922
+ program
1923
+ .command("request-status <request-id>")
1924
+ .description(
1925
+ "Show whether a capture/remember request ID is pending, committed, or unknown"
1926
+ )
1927
+ .option("--json", "JSON output")
1928
+ .action(async (requestId: string, cmdOpts: Record<string, unknown>) => {
1929
+ const format = getFormat(cmdOpts);
1930
+ const globals = getGlobals();
1931
+ const { formatRequestStatusOutput, requestStatus } =
1932
+ await import("./commands/request-status");
1933
+ const result = await requestStatus({
1934
+ requestId,
1935
+ indexName: globals.index,
1936
+ });
1937
+ await writeOutput(
1938
+ formatRequestStatusOutput(result, { json: format === "json" }),
1939
+ format
1940
+ );
1941
+ });
1913
1942
  }
1914
1943
 
1915
1944
  // ─────────────────────────────────────────────────────────────────────────────
@@ -1957,6 +1986,10 @@ function wireMemoryCommands(program: Command): void {
1957
1986
  "--session <id>",
1958
1987
  "session identity (default: $GNO_MEMORY_SESSION or ppid:<pid>)"
1959
1988
  )
1989
+ .option(
1990
+ "--request-id <id>",
1991
+ "retry identity: reuse it to retry this same write after a lost response"
1992
+ )
1960
1993
  .option("--json", "JSON output")
1961
1994
  .action(async (text: string, cmdOpts: Record<string, unknown>) => {
1962
1995
  const format = getFormat(cmdOpts);
@@ -1979,6 +2012,7 @@ function wireMemoryCommands(program: Command): void {
1979
2012
  source: cmdOpts.source as string | undefined,
1980
2013
  caller: cmdOpts.caller as string | undefined,
1981
2014
  session: cmdOpts.session as string | undefined,
2015
+ requestId: cmdOpts.requestId as string | undefined,
1982
2016
  });
1983
2017
  await writeOutput(
1984
2018
  formatRememberResult(result, {
@@ -2038,6 +2072,358 @@ function wireMemoryCommands(program: Command): void {
2038
2072
  });
2039
2073
  }
2040
2074
 
2075
+ // ─────────────────────────────────────────────────────────────────────────────
2076
+ // Session archive commands (manual import only)
2077
+ // ─────────────────────────────────────────────────────────────────────────────
2078
+
2079
+ function wireSessionsCommands(program: Command): void {
2080
+ const sessionsCmd = program
2081
+ .command("sessions")
2082
+ .description(
2083
+ "Discover and manually import local agent sessions into a dedicated archive"
2084
+ );
2085
+ const context = () => {
2086
+ const globals = getGlobals();
2087
+ return { configPath: globals.config, indexName: globals.index };
2088
+ };
2089
+ const asJson = (cmdOpts: Record<string, unknown>) =>
2090
+ getFormat(cmdOpts) === "json";
2091
+
2092
+ sessionsCmd
2093
+ .command("discover", { isDefault: true })
2094
+ .description("Preview supported local session sources (never imports)")
2095
+ .option("--json", "JSON output")
2096
+ .action(async (cmdOpts: Record<string, unknown>) => {
2097
+ const { discoverSessions, formatDiscovery } =
2098
+ await import("./commands/sessions");
2099
+ const result = await discoverSessions(context());
2100
+ await writeOutput(
2101
+ formatDiscovery(result, asJson(cmdOpts)),
2102
+ getFormat(cmdOpts)
2103
+ );
2104
+ });
2105
+
2106
+ sessionsCmd
2107
+ .command("init")
2108
+ .description("Create or extend the dedicated archive config/index pair")
2109
+ .option(
2110
+ "--archive <dir>",
2111
+ "absolute archive directory (outside the curated vault)"
2112
+ )
2113
+ .option("--collection <name>", "archive collection to create")
2114
+ .option("--json", "JSON output")
2115
+ .action(async (cmdOpts: Record<string, unknown>) => {
2116
+ const { initSessions } = await import("./commands/sessions");
2117
+ const result = await initSessions(context(), {
2118
+ archive: cmdOpts.archive as string | undefined,
2119
+ collection: cmdOpts.collection as string | undefined,
2120
+ });
2121
+ await writeOutput(
2122
+ asJson(cmdOpts)
2123
+ ? JSON.stringify(result, null, 2)
2124
+ : `Session archive ${result.created ? "created" : "ready"}: ${result.archiveRoot} (collection ${result.collection}, index ${result.index}, config ${result.configPath})`,
2125
+ getFormat(cmdOpts)
2126
+ );
2127
+ });
2128
+
2129
+ const sourceCmd = sessionsCmd
2130
+ .command("source")
2131
+ .description("Register or unregister owner session sources");
2132
+ sourceCmd
2133
+ .command("add <id>")
2134
+ .description("Register a harness session root for manual import")
2135
+ .option("--harness <harness>", "codex | claude-code | openclaw | hermes")
2136
+ .option("--path <path>", "absolute harness session root, file or database")
2137
+ .option("--collection <name>", "default archive collection")
2138
+ .option(
2139
+ "--project <prefix=collection>",
2140
+ "owner-approved working-directory mapping (repeatable)",
2141
+ collectRepeatableValue,
2142
+ []
2143
+ )
2144
+ .option("--json", "JSON output")
2145
+ .action(async (id: string, cmdOpts: Record<string, unknown>) => {
2146
+ const { addSource } = await import("./commands/sessions");
2147
+ const result = await addSource(context(), {
2148
+ id,
2149
+ harness: cmdOpts.harness as string | undefined,
2150
+ path: cmdOpts.path as string | undefined,
2151
+ collection: cmdOpts.collection as string | undefined,
2152
+ projects: cmdOpts.project as string[],
2153
+ });
2154
+ await writeOutput(
2155
+ asJson(cmdOpts)
2156
+ ? JSON.stringify(result, null, 2)
2157
+ : `Source ${result.id} registered. Nothing was imported.`,
2158
+ getFormat(cmdOpts)
2159
+ );
2160
+ });
2161
+ sourceCmd
2162
+ .command("remove <id>")
2163
+ .description("Unregister a source (its archive is retained)")
2164
+ .option("--json", "JSON output")
2165
+ .action(async (id: string, cmdOpts: Record<string, unknown>) => {
2166
+ const { removeSource } = await import("./commands/sessions");
2167
+ const result = await removeSource(context(), id);
2168
+ await writeOutput(
2169
+ asJson(cmdOpts)
2170
+ ? JSON.stringify(result, null, 2)
2171
+ : `Source ${result.id} removed; archived sessions were retained.`,
2172
+ getFormat(cmdOpts)
2173
+ );
2174
+ });
2175
+
2176
+ sessionsCmd
2177
+ .command("import [paths...]")
2178
+ .description("Import selected sources or paths into the archive (manual)")
2179
+ .option("--source <id>", "registered source to import")
2180
+ .option(
2181
+ "--collection <name>",
2182
+ "destination archive collection (path imports)"
2183
+ )
2184
+ .option("--format <harness>", "codex | claude-code | openclaw | hermes")
2185
+ .option("--dry-run", "parse and report without writing archive or index")
2186
+ .option("--limit <n>", "maximum changed units processed this run")
2187
+ .option("--json", "JSON output")
2188
+ .action(async (paths: string[], cmdOpts: Record<string, unknown>) => {
2189
+ const { formatImportReceipt, importSessions } =
2190
+ await import("./commands/sessions");
2191
+ const receipt = await importSessions(context(), {
2192
+ paths,
2193
+ source: cmdOpts.source as string | undefined,
2194
+ collection: cmdOpts.collection as string | undefined,
2195
+ format: cmdOpts.format as string | undefined,
2196
+ dryRun: Boolean(cmdOpts.dryRun),
2197
+ limit: cmdOpts.limit,
2198
+ });
2199
+ await writeOutput(
2200
+ formatImportReceipt(receipt, asJson(cmdOpts)),
2201
+ getFormat(cmdOpts)
2202
+ );
2203
+ if (receipt.status === "failed") {
2204
+ const unsupportedOnly =
2205
+ receipt.counts.unsupported > 0 &&
2206
+ receipt.counts.failed === 0 &&
2207
+ receipt.counts.incomplete === 0;
2208
+ throw unsupportedOnly
2209
+ ? new CliError(
2210
+ "VALIDATION",
2211
+ "No selected unit is a supported session format; see receipt.",
2212
+ { details: { sessionsCode: "SESSIONS_UNSUPPORTED_FORMAT" } }
2213
+ )
2214
+ : new CliError("RUNTIME", "Session import failed; see receipt.", {
2215
+ details: { sessionsCode: "SESSIONS_IMPORT_FAILED" },
2216
+ });
2217
+ }
2218
+ });
2219
+
2220
+ sessionsCmd
2221
+ .command("status")
2222
+ .description("Show archive, source and checkpoint status")
2223
+ .option("--json", "JSON output")
2224
+ .action(async (cmdOpts: Record<string, unknown>) => {
2225
+ const { formatStatus, sessionsStatus } =
2226
+ await import("./commands/sessions");
2227
+ const result = await sessionsStatus(context());
2228
+ await writeOutput(
2229
+ formatStatus(result, asJson(cmdOpts)),
2230
+ getFormat(cmdOpts)
2231
+ );
2232
+ });
2233
+
2234
+ wireSessionsAutomationCommands(sessionsCmd, context, asJson);
2235
+
2236
+ sessionsCmd
2237
+ .command("prune")
2238
+ .description(
2239
+ "Preview (or --apply) removal of archives whose source is gone"
2240
+ )
2241
+ .option("--source <id>", "source to prune")
2242
+ .option("--apply", "delete the previewed archive files")
2243
+ .option("--json", "JSON output")
2244
+ .action(async (cmdOpts: Record<string, unknown>) => {
2245
+ const { formatPrune, pruneSessions } =
2246
+ await import("./commands/sessions");
2247
+ const result = await pruneSessions(context(), {
2248
+ source: cmdOpts.source as string | undefined,
2249
+ apply: Boolean(cmdOpts.apply),
2250
+ });
2251
+ await writeOutput(
2252
+ formatPrune(result, asJson(cmdOpts)),
2253
+ getFormat(cmdOpts)
2254
+ );
2255
+ });
2256
+ }
2257
+
2258
+ /** Opt-in automation: profiles, explicit trigger switches, run-now, host hook. */
2259
+ function wireSessionsAutomationCommands(
2260
+ sessionsCmd: Command,
2261
+ context: () => { configPath?: string; indexName: string },
2262
+ asJson: (cmdOpts: Record<string, unknown>) => boolean
2263
+ ): void {
2264
+ const automationCmd = sessionsCmd
2265
+ .command("automation")
2266
+ .description(
2267
+ "Opt-in hooks and daemon schedules for registered sources (off until enabled)"
2268
+ );
2269
+
2270
+ automationCmd
2271
+ .command("set <profile>")
2272
+ .description(
2273
+ "Create or reconfigure a profile (sources, cadence, budget); enables nothing"
2274
+ )
2275
+ .option(
2276
+ "--source <id>",
2277
+ "registered source to include (repeatable)",
2278
+ collectRepeatableValue,
2279
+ []
2280
+ )
2281
+ .option("--cadence <n>", "elapsed schedule cadence <n>s|m|h|d (min 1m)")
2282
+ .option("--limit <n>", "changed units per source per run")
2283
+ .option("--retries <n>", "automatic retries after a failed run")
2284
+ .option("--json", "JSON output")
2285
+ .action(async (id: string, cmdOpts: Record<string, unknown>) => {
2286
+ const { formatAutomationPreview, setAutomation } =
2287
+ await import("./commands/sessions");
2288
+ const preview = await setAutomation(context(), id, {
2289
+ sources: cmdOpts.source as string[],
2290
+ cadence: cmdOpts.cadence as string | undefined,
2291
+ limit: cmdOpts.limit,
2292
+ retries: cmdOpts.retries,
2293
+ });
2294
+ await writeOutput(
2295
+ formatAutomationPreview(preview, asJson(cmdOpts)),
2296
+ getFormat(cmdOpts)
2297
+ );
2298
+ });
2299
+
2300
+ automationCmd
2301
+ .command("preview <profile>")
2302
+ .description(
2303
+ "Show sources, destinations, hook command, schedule and daemon prerequisite"
2304
+ )
2305
+ .option("--settings <path>", "Claude Code settings file to preview")
2306
+ .option("--json", "JSON output")
2307
+ .action(async (id: string, cmdOpts: Record<string, unknown>) => {
2308
+ const { formatAutomationPreview, previewAutomation } =
2309
+ await import("./commands/sessions");
2310
+ const preview = await previewAutomation(context(), id, {
2311
+ settings: cmdOpts.settings as string | undefined,
2312
+ });
2313
+ await writeOutput(
2314
+ formatAutomationPreview(preview, asJson(cmdOpts)),
2315
+ getFormat(cmdOpts)
2316
+ );
2317
+ });
2318
+
2319
+ automationCmd
2320
+ .command("enable <profile>")
2321
+ .description(
2322
+ "Explicitly switch on a host hook and/or the daemon schedule for a profile"
2323
+ )
2324
+ .option("--hook <harness>", "install the owned hook (claude-code)")
2325
+ .option(
2326
+ "--settings <path>",
2327
+ "Claude Code settings file (default: $CLAUDE_CONFIG_DIR or ~/.claude settings.json)"
2328
+ )
2329
+ .option("--schedule", "run on the daemon's elapsed cadence")
2330
+ .option("--cadence <n>", "elapsed cadence <n>s|m|h|d (min 1m)")
2331
+ .option("--json", "JSON output")
2332
+ .action(async (id: string, cmdOpts: Record<string, unknown>) => {
2333
+ const { enableAutomationCli, formatAutomationPreview } =
2334
+ await import("./commands/sessions");
2335
+ const preview = await enableAutomationCli(context(), id, {
2336
+ hook: cmdOpts.hook as string | undefined,
2337
+ settings: cmdOpts.settings as string | undefined,
2338
+ schedule: Boolean(cmdOpts.schedule),
2339
+ cadence: cmdOpts.cadence as string | undefined,
2340
+ });
2341
+ await writeOutput(
2342
+ formatAutomationPreview(preview, asJson(cmdOpts)),
2343
+ getFormat(cmdOpts)
2344
+ );
2345
+ });
2346
+
2347
+ automationCmd
2348
+ .command("disable <profile>")
2349
+ .description(
2350
+ "Pause: switch triggers off, remove the owned hook entry, clear pending work"
2351
+ )
2352
+ .option("--hook", "only the hook")
2353
+ .option("--schedule", "only the schedule")
2354
+ .option("--json", "JSON output")
2355
+ .action(async (id: string, cmdOpts: Record<string, unknown>) => {
2356
+ const { disableAutomationCli, formatAutomationChange } =
2357
+ await import("./commands/sessions");
2358
+ const change = await disableAutomationCli(context(), id, {
2359
+ hook: Boolean(cmdOpts.hook),
2360
+ schedule: Boolean(cmdOpts.schedule),
2361
+ });
2362
+ await writeOutput(
2363
+ formatAutomationChange(change, asJson(cmdOpts)),
2364
+ getFormat(cmdOpts)
2365
+ );
2366
+ });
2367
+
2368
+ automationCmd
2369
+ .command("remove <profile>")
2370
+ .description(
2371
+ "Uninstall owned integrations and delete the profile (archive retained)"
2372
+ )
2373
+ .option("--json", "JSON output")
2374
+ .action(async (id: string, cmdOpts: Record<string, unknown>) => {
2375
+ const { formatAutomationChange, removeAutomation } =
2376
+ await import("./commands/sessions");
2377
+ const change = await removeAutomation(context(), id);
2378
+ await writeOutput(
2379
+ formatAutomationChange(change, asJson(cmdOpts)),
2380
+ getFormat(cmdOpts)
2381
+ );
2382
+ });
2383
+
2384
+ automationCmd
2385
+ .command("run <profile>")
2386
+ .description("Run a profile now through the manual importer")
2387
+ .option("--json", "JSON output")
2388
+ .action(async (id: string, cmdOpts: Record<string, unknown>) => {
2389
+ const { formatAutomationRun, runAutomation } =
2390
+ await import("./commands/sessions");
2391
+ const result = await runAutomation(context(), id);
2392
+ await writeOutput(
2393
+ formatAutomationRun(result, asJson(cmdOpts)),
2394
+ getFormat(cmdOpts)
2395
+ );
2396
+ if (result.outcome === "failed" && result.reason === "busy") {
2397
+ throw new CliError(
2398
+ "BUSY",
2399
+ "The archive is busy (another import or index writer); the run is recorded; a running daemon retries it, or run it again once the archive is free.",
2400
+ { details: { sessionsCode: "SESSIONS_BUSY" } }
2401
+ );
2402
+ }
2403
+ if (result.outcome === "failed") {
2404
+ throw new CliError(
2405
+ "RUNTIME",
2406
+ `Automation run failed (${result.reason ?? "unknown"}); see gno sessions status.`,
2407
+ { details: { sessionsCode: "SESSIONS_IMPORT_FAILED" } }
2408
+ );
2409
+ }
2410
+ });
2411
+
2412
+ sessionsCmd
2413
+ .command("hook <harness>")
2414
+ .description(
2415
+ "Host hook entrypoint: durably mark a profile pending (installed by automation enable)"
2416
+ )
2417
+ .option("--profile <id>", "automation profile")
2418
+ .action(async (harness: string, cmdOpts: Record<string, unknown>) => {
2419
+ const { runSessionsHook } = await import("./commands/sessions");
2420
+ const line = await runSessionsHook(context(), harness, {
2421
+ profile: cmdOpts.profile as string | undefined,
2422
+ });
2423
+ process.stdout.write(`${line}\n`);
2424
+ });
2425
+ }
2426
+
2041
2427
  // ─────────────────────────────────────────────────────────────────────────────
2042
2428
  // Retrieval Commands (get, multi-get, ls)
2043
2429
  // ─────────────────────────────────────────────────────────────────────────────
@@ -2169,6 +2555,7 @@ function wireRetrievalCommands(program: Command): void {
2169
2555
  const { ls, formatLs } = await import("./commands/ls");
2170
2556
  const result = await ls(scope, {
2171
2557
  configPath: globals.config,
2558
+ indexName: globals.index,
2172
2559
  limit: cmdOpts.limit as number | undefined,
2173
2560
  offset: cmdOpts.offset as number | undefined,
2174
2561
  json: format === "json",
@@ -2803,6 +3190,76 @@ function wireManagementCommands(program: Command): void {
2803
3190
  await writeOutput(output, format === "json" ? "json" : "md");
2804
3191
  });
2805
3192
 
3193
+ const compiledCmd = contextCmd
3194
+ .command("compiled")
3195
+ .description("Compile, check and refresh verified project context");
3196
+ for (const operation of ["preview", "compile", "check", "refresh"] as const) {
3197
+ const command = compiledCmd
3198
+ .command(
3199
+ operation === "check" || operation === "refresh"
3200
+ ? `${operation} <file>`
3201
+ : operation
3202
+ )
3203
+ .description(`${operation} a separate managed context artifact`)
3204
+ .option("--json", "JSON output");
3205
+ if (operation === "preview" || operation === "compile") {
3206
+ command
3207
+ .requiredOption("--capsule <file>", "explicit Capsule JSON file")
3208
+ .requiredOption("--budget <tokens>", "whole-output token budget")
3209
+ .option("--bytes <bytes>", "whole-output byte budget");
3210
+ }
3211
+ if (operation === "compile")
3212
+ command.requiredOption(
3213
+ "--output <file>",
3214
+ "new *.gno-context.md artifact"
3215
+ );
3216
+ if (operation === "check")
3217
+ command.option("--capsule <file>", "relocated Capsule JSON file");
3218
+ if (operation === "refresh")
3219
+ command.requiredOption(
3220
+ "--capsule-output <file>",
3221
+ "explicit refreshed Capsule destination"
3222
+ );
3223
+ command.action(async (...args: unknown[]) => {
3224
+ const file =
3225
+ operation === "check" || operation === "refresh"
3226
+ ? (args[0] as string)
3227
+ : undefined;
3228
+ const opts = args[file === undefined ? 0 : 1] as Record<string, unknown>;
3229
+ const globals = getGlobals();
3230
+ const { contextCompiled } = await import("./commands/context-compiled");
3231
+ const result = await contextCompiled(operation, {
3232
+ configPath: globals.config,
3233
+ indexName: globals.index,
3234
+ capsulePath: opts.capsule as string | undefined,
3235
+ capsuleOutputPath: opts.capsuleOutput as string | undefined,
3236
+ outputPath: file ?? (opts.output as string | undefined),
3237
+ budgetTokens:
3238
+ opts.budget === undefined
3239
+ ? undefined
3240
+ : parseContextInteger("budget", opts.budget),
3241
+ budgetBytes:
3242
+ opts.bytes === undefined
3243
+ ? undefined
3244
+ : parseContextInteger("bytes", opts.bytes),
3245
+ json: Boolean(opts.json || globals.json),
3246
+ });
3247
+ await writeOutput(
3248
+ result.output,
3249
+ opts.json || globals.json ? "json" : "terminal"
3250
+ );
3251
+ if (result.status && result.status !== "current") {
3252
+ const code =
3253
+ result.status === "stale"
3254
+ ? "CONTEXT_STALE"
3255
+ : result.status === "conflict"
3256
+ ? "CONTEXT_CONFLICT"
3257
+ : "RUNTIME";
3258
+ throw new CliError(code, result.status, { silent: true });
3259
+ }
3260
+ });
3261
+ }
3262
+
2806
3263
  contextCmd
2807
3264
  .command("verify <file>")
2808
3265
  .description("Verify a saved Context Capsule without rebuilding it")
@@ -0,0 +1,49 @@
1
+ /**
2
+ * CLI-wide enforcement of the session-archive config/index binding.
3
+ *
4
+ * Runs before every command and again whenever a command opens an index
5
+ * (`initStore`, which also covers indexes named by a `?index=` URI): a config
6
+ * bound to a session archive index cannot be used with another index, and an
7
+ * archive index cannot be opened with a different config.
8
+ *
9
+ * @module src/cli/session-binding
10
+ */
11
+
12
+ import type { Config } from "../config/types";
13
+
14
+ import { getIndexDbPath } from "../app/constants";
15
+ import { getConfigPaths, loadConfig } from "../config";
16
+ import { assertSessionBinding } from "../sessions/binding";
17
+ import { SessionsError } from "../sessions/types";
18
+ import { CliError } from "./errors";
19
+
20
+ export async function assertCliSessionBinding(
21
+ configPath: string | undefined,
22
+ indexName: string,
23
+ loadedConfig?: Config
24
+ ): Promise<void> {
25
+ const path = configPath ?? getConfigPaths().configFile;
26
+ let config = loadedConfig;
27
+ if (!config) {
28
+ if (!(await Bun.file(path).exists())) return;
29
+ const loaded = await loadConfig(path);
30
+ // Unreadable configs are reported by the command itself.
31
+ if (!loaded.ok) return;
32
+ config = loaded.value;
33
+ }
34
+ try {
35
+ await assertSessionBinding({
36
+ config,
37
+ configPath: path,
38
+ indexName,
39
+ dbPath: getIndexDbPath(indexName),
40
+ });
41
+ } catch (error) {
42
+ if (error instanceof SessionsError) {
43
+ throw new CliError("VALIDATION", error.message, {
44
+ details: { sessionsCode: error.code },
45
+ });
46
+ }
47
+ throw error;
48
+ }
49
+ }
@@ -12,6 +12,7 @@ import { z } from "zod";
12
12
  import { URI_PREFIX } from "../app/constants";
13
13
  import { JsonlFieldMappingSchema } from "../converters/adapters/jsonl/config";
14
14
  import { MCP_TOOL_PROFILES } from "../mcp/tool-profile";
15
+ import { SessionsConfigSchema } from "../sessions/config";
15
16
  import { ChunkingConfigSchema, type ChunkingParams } from "./chunking";
16
17
  import { RetrievalTraceConfigSchema } from "./retrieval-traces";
17
18
 
@@ -614,6 +615,13 @@ export const ConfigSchema = z.object({
614
615
  /** Bounded project-aware retrieval affinity. */
615
616
  projectAffinity: ProjectAffinityConfigSchema.optional(),
616
617
 
618
+ /**
619
+ * Session-archive binding. Present only in a dedicated archive config; it
620
+ * binds the file to one index name and archive root and lists
621
+ * owner-registered session sources.
622
+ */
623
+ sessions: SessionsConfigSchema.optional(),
624
+
617
625
  /** Machine-local, timestamp-free project profile provenance. */
618
626
  projectProfileBindings: z
619
627
  .array(ProjectProfileBindingSchema)