theokit 0.1.0-alpha.8 → 0.2.1

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 (138) hide show
  1. package/dist/actions-virtual-module-E6PU47GV.js +175 -0
  2. package/dist/actions-virtual-module-E6PU47GV.js.map +1 -0
  3. package/dist/actions-virtual-module-E7IELVHL.js +174 -0
  4. package/dist/actions-virtual-module-E7IELVHL.js.map +1 -0
  5. package/dist/add-2ZFFGGE4.js +0 -0
  6. package/dist/app-typed-client-4L4L4BWD.js +289 -0
  7. package/dist/app-typed-client-4L4L4BWD.js.map +1 -0
  8. package/dist/app-typed-client-6PAT6VOL.js +289 -0
  9. package/dist/app-typed-client-6PAT6VOL.js.map +1 -0
  10. package/dist/aws-lambda-PONVF3UX.js +0 -0
  11. package/dist/broadcast-QOQGUNNK.js +0 -0
  12. package/dist/{build-IQQYCA3B.js → build-EWZDEDT3.js} +37 -11
  13. package/dist/build-EWZDEDT3.js.map +1 -0
  14. package/dist/build-request-body-preview-EOP2Y4F3.js +52 -0
  15. package/dist/build-request-body-preview-EOP2Y4F3.js.map +1 -0
  16. package/dist/build-request-body-preview-II2235E6.js +54 -0
  17. package/dist/build-request-body-preview-II2235E6.js.map +1 -0
  18. package/dist/bun-DITMZMKL.js +0 -0
  19. package/dist/check-PZR67OY5.js +0 -0
  20. package/dist/{chunk-GQSTRXXB.js → chunk-2BQYEBCK.js} +77 -68
  21. package/dist/chunk-2BQYEBCK.js.map +1 -0
  22. package/dist/{chunk-FRBYJANT.js → chunk-4W447H7M.js} +195 -41
  23. package/dist/chunk-4W447H7M.js.map +1 -0
  24. package/dist/chunk-5ODOE6EF.js +0 -0
  25. package/dist/chunk-7DHGV77J.js +140 -0
  26. package/dist/chunk-7DHGV77J.js.map +1 -0
  27. package/dist/{chunk-WQCPKA2L.js → chunk-C27AZN46.js} +570 -165
  28. package/dist/chunk-C27AZN46.js.map +1 -0
  29. package/dist/chunk-ERAZNHHR.js +73 -0
  30. package/dist/chunk-ERAZNHHR.js.map +1 -0
  31. package/dist/chunk-F3TG5FJV.js +97 -0
  32. package/dist/chunk-F3TG5FJV.js.map +1 -0
  33. package/dist/chunk-KJVEJILQ.js +109 -0
  34. package/dist/chunk-KJVEJILQ.js.map +1 -0
  35. package/dist/chunk-KT3MWNZG.js +0 -0
  36. package/dist/{chunk-MCP6Y5BA.js → chunk-M23FP2AS.js} +25 -1
  37. package/dist/chunk-M23FP2AS.js.map +1 -0
  38. package/dist/{chunk-S3JBIFKS.js → chunk-MG7H3LAW.js} +2 -2
  39. package/dist/chunk-MOT42NAH.js +165 -0
  40. package/dist/chunk-MOT42NAH.js.map +1 -0
  41. package/dist/chunk-O7335ZGH.js +0 -0
  42. package/dist/{chunk-OEB3OIYP.js → chunk-OJHO2X7C.js} +497 -168
  43. package/dist/chunk-OJHO2X7C.js.map +1 -0
  44. package/dist/chunk-QLGES2HC.js +189 -0
  45. package/dist/chunk-QLGES2HC.js.map +1 -0
  46. package/dist/{chunk-JZ5WEG3S.js → chunk-SEVYARIU.js} +9 -5
  47. package/dist/chunk-SEVYARIU.js.map +1 -0
  48. package/dist/{chunk-ZK5JCE5J.js → chunk-TFECHXR5.js} +201 -43
  49. package/dist/chunk-TFECHXR5.js.map +1 -0
  50. package/dist/chunk-WSJKACWB.js +17 -0
  51. package/dist/chunk-WSJKACWB.js.map +1 -0
  52. package/dist/chunk-WZ7CPVQB.js +0 -0
  53. package/dist/chunk-XMS5VRIK.js +0 -0
  54. package/dist/chunk-Y5KE6YFP.js +0 -0
  55. package/dist/{chunk-3Q36SEDK.js → chunk-ZBWROIIO.js} +43 -2
  56. package/dist/{chunk-3Q36SEDK.js.map → chunk-ZBWROIIO.js.map} +1 -1
  57. package/dist/chunk-ZOXNJV2O.js +358 -0
  58. package/dist/chunk-ZOXNJV2O.js.map +1 -0
  59. package/dist/cli/index.js +22 -7
  60. package/dist/cli/index.js.map +1 -1
  61. package/dist/client/index.d.ts +31 -1
  62. package/dist/client/index.js +100 -0
  63. package/dist/client/index.js.map +1 -1
  64. package/dist/cloudflare-RBANU3SE.js +0 -0
  65. package/dist/configure-agent-registry-6YGTJYBC.js +0 -0
  66. package/dist/deno-deploy-J7LI6HNL.js +0 -0
  67. package/dist/{dev-VN6WS3CM.js → dev-74KPI5C5.js} +15 -7
  68. package/dist/dev-74KPI5C5.js.map +1 -0
  69. package/dist/dev-emit-3CHM73TV.js +390 -0
  70. package/dist/dev-emit-3CHM73TV.js.map +1 -0
  71. package/dist/dev-emit-I3CKOHML.js +44 -0
  72. package/dist/dev-emit-I3CKOHML.js.map +1 -0
  73. package/dist/devtools/entry.js +653 -243
  74. package/dist/devtools/entry.js.map +1 -1
  75. package/dist/dispatcher-AQJGI3HU.js +8 -0
  76. package/dist/dispatcher-E6QV32BO.js +83 -0
  77. package/dist/dispatcher-E6QV32BO.js.map +1 -0
  78. package/dist/{dist-UZV7J3YI.js → dist-R2Q7GVDW.js} +10412 -11961
  79. package/dist/dist-R2Q7GVDW.js.map +1 -0
  80. package/dist/docker-EZDA5FT7.js +0 -0
  81. package/dist/{generate-SIO4JSKJ.js → generate-DT4IIAHF.js} +88 -22
  82. package/dist/generate-DT4IIAHF.js.map +1 -0
  83. package/dist/index.d.ts +108 -1
  84. package/dist/index.js +5 -2
  85. package/dist/index.js.map +1 -1
  86. package/dist/{info-B3UOXFUF.js → info-VM7EK4JC.js} +3 -3
  87. package/dist/lib-NST3PNZX.js +0 -0
  88. package/dist/netlify-IFY6ZGFV.js +0 -0
  89. package/dist/node-KWZI3CKV.js +0 -0
  90. package/dist/openapi-CBASKC7W.js +84 -0
  91. package/dist/openapi-CBASKC7W.js.map +1 -0
  92. package/dist/proper-lockfile-MVKMQGFK.js +0 -0
  93. package/dist/{registry-BB3UYAOG.js → registry-QGSNLJKJ.js} +2 -2
  94. package/dist/{routes-CAGFZKUK.js → routes-7O4NUDYS.js} +9 -7
  95. package/dist/{routes-CAGFZKUK.js.map → routes-7O4NUDYS.js.map} +1 -1
  96. package/dist/scan-C2BOKLSY.js +0 -0
  97. package/dist/{schema-Cxq2qqwh.d.ts → schema-CjVly9c_.d.ts} +43 -1
  98. package/dist/{schema-MVB2FVK6.js → schema-JAKF23GR.js} +2 -2
  99. package/dist/server/cost/index.d.ts +7 -0
  100. package/dist/server/cost/index.js +1 -1
  101. package/dist/server/index.d.ts +219 -47
  102. package/dist/server/index.js +101 -114
  103. package/dist/server/index.js.map +1 -1
  104. package/dist/services-typed-client-ZX5JUHH3.js +0 -0
  105. package/dist/sqlite-vec-54KB4RD3.js +0 -0
  106. package/dist/{start-CSLJQXV3.js → start-5JTFWF4T.js} +20 -14
  107. package/dist/start-5JTFWF4T.js.map +1 -0
  108. package/dist/{static-BH6JQNM2.js → static-6PZZBY7G.js} +4 -4
  109. package/dist/storage-manager-D5KBXXKB.js +0 -0
  110. package/dist/theo-cloud-3K4DGB3S.js +0 -0
  111. package/dist/{upgrade-readiness-OHL36FQL.js → upgrade-readiness-Q46KTG2J.js} +6 -1
  112. package/dist/upgrade-readiness-Q46KTG2J.js.map +1 -0
  113. package/dist/vercel-RHADE5IP.js +0 -0
  114. package/dist/vite-plugin/index.d.ts +25 -1
  115. package/dist/vite-plugin/index.js +5 -2
  116. package/dist/{vite-plugin-WA3G45YY.js → vite-plugin-3EQP2JYM.js} +7 -6
  117. package/dist/vite-plugin-3EQP2JYM.js.map +1 -0
  118. package/package.json +10 -8
  119. package/LICENSE +0 -201
  120. package/dist/build-IQQYCA3B.js.map +0 -1
  121. package/dist/chunk-FRBYJANT.js.map +0 -1
  122. package/dist/chunk-GQSTRXXB.js.map +0 -1
  123. package/dist/chunk-JZ5WEG3S.js.map +0 -1
  124. package/dist/chunk-MCP6Y5BA.js.map +0 -1
  125. package/dist/chunk-OEB3OIYP.js.map +0 -1
  126. package/dist/chunk-WQCPKA2L.js.map +0 -1
  127. package/dist/chunk-ZK5JCE5J.js.map +0 -1
  128. package/dist/dev-VN6WS3CM.js.map +0 -1
  129. package/dist/dist-UZV7J3YI.js.map +0 -1
  130. package/dist/generate-SIO4JSKJ.js.map +0 -1
  131. package/dist/start-CSLJQXV3.js.map +0 -1
  132. package/dist/upgrade-readiness-OHL36FQL.js.map +0 -1
  133. /package/dist/{chunk-S3JBIFKS.js.map → chunk-MG7H3LAW.js.map} +0 -0
  134. /package/dist/{schema-MVB2FVK6.js.map → dispatcher-AQJGI3HU.js.map} +0 -0
  135. /package/dist/{info-B3UOXFUF.js.map → info-VM7EK4JC.js.map} +0 -0
  136. /package/dist/{registry-BB3UYAOG.js.map → registry-QGSNLJKJ.js.map} +0 -0
  137. /package/dist/{vite-plugin-WA3G45YY.js.map → schema-JAKF23GR.js.map} +0 -0
  138. /package/dist/{static-BH6JQNM2.js.map → static-6PZZBY7G.js.map} +0 -0
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env node
2
+ import "tsx/esm";
3
+
4
+ // src/cli/preflight-node-version.ts
5
+ import { existsSync, readFileSync } from "fs";
6
+ import { createRequire } from "module";
7
+ import { join } from "path";
8
+ var NODE_MIN_MAJOR = 22;
9
+ var NODE_MIN_FULL = "22.12.0";
10
+ var BINDINGS_TO_CHECK = [
11
+ // better-sqlite3 is the only HARD-required native dep at framework boot
12
+ // (cost ledger + cron persistence + memory index). Other native deps
13
+ // (@lancedb/lancedb) are opt-in peers — preflight skips them when absent.
14
+ { pkg: "better-sqlite3", required: true },
15
+ { pkg: "@lancedb/lancedb", required: false }
16
+ ];
17
+ function checkNodeFloor() {
18
+ const major = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
19
+ if (major >= NODE_MIN_MAJOR) return { ok: true };
20
+ return {
21
+ ok: false,
22
+ reason: `theokit requires Node >= ${NODE_MIN_FULL} (you are running v${process.versions.node}).
23
+
24
+ Quick fix:
25
+ nvm use 22 # switch to a Node 22.x already installed
26
+ nvm install 22.12 # install + switch if you don't have it
27
+
28
+ Why this matters:
29
+ theokit ships AsyncLocalStorage + native fetch + structured WHATWG
30
+ Streams optimizations that depend on Node 22 internals. Older Node
31
+ versions silently miscompile SSR streaming and break native
32
+ bindings (NODE_MODULE_VERSION mismatch).
33
+
34
+ Production tip: pin your CI runner to \`actions/setup-node\` v4 with
35
+ \`node-version-file: .nvmrc\` \u2014 that's what \`create-theokit\` writes.`
36
+ };
37
+ }
38
+ function checkBindingAbi(cwd) {
39
+ const offenders = [];
40
+ const cwdRequire = createRequire(join(cwd, "package.json"));
41
+ for (const probe of BINDINGS_TO_CHECK) {
42
+ try {
43
+ cwdRequire.resolve(probe.pkg);
44
+ } catch {
45
+ if (probe.required) {
46
+ offenders.push(`${probe.pkg} (required dep is NOT installed \u2014 run \`pnpm install\`)`);
47
+ }
48
+ continue;
49
+ }
50
+ try {
51
+ cwdRequire(probe.pkg);
52
+ } catch (err) {
53
+ const msg = err instanceof Error ? err.message : String(err);
54
+ if (/NODE_MODULE_VERSION|was compiled against|did not self-register/.test(msg)) {
55
+ offenders.push(`${probe.pkg} (compiled against different Node ABI)`);
56
+ } else if (probe.required) {
57
+ offenders.push(`${probe.pkg} (require failed: ${msg.split("\n")[0]})`);
58
+ }
59
+ }
60
+ }
61
+ if (offenders.length === 0) return { ok: true };
62
+ return {
63
+ ok: false,
64
+ reason: `Native binding ABI mismatch detected (Node v${process.versions.node}, ABI ${process.versions.modules}).
65
+
66
+ Offending packages:
67
+ ` + offenders.map((o) => ` - ${o}`).join("\n") + '\n\nQuick fix:\n pnpm rebuild # rebuild ALL native bindings for current Node\n pnpm rebuild better-sqlite3 # rebuild just one\n\nWhy this happened:\n Your `theokit dev` (or build/start) launched under a different\n Node version than the one used to install/build node_modules.\n Common cause: `nohup env ...` strips the nvm PATH prefix, so the\n dev server picked the system `node` instead of `nvm use 22`.\n\nPermanent fix:\n Prefix your dev/start command with the absolute Node 22 path:\n PATH="$(nvm which 22 | xargs dirname):$PATH" theokit dev\n Or use direnv with `.envrc` containing `use node 22.12.0`.'
68
+ };
69
+ }
70
+ function readNvmrcFloor(cwd) {
71
+ const path = join(cwd, ".nvmrc");
72
+ if (!existsSync(path)) return void 0;
73
+ try {
74
+ return readFileSync(path, "utf8").trim();
75
+ } catch {
76
+ return void 0;
77
+ }
78
+ }
79
+ function preflightNodeAndBindings(cwd) {
80
+ const nodeCheck = checkNodeFloor();
81
+ if (!nodeCheck.ok) {
82
+ const pinned = readNvmrcFloor(cwd);
83
+ const suffix = pinned !== void 0 ? `
84
+
85
+ Your project pins Node ${pinned} in .nvmrc.` : "";
86
+ throw new Error(`[theokit preflight] ${nodeCheck.reason}${suffix}`);
87
+ }
88
+ const abiCheck = checkBindingAbi(cwd);
89
+ if (!abiCheck.ok) {
90
+ throw new Error(`[theokit preflight] ${abiCheck.reason}`);
91
+ }
92
+ }
93
+
94
+ export {
95
+ preflightNodeAndBindings
96
+ };
97
+ //# sourceMappingURL=chunk-F3TG5FJV.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli/preflight-node-version.ts"],"sourcesContent":["/**\n * Node version + native-binding ABI preflight for `theokit dev` / `theokit build` / `theokit start`.\n *\n * Why this exists:\n * In multi-Node-version setups (developer workflow: Node 22 for one\n * project, Node 20 for another, nvm switching frequently), the\n * `nohup env ...` invocation pattern strips the nvm PATH prefix and\n * the dev server launches under whichever `node` binary is FIRST in\n * PATH at spawn time — NOT the version the calling shell reported.\n *\n * When `theokit dev` boots under the wrong Node, every native binding\n * in `node_modules` (`better-sqlite3`, optionally `@lancedb/lancedb`,\n * etc.) fails with NODE_MODULE_VERSION mismatch on first use. The\n * error message references a low-level binary path; the dev wastes\n * time investigating a process-management issue.\n *\n * This preflight runs UPFRONT (before any module that touches native\n * bindings loads) and emits an actionable error referencing nvm + the\n * pinned `.nvmrc` floor.\n *\n * What it does:\n * 1. Reads `process.versions.node` + `process.versions.modules` (ABI).\n * 2. If Node major < 22, refuse to boot with a nvm install/use hint.\n * 3. If at least one native binding's compiled ABI != current ABI,\n * refuse to boot with a `pnpm rebuild <pkg>` hint.\n *\n * What it does NOT do:\n * * Auto-rebuild — that's the SDK's preflight (vitest setup, sentinel\n * cached). Auto-rebuild in a dev CLI is risky (slow, can run during\n * `pnpm dev` watch loops).\n * * Probe every native dep — only checks the ones the framework KNOWS\n * are required (currently `better-sqlite3` because of the cost\n * ledger + cron persistence + memory index DB).\n *\n * @internal\n */\n\nimport { existsSync, readFileSync } from 'node:fs'\nimport { createRequire } from 'node:module'\nimport { join } from 'node:path'\n\nconst NODE_MIN_MAJOR = 22\nconst NODE_MIN_FULL = '22.12.0'\n\ninterface BindingProbe {\n pkg: string\n /** path under <cwd>/node_modules — used only for the diagnostic message */\n hintPath?: string\n required: boolean\n}\n\nconst BINDINGS_TO_CHECK: readonly BindingProbe[] = [\n // better-sqlite3 is the only HARD-required native dep at framework boot\n // (cost ledger + cron persistence + memory index). Other native deps\n // (@lancedb/lancedb) are opt-in peers — preflight skips them when absent.\n { pkg: 'better-sqlite3', required: true },\n { pkg: '@lancedb/lancedb', required: false },\n]\n\nfunction checkNodeFloor(): { ok: true } | { ok: false; reason: string } {\n const major = Number.parseInt(process.versions.node.split('.')[0] ?? '0', 10)\n if (major >= NODE_MIN_MAJOR) return { ok: true }\n return {\n ok: false,\n reason:\n `theokit requires Node >= ${NODE_MIN_FULL} (you are running v${process.versions.node}).\\n\\n` +\n 'Quick fix:\\n' +\n ' nvm use 22 # switch to a Node 22.x already installed\\n' +\n \" nvm install 22.12 # install + switch if you don't have it\\n\\n\" +\n 'Why this matters:\\n' +\n ' theokit ships AsyncLocalStorage + native fetch + structured WHATWG\\n' +\n ' Streams optimizations that depend on Node 22 internals. Older Node\\n' +\n ' versions silently miscompile SSR streaming and break native\\n' +\n ' bindings (NODE_MODULE_VERSION mismatch).\\n\\n' +\n 'Production tip: pin your CI runner to `actions/setup-node` v4 with\\n' +\n \" `node-version-file: .nvmrc` — that's what `create-theokit` writes.\",\n }\n}\n\nfunction checkBindingAbi(cwd: string): { ok: true } | { ok: false; reason: string } {\n const offenders: string[] = []\n // Build createRequire from the cwd so we resolve the consumer's deps,\n // NOT theokit's own node_modules.\n const cwdRequire = createRequire(join(cwd, 'package.json'))\n for (const probe of BINDINGS_TO_CHECK) {\n try {\n cwdRequire.resolve(probe.pkg)\n } catch {\n // not installed — required vs optional gates the behaviour\n if (probe.required) {\n offenders.push(`${probe.pkg} (required dep is NOT installed — run \\`pnpm install\\`)`)\n }\n continue\n }\n // Try a dry require — better-sqlite3 dlopens at load-time, so the ABI\n // mismatch surfaces here as a sync throw.\n try {\n cwdRequire(probe.pkg)\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err)\n if (/NODE_MODULE_VERSION|was compiled against|did not self-register/.test(msg)) {\n offenders.push(`${probe.pkg} (compiled against different Node ABI)`)\n } else if (probe.required) {\n // surface non-ABI errors verbatim for required deps\n offenders.push(`${probe.pkg} (require failed: ${msg.split('\\n')[0]})`)\n }\n }\n }\n if (offenders.length === 0) return { ok: true }\n return {\n ok: false,\n reason:\n `Native binding ABI mismatch detected (Node v${process.versions.node}, ABI ${process.versions.modules}).\\n\\n` +\n 'Offending packages:\\n' +\n offenders.map((o) => ` - ${o}`).join('\\n') +\n '\\n\\nQuick fix:\\n' +\n ' pnpm rebuild # rebuild ALL native bindings for current Node\\n' +\n ' pnpm rebuild better-sqlite3 # rebuild just one\\n\\n' +\n 'Why this happened:\\n' +\n ' Your `theokit dev` (or build/start) launched under a different\\n' +\n ' Node version than the one used to install/build node_modules.\\n' +\n ' Common cause: `nohup env ...` strips the nvm PATH prefix, so the\\n' +\n ' dev server picked the system `node` instead of `nvm use 22`.\\n\\n' +\n 'Permanent fix:\\n' +\n ' Prefix your dev/start command with the absolute Node 22 path:\\n' +\n ' PATH=\"$(nvm which 22 | xargs dirname):$PATH\" theokit dev\\n' +\n ' Or use direnv with `.envrc` containing `use node 22.12.0`.',\n }\n}\n\n/**\n * Read `.nvmrc` from cwd if present — used only to enrich error messages\n * with the project's pinned floor. Falls back to the framework default.\n *\n * @internal\n */\nfunction readNvmrcFloor(cwd: string): string | undefined {\n const path = join(cwd, '.nvmrc')\n // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI reads consumer's own .nvmrc; consumer owns cwd\n if (!existsSync(path)) return undefined\n try {\n // eslint-disable-next-line security/detect-non-literal-fs-filename -- same\n return readFileSync(path, 'utf8').trim()\n } catch {\n return undefined\n }\n}\n\n/**\n * Run the Node + ABI preflight. Throws a descriptive Error on mismatch\n * BEFORE any module that imports better-sqlite3 loads. The error message\n * is the only user-facing artefact — caller can rely on `error.message`\n * being a full actionable diagnostic.\n *\n * @throws Error with multi-line actionable message when Node version or\n * native binding ABI is wrong.\n * @internal\n */\nexport function preflightNodeAndBindings(cwd: string): void {\n const nodeCheck = checkNodeFloor()\n if (!nodeCheck.ok) {\n const pinned = readNvmrcFloor(cwd)\n const suffix = pinned !== undefined ? `\\n\\nYour project pins Node ${pinned} in .nvmrc.` : ''\n throw new Error(`[theokit preflight] ${nodeCheck.reason}${suffix}`)\n }\n const abiCheck = checkBindingAbi(cwd)\n if (!abiCheck.ok) {\n throw new Error(`[theokit preflight] ${abiCheck.reason}`)\n }\n}\n"],"mappings":";;;;AAqCA,SAAS,YAAY,oBAAoB;AACzC,SAAS,qBAAqB;AAC9B,SAAS,YAAY;AAErB,IAAM,iBAAiB;AACvB,IAAM,gBAAgB;AAStB,IAAM,oBAA6C;AAAA;AAAA;AAAA;AAAA,EAIjD,EAAE,KAAK,kBAAkB,UAAU,KAAK;AAAA,EACxC,EAAE,KAAK,oBAAoB,UAAU,MAAM;AAC7C;AAEA,SAAS,iBAA+D;AACtE,QAAM,QAAQ,OAAO,SAAS,QAAQ,SAAS,KAAK,MAAM,GAAG,EAAE,CAAC,KAAK,KAAK,EAAE;AAC5E,MAAI,SAAS,eAAgB,QAAO,EAAE,IAAI,KAAK;AAC/C,SAAO;AAAA,IACL,IAAI;AAAA,IACJ,QACE,4BAA4B,aAAa,sBAAsB,QAAQ,SAAS,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWxF;AACF;AAEA,SAAS,gBAAgB,KAA2D;AAClF,QAAM,YAAsB,CAAC;AAG7B,QAAM,aAAa,cAAc,KAAK,KAAK,cAAc,CAAC;AAC1D,aAAW,SAAS,mBAAmB;AACrC,QAAI;AACF,iBAAW,QAAQ,MAAM,GAAG;AAAA,IAC9B,QAAQ;AAEN,UAAI,MAAM,UAAU;AAClB,kBAAU,KAAK,GAAG,MAAM,GAAG,8DAAyD;AAAA,MACtF;AACA;AAAA,IACF;AAGA,QAAI;AACF,iBAAW,MAAM,GAAG;AAAA,IACtB,SAAS,KAAK;AACZ,YAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC3D,UAAI,iEAAiE,KAAK,GAAG,GAAG;AAC9E,kBAAU,KAAK,GAAG,MAAM,GAAG,wCAAwC;AAAA,MACrE,WAAW,MAAM,UAAU;AAEzB,kBAAU,KAAK,GAAG,MAAM,GAAG,qBAAqB,IAAI,MAAM,IAAI,EAAE,CAAC,CAAC,GAAG;AAAA,MACvE;AAAA,IACF;AAAA,EACF;AACA,MAAI,UAAU,WAAW,EAAG,QAAO,EAAE,IAAI,KAAK;AAC9C,SAAO;AAAA,IACL,IAAI;AAAA,IACJ,QACE,+CAA+C,QAAQ,SAAS,IAAI,SAAS,QAAQ,SAAS,OAAO;AAAA;AAAA;AAAA,IAErG,UAAU,IAAI,CAAC,MAAM,OAAO,CAAC,EAAE,EAAE,KAAK,IAAI,IAC1C;AAAA,EAYJ;AACF;AAQA,SAAS,eAAe,KAAiC;AACvD,QAAM,OAAO,KAAK,KAAK,QAAQ;AAE/B,MAAI,CAAC,WAAW,IAAI,EAAG,QAAO;AAC9B,MAAI;AAEF,WAAO,aAAa,MAAM,MAAM,EAAE,KAAK;AAAA,EACzC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAYO,SAAS,yBAAyB,KAAmB;AAC1D,QAAM,YAAY,eAAe;AACjC,MAAI,CAAC,UAAU,IAAI;AACjB,UAAM,SAAS,eAAe,GAAG;AACjC,UAAM,SAAS,WAAW,SAAY;AAAA;AAAA,yBAA8B,MAAM,gBAAgB;AAC1F,UAAM,IAAI,MAAM,uBAAuB,UAAU,MAAM,GAAG,MAAM,EAAE;AAAA,EACpE;AACA,QAAM,WAAW,gBAAgB,GAAG;AACpC,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI,MAAM,uBAAuB,SAAS,MAAM,EAAE;AAAA,EAC1D;AACF;","names":[]}
@@ -0,0 +1,109 @@
1
+ // src/devtools/shared.ts
2
+ var RING_BUFFER_CAP = 50;
3
+ var MAX_QUEUE_SIZE = 100;
4
+ var STORAGE_VERSION = 2;
5
+ var initialState = {
6
+ open: false,
7
+ visible: true,
8
+ position: "bottom-right",
9
+ theme: "dark",
10
+ activeTab: "requests",
11
+ requests: [],
12
+ errors: [],
13
+ agentRuns: [],
14
+ actionCalls: [],
15
+ routeManifest: null,
16
+ activeRoutePath: null,
17
+ activeChain: []
18
+ };
19
+ var CHANNEL_REQUEST = "theo:devtools:request";
20
+ var CHANNEL_ERROR = "theo:devtools:error";
21
+ var CHANNEL_CSRF_WARN = "theo:devtools:csrf.warn";
22
+ var CHANNEL_MANIFEST = "theo:devtools:manifest";
23
+ var CHANNEL_ROUTE_MATCHED = "theo:devtools:route-matched";
24
+
25
+ // src/devtools/dispatcher.ts
26
+ var _dispatch = null;
27
+ var _queue = [];
28
+ function queuable(fn) {
29
+ return (...args) => {
30
+ if (_dispatch) {
31
+ try {
32
+ fn(_dispatch, ...args);
33
+ } catch (err) {
34
+ console.error("[theo devtools] dispatch failed", err);
35
+ }
36
+ } else {
37
+ if (_queue.length >= MAX_QUEUE_SIZE) _queue.shift();
38
+ _queue.push((d) => {
39
+ fn(d, ...args);
40
+ });
41
+ }
42
+ };
43
+ }
44
+ function flushQueue() {
45
+ while (_queue.length) {
46
+ const fn = _queue.shift();
47
+ if (!fn || !_dispatch) break;
48
+ try {
49
+ fn(_dispatch);
50
+ } catch (err) {
51
+ console.error("[theo devtools] queued event failed", err);
52
+ }
53
+ }
54
+ }
55
+ var dispatcher = {
56
+ onRequest: queuable((d, req) => {
57
+ d({ type: "REQUEST_ADD", request: req });
58
+ }),
59
+ onError: queuable((d, err) => {
60
+ d({ type: "ERROR_ADD", error: err });
61
+ }),
62
+ onCsrfWarn: queuable((d, payload) => {
63
+ d({ type: "CSRF_WARN", payload });
64
+ }),
65
+ onManifestUpdated: queuable((d, manifest) => {
66
+ d({ type: "MANIFEST_UPDATED", manifest });
67
+ }),
68
+ onRouteMatched: queuable((d, path, chain) => {
69
+ d({ type: "ROUTE_MATCHED", path, chain });
70
+ }),
71
+ onAgentRun: queuable((d, run) => {
72
+ d({ type: "AGENT_RUN_ADD", run });
73
+ }),
74
+ onActionCall: queuable((d, record) => {
75
+ d({ type: "ACTION_CALL_ADD", record });
76
+ }),
77
+ /**
78
+ * Wire React's dispatch function in. Idempotent (EC-24): only the
79
+ * NULL → non-null transition flushes the queue; subsequent non-null
80
+ * sets just replace the reference (used for StrictMode re-mount).
81
+ */
82
+ setDispatch(d) {
83
+ const prev = _dispatch;
84
+ _dispatch = d;
85
+ if (d && !prev) flushQueue();
86
+ },
87
+ /** Testing helper — clear queue + dispatch reference between tests. */
88
+ _reset() {
89
+ _dispatch = null;
90
+ _queue.length = 0;
91
+ },
92
+ /** Testing helper — observe queue length. */
93
+ _queueLength() {
94
+ return _queue.length;
95
+ }
96
+ };
97
+
98
+ export {
99
+ RING_BUFFER_CAP,
100
+ STORAGE_VERSION,
101
+ initialState,
102
+ CHANNEL_REQUEST,
103
+ CHANNEL_ERROR,
104
+ CHANNEL_CSRF_WARN,
105
+ CHANNEL_MANIFEST,
106
+ CHANNEL_ROUTE_MATCHED,
107
+ dispatcher
108
+ };
109
+ //# sourceMappingURL=chunk-KJVEJILQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/devtools/shared.ts","../src/devtools/dispatcher.ts"],"sourcesContent":["/**\n * Devtools — shared types.\n *\n * NEVER use dangerouslySetInnerHTML in any devtools component — see plan EC-20.\n */\n\nexport type DevtoolsPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'\nexport type DevtoolsTab =\n | 'requests'\n | 'routes'\n | 'agents'\n | 'actions'\n | 'errors'\n | 'csrf-readiness'\n | 'settings'\nexport type DevtoolsTheme = 'light' | 'dark' | 'system'\n\nexport interface RequestRecord {\n id: string\n traceId: string\n method: string\n path: string\n status: number\n durationMs: number\n startedAt: number\n csrfWarn?: { code: string; docsUrl: string }\n headers?: Record<string, string>\n bodyPreview?: string\n bodyLength?: number\n bodyTruncated?: boolean\n}\n\nexport interface ErrorRecord {\n id: string\n type: 'console' | 'unhandled' | 'csrf.warn'\n message: string\n stack?: string\n code?: string\n docsUrl?: string\n timestamp: number\n}\n\nexport interface CsrfWarnPayload {\n event: 'csrf.warn'\n code: string\n docsUrl: string\n method: string\n path: string\n reason: string\n}\n\nexport interface RouteInfo {\n path: string\n absoluteFilePath: string\n layoutChain: string[]\n hasLoading: boolean\n hasError: boolean\n hasNotFound: boolean\n}\n\nexport interface RouteManifest {\n routes: RouteInfo[]\n}\n\n/**\n * Per-agent-run telemetry surfaced to the Agents devtools tab.\n *\n * Emitted by `trackAgentRun` (server-side) via the dispatcher in dev mode;\n * prod tree-shakes the entire wire (v1.1 EC-4 `__IS_DEV` IIFE guard).\n * Type lives here (devtools/shared) since both producer (server/cost) and\n * consumer (AgentsTab) need the shape; following architecture v3 ADR-0001\n * rule \"shared types in core/contracts\" is the canonical home — for this\n * one we keep it in devtools/shared to minimize churn in core/contracts/.\n */\nexport interface AgentRunRecord {\n id: string\n timestamp: number\n userId: string\n model: string\n tokensInput: number\n tokensOutput: number\n costUsd: number\n status: 'finished' | 'error' | 'aborted'\n}\n\n/**\n * T5.1 — Per-action-call telemetry surfaced to the Actions devtools tab.\n *\n * Emitted by `action-execute.ts` (server-side) via the dispatcher in dev\n * mode; prod tree-shakes the wire (mirrors AgentRunRecord pattern).\n * Field-level error pins consume `error.fields` directly when status='error'\n * (per plan ADR D6).\n */\nexport interface ActionCallRecord {\n id: string\n timestamp: number\n name: string\n input: unknown\n output?: unknown\n error?: {\n code: string\n message: string\n fields?: Record<string, string[]>\n issues?: unknown[]\n }\n durationMs: number\n status: 'success' | 'error'\n}\n\nexport interface DevtoolsState {\n open: boolean\n visible: boolean\n position: DevtoolsPosition\n theme: DevtoolsTheme\n activeTab: DevtoolsTab\n requests: RequestRecord[]\n errors: ErrorRecord[]\n agentRuns: AgentRunRecord[]\n actionCalls: ActionCallRecord[]\n routeManifest: RouteManifest | null\n activeRoutePath: string | null\n activeChain: string[]\n}\n\nexport type DevtoolsAction =\n | { type: 'TOGGLE_PANEL' }\n | { type: 'TOGGLE_VISIBLE' }\n | { type: 'SET_TAB'; tab: DevtoolsTab }\n | { type: 'SET_POSITION'; position: DevtoolsPosition }\n | { type: 'SET_THEME'; theme: DevtoolsTheme }\n | { type: 'REQUEST_ADD'; request: RequestRecord }\n | { type: 'ERROR_ADD'; error: ErrorRecord }\n | { type: 'CSRF_WARN'; payload: CsrfWarnPayload }\n | { type: 'MANIFEST_UPDATED'; manifest: RouteManifest }\n | { type: 'ROUTE_MATCHED'; path: string; chain: string[] }\n | { type: 'AGENT_RUN_ADD'; run: AgentRunRecord }\n | { type: 'ACTION_CALL_ADD'; record: ActionCallRecord }\n | { type: 'RESET_REQUESTS' }\n | { type: 'RESET_ERRORS' }\n | { type: 'RESET_AGENT_RUNS' }\n | { type: 'RESET_ACTION_CALLS' }\n\nexport const RING_BUFFER_CAP = 50\nexport const MAX_QUEUE_SIZE = 100\nexport const STORAGE_VERSION = 2\n\nexport const initialState: DevtoolsState = {\n open: false,\n visible: true,\n position: 'bottom-right',\n theme: 'dark',\n activeTab: 'requests',\n requests: [],\n errors: [],\n agentRuns: [],\n actionCalls: [],\n routeManifest: null,\n activeRoutePath: null,\n activeChain: [],\n}\n\nexport const CHANNEL_REQUEST = 'theo:devtools:request' as const\nexport const CHANNEL_ERROR = 'theo:devtools:error' as const\nexport const CHANNEL_CSRF_WARN = 'theo:devtools:csrf.warn' as const\nexport const CHANNEL_MANIFEST = 'theo:devtools:manifest' as const\nexport const CHANNEL_ROUTE_MATCHED = 'theo:devtools:route-matched' as const\nexport const CHANNEL_AGENT_RUN = 'theo:devtools:agent.run' as const\nexport const CHANNEL_ACTION_CALL = 'theo:devtools:action.call' as const\n","/**\n * T2.1 — Devtools dispatcher with pre-mount event queue.\n *\n * Pattern: events emitted from anywhere (server WS, browser console.error,\n * dispatcher.onCsrfWarn) can fire BEFORE the React tree mounts. Each call\n * either dispatches immediately (when `setDispatch` registered a dispatch\n * fn) or queues until React is ready, then flushes on first mount.\n *\n * EC-23: queue capped at MAX_QUEUE_SIZE (FIFO eviction).\n * EC-24: setDispatch is idempotent — flush only on NULL → non-null\n * transition (StrictMode re-mount safe).\n *\n * NEVER use dangerouslySetInnerHTML in any devtools component — see plan EC-20.\n */\nimport {\n type ActionCallRecord,\n type AgentRunRecord,\n type CsrfWarnPayload,\n type DevtoolsAction,\n type ErrorRecord,\n MAX_QUEUE_SIZE,\n type RequestRecord,\n type RouteManifest,\n} from './shared.js'\n\ntype Dispatch = (action: DevtoolsAction) => void\ntype QueuedItem = (d: Dispatch) => void\n\nlet _dispatch: Dispatch | null = null\nconst _queue: QueuedItem[] = []\n\nfunction queuable<Args extends unknown[]>(\n fn: (d: Dispatch, ...args: Args) => void,\n): (...args: Args) => void {\n return (...args: Args) => {\n if (_dispatch) {\n try {\n fn(_dispatch, ...args)\n } catch (err) {\n // EC-25: an error inside reducer-bound dispatch path MUST NOT bubble\n // to the original event source (e.g. logger / HMR callback / global\n // error handler). Log and continue.\n\n console.error('[theo devtools] dispatch failed', err)\n }\n } else {\n // EC-23: cap queue, FIFO-evict if full\n if (_queue.length >= MAX_QUEUE_SIZE) _queue.shift()\n _queue.push((d) => {\n fn(d, ...args)\n })\n }\n }\n}\n\nfunction flushQueue(): void {\n while (_queue.length) {\n const fn = _queue.shift()\n if (!fn || !_dispatch) break\n try {\n fn(_dispatch)\n } catch (err) {\n console.error('[theo devtools] queued event failed', err)\n }\n }\n}\n\nexport const dispatcher = {\n onRequest: queuable((d: Dispatch, req: RequestRecord) => {\n d({ type: 'REQUEST_ADD', request: req })\n }),\n onError: queuable((d: Dispatch, err: ErrorRecord) => {\n d({ type: 'ERROR_ADD', error: err })\n }),\n onCsrfWarn: queuable((d: Dispatch, payload: CsrfWarnPayload) => {\n d({ type: 'CSRF_WARN', payload })\n }),\n onManifestUpdated: queuable((d: Dispatch, manifest: RouteManifest) => {\n d({ type: 'MANIFEST_UPDATED', manifest })\n }),\n onRouteMatched: queuable((d: Dispatch, path: string, chain: string[]) => {\n d({ type: 'ROUTE_MATCHED', path, chain })\n }),\n onAgentRun: queuable((d: Dispatch, run: AgentRunRecord) => {\n d({ type: 'AGENT_RUN_ADD', run })\n }),\n onActionCall: queuable((d: Dispatch, record: ActionCallRecord) => {\n d({ type: 'ACTION_CALL_ADD', record })\n }),\n\n /**\n * Wire React's dispatch function in. Idempotent (EC-24): only the\n * NULL → non-null transition flushes the queue; subsequent non-null\n * sets just replace the reference (used for StrictMode re-mount).\n */\n setDispatch(d: Dispatch | null): void {\n const prev = _dispatch\n _dispatch = d\n if (d && !prev) flushQueue()\n },\n\n /** Testing helper — clear queue + dispatch reference between tests. */\n _reset(): void {\n _dispatch = null\n _queue.length = 0\n },\n\n /** Testing helper — observe queue length. */\n _queueLength(): number {\n return _queue.length\n },\n}\n\nexport type Dispatcher = typeof dispatcher\n"],"mappings":";AA8IO,IAAM,kBAAkB;AACxB,IAAM,iBAAiB;AACvB,IAAM,kBAAkB;AAExB,IAAM,eAA8B;AAAA,EACzC,MAAM;AAAA,EACN,SAAS;AAAA,EACT,UAAU;AAAA,EACV,OAAO;AAAA,EACP,WAAW;AAAA,EACX,UAAU,CAAC;AAAA,EACX,QAAQ,CAAC;AAAA,EACT,WAAW,CAAC;AAAA,EACZ,aAAa,CAAC;AAAA,EACd,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,aAAa,CAAC;AAChB;AAEO,IAAM,kBAAkB;AACxB,IAAM,gBAAgB;AACtB,IAAM,oBAAoB;AAC1B,IAAM,mBAAmB;AACzB,IAAM,wBAAwB;;;ACzIrC,IAAI,YAA6B;AACjC,IAAM,SAAuB,CAAC;AAE9B,SAAS,SACP,IACyB;AACzB,SAAO,IAAI,SAAe;AACxB,QAAI,WAAW;AACb,UAAI;AACF,WAAG,WAAW,GAAG,IAAI;AAAA,MACvB,SAAS,KAAK;AAKZ,gBAAQ,MAAM,mCAAmC,GAAG;AAAA,MACtD;AAAA,IACF,OAAO;AAEL,UAAI,OAAO,UAAU,eAAgB,QAAO,MAAM;AAClD,aAAO,KAAK,CAAC,MAAM;AACjB,WAAG,GAAG,GAAG,IAAI;AAAA,MACf,CAAC;AAAA,IACH;AAAA,EACF;AACF;AAEA,SAAS,aAAmB;AAC1B,SAAO,OAAO,QAAQ;AACpB,UAAM,KAAK,OAAO,MAAM;AACxB,QAAI,CAAC,MAAM,CAAC,UAAW;AACvB,QAAI;AACF,SAAG,SAAS;AAAA,IACd,SAAS,KAAK;AACZ,cAAQ,MAAM,uCAAuC,GAAG;AAAA,IAC1D;AAAA,EACF;AACF;AAEO,IAAM,aAAa;AAAA,EACxB,WAAW,SAAS,CAAC,GAAa,QAAuB;AACvD,MAAE,EAAE,MAAM,eAAe,SAAS,IAAI,CAAC;AAAA,EACzC,CAAC;AAAA,EACD,SAAS,SAAS,CAAC,GAAa,QAAqB;AACnD,MAAE,EAAE,MAAM,aAAa,OAAO,IAAI,CAAC;AAAA,EACrC,CAAC;AAAA,EACD,YAAY,SAAS,CAAC,GAAa,YAA6B;AAC9D,MAAE,EAAE,MAAM,aAAa,QAAQ,CAAC;AAAA,EAClC,CAAC;AAAA,EACD,mBAAmB,SAAS,CAAC,GAAa,aAA4B;AACpE,MAAE,EAAE,MAAM,oBAAoB,SAAS,CAAC;AAAA,EAC1C,CAAC;AAAA,EACD,gBAAgB,SAAS,CAAC,GAAa,MAAc,UAAoB;AACvE,MAAE,EAAE,MAAM,iBAAiB,MAAM,MAAM,CAAC;AAAA,EAC1C,CAAC;AAAA,EACD,YAAY,SAAS,CAAC,GAAa,QAAwB;AACzD,MAAE,EAAE,MAAM,iBAAiB,IAAI,CAAC;AAAA,EAClC,CAAC;AAAA,EACD,cAAc,SAAS,CAAC,GAAa,WAA6B;AAChE,MAAE,EAAE,MAAM,mBAAmB,OAAO,CAAC;AAAA,EACvC,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOD,YAAY,GAA0B;AACpC,UAAM,OAAO;AACb,gBAAY;AACZ,QAAI,KAAK,CAAC,KAAM,YAAW;AAAA,EAC7B;AAAA;AAAA,EAGA,SAAe;AACb,gBAAY;AACZ,WAAO,SAAS;AAAA,EAClB;AAAA;AAAA,EAGA,eAAuB;AACrB,WAAO,OAAO;AAAA,EAChB;AACF;","names":[]}
File without changes
@@ -1,5 +1,29 @@
1
1
  // src/server/cost/track-agent-run.ts
2
+ var __IS_DEV = (() => {
3
+ try {
4
+ return import.meta.env?.DEV === true;
5
+ } catch {
6
+ return process.env.NODE_ENV !== "production";
7
+ }
8
+ })();
2
9
  async function trackAgentRun(input, opts) {
10
+ if (__IS_DEV) {
11
+ try {
12
+ const mod = await import("./dispatcher-AQJGI3HU.js");
13
+ mod.dispatcher.onAgentRun({
14
+ // eslint-disable-next-line sonarjs/pseudo-random -- non-secret correlation id
15
+ id: `run-${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`,
16
+ timestamp: (input.timestamp ?? /* @__PURE__ */ new Date()).getTime(),
17
+ userId: input.userId,
18
+ model: input.model,
19
+ tokensInput: input.tokens.input,
20
+ tokensOutput: input.tokens.output,
21
+ costUsd: input.costUsd,
22
+ status: input.status ?? "finished"
23
+ });
24
+ } catch {
25
+ }
26
+ }
3
27
  if (!opts.storage) return;
4
28
  const record = {
5
29
  userId: input.userId,
@@ -158,4 +182,4 @@ export {
158
182
  InMemoryUsageStorage,
159
183
  trackAgentTools
160
184
  };
161
- //# sourceMappingURL=chunk-MCP6Y5BA.js.map
185
+ //# sourceMappingURL=chunk-M23FP2AS.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server/cost/track-agent-run.ts","../src/server/cost/usage-storage-memory.ts","../src/server/cost/track-agent-tools.ts"],"sourcesContent":["import type { AgentRunRecord } from '../../devtools/shared.js'\n\nimport type { UsageRecord, UsageStorageAdapter } from './cost-types.js'\n\nexport interface TrackAgentRunInput {\n userId: string\n model: string\n tokens: { input: number; output: number }\n costUsd: number\n /** Defaults to `new Date()`. */\n timestamp?: Date\n /** Optional execution status surfaced to devtools (default 'finished'). */\n status?: 'finished' | 'error' | 'aborted'\n}\n\nexport interface TrackAgentRunOptions {\n /** Adapter resolved from `theo.config.ts > cost.storage`. */\n storage: UsageStorageAdapter | undefined\n}\n\n// v1.1 EC-4 MUST FIX — universal dev gate (Vite OR tsup-bundled).\n// Both checks are statically replaceable by bundlers in prod build,\n// so the entire dispatcher import is tree-shaken from the prod bundle.\nconst __IS_DEV = (() => {\n try {\n return (import.meta as { env?: { DEV?: boolean } }).env?.DEV === true\n } catch {\n return process.env.NODE_ENV !== 'production'\n }\n})()\n\n/**\n * Record a single agent run's usage + cost. Companion to the client-side\n * `<CostMeter>` from `@usetheo/ui`.\n *\n * EC-14: this function NEVER bubbles errors back to the caller. Adapter\n * failures (network outage, DB down, etc.) are logged via `console.warn`\n * and swallowed. The agent response MUST NOT fail because cost tracking\n * is degraded.\n *\n * No-op when `storage` is undefined (cost tracking unconfigured).\n *\n * In dev mode (Vite OR tsup-built with NODE_ENV != 'production'), fires a\n * `theokit-evolution-ci-and-dx` Phase 3 dispatcher event so the devtools\n * Agents tab can render the run. Prod tree-shakes the entire dispatcher\n * import via the `__IS_DEV` IIFE guard.\n *\n * @see docs/concepts/cost-tracking.md (when implemented in T6.3)\n */\nexport async function trackAgentRun(\n input: TrackAgentRunInput,\n opts: TrackAgentRunOptions,\n): Promise<void> {\n // Dev-only: surface run to devtools Agents tab (T3.1)\n if (__IS_DEV) {\n try {\n const mod = (await import('../../devtools/dispatcher.js')) as {\n dispatcher: { onAgentRun: (r: AgentRunRecord) => void }\n }\n mod.dispatcher.onAgentRun({\n // eslint-disable-next-line sonarjs/pseudo-random -- non-secret correlation id\n id: `run-${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`,\n timestamp: (input.timestamp ?? new Date()).getTime(),\n userId: input.userId,\n model: input.model,\n tokensInput: input.tokens.input,\n tokensOutput: input.tokens.output,\n costUsd: input.costUsd,\n status: input.status ?? 'finished',\n })\n } catch {\n // devtools dispatcher missing in some prod-like bundle — silently skip\n }\n }\n\n if (!opts.storage) return\n const record: UsageRecord = {\n userId: input.userId,\n model: input.model,\n tokens: input.tokens,\n costUsd: input.costUsd,\n timestamp: input.timestamp ?? new Date(),\n }\n try {\n await opts.storage.record(record)\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n\n console.warn(\n `[theokit:cost] usage storage record failed: ${message}. ` +\n '(Response is unaffected — cost tracking degraded.)',\n )\n }\n}\n","import type { StorageAdapter } from '../storage/storage-types.js'\n\nimport type {\n ToolUsageRecord,\n UsageQuery,\n UsageRecord,\n UsageResult,\n UsageStorageAdapter,\n} from './cost-types.js'\n\n/**\n * In-memory usage storage for dev/tests. Unbounded — production\n * deployments MUST plug in a durable adapter (Postgres/Redis recipes\n * land with R0.6.7; documented in EC-114).\n *\n * Phase 5 — Production-Readiness #4: accepts both `UsageRecord` (LLM call,\n * kind='llm' or omitted) AND `ToolUsageRecord` (tool call, kind='tool').\n * `getUsage` only sums LLM records (tools have no token/cost dimension).\n *\n * EC-9 (backward compat): input without `kind` is normalized to `kind:'llm'`\n * — adapters from older versions stay working.\n *\n * Concurrency: Node's single-threaded event loop guarantees that\n * `Array.push` is atomic, so concurrent record() calls cannot lose data.\n */\nexport class InMemoryUsageStorage implements UsageStorageAdapter, StorageAdapter {\n readonly name = 'memory'\n readonly #records: (UsageRecord | ToolUsageRecord)[] = []\n\n /**\n * T2.3 — `StorageAdapter` lifecycle hook (ADR-0007 D6). In-memory storage\n * has no real cleanup to perform — this is intentionally a noop so the\n * adapter can be registered with `StorageManager.register()` and\n * participate in graceful shutdown without changing behavior.\n *\n * Does NOT clear stored records (dispose ≠ reset). For test reset use\n * a fresh instance.\n */\n dispose(): Promise<void> {\n // Intentional noop — see jsdoc above\n return Promise.resolve()\n }\n\n async record(input: UsageRecord | ToolUsageRecord): Promise<void> {\n // EC-9: normalize legacy input (no `kind` field) to kind:'llm'.\n const normalized: UsageRecord | ToolUsageRecord =\n 'kind' in input && input.kind === 'tool'\n ? { ...input }\n : ({ ...input, kind: 'llm' as const } satisfies UsageRecord)\n this.#records.push(normalized)\n return Promise.resolve()\n }\n\n async getUsage(query: UsageQuery): Promise<UsageResult> {\n const from = query.period.from.getTime()\n const to = query.period.to.getTime()\n let totalTokens = 0\n let totalCostUsd = 0\n let runs = 0\n for (const r of this.#records) {\n if (r.kind === 'tool') continue\n if (r.userId !== query.userId) continue\n const t = r.timestamp.getTime()\n if (t < from || t > to) continue\n totalTokens += r.tokens.input + r.tokens.output\n totalCostUsd += r.costUsd\n runs += 1\n }\n return Promise.resolve({ totalTokens, totalCostUsd, runs })\n }\n\n /** @internal — test helper to inspect raw record stream */\n __getRecords(): readonly (UsageRecord | ToolUsageRecord)[] {\n return this.#records\n }\n}\n","import type { ToolUsageRecord, UsageStorageAdapter } from './cost-types.js'\n\n/**\n * Phase 5 — Production-Readiness #4: tool lifecycle hooks for cost tracking.\n *\n * The SDK calls `onToolStart` → handler runs → `onToolEnd` (success) or\n * `onToolError` (throw). This factory returns the three callbacks ready to\n * attach to `Agent.create({ tools, onToolStart, onToolEnd, onToolError })`.\n *\n * Each successful tool call writes a `ToolUsageRecord{success:true}`; each\n * failure writes `ToolUsageRecord{success:false}` with `errorMessage` from\n * the thrown Error.\n *\n * Invariants:\n * - `onToolStart` records nothing yet (no duration available). Only stashes\n * a `startedAt` timestamp by `callId` in a per-factory Map.\n * - `onToolEnd` reads the stashed timestamp, computes `durationMs`, writes\n * a successful record. Removes the Map entry.\n * - `onToolError` analog with `success:false`.\n * - Hook throws are SWALLOWED to stderr — observation hooks must not crash\n * the run (per the v1.1.0 release contract).\n * - EC-8 (SHOULD TEST) — orphan starts (no matching End/Error) are pruned\n * on every onToolStart if older than 5 minutes. Bounded memory.\n * - EC-16 (DOCUMENT) — callId uniqueness is SDK contract; duplicate Start\n * for the same callId uses last-write-wins.\n */\n\nconst ORPHAN_TTL_MS = 5 * 60 * 1000\n\ninterface PendingStart {\n startedAt: number\n userId: string\n conversationId: string\n}\n\nexport interface TrackAgentToolsOptions {\n storage: UsageStorageAdapter\n /** Defaults to () => new Date() — overridable for deterministic testing. */\n now?: () => Date\n /** Identifier for the user — passed through to ToolUsageRecord.userId. */\n userId?: string\n /** Identifier for the conversation — passed through to ToolUsageRecord.conversationId. */\n conversationId?: string\n}\n\nexport interface ToolHookEvent {\n callId: string\n name: string\n // SDK shape — we only read these structurally\n [key: string]: unknown\n}\n\nexport interface TrackAgentToolsHooks {\n onToolStart: (event: ToolHookEvent) => void\n onToolEnd: (event: ToolHookEvent) => void\n onToolError: (event: ToolHookEvent) => void\n}\n\n/**\n * Create the three tool-lifecycle callbacks. Hand the returned object's\n * methods to `Agent.create({ tools, onToolStart, onToolEnd, onToolError })`.\n */\nexport function trackAgentTools(opts: TrackAgentToolsOptions): TrackAgentToolsHooks {\n const pending = new Map<string, PendingStart>()\n const now = opts.now ?? ((): Date => new Date())\n const userId = opts.userId ?? 'anonymous'\n const conversationId = opts.conversationId ?? 'unknown'\n\n function pruneOrphans(): void {\n const cutoff = now().getTime() - ORPHAN_TTL_MS\n for (const [id, p] of pending) {\n if (p.startedAt < cutoff) pending.delete(id)\n }\n }\n\n function safeRecord(record: ToolUsageRecord): void {\n Promise.resolve(opts.storage.record(record)).catch((err: unknown) => {\n const msg = err instanceof Error ? err.message : String(err)\n console.warn(`[theokit:trackAgentTools] storage.record failed: ${msg}`)\n })\n }\n\n return {\n onToolStart(event: ToolHookEvent): void {\n try {\n pruneOrphans()\n pending.set(event.callId, {\n startedAt: now().getTime(),\n userId,\n conversationId,\n })\n } catch (err) {\n console.warn(\n `[theokit:trackAgentTools] onToolStart hook crashed (swallowed): ${\n err instanceof Error ? err.message : String(err)\n }`,\n )\n }\n },\n onToolEnd(event: ToolHookEvent): void {\n try {\n const start = pending.get(event.callId)\n const startedAt = start?.startedAt ?? now().getTime()\n const durationMs = start ? now().getTime() - startedAt : 0\n pending.delete(event.callId)\n if (!start) {\n console.warn(\n `[theokit:trackAgentTools] orphan onToolEnd for callId=${event.callId}; durationMs:0 recorded`,\n )\n }\n safeRecord({\n kind: 'tool',\n userId: start?.userId ?? userId,\n conversationId: start?.conversationId ?? conversationId,\n toolName: event.name,\n callId: event.callId,\n success: true,\n durationMs,\n timestamp: now(),\n })\n } catch (err) {\n console.warn(\n `[theokit:trackAgentTools] onToolEnd hook crashed (swallowed): ${\n err instanceof Error ? err.message : String(err)\n }`,\n )\n }\n },\n onToolError(event: ToolHookEvent): void {\n try {\n const start = pending.get(event.callId)\n const startedAt = start?.startedAt ?? now().getTime()\n const durationMs = start ? now().getTime() - startedAt : 0\n pending.delete(event.callId)\n const error = event.error\n let errorMessage = 'unknown error'\n if (error instanceof Error) errorMessage = error.message\n else if (typeof error === 'string') errorMessage = error\n safeRecord({\n kind: 'tool',\n userId: start?.userId ?? userId,\n conversationId: start?.conversationId ?? conversationId,\n toolName: event.name,\n callId: event.callId,\n success: false,\n durationMs,\n errorMessage,\n timestamp: now(),\n })\n } catch (err) {\n console.warn(\n `[theokit:trackAgentTools] onToolError hook crashed (swallowed): ${\n err instanceof Error ? err.message : String(err)\n }`,\n )\n }\n },\n }\n}\n\n/**\n * @internal — testing helper. Inspects the size of the pending-starts Map of\n * a factory instance. Not in the public API.\n */\nexport const __internalsForTesting = {\n ORPHAN_TTL_MS,\n}\n"],"mappings":"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","names":[]}
@@ -2,7 +2,7 @@
2
2
  import "tsx/esm";
3
3
  import {
4
4
  theoConfigSchema
5
- } from "./chunk-3Q36SEDK.js";
5
+ } from "./chunk-ZBWROIIO.js";
6
6
 
7
7
  // src/config/load-env.ts
8
8
  import { lstatSync, readFileSync, realpathSync, statSync } from "fs";
@@ -199,4 +199,4 @@ export {
199
199
  loadEnv,
200
200
  loadConfig
201
201
  };
202
- //# sourceMappingURL=chunk-S3JBIFKS.js.map
202
+ //# sourceMappingURL=chunk-MG7H3LAW.js.map
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env node
2
+ import "tsx/esm";
3
+
4
+ // src/server/scan/action-scan.ts
5
+ import { existsSync, readFileSync, statSync } from "fs";
6
+ import { extname as extname2, join as join2, relative } from "path";
7
+
8
+ // src/server/_internal/scan-walker.ts
9
+ import { readdirSync } from "fs";
10
+ import { extname, join, resolve } from "path";
11
+ function walkSourceFiles(root, opts, onFile) {
12
+ const skipPrefixes = opts.skipPrefixes ?? ["_", "."];
13
+ const visit = (dir) => {
14
+ let entries;
15
+ try {
16
+ entries = readdirSync(dir, { withFileTypes: true });
17
+ } catch {
18
+ return;
19
+ }
20
+ for (const entry of entries) {
21
+ const fullPath = join(dir, entry.name);
22
+ if (entry.isDirectory() && !skipPrefixes.some((p) => entry.name.startsWith(p))) {
23
+ visit(fullPath);
24
+ } else if (entry.isFile() && opts.extensions.has(extname(entry.name))) {
25
+ onFile(resolve(fullPath));
26
+ }
27
+ }
28
+ };
29
+ visit(root);
30
+ }
31
+
32
+ // src/server/scan/action-scan.ts
33
+ var ActionScanError = class extends Error {
34
+ code;
35
+ conflictingPaths;
36
+ constructor(code, message, conflictingPaths) {
37
+ super(message);
38
+ this.code = code;
39
+ this.conflictingPaths = conflictingPaths;
40
+ }
41
+ };
42
+ var ACTION_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
43
+ var TEST_FILE_RE = /\.(test|spec)\.(ts|tsx|js|jsx)$/;
44
+ var RESERVED_NAMES = /* @__PURE__ */ new Set(["index", "constructor", "__proto__", "prototype", "hasOwnProperty"]);
45
+ function scanServerActions(serverDir) {
46
+ const actionsDir = join2(serverDir, "actions");
47
+ if (!existsSync(actionsDir) || !statSync(actionsDir).isDirectory()) {
48
+ return [];
49
+ }
50
+ const results = [];
51
+ walkSourceFiles(actionsDir, { extensions: ACTION_EXTENSIONS }, (absPath) => {
52
+ if (TEST_FILE_RE.test(absPath)) return;
53
+ let rel = relative(actionsDir, absPath);
54
+ rel = rel.replace(/\\/g, "/");
55
+ rel = rel.slice(0, -extname2(rel).length);
56
+ results.push({
57
+ filePath: absPath,
58
+ actionPath: rel
59
+ });
60
+ });
61
+ return results;
62
+ }
63
+ function scanServerActionsEnriched(serverDir) {
64
+ const actionsDir = join2(serverDir, "actions");
65
+ if (!existsSync(actionsDir) || !statSync(actionsDir).isDirectory()) {
66
+ return [];
67
+ }
68
+ const seenNames = /* @__PURE__ */ new Set();
69
+ const entries = [];
70
+ walkSourceFiles(actionsDir, { extensions: ACTION_EXTENSIONS }, (absPath) => {
71
+ if (TEST_FILE_RE.test(absPath)) return;
72
+ const rel = relative(actionsDir, absPath).replace(/\\/g, "/");
73
+ const name = rel.slice(0, -extname2(rel).length);
74
+ const basename = name.includes("/") ? name.split("/").pop() ?? name : name;
75
+ if (RESERVED_NAMES.has(basename)) {
76
+ throw new ActionScanError(
77
+ "RESERVED_NAME",
78
+ `Reserved JS identifier "${basename}" cannot be an action name (${absPath})`,
79
+ [absPath]
80
+ );
81
+ }
82
+ const source = readFileSync(absPath, "utf8");
83
+ const stripped = stripComments(source);
84
+ const accept = /\baccept\s*:\s*['"]form['"]/.test(stripped) ? "form" : "json";
85
+ const hasInput = /\binput\s*:\s*z\./.test(stripped) || /\binput\s*:\s*\w+\(/.test(stripped);
86
+ const exportNames = extractActionExportNames(stripped);
87
+ for (const exportName of exportNames) {
88
+ const proxyKey = exportName === "default" ? name : exportName;
89
+ if (seenNames.has(proxyKey)) {
90
+ throw new ActionScanError(
91
+ "NAME_COLLISION",
92
+ `Duplicate action proxy key "${proxyKey}" (two files export the same name)`,
93
+ [absPath]
94
+ );
95
+ }
96
+ seenNames.add(proxyKey);
97
+ entries.push({
98
+ name: proxyKey,
99
+ filePath: absPath,
100
+ urlPath: `/api/__actions/${name}/${exportName}`,
101
+ accept,
102
+ hasInput
103
+ });
104
+ }
105
+ });
106
+ for (const entry of entries) {
107
+ const childPrefix = `${entry.name}/`;
108
+ const conflictingChild = entries.find((other) => other.name.startsWith(childPrefix));
109
+ if (conflictingChild) {
110
+ throw new ActionScanError(
111
+ "NAME_COLLISION",
112
+ `Action "${entry.name}" conflicts with directory of same name containing "${conflictingChild.name}"`,
113
+ [entry.filePath, conflictingChild.filePath]
114
+ );
115
+ }
116
+ }
117
+ entries.sort((a, b) => {
118
+ if (a.name < b.name) return -1;
119
+ if (a.name > b.name) return 1;
120
+ return 0;
121
+ });
122
+ return entries;
123
+ }
124
+ function extractActionExportNames(stripped) {
125
+ const names = /* @__PURE__ */ new Set();
126
+ const namedRe = /\bexport\s+(?:const|let|var|function\*?)\s+([a-zA-Z_$][\w$]*)\s*[=(]/g;
127
+ let m;
128
+ while ((m = namedRe.exec(stripped)) !== null) {
129
+ const name = m[1];
130
+ if (typeof name === "string" && name.length > 0) names.add(name);
131
+ }
132
+ if (/\bexport\s+default\s+defineAction\b/.test(stripped)) {
133
+ names.add("default");
134
+ }
135
+ if (names.size === 0) names.add("default");
136
+ return [...names];
137
+ }
138
+ function stripComments(source) {
139
+ let out = "";
140
+ let i = 0;
141
+ while (i < source.length) {
142
+ const ch = source[i];
143
+ const next = source[i + 1];
144
+ if (ch === "/" && next === "/") {
145
+ while (i < source.length && source[i] !== "\n") i++;
146
+ continue;
147
+ }
148
+ if (ch === "/" && next === "*") {
149
+ i += 2;
150
+ while (i < source.length - 1 && !(source[i] === "*" && source[i + 1] === "/")) i++;
151
+ i += 2;
152
+ continue;
153
+ }
154
+ out += ch;
155
+ i++;
156
+ }
157
+ return out;
158
+ }
159
+
160
+ export {
161
+ walkSourceFiles,
162
+ scanServerActions,
163
+ scanServerActionsEnriched
164
+ };
165
+ //# sourceMappingURL=chunk-MOT42NAH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server/scan/action-scan.ts","../src/server/_internal/scan-walker.ts"],"sourcesContent":["/* eslint-disable security/detect-non-literal-fs-filename --\n * Build-time scanner: walks `serverDir/actions/` derived from cwd.\n * No HTTP input ever reaches these fs calls.\n */\nimport { existsSync, readFileSync, statSync } from 'node:fs'\nimport { extname, join, relative } from 'node:path'\n\nimport { walkSourceFiles } from '../_internal/scan-walker.js'\n\nexport interface ActionNode {\n filePath: string\n actionPath: string\n}\n\n/**\n * Enriched manifest entry per plan g3-server-actions-and-useaction v1.2\n * § Phase 1 / T1.4 + ADR D4. Consumed by virtual module `@theo/actions`\n * (T3.1) and G4 devtools \"Actions\" tab (T5.1).\n */\nexport interface ActionManifestEntry {\n name: string\n filePath: string\n urlPath: string\n accept: 'form' | 'json'\n hasInput: boolean\n}\n\n/**\n * EC-2: structured error for scan-time defects (name collision, reserved\n * identifier, etc.). Throw at scan time to fail loud — silent shadowing is\n * a security/correctness footgun.\n */\nexport class ActionScanError extends Error {\n readonly code: 'NAME_COLLISION' | 'RESERVED_NAME'\n readonly conflictingPaths: readonly string[]\n\n constructor(\n code: 'NAME_COLLISION' | 'RESERVED_NAME',\n message: string,\n conflictingPaths: readonly string[],\n ) {\n super(message)\n this.code = code\n this.conflictingPaths = conflictingPaths\n }\n}\n\nconst ACTION_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx'])\nconst TEST_FILE_RE = /\\.(test|spec)\\.(ts|tsx|js|jsx)$/\nconst RESERVED_NAMES = new Set(['index', 'constructor', '__proto__', 'prototype', 'hasOwnProperty'])\n\n/**\n * Backward-compatible: original simple-shape scanner used by existing\n * consumers. Preserved verbatim; new consumers should call\n * `scanServerActionsEnriched`.\n */\nexport function scanServerActions(serverDir: string): ActionNode[] {\n const actionsDir = join(serverDir, 'actions')\n if (!existsSync(actionsDir) || !statSync(actionsDir).isDirectory()) {\n return []\n }\n\n const results: ActionNode[] = []\n walkSourceFiles(actionsDir, { extensions: ACTION_EXTENSIONS }, (absPath) => {\n if (TEST_FILE_RE.test(absPath)) return\n let rel = relative(actionsDir, absPath)\n rel = rel.replace(/\\\\/g, '/')\n rel = rel.slice(0, -extname(rel).length)\n results.push({\n filePath: absPath,\n actionPath: rel,\n })\n })\n return results\n}\n\n/**\n * Enriched scan: light AST detection of `accept: 'form'|'json'` + `input:`\n * presence via regex-after-comment-stripping (EC-9). Throws ActionScanError\n * on file/dir name collision (EC-2) or reserved JS identifier names.\n *\n * Output `ActionManifestEntry[]` is sorted by `name` for deterministic\n * `.theo/actions-manifest.json` emission.\n */\nexport function scanServerActionsEnriched(serverDir: string): ActionManifestEntry[] {\n const actionsDir = join(serverDir, 'actions')\n if (!existsSync(actionsDir) || !statSync(actionsDir).isDirectory()) {\n return []\n }\n\n const seenNames = new Set<string>()\n const entries: ActionManifestEntry[] = []\n\n // Collect first; collision-check after the full walk.\n walkSourceFiles(actionsDir, { extensions: ACTION_EXTENSIONS }, (absPath) => {\n if (TEST_FILE_RE.test(absPath)) return\n const rel = relative(actionsDir, absPath).replace(/\\\\/g, '/')\n const name = rel.slice(0, -extname(rel).length)\n\n const basename = name.includes('/') ? (name.split('/').pop() ?? name) : name\n if (RESERVED_NAMES.has(basename)) {\n throw new ActionScanError(\n 'RESERVED_NAME',\n `Reserved JS identifier \"${basename}\" cannot be an action name (${absPath})`,\n [absPath],\n )\n }\n // File-level collision (one file appearing twice) is impossible via the\n // walker; per-export collision check happens inside the export loop below.\n const source = readFileSync(absPath, 'utf8')\n const stripped = stripComments(source)\n const accept = /\\baccept\\s*:\\s*['\"]form['\"]/.test(stripped) ? 'form' : 'json'\n const hasInput = /\\binput\\s*:\\s*z\\./.test(stripped) || /\\binput\\s*:\\s*\\w+\\(/.test(stripped)\n\n // T7.1 wire fix — extract exports so the urlPath includes the second\n // segment required by action-middleware (`/api/__actions/<file>/<export>`).\n // Each named action export becomes its own manifest entry.\n // Proxy key on the EXPORT name (so consumers write `actions.saveMemory(input)`).\n // URL keeps the runtime 2-segment shape `/api/__actions/<file>/<export>`\n // expected by action-middleware. Cross-file export collisions become a\n // scan error to surface the ambiguity early.\n const exportNames = extractActionExportNames(stripped)\n for (const exportName of exportNames) {\n const proxyKey = exportName === 'default' ? name : exportName\n if (seenNames.has(proxyKey)) {\n throw new ActionScanError(\n 'NAME_COLLISION',\n `Duplicate action proxy key \"${proxyKey}\" (two files export the same name)`,\n [absPath],\n )\n }\n seenNames.add(proxyKey)\n entries.push({\n name: proxyKey,\n filePath: absPath,\n urlPath: `/api/__actions/${name}/${exportName}`,\n accept,\n hasInput,\n })\n }\n })\n\n // EC-2: detect file vs dir collisions (e.g., foo.ts AND foo/bar.ts).\n // After walk completes, any name that has children prefixed `name/` triggers collision.\n for (const entry of entries) {\n const childPrefix = `${entry.name}/`\n const conflictingChild = entries.find((other) => other.name.startsWith(childPrefix))\n if (conflictingChild) {\n throw new ActionScanError(\n 'NAME_COLLISION',\n `Action \"${entry.name}\" conflicts with directory of same name containing \"${conflictingChild.name}\"`,\n [entry.filePath, conflictingChild.filePath],\n )\n }\n }\n\n entries.sort((a, b) => {\n if (a.name < b.name) return -1\n if (a.name > b.name) return 1\n return 0\n })\n return entries\n}\n\n/**\n * Strip JavaScript line + block comments from source. Simple state machine\n * (does not parse strings — false positives if a comment marker appears\n * inside a string literal, but that's an acceptable trade-off for v1 vs\n * full AST parse).\n */\n/**\n * Extract action export names via regex over comment-stripped source.\n * Matches `export const <name> = defineAction(...)`, `export default\n * defineAction(...)`, and `export function <name>(...)` forms. Returns\n * `['default']` when no named action exports are found (best-effort fallback\n * for default-export shapes the regex misses).\n */\nfunction extractActionExportNames(stripped: string): string[] {\n const names = new Set<string>()\n const namedRe = /\\bexport\\s+(?:const|let|var|function\\*?)\\s+([a-zA-Z_$][\\w$]*)\\s*[=(]/g\n let m: RegExpExecArray | null\n while ((m = namedRe.exec(stripped)) !== null) {\n const name = m[1]\n if (typeof name === 'string' && name.length > 0) names.add(name)\n }\n if (/\\bexport\\s+default\\s+defineAction\\b/.test(stripped)) {\n names.add('default')\n }\n if (names.size === 0) names.add('default')\n return [...names]\n}\n\nfunction stripComments(source: string): string {\n let out = ''\n let i = 0\n while (i < source.length) {\n const ch = source[i]\n const next = source[i + 1]\n if (ch === '/' && next === '/') {\n // Line comment: skip until newline\n while (i < source.length && source[i] !== '\\n') i++\n continue\n }\n if (ch === '/' && next === '*') {\n // Block comment: skip until */\n i += 2\n while (i < source.length - 1 && !(source[i] === '*' && source[i + 1] === '/')) i++\n i += 2\n continue\n }\n out += ch\n i++\n }\n return out\n}\n","/* eslint-disable security/detect-non-literal-fs-filename --\n * Build-time scanner: walks directories derived from cwd.\n * No HTTP input ever reaches these fs calls.\n */\nimport { readdirSync } from 'node:fs'\nimport { extname, join, resolve } from 'node:path'\n\n/**\n * Options for walkSourceFiles.\n *\n * Sequential by design — callers (route precedence) depend on insertion\n * order (EC-19 documented decision). Async-callback support is implicit\n * via JavaScript's serial await loop.\n *\n * Tested on macOS + Linux. Windows long-path support not validated (EC-20).\n */\nexport interface WalkOptions {\n /** File extensions to include (e.g., new Set(['.ts', '.tsx'])). */\n extensions: ReadonlySet<string>\n /** Skip directories whose name starts with any of these (default: ['_', '.']). */\n skipPrefixes?: readonly string[]\n}\n\n/**\n * Recursively walk `root`, invoking `onFile(absPath)` for every file matching\n * `opts.extensions`. Directories whose name starts with any `skipPrefixes`\n * char are skipped (defaults to `_` and `.`).\n *\n * Replaces 3 near-identical recursive walkers in scan.ts, action-scan.ts,\n * ws-scan.ts (PV-3 — DRY consolidation). Resolves T3.1 of\n * architecture-review-remediation-plan.\n *\n * Symlink loops are NOT tracked — callers must avoid them or pre-resolve\n * via `fs.realpath`. EC-11 documented but not implemented (rare in dev).\n */\nexport function walkSourceFiles(\n root: string,\n opts: WalkOptions,\n onFile: (absPath: string) => void,\n): void {\n const skipPrefixes = opts.skipPrefixes ?? ['_', '.']\n const visit = (dir: string): void => {\n let entries\n try {\n entries = readdirSync(dir, { withFileTypes: true })\n } catch {\n // Silently skip unreadable directories — caller controls discoverability\n return\n }\n for (const entry of entries) {\n const fullPath = join(dir, entry.name)\n if (entry.isDirectory() && !skipPrefixes.some((p) => entry.name.startsWith(p))) {\n visit(fullPath)\n } else if (entry.isFile() && opts.extensions.has(extname(entry.name))) {\n onFile(resolve(fullPath))\n }\n }\n }\n visit(root)\n}\n"],"mappings":"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","names":["extname","join","join","extname"]}
File without changes