@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
@@ -12,7 +12,13 @@ import { basename, dirname, join as pathJoin } from "node:path";
12
12
 
13
13
  import type { Collection, Config } from "../config/types";
14
14
  import type { PreparedBrowserClip } from "../core/browser-clip";
15
- import type { CaptureInput, CapturePlan, CaptureSource } from "../core/capture";
15
+ import type {
16
+ CaptureInput,
17
+ CapturePlan,
18
+ CaptureReceipt,
19
+ CaptureSource,
20
+ SyncCapturedFileResult,
21
+ } from "../core/capture";
16
22
  import type { JobManager } from "../core/job-manager";
17
23
  import type { SqliteAdapter } from "../store/sqlite/adapter";
18
24
  import type { ClipperIdempotencyPlan } from "../store/sqlite/clipper-store-types";
@@ -30,12 +36,13 @@ import {
30
36
  hashCaptureContent,
31
37
  listCaptureDiskRelPaths,
32
38
  planCapture,
33
- syncCapturedFile,
34
39
  } from "../core/capture";
40
+ import { publishCapture } from "../core/capture-publish";
35
41
  import { writeCapturePlanFile } from "../core/capture-write";
36
42
  import { MCP_ERRORS } from "../core/errors";
37
43
  import { withWriteLock } from "../core/file-lock";
38
44
  import { recordContentMutation } from "../core/mutation-generations";
45
+ import { localRequestLedger } from "../core/request-receipts";
39
46
  import { DEFAULT_LOCK_WAIT_MS, writeLeasePath } from "../core/write-lease";
40
47
  import {
41
48
  type CollectionSyncResult,
@@ -331,6 +338,37 @@ const executeCaptureAsJob = async (
331
338
  };
332
339
  };
333
340
 
341
+ /** Shared leased publication wired to the resident watch/event hooks. */
342
+ const residentPublication = (
343
+ context: ResidentCaptureContext,
344
+ store: SqliteAdapter,
345
+ collection: Collection,
346
+ dependencies: ResidentCaptureDependencies
347
+ ) => ({
348
+ collection,
349
+ store,
350
+ lockPath: resolveCaptureLockPath(context, dependencies),
351
+ lockWaitMs: dependencies.lockWaitMs ?? DEFAULT_LOCK_WAIT_MS,
352
+ config: context.config,
353
+ syncPaths: dependencies.syncPaths,
354
+ beforeWrite: (absPath: string) => context.watchService?.suppress(absPath),
355
+ afterSync: (synced: SyncCapturedFileResult, receipt: CaptureReceipt) => {
356
+ if (synced.result) {
357
+ recordContentMutation(synced.result, context.markContentMutation);
358
+ }
359
+ if (!receipt.openedExisting) {
360
+ emitCaptureCreated(context, collection, receipt.relPath);
361
+ }
362
+ },
363
+ });
364
+
365
+ const residentCaptureStatus = (receipt: CaptureReceipt): number =>
366
+ receipt.collisionPolicyResult === "conflict"
367
+ ? HTTP_CONFLICT
368
+ : receipt.openedExisting
369
+ ? HTTP_OK
370
+ : HTTP_CREATED;
371
+
334
372
  /**
335
373
  * Write + lexical sync under the shared write lease; the response is sent
336
374
  * only once the capture is retrievable. Throws `CaptureSyncError` when the
@@ -343,37 +381,65 @@ const executeCaptureAwaitingSync = async (
343
381
  planned: Extract<ResidentCapturePlanResult, { ok: true }>,
344
382
  dependencies: ResidentCaptureDependencies
345
383
  ): Promise<{ body: unknown; status: number }> => {
346
- const { collection, fullPath, plan } = planned;
347
- return withWriteLock(
348
- resolveCaptureLockPath(context, dependencies),
349
- async () => {
350
- await mkdir(dirname(fullPath), { recursive: true });
351
- context.watchService?.suppress(fullPath);
352
- await writeCapturePlanFile(plan, fullPath);
353
- const synced = await syncCapturedFile({
354
- collection,
355
- store,
356
- relPath: plan.relPath,
357
- absPath: fullPath,
358
- config: context.config,
359
- syncPaths: dependencies.syncPaths,
360
- });
361
- if (synced.result) {
362
- recordContentMutation(synced.result, context.markContentMutation);
363
- }
364
- emitCaptureCreated(context, collection, plan.relPath);
365
- return {
366
- body: buildCaptureReceipt({
367
- plan,
368
- absPath: fullPath,
369
- docid: synced.docid,
370
- sync: synced.sync,
371
- }),
372
- status: HTTP_CREATED,
373
- };
374
- },
375
- dependencies.lockWaitMs ?? DEFAULT_LOCK_WAIT_MS
384
+ const { receipt } = await publishCapture({
385
+ ...residentPublication(context, store, planned.collection, dependencies),
386
+ plan: async () => planned.plan,
387
+ });
388
+ return { body: receipt, status: HTTP_CREATED };
389
+ };
390
+
391
+ /** A resident plan rejection, carried through the leased planner. */
392
+ export class ResidentCapturePlanError extends Error {
393
+ readonly shape: Extract<ResidentCapturePlanResult, { ok: false }>;
394
+
395
+ constructor(shape: Extract<ResidentCapturePlanResult, { ok: false }>) {
396
+ super(shape.message);
397
+ this.name = "ResidentCapturePlanError";
398
+ this.shape = shape;
399
+ }
400
+ }
401
+
402
+ /**
403
+ * `/api/capture` with a request ID: planning moves under the lease and the
404
+ * request-receipt service replays, finishes, or admits the capture.
405
+ */
406
+ export const executeRequestedResidentCapture = async (
407
+ context: ResidentCaptureContext,
408
+ store: SqliteAdapter,
409
+ input: CaptureInput,
410
+ requestId: string,
411
+ dependencies: ResidentCaptureDependencies = {}
412
+ ): Promise<{ body: unknown; status: number }> => {
413
+ const collection = context.config.collections.find(
414
+ (candidate) =>
415
+ candidate.name.toLowerCase() === input.collection.toLowerCase()
376
416
  );
417
+ if (!collection) {
418
+ throw new ResidentCapturePlanError({
419
+ ok: false,
420
+ code: "NOT_FOUND",
421
+ message: `Collection not found: ${input.collection}`,
422
+ status: 404,
423
+ });
424
+ }
425
+ const normalized = { ...input, collection: collection.name };
426
+ const published = await publishCapture({
427
+ ...residentPublication(context, store, collection, dependencies),
428
+ plan: async () => {
429
+ const planned = await planResidentCapture(context, store, normalized);
430
+ if (!planned.ok) throw new ResidentCapturePlanError(planned);
431
+ return planned.plan;
432
+ },
433
+ request: {
434
+ ...localRequestLedger(store.getDbPath()),
435
+ requestId,
436
+ input: normalized,
437
+ },
438
+ });
439
+ return {
440
+ body: { ...published.receipt, request: published.request },
441
+ status: residentCaptureStatus(published.receipt),
442
+ };
377
443
  };
378
444
 
379
445
  /**
@@ -0,0 +1,84 @@
1
+ import type { ServerContext } from "./context";
2
+
3
+ /** Read-only compiled context adapters; caller authority comes from the socket. */
4
+ import {
5
+ previewCompiledContext,
6
+ checkCompiledContext,
7
+ } from "../app/compiled-context";
8
+ import {
9
+ compiledContextPreviewInputSchema,
10
+ compiledContextCheckInputSchema,
11
+ } from "../core/compiled-context";
12
+ import {
13
+ isLocalClientRequest,
14
+ type RequestPeerServer,
15
+ } from "./request-locality";
16
+
17
+ export async function handleCompiledContext(
18
+ context: ServerContext,
19
+ request: Request,
20
+ server: RequestPeerServer | undefined,
21
+ operation: "preview" | "check"
22
+ ): Promise<Response> {
23
+ try {
24
+ // Bound bytes before JSON parsing, even when Content-Length is absent.
25
+ const reader = request.body?.getReader();
26
+ if (!reader)
27
+ return Response.json(
28
+ { error: { code: "invalid_input", message: "JSON body required" } },
29
+ { status: 400 }
30
+ );
31
+ const chunks: Uint8Array[] = [];
32
+ let size = 0;
33
+ while (true) {
34
+ const part = await reader.read();
35
+ if (part.done) break;
36
+ size += part.value.byteLength;
37
+ if (size > 8 * 1024 * 1024 + 65536) {
38
+ await reader.cancel();
39
+ return Response.json(
40
+ {
41
+ error: {
42
+ code: "invalid_input",
43
+ message: "Compiled context request exceeds byte limit",
44
+ },
45
+ },
46
+ { status: 413 }
47
+ );
48
+ }
49
+ chunks.push(part.value);
50
+ }
51
+ const raw: unknown = JSON.parse(
52
+ await new Blob(chunks.map((chunk) => new Uint8Array(chunk))).text()
53
+ );
54
+ const input =
55
+ operation === "preview"
56
+ ? compiledContextPreviewInputSchema.parse(raw)
57
+ : compiledContextCheckInputSchema.parse(raw);
58
+ const local = isLocalClientRequest(request, server);
59
+ const deps = {
60
+ store: context.store,
61
+ config: context.config,
62
+ indexName: context.indexName,
63
+ destinationZone: local ? ("local_process" as const) : ("remote" as const),
64
+ caller: { authenticated: local, operationAuthorized: true },
65
+ };
66
+ const result =
67
+ operation === "preview"
68
+ ? await previewCompiledContext(input, deps)
69
+ : await checkCompiledContext(input, deps);
70
+ return Response.json(result);
71
+ } catch {
72
+ // Supplied Capsules may contain private metadata; errors never echo inputs.
73
+ return Response.json(
74
+ {
75
+ error: {
76
+ code: "compiled_context_unavailable",
77
+ message:
78
+ "Compiled context is invalid, stale, or unavailable for this caller",
79
+ },
80
+ },
81
+ { status: 400 }
82
+ );
83
+ }
84
+ }
@@ -16,6 +16,7 @@ import {
16
16
  type MutationResult,
17
17
  } from "../core/config-mutation";
18
18
  import { withContentTypeRules } from "../ingestion";
19
+ import { watchedCollections } from "../sessions/config";
19
20
 
20
21
  /**
21
22
  * Apply a config mutation atomically with serialization.
@@ -56,7 +57,7 @@ export async function applyConfigChange(
56
57
 
57
58
  if (result.ok) {
58
59
  ctxHolder.watchService?.updateCollections(
59
- ctxHolder.config.collections,
60
+ watchedCollections(ctxHolder.config),
60
61
  withContentTypeRules({}, ctxHolder.config)
61
62
  );
62
63
  }
@@ -86,7 +87,7 @@ export async function applyConfigChangeTyped<T>(
86
87
 
87
88
  if (result.ok) {
88
89
  ctxHolder.watchService?.updateCollections(
89
- ctxHolder.config.collections,
90
+ watchedCollections(ctxHolder.config),
90
91
  withContentTypeRules({}, ctxHolder.config)
91
92
  );
92
93
  }
@@ -47,7 +47,9 @@ const Collections = lazy(() => import("./pages/Collections"));
47
47
  const Connectors = lazy(() => import("./pages/Connectors"));
48
48
  const Ask = lazy(() => import("./pages/Ask"));
49
49
  const GraphView = lazy(() => import("./pages/GraphView"));
50
+ const CompiledContext = lazy(() => import("./pages/CompiledContext"));
50
51
  const TraceHistory = lazy(() => import("./pages/TraceHistory"));
52
+ const Sessions = lazy(() => import("./pages/Sessions"));
51
53
 
52
54
  type Route =
53
55
  | "/"
@@ -59,7 +61,9 @@ type Route =
59
61
  | "/collections"
60
62
  | "/graph"
61
63
  | "/connectors"
62
- | "/traces";
64
+ | "/context/compiled"
65
+ | "/traces"
66
+ | "/sessions";
63
67
  type Navigate = (to: string | number) => void;
64
68
 
65
69
  interface RoutePageProps {
@@ -78,6 +82,8 @@ const routes: Record<Route, React.ComponentType<RoutePageProps>> = {
78
82
  "/ask": Ask,
79
83
  "/graph": GraphView,
80
84
  "/traces": TraceHistory,
85
+ "/sessions": Sessions,
86
+ "/context/compiled": CompiledContext,
81
87
  };
82
88
 
83
89
  interface AppContentProps {
@@ -196,6 +202,14 @@ function AppContent({
196
202
  Trace history
197
203
  </button>
198
204
  <span className="text-border/30">—</span>
205
+ <button
206
+ className="transition-colors duration-300 hover:text-primary"
207
+ onClick={() => navigate("/context/compiled")}
208
+ type="button"
209
+ >
210
+ Compiled context
211
+ </button>
212
+ <span className="text-border/30">—</span>
199
213
  <a
200
214
  className="transition-colors duration-300 hover:text-primary"
201
215
  href="https://github.com/gmickel/gno"
@@ -26,6 +26,13 @@ import {
26
26
  resolveNotePreset,
27
27
  } from "../../../core/note-presets";
28
28
  import { apiFetch } from "../hooks/use-api";
29
+ import {
30
+ clearRequestIntent,
31
+ type RequestIntent,
32
+ requestIdForIntent,
33
+ } from "../lib/request-intent";
34
+
35
+ const CAPTURE_INTENT_KEY = "gno:capture-intent";
29
36
  import { getActiveWikiLinkQuery } from "../lib/wiki-link";
30
37
  import { IndexingProgress } from "./IndexingProgress";
31
38
  import { TagInput } from "./TagInput";
@@ -169,6 +176,7 @@ export function CaptureModal({
169
176
  const [wikiLinkPosition, setWikiLinkPosition] = useState({ x: 24, y: 24 });
170
177
  const [wikiLinkActiveIndex, setWikiLinkActiveIndex] = useState(-1);
171
178
  const contentRef = useRef<HTMLTextAreaElement | null>(null);
179
+ const captureIntentRef = useRef<RequestIntent | null>(null);
172
180
 
173
181
  // Submission state
174
182
  const [state, setState] = useState<ModalState>("form");
@@ -311,19 +319,28 @@ export function CaptureModal({
311
319
  ? selectedPresetId
312
320
  : undefined;
313
321
 
322
+ const payload = {
323
+ collection,
324
+ title,
325
+ folderPath: defaultFolderPath || undefined,
326
+ content: presetOnly ? undefined : content,
327
+ presetId: submitPresetId,
328
+ collisionPolicy: "create_with_suffix",
329
+ source,
330
+ ...(tags.length > 0 && { tags }),
331
+ };
314
332
  const { data, error: err } = await apiFetch<CaptureResponse>(
315
333
  "/api/capture",
316
334
  {
317
335
  method: "POST",
318
336
  body: JSON.stringify({
319
- collection,
320
- title,
321
- folderPath: defaultFolderPath || undefined,
322
- content: presetOnly ? undefined : content,
323
- presetId: submitPresetId,
324
- collisionPolicy: "create_with_suffix",
325
- source,
326
- ...(tags.length > 0 && { tags }),
337
+ ...payload,
338
+ // A retry of this exact capture replays instead of adding a suffix copy.
339
+ requestId: requestIdForIntent(
340
+ captureIntentRef,
341
+ JSON.stringify(payload),
342
+ CAPTURE_INTENT_KEY
343
+ ),
327
344
  }),
328
345
  }
329
346
  );
@@ -335,6 +352,7 @@ export function CaptureModal({
335
352
  }
336
353
 
337
354
  if (data) {
355
+ clearRequestIntent(captureIntentRef, CAPTURE_INTENT_KEY);
338
356
  // Save last used collection
339
357
  localStorage.setItem(STORAGE_KEY, collection);
340
358