@gmickel/gno 2.5.1 → 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 (105) hide show
  1. package/README.md +60 -5
  2. package/assets/skill/README.md +5 -1
  3. package/assets/skill/SKILL.md +75 -1
  4. package/assets/skill/cli-reference.md +123 -0
  5. package/assets/skill/examples.md +30 -0
  6. package/assets/skill/mcp-reference.md +52 -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.5.1.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 +347 -24
  17. package/spec/mcp.md +198 -2
  18. package/spec/output-schemas/capture-receipt.schema.json +3 -0
  19. package/spec/output-schemas/mcp-capture-result.schema.json +3 -0
  20. package/spec/output-schemas/memory-remember.schema.json +8 -2
  21. package/spec/output-schemas/request-status.schema.json +113 -0
  22. package/spec/output-schemas/sessions-automation-run.schema.json +46 -0
  23. package/spec/output-schemas/sessions-discovery.schema.json +38 -0
  24. package/spec/output-schemas/sessions-import-receipt.schema.json +156 -0
  25. package/spec/output-schemas/sessions-status.schema.json +432 -0
  26. package/src/cli/commands/ask.ts +14 -2
  27. package/src/cli/commands/capture.ts +55 -96
  28. package/src/cli/commands/daemon.ts +41 -0
  29. package/src/cli/commands/ls.ts +3 -0
  30. package/src/cli/commands/memory.ts +12 -3
  31. package/src/cli/commands/request-status.ts +59 -0
  32. package/src/cli/commands/reset.ts +39 -5
  33. package/src/cli/commands/sessions.ts +713 -0
  34. package/src/cli/commands/shared.ts +14 -1
  35. package/src/cli/program.ts +388 -1
  36. package/src/cli/session-binding.ts +49 -0
  37. package/src/config/types.ts +8 -0
  38. package/src/core/capture-publish.ts +239 -0
  39. package/src/core/capture-sync.ts +3 -0
  40. package/src/core/memory-remember.ts +233 -122
  41. package/src/core/memory-types.ts +11 -0
  42. package/src/core/network-boundary-inventory.ts +8 -0
  43. package/src/core/request-receipts.ts +671 -0
  44. package/src/index.ts +9 -0
  45. package/src/mcp/context.ts +8 -0
  46. package/src/mcp/http-egress.ts +4 -0
  47. package/src/mcp/http-transport.ts +2 -0
  48. package/src/mcp/tools/capture.ts +87 -83
  49. package/src/mcp/tools/index.ts +66 -0
  50. package/src/mcp/tools/memory-remember.ts +7 -0
  51. package/src/mcp/tools/memory-shared.ts +7 -1
  52. package/src/mcp/tools/request-status.ts +73 -0
  53. package/src/mcp/tools/sessions.ts +208 -0
  54. package/src/sdk/client.ts +180 -84
  55. package/src/sdk/index.ts +6 -0
  56. package/src/sdk/types.ts +54 -2
  57. package/src/serve/capture-service.ts +98 -32
  58. package/src/serve/config-sync.ts +3 -2
  59. package/src/serve/public/app.tsx +4 -1
  60. package/src/serve/public/components/CaptureModal.tsx +26 -8
  61. package/src/serve/public/components/sessions/AutomationPanel.tsx +800 -0
  62. package/src/serve/public/components/sessions/ImportReceipt.tsx +238 -0
  63. package/src/serve/public/components/sessions/SessionSearch.tsx +286 -0
  64. package/src/serve/public/components/sessions/SourcesPanel.tsx +541 -0
  65. package/src/serve/public/components/sessions/api.ts +40 -0
  66. package/src/serve/public/components/sessions/snippet.tsx +53 -0
  67. package/src/serve/public/globals.built.css +1 -1
  68. package/src/serve/public/hooks/use-api.ts +10 -2
  69. package/src/serve/public/lib/request-intent.ts +69 -0
  70. package/src/serve/public/lib/workspace-actions.ts +12 -1
  71. package/src/serve/public/lib/workspace-tabs.ts +2 -0
  72. package/src/serve/public/pages/Dashboard.tsx +10 -0
  73. package/src/serve/public/pages/DocView.tsx +15 -1
  74. package/src/serve/public/pages/DocumentEditor.tsx +139 -96
  75. package/src/serve/public/pages/Sessions.tsx +350 -0
  76. package/src/serve/resident-runtime.ts +43 -3
  77. package/src/serve/routes/api.ts +476 -147
  78. package/src/serve/routes/sessions.ts +766 -0
  79. package/src/serve/security.ts +9 -0
  80. package/src/serve/server.ts +205 -1
  81. package/src/serve/session-automation.ts +146 -0
  82. package/src/sessions/archive.ts +348 -0
  83. package/src/sessions/automation-state.ts +444 -0
  84. package/src/sessions/automation-status.ts +239 -0
  85. package/src/sessions/automation.ts +1169 -0
  86. package/src/sessions/binding.ts +105 -0
  87. package/src/sessions/claude-hook.ts +240 -0
  88. package/src/sessions/config.ts +176 -0
  89. package/src/sessions/format.ts +191 -0
  90. package/src/sessions/import-child-env.ts +8 -0
  91. package/src/sessions/import-child.ts +152 -0
  92. package/src/sessions/parsers/claude-code.ts +259 -0
  93. package/src/sessions/parsers/codex.ts +303 -0
  94. package/src/sessions/parsers/hermes.ts +248 -0
  95. package/src/sessions/parsers/openclaw.ts +496 -0
  96. package/src/sessions/parsers/shared.ts +184 -0
  97. package/src/sessions/sanitize.ts +222 -0
  98. package/src/sessions/service.ts +1533 -0
  99. package/src/sessions/setup.ts +477 -0
  100. package/src/sessions/sources.ts +518 -0
  101. package/src/sessions/state.ts +118 -0
  102. package/src/sessions/types.ts +457 -0
  103. package/src/store/sqlite/adapter.ts +54 -15
  104. package/src/store/sqlite/scoped-index.ts +9 -0
  105. package/browser-extension/artifacts/gno-browser-clipper-v2.5.1.zip.sha256 +0 -1
@@ -69,6 +69,15 @@ export function isRequestAllowed(req: Request, port: number): boolean {
69
69
  }
70
70
 
71
71
  // Unsafe methods - require valid Origin or token
72
+ return isOriginOrTokenAllowed(req, port);
73
+ }
74
+
75
+ /**
76
+ * Origin/token check without the safe-method exemption. Owner-only reads
77
+ * that return host paths use it so a cross-origin page is refused on GET
78
+ * the same way as on POST.
79
+ */
80
+ export function isOriginOrTokenAllowed(req: Request, port: number): boolean {
72
81
  return validateToken(req) || validateOrigin(req, port);
73
82
  }
74
83
 
@@ -82,6 +82,7 @@ import {
82
82
  handleTrashDoc,
83
83
  handleUpdateCollection,
84
84
  handleUpdateCollectionEgressPolicy,
85
+ handleRequestStatus,
85
86
  handleUpdateDoc,
86
87
  handleVerifyConnector,
87
88
  } from "./routes/api";
@@ -101,6 +102,24 @@ import {
101
102
  handleCreateSectionTarget,
102
103
  handleResolveSectionTarget,
103
104
  } from "./routes/section-targets";
105
+ import {
106
+ handleSessionsAddSource,
107
+ handleSessionsAutomationDisable,
108
+ handleSessionsAutomationEnable,
109
+ handleSessionsAutomationPreview,
110
+ handleSessionsAutomationRemove,
111
+ handleSessionsAutomationRun,
112
+ handleSessionsAutomationSet,
113
+ handleSessionsDiscover,
114
+ handleSessionsImport,
115
+ handleSessionsInit,
116
+ handleSessionsRemoveSource,
117
+ handleSessionsStatus,
118
+ } from "./routes/sessions";
119
+
120
+ /** `/api/sessions/automation/:id[/...]` path parameter. */
121
+ const automationProfileId = (req: Request): string =>
122
+ decodeURIComponent(new URL(req.url).pathname.split("/")[4] ?? "");
104
123
  import {
105
124
  handleTraceDelete,
106
125
  handleTraceExport,
@@ -109,7 +128,11 @@ import {
109
128
  handleTracePurge,
110
129
  handleTraceShow,
111
130
  } from "./routes/traces";
112
- import { forbiddenResponse, isRequestAllowed } from "./security";
131
+ import {
132
+ forbiddenResponse,
133
+ isOriginOrTokenAllowed,
134
+ isRequestAllowed,
135
+ } from "./security";
113
136
  import {
114
137
  createSpaBundleSource,
115
138
  type SpaBundleSource,
@@ -566,6 +589,7 @@ export async function startServer(
566
589
  "/collections": spaPageRoute,
567
590
  "/connectors": spaPageRoute,
568
591
  "/traces": spaPageRoute,
592
+ "/sessions": spaPageRoute,
569
593
  "/context/compiled": spaPageRoute,
570
594
  "/ask": spaPageRoute,
571
595
  "/graph": spaPageRoute,
@@ -833,6 +857,174 @@ export async function startServer(
833
857
  );
834
858
  },
835
859
  },
860
+ "/api/sessions/status": {
861
+ GET: async (req: Request) =>
862
+ withSecurityHeaders(
863
+ await handleResidentRead(runtime as ResidentRuntime, req, () =>
864
+ handleSessionsStatus(ctxHolder)
865
+ ),
866
+ isDev
867
+ ),
868
+ },
869
+ "/api/sessions/discover": {
870
+ GET: async (req: Request, server: RequestPeerServer) => {
871
+ // Returns host paths: a cross-origin page is refused like a write.
872
+ if (!isOriginOrTokenAllowed(req, port)) {
873
+ return withSecurityHeaders(forbiddenResponse(), isDev);
874
+ }
875
+ return withSecurityHeaders(
876
+ await handleResidentRead(runtime as ResidentRuntime, req, () =>
877
+ handleSessionsDiscover(ctxHolder, req, { server })
878
+ ),
879
+ isDev
880
+ );
881
+ },
882
+ },
883
+ "/api/sessions/import": {
884
+ POST: async (req: Request) => {
885
+ if (!isRequestAllowed(req, port)) {
886
+ return withSecurityHeaders(forbiddenResponse(), isDev);
887
+ }
888
+ return withSecurityHeaders(
889
+ await handleSessionsImport(ctxHolder, req),
890
+ isDev
891
+ );
892
+ },
893
+ },
894
+ "/api/sessions/sources": {
895
+ POST: async (req: Request, server: RequestPeerServer) => {
896
+ if (!isRequestAllowed(req, port)) {
897
+ return withSecurityHeaders(forbiddenResponse(), isDev);
898
+ }
899
+ return withSecurityHeaders(
900
+ await handleSessionsAddSource(ctxHolder, store, req, { server }),
901
+ isDev
902
+ );
903
+ },
904
+ },
905
+ "/api/sessions/sources/:id": {
906
+ DELETE: async (req: Request, server: RequestPeerServer) => {
907
+ if (!isRequestAllowed(req, port)) {
908
+ return withSecurityHeaders(forbiddenResponse(), isDev);
909
+ }
910
+ const id = decodeURIComponent(
911
+ new URL(req.url).pathname.split("/")[4] ?? ""
912
+ );
913
+ return withSecurityHeaders(
914
+ await handleSessionsRemoveSource(ctxHolder, store, id, req, {
915
+ server,
916
+ }),
917
+ isDev
918
+ );
919
+ },
920
+ },
921
+ "/api/sessions/automation/run": {
922
+ POST: async (req: Request) => {
923
+ if (!isRequestAllowed(req, port)) {
924
+ return withSecurityHeaders(forbiddenResponse(), isDev);
925
+ }
926
+ return withSecurityHeaders(
927
+ await handleSessionsAutomationRun(ctxHolder, store, req),
928
+ isDev
929
+ );
930
+ },
931
+ },
932
+ "/api/sessions/automation/:id": {
933
+ PUT: async (req: Request, server: RequestPeerServer) => {
934
+ if (!isRequestAllowed(req, port)) {
935
+ return withSecurityHeaders(forbiddenResponse(), isDev);
936
+ }
937
+ return withSecurityHeaders(
938
+ await handleSessionsAutomationSet(
939
+ ctxHolder,
940
+ store,
941
+ automationProfileId(req),
942
+ req,
943
+ { server }
944
+ ),
945
+ isDev
946
+ );
947
+ },
948
+ DELETE: async (req: Request, server: RequestPeerServer) => {
949
+ if (!isRequestAllowed(req, port)) {
950
+ return withSecurityHeaders(forbiddenResponse(), isDev);
951
+ }
952
+ return withSecurityHeaders(
953
+ await handleSessionsAutomationRemove(
954
+ ctxHolder,
955
+ store,
956
+ automationProfileId(req),
957
+ req,
958
+ { server }
959
+ ),
960
+ isDev
961
+ );
962
+ },
963
+ },
964
+ "/api/sessions/automation/:id/preview": {
965
+ GET: async (req: Request, server: RequestPeerServer) =>
966
+ !isOriginOrTokenAllowed(req, port)
967
+ ? withSecurityHeaders(forbiddenResponse(), isDev)
968
+ : withSecurityHeaders(
969
+ await handleResidentRead(
970
+ runtime as ResidentRuntime,
971
+ req,
972
+ () =>
973
+ handleSessionsAutomationPreview(
974
+ ctxHolder,
975
+ automationProfileId(req),
976
+ req,
977
+ { server }
978
+ )
979
+ ),
980
+ isDev
981
+ ),
982
+ },
983
+ "/api/sessions/automation/:id/enable": {
984
+ POST: async (req: Request, server: RequestPeerServer) => {
985
+ if (!isRequestAllowed(req, port)) {
986
+ return withSecurityHeaders(forbiddenResponse(), isDev);
987
+ }
988
+ return withSecurityHeaders(
989
+ await handleSessionsAutomationEnable(
990
+ ctxHolder,
991
+ store,
992
+ automationProfileId(req),
993
+ req,
994
+ { server }
995
+ ),
996
+ isDev
997
+ );
998
+ },
999
+ },
1000
+ "/api/sessions/automation/:id/disable": {
1001
+ POST: async (req: Request, server: RequestPeerServer) => {
1002
+ if (!isRequestAllowed(req, port)) {
1003
+ return withSecurityHeaders(forbiddenResponse(), isDev);
1004
+ }
1005
+ return withSecurityHeaders(
1006
+ await handleSessionsAutomationDisable(
1007
+ ctxHolder,
1008
+ store,
1009
+ automationProfileId(req),
1010
+ req,
1011
+ { server }
1012
+ ),
1013
+ isDev
1014
+ );
1015
+ },
1016
+ },
1017
+ "/api/sessions/init": {
1018
+ POST: async (req: Request, server: RequestPeerServer) => {
1019
+ if (!isRequestAllowed(req, port)) {
1020
+ return withSecurityHeaders(forbiddenResponse(), isDev);
1021
+ }
1022
+ return withSecurityHeaders(
1023
+ await handleSessionsInit(ctxHolder, store, req, { server }),
1024
+ isDev
1025
+ );
1026
+ },
1027
+ },
836
1028
  "/api/memory/remember": {
837
1029
  POST: async (req: Request) => {
838
1030
  if (!isRequestAllowed(req, port)) {
@@ -1046,6 +1238,18 @@ export async function startServer(
1046
1238
  );
1047
1239
  },
1048
1240
  },
1241
+ "/api/requests/:requestId": {
1242
+ GET: async (req: Request) => {
1243
+ const url = new URL(req.url);
1244
+ const requestId = decodeURIComponent(
1245
+ url.pathname.split("/").pop() || ""
1246
+ );
1247
+ return withSecurityHeaders(
1248
+ await handleRequestStatus(store, requestId),
1249
+ isDev
1250
+ );
1251
+ },
1252
+ },
1049
1253
  "/api/doc": {
1050
1254
  GET: async (req: Request) => {
1051
1255
  const url = new URL(req.url);
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Daemon-only session automation tick: heartbeat, coalesced schedule
3
+ * admissions and a drain of pending profiles through the manual importer.
4
+ *
5
+ * Runs only in `gno daemon` on a session-archive config (never in `serve`).
6
+ * One tick at a time over the daemon's background-work tracker; the owner
7
+ * config is reread every tick, so enable/disable take effect without a
8
+ * restart. Imports take the shared write lease; a busy lease backs off.
9
+ */
10
+
11
+ import type { SessionAutomationRunResult } from "../sessions/types";
12
+ import type { SqliteAdapter } from "../store/sqlite/adapter";
13
+
14
+ import { acquireCliWriteLease } from "../core/write-lease";
15
+ import { heartbeatAutomation, tickAutomation } from "../sessions/automation";
16
+ import { AUTOMATION_TICK_MS } from "../sessions/automation-state";
17
+
18
+ const LEASE_HOLDER_COMMAND = "gno daemon (session automation)";
19
+
20
+ /**
21
+ * The importer syncs the lexical index itself, so the resident watcher later
22
+ * sees unchanged files and never queues embedding: do it here for the
23
+ * collections a run synced, and mark the mutation (as the REST import does).
24
+ */
25
+ export function notifyAutomationImport(
26
+ result: SessionAutomationRunResult,
27
+ sink: {
28
+ markMutation: () => void;
29
+ notifySyncComplete: (collections: string[]) => void;
30
+ }
31
+ ): void {
32
+ const collections = [
33
+ ...new Set(
34
+ result.receipts.flatMap((receipt) => receipt.lexical.collections)
35
+ ),
36
+ ];
37
+ if (collections.length === 0) return;
38
+ sink.markMutation();
39
+ sink.notifySyncComplete(collections);
40
+ }
41
+
42
+ export interface SessionAutomationSchedulerOptions {
43
+ store: SqliteAdapter;
44
+ configPath: string;
45
+ indexName: string;
46
+ dbPath: string;
47
+ startBackgroundWork: (
48
+ operation: (signal: AbortSignal) => Promise<void>
49
+ ) => boolean;
50
+ /** Called after every run; the daemon logs content-free counts. */
51
+ onResult?: (result: SessionAutomationRunResult) => void;
52
+ onError?: (error: unknown) => void;
53
+ now?: () => Date;
54
+ tickMs?: number;
55
+ }
56
+
57
+ export class SessionAutomationScheduler {
58
+ readonly #options: SessionAutomationSchedulerOptions;
59
+ readonly #startedAt: Date;
60
+ #timer: ReturnType<typeof setTimeout> | null = null;
61
+ #heartbeat: ReturnType<typeof setInterval> | null = null;
62
+ #running: Promise<void> | null = null;
63
+ #disposed = false;
64
+
65
+ constructor(options: SessionAutomationSchedulerOptions) {
66
+ this.#options = options;
67
+ this.#startedAt = (options.now ?? (() => new Date()))();
68
+ }
69
+
70
+ /** Drain once at startup, then tick; heartbeat independently of ticks. */
71
+ start(): void {
72
+ this.#beat();
73
+ this.#heartbeat = setInterval(
74
+ () => this.#beat(),
75
+ this.#options.tickMs ?? AUTOMATION_TICK_MS
76
+ );
77
+ this.#heartbeat.unref?.();
78
+ this.#schedule(0);
79
+ }
80
+
81
+ /** A tick or import in progress must not let the heartbeat go stale. */
82
+ #beat(): void {
83
+ if (this.#disposed) return;
84
+ const options = this.#options;
85
+ heartbeatAutomation({
86
+ configPath: options.configPath,
87
+ indexName: options.indexName,
88
+ now: options.now,
89
+ daemonStartedAt: this.#startedAt,
90
+ }).catch((error: unknown) => options.onError?.(error));
91
+ }
92
+
93
+ /** One tick; coalesces with a tick already in flight. */
94
+ tick(): Promise<void> {
95
+ if (this.#running) return this.#running;
96
+ const options = this.#options;
97
+ const run = tickAutomation({
98
+ configPath: options.configPath,
99
+ indexName: options.indexName,
100
+ store: options.store,
101
+ now: options.now,
102
+ daemonStartedAt: this.#startedAt,
103
+ // The daemon serves MCP and status over HTTP: keep its loop free.
104
+ inChildProcess: true,
105
+ acquireLease: async () => {
106
+ const lease = await acquireCliWriteLease({
107
+ dbPath: options.dbPath,
108
+ waitMs: 0,
109
+ noWait: true,
110
+ command: LEASE_HOLDER_COMMAND,
111
+ });
112
+ return lease.ok ? { ok: true, release: lease.release } : { ok: false };
113
+ },
114
+ })
115
+ .then((results) => {
116
+ for (const result of results) options.onResult?.(result);
117
+ })
118
+ .catch((error: unknown) => options.onError?.(error))
119
+ .finally(() => {
120
+ this.#running = null;
121
+ this.#schedule(options.tickMs ?? AUTOMATION_TICK_MS);
122
+ });
123
+ this.#running = run;
124
+ return run;
125
+ }
126
+
127
+ dispose(): void {
128
+ this.#disposed = true;
129
+ if (this.#heartbeat) clearInterval(this.#heartbeat);
130
+ this.#heartbeat = null;
131
+ if (this.#timer) clearTimeout(this.#timer);
132
+ this.#timer = null;
133
+ }
134
+
135
+ #schedule(delayMs: number): void {
136
+ if (this.#disposed) return;
137
+ if (this.#timer) clearTimeout(this.#timer);
138
+ this.#timer = setTimeout(() => {
139
+ this.#timer = null;
140
+ if (this.#disposed || this.#running) return;
141
+ const started = this.#options.startBackgroundWork(() => this.tick());
142
+ if (!started) this.#schedule(this.#options.tickMs ?? AUTOMATION_TICK_MS);
143
+ }, delayMs);
144
+ this.#timer.unref?.();
145
+ }
146
+ }