@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.
- package/README.md +69 -6
- package/assets/skill/README.md +5 -1
- package/assets/skill/SKILL.md +98 -1
- package/assets/skill/cli-reference.md +140 -0
- package/assets/skill/examples.md +45 -0
- package/assets/skill/mcp-reference.md +62 -0
- package/assets/skill/recipes/capture-and-file.md +6 -0
- package/assets/skill/recipes/memory-file-decision.md +6 -0
- package/assets/skill/recipes/memory-supersede-fact.md +5 -0
- package/assets/skill/recipes/session-evidence-lookup.md +98 -0
- package/assets/spa-production.json.gz +0 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v2.4.0.zip → gno-browser-clipper-v2.6.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.6.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +2 -1
- package/spec/cli.md +358 -24
- package/spec/compiled-context.md +68 -0
- package/spec/mcp.md +223 -2
- package/spec/output-schemas/capture-receipt.schema.json +3 -0
- package/spec/output-schemas/compiled-context-check.schema.json +44 -0
- package/spec/output-schemas/compiled-context-file.schema.json +165 -0
- package/spec/output-schemas/compiled-context-preview.schema.json +142 -0
- package/spec/output-schemas/mcp-capture-result.schema.json +3 -0
- package/spec/output-schemas/memory-remember.schema.json +8 -2
- package/spec/output-schemas/request-status.schema.json +113 -0
- package/spec/output-schemas/sessions-automation-run.schema.json +46 -0
- package/spec/output-schemas/sessions-discovery.schema.json +38 -0
- package/spec/output-schemas/sessions-import-receipt.schema.json +156 -0
- package/spec/output-schemas/sessions-status.schema.json +432 -0
- package/src/app/compiled-context-files.ts +361 -0
- package/src/app/compiled-context.ts +240 -0
- package/src/app/context-surface.ts +7 -2
- package/src/cli/commands/ask.ts +14 -2
- package/src/cli/commands/audit.ts +4 -0
- package/src/cli/commands/capture.ts +55 -96
- package/src/cli/commands/context-compiled.ts +136 -0
- package/src/cli/commands/daemon.ts +41 -0
- package/src/cli/commands/ls.ts +3 -0
- package/src/cli/commands/memory.ts +12 -3
- package/src/cli/commands/request-status.ts +59 -0
- package/src/cli/commands/reset.ts +39 -5
- package/src/cli/commands/sessions.ts +713 -0
- package/src/cli/commands/shared.ts +14 -1
- package/src/cli/errors.ts +10 -3
- package/src/cli/program.ts +458 -1
- package/src/cli/session-binding.ts +49 -0
- package/src/config/types.ts +8 -0
- package/src/core/capture-publish.ts +239 -0
- package/src/core/capture-sync.ts +3 -0
- package/src/core/compiled-context.ts +254 -0
- package/src/core/context-budget.ts +2 -10
- package/src/core/file-lock.ts +22 -5
- package/src/core/folder-setup-planning.ts +2 -1
- package/src/core/memory-remember.ts +233 -122
- package/src/core/memory-types.ts +11 -0
- package/src/core/network-boundary-inventory.ts +16 -0
- package/src/core/request-receipts.ts +671 -0
- package/src/core/setup-receipt.ts +27 -20
- package/src/core/typed-metadata.ts +4 -0
- package/src/core/validation.ts +9 -2
- package/src/core/windows-private-path.ts +96 -0
- package/src/index.ts +11 -2
- package/src/ingestion/compiled-context.ts +15 -0
- package/src/ingestion/sync.ts +40 -8
- package/src/ingestion/walker.ts +5 -4
- package/src/llm/nodeLlamaCpp/simulator-install.ts +6 -2
- package/src/mcp/context.ts +8 -0
- package/src/mcp/http-egress.ts +15 -3
- package/src/mcp/http-transport.ts +2 -0
- package/src/mcp/retrieval-warnings.ts +24 -0
- package/src/mcp/tools/ask.ts +14 -1
- package/src/mcp/tools/capture.ts +87 -83
- package/src/mcp/tools/context.ts +46 -2
- package/src/mcp/tools/index.ts +119 -7
- package/src/mcp/tools/memory-remember.ts +7 -0
- package/src/mcp/tools/memory-shared.ts +7 -1
- package/src/mcp/tools/query.ts +3 -1
- package/src/mcp/tools/request-status.ts +73 -0
- package/src/mcp/tools/search.ts +3 -1
- package/src/mcp/tools/sessions.ts +208 -0
- package/src/mcp/tools/vsearch.ts +3 -1
- package/src/sdk/client.ts +252 -89
- package/src/sdk/index.ts +13 -0
- package/src/sdk/types.ts +82 -3
- package/src/serve/capture-service.ts +98 -32
- package/src/serve/compiled-context.ts +84 -0
- package/src/serve/config-sync.ts +3 -2
- package/src/serve/public/app.tsx +15 -1
- package/src/serve/public/components/CaptureModal.tsx +26 -8
- package/src/serve/public/components/sessions/AutomationPanel.tsx +800 -0
- package/src/serve/public/components/sessions/ImportReceipt.tsx +238 -0
- package/src/serve/public/components/sessions/SessionSearch.tsx +286 -0
- package/src/serve/public/components/sessions/SourcesPanel.tsx +541 -0
- package/src/serve/public/components/sessions/api.ts +40 -0
- package/src/serve/public/components/sessions/snippet.tsx +53 -0
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/hooks/use-api.ts +10 -2
- package/src/serve/public/lib/request-intent.ts +69 -0
- package/src/serve/public/lib/workspace-actions.ts +12 -1
- package/src/serve/public/lib/workspace-tabs.ts +4 -0
- package/src/serve/public/pages/CompiledContext.tsx +364 -0
- package/src/serve/public/pages/Dashboard.tsx +17 -0
- package/src/serve/public/pages/DocView.tsx +15 -1
- package/src/serve/public/pages/DocumentEditor.tsx +139 -96
- package/src/serve/public/pages/Sessions.tsx +350 -0
- package/src/serve/resident-runtime.ts +43 -3
- package/src/serve/routes/api.ts +476 -147
- package/src/serve/routes/sessions.ts +766 -0
- package/src/serve/security.ts +9 -0
- package/src/serve/server.ts +248 -3
- package/src/serve/session-automation.ts +146 -0
- package/src/serve/spa-production-build.ts +6 -5
- package/src/sessions/archive.ts +348 -0
- package/src/sessions/automation-state.ts +444 -0
- package/src/sessions/automation-status.ts +239 -0
- package/src/sessions/automation.ts +1169 -0
- package/src/sessions/binding.ts +105 -0
- package/src/sessions/claude-hook.ts +240 -0
- package/src/sessions/config.ts +176 -0
- package/src/sessions/format.ts +191 -0
- package/src/sessions/import-child-env.ts +8 -0
- package/src/sessions/import-child.ts +152 -0
- package/src/sessions/parsers/claude-code.ts +259 -0
- package/src/sessions/parsers/codex.ts +303 -0
- package/src/sessions/parsers/hermes.ts +248 -0
- package/src/sessions/parsers/openclaw.ts +496 -0
- package/src/sessions/parsers/shared.ts +184 -0
- package/src/sessions/sanitize.ts +222 -0
- package/src/sessions/service.ts +1533 -0
- package/src/sessions/setup.ts +477 -0
- package/src/sessions/sources.ts +518 -0
- package/src/sessions/state.ts +118 -0
- package/src/sessions/types.ts +457 -0
- package/src/store/sqlite/adapter.ts +68 -16
- package/src/store/sqlite/scoped-index.ts +9 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.4.0.zip.sha256 +0 -1
package/src/serve/security.ts
CHANGED
|
@@ -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
|
|
package/src/serve/server.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
87
|
-
|
|
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>`;
|