@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
@@ -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
 
@@ -1,3 +1,4 @@
1
+ import type { HttpGatewayOverrides } from "../mcp/http-security";
1
2
  /**
2
3
  * Bun.serve() web server for GNO web UI.
3
4
  * Uses Bun's fullstack dev server with HTML imports.
@@ -5,8 +6,6 @@
5
6
  *
6
7
  * @module src/serve/server
7
8
  */
8
-
9
- import type { HttpGatewayOverrides } from "../mcp/http-security";
10
9
  import type { RequestPeerServer } from "./request-locality";
11
10
  import type { ResidentRuntime } from "./resident-runtime";
12
11
  import type { ContextHolder } from "./routes/api";
@@ -16,6 +15,7 @@ import {
16
15
  resolveHttpGatewayConfig,
17
16
  } from "../mcp/http-security";
18
17
  import { startBackgroundRuntime } from "./background-runtime";
18
+ import { handleCompiledContext } from "./compiled-context";
19
19
  import { handleContextBuild, handleContextVerify } from "./context-capsule";
20
20
  import { DocumentEventBus } from "./doc-events";
21
21
  import {
@@ -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,8 @@ export async function startServer(
566
589
  "/collections": spaPageRoute,
567
590
  "/connectors": spaPageRoute,
568
591
  "/traces": spaPageRoute,
592
+ "/sessions": spaPageRoute,
593
+ "/context/compiled": spaPageRoute,
569
594
  "/ask": spaPageRoute,
570
595
  "/graph": spaPageRoute,
571
596
  "/clipper/pair": spaPageRoute,
@@ -832,6 +857,174 @@ export async function startServer(
832
857
  );
833
858
  },
834
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
+ },
835
1028
  "/api/memory/remember": {
836
1029
  POST: async (req: Request) => {
837
1030
  if (!isRequestAllowed(req, port)) {
@@ -1045,6 +1238,18 @@ export async function startServer(
1045
1238
  );
1046
1239
  },
1047
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
+ },
1048
1253
  "/api/doc": {
1049
1254
  GET: async (req: Request) => {
1050
1255
  const url = new URL(req.url);
@@ -1164,6 +1369,46 @@ export async function startServer(
1164
1369
  );
1165
1370
  },
1166
1371
  },
1372
+ "/api/context/compiled/preview": {
1373
+ POST: async (req: Request, server: RequestPeerServer) => {
1374
+ if (!isRequestAllowed(req, port))
1375
+ return withSecurityHeaders(forbiddenResponse(), isDev);
1376
+ return withSecurityHeaders(
1377
+ await handleResidentRead(
1378
+ runtime as ResidentRuntime,
1379
+ req,
1380
+ (signal) =>
1381
+ handleCompiledContext(
1382
+ ctxHolder.current,
1383
+ new Request(req, { signal }),
1384
+ { requestIP: () => server.requestIP(req) },
1385
+ "preview"
1386
+ )
1387
+ ),
1388
+ isDev
1389
+ );
1390
+ },
1391
+ },
1392
+ "/api/context/compiled/check": {
1393
+ POST: async (req: Request, server: RequestPeerServer) => {
1394
+ if (!isRequestAllowed(req, port))
1395
+ return withSecurityHeaders(forbiddenResponse(), isDev);
1396
+ return withSecurityHeaders(
1397
+ await handleResidentRead(
1398
+ runtime as ResidentRuntime,
1399
+ req,
1400
+ (signal) =>
1401
+ handleCompiledContext(
1402
+ ctxHolder.current,
1403
+ new Request(req, { signal }),
1404
+ { requestIP: () => server.requestIP(req) },
1405
+ "check"
1406
+ )
1407
+ ),
1408
+ isDev
1409
+ );
1410
+ },
1411
+ },
1167
1412
  "/api/context/verify": {
1168
1413
  POST: async (req: Request) => {
1169
1414
  if (!isRequestAllowed(req, port)) {
@@ -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
+ }
@@ -79,12 +79,13 @@ const contentTypeFor = (path: string): string => {
79
79
  return "application/octet-stream";
80
80
  };
81
81
 
82
- export const isStandaloneExecutable = (): boolean =>
83
- import.meta.path.includes("/$bunfs/") ||
84
- import.meta.path.includes("\\$bunfs\\");
82
+ export const isBunfsPath = (path: string): boolean => {
83
+ const normalized = path.replaceAll("\\", "/");
84
+ return normalized.includes("/$bunfs/") || normalized.startsWith("B:/~BUN/");
85
+ };
85
86
 
86
- export const isBunfsPath = (path: string): boolean =>
87
- path.includes("/$bunfs/") || path.includes("\\$bunfs\\");
87
+ export const isStandaloneExecutable = (): boolean =>
88
+ isBunfsPath(import.meta.path);
88
89
 
89
90
  const rewriteProductionHtml = (html: string, jsEntryPath: string): string => {
90
91
  const script = `<script type="module" src="/${basename(jsEntryPath)}"></script>`;