@gmickel/gno 2.5.1 → 2.7.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 (160) hide show
  1. package/README.md +60 -5
  2. package/assets/skill/README.md +5 -1
  3. package/assets/skill/SKILL.md +80 -4
  4. package/assets/skill/cli-reference.md +132 -2
  5. package/assets/skill/examples.md +30 -0
  6. package/assets/skill/mcp-reference.md +54 -1
  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.5.1.zip → gno-browser-clipper-v2.7.0.zip} +0 -0
  13. package/browser-extension/artifacts/gno-browser-clipper-v2.7.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 +449 -35
  17. package/spec/mcp.md +234 -4
  18. package/spec/output-schemas/ask.schema.json +1 -1
  19. package/spec/output-schemas/capture-receipt.schema.json +4 -1
  20. package/spec/output-schemas/doctor.schema.json +88 -0
  21. package/spec/output-schemas/error.schema.json +11 -2
  22. package/spec/output-schemas/get.schema.json +1 -1
  23. package/spec/output-schemas/mcp-capture-result.schema.json +4 -2
  24. package/spec/output-schemas/memory-remember.schema.json +10 -4
  25. package/spec/output-schemas/multi-get.schema.json +4 -1
  26. package/spec/output-schemas/peek.schema.json +2 -9
  27. package/spec/output-schemas/request-status.schema.json +113 -0
  28. package/spec/output-schemas/resident-status.schema.json +22 -0
  29. package/spec/output-schemas/search-result.schema.json +1 -1
  30. package/spec/output-schemas/search-results.schema.json +1 -1
  31. package/spec/output-schemas/sessions-automation-run.schema.json +46 -0
  32. package/spec/output-schemas/sessions-discovery.schema.json +38 -0
  33. package/spec/output-schemas/sessions-import-receipt.schema.json +156 -0
  34. package/spec/output-schemas/sessions-status.schema.json +432 -0
  35. package/spec/output-schemas/status.schema.json +98 -0
  36. package/src/cli/commands/ask.ts +14 -2
  37. package/src/cli/commands/capture.ts +55 -96
  38. package/src/cli/commands/daemon.ts +41 -0
  39. package/src/cli/commands/doctor.ts +54 -20
  40. package/src/cli/commands/embed.ts +41 -3
  41. package/src/cli/commands/ls.ts +3 -0
  42. package/src/cli/commands/memory.ts +12 -3
  43. package/src/cli/commands/query.ts +5 -0
  44. package/src/cli/commands/request-status.ts +59 -0
  45. package/src/cli/commands/reset.ts +39 -5
  46. package/src/cli/commands/sessions.ts +713 -0
  47. package/src/cli/commands/shared.ts +14 -1
  48. package/src/cli/commands/status.ts +63 -5
  49. package/src/cli/commands/vec.ts +54 -0
  50. package/src/cli/detach.ts +29 -1
  51. package/src/cli/errors.ts +13 -9
  52. package/src/cli/program.ts +441 -2
  53. package/src/cli/session-binding.ts +49 -0
  54. package/src/config/types.ts +8 -0
  55. package/src/core/capture-publish.ts +239 -0
  56. package/src/core/capture-sync.ts +12 -2
  57. package/src/core/host-paths.ts +31 -0
  58. package/src/core/memory-remember.ts +234 -122
  59. package/src/core/memory-types.ts +11 -0
  60. package/src/core/network-boundary-inventory.ts +8 -0
  61. package/src/core/request-receipts.ts +671 -0
  62. package/src/core/shutdown-budget.ts +6 -0
  63. package/src/core/vector-partition-status.ts +52 -0
  64. package/src/embed/backlog.ts +124 -18
  65. package/src/embed/fingerprint.ts +6 -3
  66. package/src/embed/retry.ts +66 -27
  67. package/src/embed/variant-backlog.ts +15 -10
  68. package/src/embed/variant-retry.ts +31 -22
  69. package/src/index.ts +30 -2
  70. package/src/llm/native-worker/dispatcher.ts +2 -0
  71. package/src/llm/native-worker/embedding-identity.ts +42 -0
  72. package/src/llm/native-worker/protocol.ts +1 -0
  73. package/src/llm/types.ts +3 -0
  74. package/src/mcp/context.ts +17 -0
  75. package/src/mcp/http-egress.ts +4 -0
  76. package/src/mcp/http-transport.ts +2 -0
  77. package/src/mcp/resources/index.ts +6 -5
  78. package/src/mcp/tool-descriptions-core.ts +1 -1
  79. package/src/mcp/tools/capture.ts +87 -85
  80. package/src/mcp/tools/index.ts +77 -4
  81. package/src/mcp/tools/memory-remember.ts +8 -1
  82. package/src/mcp/tools/memory-shared.ts +7 -1
  83. package/src/mcp/tools/request-status.ts +73 -0
  84. package/src/mcp/tools/sessions.ts +208 -0
  85. package/src/mcp/tools/status.ts +4 -0
  86. package/src/pipeline/hybrid.ts +37 -7
  87. package/src/pipeline/vsearch.ts +14 -2
  88. package/src/sdk/client.ts +180 -84
  89. package/src/sdk/index.ts +6 -0
  90. package/src/sdk/types.ts +54 -2
  91. package/src/serve/capture-service.ts +98 -32
  92. package/src/serve/config-sync.ts +3 -2
  93. package/src/serve/embed-scheduler.ts +133 -19
  94. package/src/serve/host-path-redaction.ts +79 -0
  95. package/src/serve/public/app.tsx +4 -1
  96. package/src/serve/public/components/CaptureModal.tsx +26 -8
  97. package/src/serve/public/components/sessions/AutomationPanel.tsx +800 -0
  98. package/src/serve/public/components/sessions/ImportReceipt.tsx +238 -0
  99. package/src/serve/public/components/sessions/SessionSearch.tsx +286 -0
  100. package/src/serve/public/components/sessions/SourcesPanel.tsx +541 -0
  101. package/src/serve/public/components/sessions/api.ts +40 -0
  102. package/src/serve/public/globals.built.css +1 -1
  103. package/src/serve/public/hooks/use-api.ts +26 -3
  104. package/src/serve/public/lib/request-intent.ts +77 -0
  105. package/src/serve/public/lib/snippet.tsx +52 -0
  106. package/src/serve/public/lib/workspace-actions.ts +12 -1
  107. package/src/serve/public/lib/workspace-tabs.ts +2 -0
  108. package/src/serve/public/pages/Dashboard.tsx +22 -9
  109. package/src/serve/public/pages/DocView.tsx +25 -6
  110. package/src/serve/public/pages/DocumentEditor.tsx +224 -104
  111. package/src/serve/public/pages/Search.tsx +1 -41
  112. package/src/serve/public/pages/Sessions.tsx +350 -0
  113. package/src/serve/resident-runtime.ts +69 -4
  114. package/src/serve/resident-status.ts +13 -1
  115. package/src/serve/routes/api.ts +476 -147
  116. package/src/serve/routes/sessions.ts +766 -0
  117. package/src/serve/security.ts +9 -0
  118. package/src/serve/server.ts +215 -10
  119. package/src/serve/session-automation.ts +146 -0
  120. package/src/serve/status-model.ts +16 -0
  121. package/src/serve/status.ts +2 -0
  122. package/src/serve/watch-reconciliation-shared.ts +3 -0
  123. package/src/serve/watch-service-events.ts +3 -2
  124. package/src/serve/watch-service-run-flush.ts +35 -2
  125. package/src/serve/watch-service.ts +5 -0
  126. package/src/sessions/archive.ts +348 -0
  127. package/src/sessions/automation-state.ts +444 -0
  128. package/src/sessions/automation-status.ts +239 -0
  129. package/src/sessions/automation.ts +1169 -0
  130. package/src/sessions/binding.ts +105 -0
  131. package/src/sessions/claude-hook.ts +240 -0
  132. package/src/sessions/config.ts +176 -0
  133. package/src/sessions/format.ts +191 -0
  134. package/src/sessions/import-child-env.ts +8 -0
  135. package/src/sessions/import-child.ts +152 -0
  136. package/src/sessions/parsers/claude-code.ts +259 -0
  137. package/src/sessions/parsers/codex.ts +303 -0
  138. package/src/sessions/parsers/hermes.ts +248 -0
  139. package/src/sessions/parsers/openclaw.ts +496 -0
  140. package/src/sessions/parsers/shared.ts +184 -0
  141. package/src/sessions/sanitize.ts +222 -0
  142. package/src/sessions/service.ts +1533 -0
  143. package/src/sessions/setup.ts +477 -0
  144. package/src/sessions/sources.ts +518 -0
  145. package/src/sessions/state.ts +118 -0
  146. package/src/sessions/types.ts +457 -0
  147. package/src/store/migrations/031-runtime-independent-vectors.ts +29 -0
  148. package/src/store/migrations/032-vector-runtime-callers.ts +17 -0
  149. package/src/store/migrations/index.ts +4 -0
  150. package/src/store/sqlite/adapter.ts +76 -16
  151. package/src/store/sqlite/scoped-index.ts +9 -0
  152. package/src/store/types.ts +11 -1
  153. package/src/store/vector/lazy.ts +46 -43
  154. package/src/store/vector/runtime-compat.ts +651 -0
  155. package/src/store/vector/sqlite-vec.ts +20 -2
  156. package/src/store/vector/status.ts +276 -35
  157. package/src/store/vector/types.ts +2 -0
  158. package/src/store/vector/variant-search.ts +71 -23
  159. package/src/store/vector/variants.ts +49 -14
  160. package/browser-extension/artifacts/gno-browser-clipper-v2.5.1.zip.sha256 +0 -1
@@ -0,0 +1,348 @@
1
+ /**
2
+ * Durable session archive: one sanitized JSONL file per thread.
3
+ *
4
+ * Archive files are ordinary JSONL records read back through the existing
5
+ * JSONL record adapter with {@link SESSION_ARCHIVE_FIELD_MAPPING}, so the
6
+ * archive needs no dedicated ingestion or ranking path. Each line is one
7
+ * dialogue turn; the body carries the sanitized text plus a bounded
8
+ * provenance block so `get` shows who spoke, when, and where it came from.
9
+ *
10
+ * @module src/sessions/archive
11
+ */
12
+
13
+ // node:path: no Bun path utilities.
14
+ import { basename, join } from "node:path";
15
+
16
+ import type { JsonlFieldMapping } from "../converters/adapters/jsonl/config";
17
+ import type { RedactionPolicy } from "./sanitize";
18
+
19
+ import { hashRecordValue } from "../converters/adapters/shared/record-utils";
20
+ import { SESSION_REDACTION_VERSION, ThreadSanitizer } from "./sanitize";
21
+ import {
22
+ type ParsedThread,
23
+ SESSION_ARCHIVE_FORMAT_VERSION,
24
+ SESSION_HARNESS_LABELS,
25
+ SESSION_LIMITS,
26
+ type SessionHarness,
27
+ } from "./types";
28
+
29
+ /** Record mapping registered on every archive collection. */
30
+ export const SESSION_ARCHIVE_FIELD_MAPPING: JsonlFieldMapping = {
31
+ id: "/id",
32
+ body: "/body",
33
+ title: "/title",
34
+ author: "/author",
35
+ categories: "/categories",
36
+ sessionId: "/sessionId",
37
+ threadId: "/threadId",
38
+ dateFields: { recorded: "/recordedAt" },
39
+ };
40
+
41
+ /** Directory inside the archive root that holds import state. */
42
+ export const SESSION_STATE_DIRNAME = ".gno-sessions";
43
+
44
+ const PROVENANCE_FIELD_CHARS = 256;
45
+
46
+ export interface ArchiveLine {
47
+ id: string;
48
+ title: string;
49
+ body: string;
50
+ author: "human" | "assistant";
51
+ categories: string[];
52
+ sessionId?: string;
53
+ threadId: string;
54
+ recordedAt?: string;
55
+ provenance: {
56
+ sourceId: string;
57
+ harness: SessionHarness;
58
+ unit: string;
59
+ locator: string;
60
+ turnId: string;
61
+ threadKind: string;
62
+ parentThreadId?: string;
63
+ parser: string;
64
+ redaction: number;
65
+ format: number;
66
+ };
67
+ }
68
+
69
+ export interface RenderedThread {
70
+ /** Archive file path relative to the collection root. */
71
+ relPath: string;
72
+ content: string;
73
+ lines: number;
74
+ humanTurns: number;
75
+ assistantTurns: number;
76
+ redactions: number;
77
+ overLimit: boolean;
78
+ }
79
+
80
+ const bound = (value: string): string =>
81
+ value.length > PROVENANCE_FIELD_CHARS
82
+ ? `${value.slice(0, PROVENANCE_FIELD_CHARS - 1)}…`
83
+ : value;
84
+
85
+ /** Lowercase tag segment accepted by GNO's tag grammar. */
86
+ function tagSegment(value: string): string {
87
+ const slug = value
88
+ .normalize("NFC")
89
+ .toLowerCase()
90
+ .replace(/[^\p{Ll}\p{Lo}\p{N}.-]+/gu, "-")
91
+ .replace(/^[^\p{Ll}\p{Lo}\p{N}]+/u, "")
92
+ .replace(/-+$/u, "")
93
+ .slice(0, 64);
94
+ return slug || "unknown";
95
+ }
96
+
97
+ /** Opaque identity of a working directory; the raw path is never stored. */
98
+ function projectIdentity(cwd: string | undefined): {
99
+ label: string;
100
+ id: string;
101
+ } | null {
102
+ if (!cwd) return null;
103
+ const normalized = cwd.replaceAll("\\", "/").replace(/\/+$/, "");
104
+ return {
105
+ label: basename(normalized) || "root",
106
+ id: hashRecordValue("gno-session-project-v1", normalized).slice(0, 12),
107
+ };
108
+ }
109
+
110
+ /**
111
+ * Stable, path-free archive file for one thread of one unit. The unit key
112
+ * namespaces the file, so two units that report the same thread ID never
113
+ * overwrite each other.
114
+ */
115
+ export function threadRelPath(
116
+ sourceId: string,
117
+ harness: SessionHarness,
118
+ unitKey: string,
119
+ threadId: string
120
+ ): string {
121
+ const fileKey = hashRecordValue(
122
+ "gno-session-thread-v2",
123
+ `${harness}\0${sourceId}\0${unitKey}\0${threadId}`
124
+ ).slice(0, 32);
125
+ return `${harness}/${sourceId}/${fileKey}.jsonl`;
126
+ }
127
+
128
+ export function archiveFilePath(
129
+ archiveRoot: string,
130
+ collection: string,
131
+ relPath: string
132
+ ): string {
133
+ return join(archiveRoot, collection, relPath);
134
+ }
135
+
136
+ const roleLabel = (role: "human" | "assistant"): string =>
137
+ role === "human" ? "Human" : "Assistant";
138
+
139
+ /**
140
+ * Render a parsed thread into sanitized archive lines. Sanitization runs
141
+ * over every persisted field (text, titles, labels, locators) before
142
+ * anything is written, with a second pass propagating detected values
143
+ * across the whole thread.
144
+ */
145
+ export function renderThread(options: {
146
+ thread: ParsedThread;
147
+ sourceId: string;
148
+ unitKey: string;
149
+ unitLocator: string;
150
+ parser: string;
151
+ redaction: RedactionPolicy;
152
+ }): RenderedThread {
153
+ const { thread, sourceId, parser } = options;
154
+ const sanitizer = new ThreadSanitizer(options.redaction);
155
+ const project = projectIdentity(thread.cwd);
156
+ // Pass 1 detects secrets in every persisted field; pass 2 (`clean`)
157
+ // propagates values found anywhere in the thread into every field, so a
158
+ // secret revealed in a later turn is also removed from titles, tags and IDs.
159
+ const raw = {
160
+ project: project ? sanitizer.sanitize(project.label) : null,
161
+ sessionId: sanitizer.sanitize(thread.sessionId),
162
+ threadId: sanitizer.sanitize(thread.threadId),
163
+ parentThreadId: thread.parentThreadId
164
+ ? sanitizer.sanitize(thread.parentThreadId)
165
+ : null,
166
+ unit: sanitizer.sanitize(options.unitLocator),
167
+ };
168
+ const rawDrafts = thread.turns.map((turn) => ({
169
+ turn,
170
+ text: sanitizer.sanitize(turn.text),
171
+ turnId: sanitizer.sanitize(turn.turnId),
172
+ locator: sanitizer.sanitize(turn.locator),
173
+ }));
174
+ const clean = (value: string): string => sanitizer.propagate(value);
175
+ const projectLabel = raw.project === null ? null : clean(raw.project);
176
+ const harnessLabel = SESSION_HARNESS_LABELS[thread.harness];
177
+ const sessionId = `${thread.harness}/${sourceId}/${clean(raw.sessionId)}`;
178
+ const threadId = `${thread.harness}/${sourceId}/${clean(raw.threadId)}`;
179
+ const parentThreadId =
180
+ raw.parentThreadId === null
181
+ ? null
182
+ : `${thread.harness}/${sourceId}/${clean(raw.parentThreadId)}`;
183
+ const unit = clean(raw.unit);
184
+
185
+ const categories = [
186
+ "session",
187
+ `harness/${tagSegment(thread.harness)}`,
188
+ `session-kind/${thread.kind}`,
189
+ ];
190
+ if (projectLabel && project) {
191
+ categories.push(`project/${tagSegment(projectLabel)}`);
192
+ categories.push(`project-id/${project.id}`);
193
+ }
194
+
195
+ const drafts = rawDrafts.map((draft) => ({
196
+ turn: draft.turn,
197
+ text: clean(draft.text),
198
+ turnId: clean(draft.turnId),
199
+ locator: clean(draft.locator),
200
+ }));
201
+
202
+ const lines: string[] = [];
203
+ let bytes = 0;
204
+ let human = 0;
205
+ let assistant = 0;
206
+ let overLimit = false;
207
+ for (const draft of drafts) {
208
+ const { turn } = draft;
209
+ const { text } = draft;
210
+ const speaker = roleLabel(turn.role);
211
+ const titleParts = [speaker, harnessLabel, projectLabel ?? "no project"];
212
+ // One bounded line keeps each record small in bounded context delivery.
213
+ const provenance = [
214
+ turn.role === "assistant"
215
+ ? "Assistant (assistant output, not a user decision)"
216
+ : "Human",
217
+ // A known time travels as record metadata (dateFields.recorded).
218
+ ...(turn.timestamp ? [] : ["recorded unknown"]),
219
+ `source ${bound(sourceId)}`,
220
+ `locator ${bound(`${unit}#${draft.locator}`)}`,
221
+ `turn ${bound(draft.turnId)}`,
222
+ ].join(" · ");
223
+ const line: ArchiveLine = {
224
+ id: hashRecordValue(
225
+ "gno-session-turn-v1",
226
+ `${threadId}\0${draft.turnId}`
227
+ ).slice(0, 32),
228
+ title: titleParts.join(" · "),
229
+ body: `${speaker}: ${text}\n\nProvenance: ${provenance}`,
230
+ author: turn.role,
231
+ categories: [...categories, `role/${turn.role}`],
232
+ // A main thread is its own session; the duplicate ID is omitted.
233
+ ...(sessionId === threadId ? {} : { sessionId }),
234
+ threadId,
235
+ ...(turn.timestamp ? { recordedAt: turn.timestamp } : {}),
236
+ provenance: {
237
+ sourceId,
238
+ harness: thread.harness,
239
+ unit,
240
+ locator: draft.locator,
241
+ turnId: draft.turnId,
242
+ threadKind: thread.kind,
243
+ ...(parentThreadId ? { parentThreadId } : {}),
244
+ parser,
245
+ redaction: SESSION_REDACTION_VERSION,
246
+ format: SESSION_ARCHIVE_FORMAT_VERSION,
247
+ },
248
+ };
249
+ const serialized = JSON.stringify(line);
250
+ bytes += serialized.length + 1;
251
+ if (bytes > SESSION_LIMITS.maxArchiveBytesPerThread) {
252
+ overLimit = true;
253
+ break;
254
+ }
255
+ lines.push(serialized);
256
+ if (turn.role === "human") human += 1;
257
+ else assistant += 1;
258
+ }
259
+
260
+ return {
261
+ relPath: threadRelPath(
262
+ sourceId,
263
+ thread.harness,
264
+ options.unitKey,
265
+ thread.threadId
266
+ ),
267
+ content: lines.length > 0 ? `${lines.join("\n")}\n` : "",
268
+ lines: lines.length,
269
+ humanTurns: human,
270
+ assistantTurns: assistant,
271
+ redactions: sanitizer.redactions,
272
+ overLimit,
273
+ };
274
+ }
275
+
276
+ type JsonValue =
277
+ | string
278
+ | number
279
+ | boolean
280
+ | null
281
+ | JsonValue[]
282
+ | { [key: string]: JsonValue };
283
+
284
+ /** Apply `fn` to every string leaf of a parsed archive line. */
285
+ function mapStrings(value: JsonValue, fn: (text: string) => string): JsonValue {
286
+ if (typeof value === "string") return fn(value);
287
+ if (Array.isArray(value)) return value.map((item) => mapStrings(item, fn));
288
+ if (value && typeof value === "object") {
289
+ return Object.fromEntries(
290
+ Object.entries(value).map(([key, item]) => [key, mapStrings(item, fn)])
291
+ );
292
+ }
293
+ return value;
294
+ }
295
+
296
+ /**
297
+ * Re-sanitize an existing archive file with the current redaction rules
298
+ * (used when the original source is no longer available). Every persisted
299
+ * string field is rescanned, not only the text. Returns null when the file
300
+ * does not parse as an archive; the caller must then withhold it.
301
+ */
302
+ export function rescanArchiveContent(
303
+ content: string,
304
+ redaction: RedactionPolicy
305
+ ): { content: string; redactions: number } | null {
306
+ const sanitizer = new ThreadSanitizer(redaction);
307
+ const parsed: JsonValue[] = [];
308
+ for (const raw of content.split("\n")) {
309
+ if (!raw.trim()) continue;
310
+ let line: unknown;
311
+ try {
312
+ line = JSON.parse(raw);
313
+ } catch {
314
+ return null;
315
+ }
316
+ const record = line as Partial<ArchiveLine> | null;
317
+ if (
318
+ !record ||
319
+ typeof record !== "object" ||
320
+ typeof record.body !== "string" ||
321
+ !record.provenance ||
322
+ typeof record.provenance !== "object"
323
+ ) {
324
+ return null;
325
+ }
326
+ parsed.push(
327
+ mapStrings(line as JsonValue, (text) => sanitizer.sanitize(text))
328
+ );
329
+ }
330
+ const output = parsed.map((line) => {
331
+ const propagated = mapStrings(line, (text) =>
332
+ sanitizer.propagate(text)
333
+ ) as {
334
+ provenance: Record<string, JsonValue>;
335
+ };
336
+ return JSON.stringify({
337
+ ...propagated,
338
+ provenance: {
339
+ ...propagated.provenance,
340
+ redaction: SESSION_REDACTION_VERSION,
341
+ },
342
+ });
343
+ });
344
+ return {
345
+ content: output.length > 0 ? `${output.join("\n")}\n` : "",
346
+ redactions: sanitizer.redactions,
347
+ };
348
+ }