theokit 0.12.1 → 0.13.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/dist/{actions-virtual-module-SQDY3V5X.js → actions-virtual-module-3CDQTWOC.js} +6 -6
- package/dist/{actions-virtual-module-PNPRCEOS.js → actions-virtual-module-EIPXX4ZB.js} +3 -3
- package/dist/agents-typed-client-SAWAAH7K.js +142 -0
- package/dist/agents-typed-client-SAWAAH7K.js.map +1 -0
- package/dist/agents-typed-client-UTEQUA63.js +143 -0
- package/dist/agents-typed-client-UTEQUA63.js.map +1 -0
- package/dist/{app-typed-client-5GYEOYP3.js → app-typed-client-7PBFWZUE.js} +3 -3
- package/dist/{app-typed-client-QG7BVZYW.js → app-typed-client-CSOK7NPC.js} +6 -6
- package/dist/body-parser-web-FV5HWCY3.js +71 -0
- package/dist/body-parser-web-FV5HWCY3.js.map +1 -0
- package/dist/{build-QFRLSEZ4.js → build-HXND27XG.js} +11 -11
- package/dist/{chunk-223EFY5X.js → chunk-2J7XU3PW.js} +68 -27
- package/dist/chunk-2J7XU3PW.js.map +1 -0
- package/dist/{chunk-RESN62GB.js → chunk-2KZQPDYR.js} +5 -48
- package/dist/chunk-2KZQPDYR.js.map +1 -0
- package/dist/chunk-3S3BNW5K.js +445 -0
- package/dist/chunk-3S3BNW5K.js.map +1 -0
- package/dist/{chunk-6FYD34NX.js → chunk-BQDGES7C.js} +28 -28
- package/dist/{chunk-6FYD34NX.js.map → chunk-BQDGES7C.js.map} +1 -1
- package/dist/chunk-EXP56GFQ.js +52 -0
- package/dist/chunk-EXP56GFQ.js.map +1 -0
- package/dist/chunk-F4YUPDJ2.js +115 -0
- package/dist/chunk-F4YUPDJ2.js.map +1 -0
- package/dist/{chunk-NAZ4E2GT.js → chunk-KXA37ONC.js} +2 -2
- package/dist/chunk-NHJMZCAS.js +32 -0
- package/dist/chunk-NHJMZCAS.js.map +1 -0
- package/dist/{chunk-43D6XNDR.js → chunk-O62MW4MT.js} +91 -18
- package/dist/chunk-O62MW4MT.js.map +1 -0
- package/dist/chunk-RSVN727G.js +1 -0
- package/dist/{chunk-7CBRKNQA.js → chunk-RYTZYFSD.js} +198 -6
- package/dist/chunk-RYTZYFSD.js.map +1 -0
- package/dist/chunk-UNLA45FY.js +235 -0
- package/dist/chunk-UNLA45FY.js.map +1 -0
- package/dist/{chunk-GFMQJHXX.js → chunk-WR4F4EEZ.js} +1082 -1074
- package/dist/chunk-WR4F4EEZ.js.map +1 -0
- package/dist/{chunk-AD74EAK3.js → chunk-ZSTZXR2D.js} +1 -30
- package/dist/chunk-ZSTZXR2D.js.map +1 -0
- package/dist/cli/index.js +5 -5
- package/dist/client/index.d.ts +57 -1
- package/dist/client/index.js +84 -3
- package/dist/client/index.js.map +1 -1
- package/dist/{dev-GBXOTXUP.js → dev-OWW4XVIH.js} +10 -10
- package/dist/{dev-emit-FEFEDLZF.js → dev-emit-5MDSBP5D.js} +3 -3
- package/dist/{dev-emit-O4EGOSNV.js → dev-emit-QH2YGZXN.js} +2 -2
- package/dist/index.js +6 -4
- package/dist/index.js.map +1 -1
- package/dist/internal-api-4YTJDITC.js +83 -0
- package/dist/internal-api-EFKZWIYZ.js +66 -0
- package/dist/internal-api-EFKZWIYZ.js.map +1 -0
- package/dist/{openapi-VR6AFBLJ.js → openapi-FHY6HC6I.js} +7 -7
- package/dist/{registry-Q2TZQLUH.js → registry-34LL7NF4.js} +1 -1
- package/dist/{routes-LRYOIIAI.js → routes-EW7TP7NJ.js} +2 -2
- package/dist/server/agent/index.js +2 -1
- package/dist/server/define/index.js +4 -2
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +9 -294
- package/dist/server/index.js.map +1 -1
- package/dist/server/scan/index.d.ts +22 -2
- package/dist/server/scan/index.js +1 -1
- package/dist/{start-3ZHAXSJE.js → start-KIQ5TTLR.js} +76 -13
- package/dist/start-KIQ5TTLR.js.map +1 -0
- package/dist/vite-plugin/index.js +6 -4
- package/dist/{vite-plugin-WO72VLYR.js → vite-plugin-RK66K26Z.js} +7 -7
- package/dist/vite-plugin-RK66K26Z.js.map +1 -0
- package/package.json +3 -3
- package/dist/chunk-223EFY5X.js.map +0 -1
- package/dist/chunk-3LVRAGAZ.js +0 -73
- package/dist/chunk-3LVRAGAZ.js.map +0 -1
- package/dist/chunk-43D6XNDR.js.map +0 -1
- package/dist/chunk-7CBRKNQA.js.map +0 -1
- package/dist/chunk-AD74EAK3.js.map +0 -1
- package/dist/chunk-GFMQJHXX.js.map +0 -1
- package/dist/chunk-PBEH6NXR.js +0 -44
- package/dist/chunk-PBEH6NXR.js.map +0 -1
- package/dist/chunk-PIVX3DYW.js +0 -142
- package/dist/chunk-PIVX3DYW.js.map +0 -1
- package/dist/chunk-PPPR5DGR.js +0 -1
- package/dist/chunk-RESN62GB.js.map +0 -1
- package/dist/start-3ZHAXSJE.js.map +0 -1
- /package/dist/{actions-virtual-module-SQDY3V5X.js.map → actions-virtual-module-3CDQTWOC.js.map} +0 -0
- /package/dist/{actions-virtual-module-PNPRCEOS.js.map → actions-virtual-module-EIPXX4ZB.js.map} +0 -0
- /package/dist/{app-typed-client-5GYEOYP3.js.map → app-typed-client-7PBFWZUE.js.map} +0 -0
- /package/dist/{app-typed-client-QG7BVZYW.js.map → app-typed-client-CSOK7NPC.js.map} +0 -0
- /package/dist/{build-QFRLSEZ4.js.map → build-HXND27XG.js.map} +0 -0
- /package/dist/{chunk-NAZ4E2GT.js.map → chunk-KXA37ONC.js.map} +0 -0
- /package/dist/{chunk-PPPR5DGR.js.map → chunk-RSVN727G.js.map} +0 -0
- /package/dist/{dev-GBXOTXUP.js.map → dev-OWW4XVIH.js.map} +0 -0
- /package/dist/{dev-emit-FEFEDLZF.js.map → dev-emit-5MDSBP5D.js.map} +0 -0
- /package/dist/{dev-emit-O4EGOSNV.js.map → dev-emit-QH2YGZXN.js.map} +0 -0
- /package/dist/{vite-plugin-WO72VLYR.js.map → internal-api-4YTJDITC.js.map} +0 -0
- /package/dist/{openapi-VR6AFBLJ.js.map → openapi-FHY6HC6I.js.map} +0 -0
- /package/dist/{registry-Q2TZQLUH.js.map → registry-34LL7NF4.js.map} +0 -0
- /package/dist/{routes-LRYOIIAI.js.map → routes-EW7TP7NJ.js.map} +0 -0
|
@@ -151,6 +151,33 @@ function stripComments(source) {
|
|
|
151
151
|
return out;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
// src/server/scan/agent-scan.ts
|
|
155
|
+
import { existsSync as existsSync2, statSync as statSync2 } from "fs";
|
|
156
|
+
import { extname as extname2, join as join2, relative as relative2 } from "path";
|
|
157
|
+
var AGENT_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
158
|
+
var TEST_FILE = /\.(test|spec)$/;
|
|
159
|
+
function scanAgents(projectRoot) {
|
|
160
|
+
const agentsDir = join2(projectRoot, "agents");
|
|
161
|
+
if (!existsSync2(agentsDir) || !statSync2(agentsDir).isDirectory()) {
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
const results = [];
|
|
165
|
+
walkSourceFiles(agentsDir, { extensions: AGENT_EXTENSIONS }, (absPath) => {
|
|
166
|
+
let rel = relative2(agentsDir, absPath);
|
|
167
|
+
rel = rel.replace(/\\/g, "/");
|
|
168
|
+
rel = rel.slice(0, -extname2(rel).length);
|
|
169
|
+
if (TEST_FILE.test(rel)) return;
|
|
170
|
+
if (rel.endsWith("/index")) rel = rel.slice(0, -6);
|
|
171
|
+
if (rel === "index" || rel === "") return;
|
|
172
|
+
results.push({
|
|
173
|
+
filePath: absPath,
|
|
174
|
+
agentPath: `/api/agents/${rel}`,
|
|
175
|
+
name: rel
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
return results;
|
|
179
|
+
}
|
|
180
|
+
|
|
154
181
|
// src/server/scan/match.ts
|
|
155
182
|
function compilePattern(routePath) {
|
|
156
183
|
const paramNames = [];
|
|
@@ -179,8 +206,8 @@ function matchRoute(url, routes) {
|
|
|
179
206
|
}
|
|
180
207
|
|
|
181
208
|
// src/server/scan/scan.ts
|
|
182
|
-
import { existsSync as
|
|
183
|
-
import { basename, extname as
|
|
209
|
+
import { existsSync as existsSync3, statSync as statSync3 } from "fs";
|
|
210
|
+
import { basename, extname as extname3, join as join3, relative as relative3 } from "path";
|
|
184
211
|
|
|
185
212
|
// src/server/scan/detect-http-methods.ts
|
|
186
213
|
import { readFileSync as readFileSync2 } from "fs";
|
|
@@ -297,15 +324,15 @@ function splitDottedSegmentOutsideBrackets(segment) {
|
|
|
297
324
|
return parts;
|
|
298
325
|
}
|
|
299
326
|
function buildDirectoryNestedSuggestion(filePath, routesDir) {
|
|
300
|
-
const rel =
|
|
301
|
-
const ext =
|
|
327
|
+
const rel = relative3(routesDir, filePath).replace(/\\/g, "/");
|
|
328
|
+
const ext = extname3(rel);
|
|
302
329
|
const withoutExt = rel.slice(0, -ext.length);
|
|
303
330
|
const segments = withoutExt.split("/").flatMap(splitDottedSegmentOutsideBrackets);
|
|
304
331
|
return `routes/${segments.join("/")}${ext}`;
|
|
305
332
|
}
|
|
306
333
|
function assertNoDottedSegment(filePath, routesDir) {
|
|
307
|
-
const rel =
|
|
308
|
-
const ext =
|
|
334
|
+
const rel = relative3(routesDir, filePath).replace(/\\/g, "/");
|
|
335
|
+
const ext = extname3(rel);
|
|
309
336
|
const withoutExt = rel.slice(0, -ext.length);
|
|
310
337
|
const segments = withoutExt.split("/");
|
|
311
338
|
for (const seg of segments) {
|
|
@@ -318,8 +345,8 @@ function assertNoDottedSegment(filePath, routesDir) {
|
|
|
318
345
|
}
|
|
319
346
|
}
|
|
320
347
|
function fileToRoutePath(filePath, routesDir) {
|
|
321
|
-
let rel =
|
|
322
|
-
const ext =
|
|
348
|
+
let rel = relative3(routesDir, filePath);
|
|
349
|
+
const ext = extname3(rel);
|
|
323
350
|
rel = rel.slice(0, -ext.length);
|
|
324
351
|
rel = rel.replace(/\\/g, "/");
|
|
325
352
|
if (rel.endsWith("/index")) {
|
|
@@ -332,8 +359,8 @@ function fileToRoutePath(filePath, routesDir) {
|
|
|
332
359
|
return `/api/${rel}`;
|
|
333
360
|
}
|
|
334
361
|
function scanServerRoutes(serverDir) {
|
|
335
|
-
const routesDir =
|
|
336
|
-
if (!
|
|
362
|
+
const routesDir = join3(serverDir, "routes");
|
|
363
|
+
if (!existsSync3(routesDir) || !statSync3(routesDir).isDirectory()) {
|
|
337
364
|
return [];
|
|
338
365
|
}
|
|
339
366
|
const results = [];
|
|
@@ -373,19 +400,19 @@ function scanServerRoutes(serverDir) {
|
|
|
373
400
|
}
|
|
374
401
|
|
|
375
402
|
// src/server/scan/ws-scan.ts
|
|
376
|
-
import { existsSync as
|
|
377
|
-
import { extname as
|
|
403
|
+
import { existsSync as existsSync4, statSync as statSync4 } from "fs";
|
|
404
|
+
import { extname as extname4, join as join4, relative as relative4 } from "path";
|
|
378
405
|
var WS_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
379
406
|
function scanWebSocketRoutes(serverDir) {
|
|
380
|
-
const wsDir =
|
|
381
|
-
if (!
|
|
407
|
+
const wsDir = join4(serverDir, "ws");
|
|
408
|
+
if (!existsSync4(wsDir) || !statSync4(wsDir).isDirectory()) {
|
|
382
409
|
return [];
|
|
383
410
|
}
|
|
384
411
|
const results = [];
|
|
385
412
|
walkSourceFiles(wsDir, { extensions: WS_EXTENSIONS }, (absPath) => {
|
|
386
|
-
let rel =
|
|
413
|
+
let rel = relative4(wsDir, absPath);
|
|
387
414
|
rel = rel.replace(/\\/g, "/");
|
|
388
|
-
rel = rel.slice(0, -
|
|
415
|
+
rel = rel.slice(0, -extname4(rel).length);
|
|
389
416
|
if (rel.endsWith("/index")) rel = rel.slice(0, -6);
|
|
390
417
|
else if (rel === "index") rel = "";
|
|
391
418
|
results.push({
|
|
@@ -397,39 +424,46 @@ function scanWebSocketRoutes(serverDir) {
|
|
|
397
424
|
}
|
|
398
425
|
|
|
399
426
|
// src/server/scan/manifest.ts
|
|
400
|
-
import { existsSync as
|
|
401
|
-
import { join as
|
|
402
|
-
function generateManifest(serverDir) {
|
|
427
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync, mkdirSync } from "fs";
|
|
428
|
+
import { join as join5, resolve, relative as relative5, dirname } from "path";
|
|
429
|
+
function generateManifest(serverDir, projectRoot = dirname(serverDir)) {
|
|
403
430
|
const routes = scanServerRoutes(serverDir);
|
|
404
431
|
const actions = scanServerActions(serverDir);
|
|
405
432
|
const websockets = scanWebSocketRoutes(serverDir);
|
|
433
|
+
const agents = scanAgents(projectRoot);
|
|
406
434
|
return {
|
|
407
435
|
version: 1,
|
|
408
436
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
409
437
|
routes: routes.map((r) => ({
|
|
410
|
-
filePath:
|
|
438
|
+
filePath: relative5(serverDir, r.filePath),
|
|
411
439
|
routePath: r.routePath,
|
|
412
440
|
paramNames: r.paramNames,
|
|
413
441
|
...r.methods !== void 0 ? { methods: r.methods } : {}
|
|
414
442
|
})),
|
|
415
443
|
actions: actions.map((a) => ({
|
|
416
|
-
filePath:
|
|
444
|
+
filePath: relative5(serverDir, a.filePath),
|
|
417
445
|
actionPath: a.actionPath
|
|
418
446
|
})),
|
|
419
447
|
websockets: websockets.map((w) => ({
|
|
420
|
-
filePath:
|
|
448
|
+
filePath: relative5(serverDir, w.filePath),
|
|
421
449
|
wsPath: w.wsPath
|
|
450
|
+
})),
|
|
451
|
+
agents: agents.map((a) => ({
|
|
452
|
+
// Relative to projectRoot (agents/ is outside serverDir).
|
|
453
|
+
filePath: relative5(projectRoot, a.filePath),
|
|
454
|
+
agentPath: a.agentPath,
|
|
455
|
+
name: a.name
|
|
422
456
|
}))
|
|
423
457
|
};
|
|
424
458
|
}
|
|
425
459
|
function writeManifest(manifest, outputDir) {
|
|
426
460
|
mkdirSync(outputDir, { recursive: true });
|
|
427
|
-
const manifestPath =
|
|
461
|
+
const manifestPath = join5(outputDir, "manifest.json");
|
|
428
462
|
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
429
463
|
}
|
|
430
464
|
function loadManifest(distDir, serverDir) {
|
|
431
|
-
const manifestPath =
|
|
432
|
-
if (!
|
|
465
|
+
const manifestPath = join5(distDir, "manifest.json");
|
|
466
|
+
if (!existsSync5(manifestPath)) {
|
|
433
467
|
throw new Error(`No manifest found at ${manifestPath}. Run "theo build" first.`);
|
|
434
468
|
}
|
|
435
469
|
const raw = JSON.parse(readFileSync3(manifestPath, "utf-8"));
|
|
@@ -451,13 +485,20 @@ function loadManifest(distDir, serverDir) {
|
|
|
451
485
|
filePath: resolve(serverDir, w.filePath),
|
|
452
486
|
wsPath: w.wsPath
|
|
453
487
|
}));
|
|
454
|
-
|
|
488
|
+
const projectRoot = dirname(serverDir);
|
|
489
|
+
const agents = (raw.agents ?? []).map((a) => ({
|
|
490
|
+
filePath: resolve(projectRoot, a.filePath),
|
|
491
|
+
agentPath: a.agentPath,
|
|
492
|
+
name: a.name
|
|
493
|
+
}));
|
|
494
|
+
return { routes, actions, websockets, agents };
|
|
455
495
|
}
|
|
456
496
|
|
|
457
497
|
export {
|
|
458
498
|
ActionScanError,
|
|
459
499
|
scanServerActions,
|
|
460
500
|
scanServerActionsEnriched,
|
|
501
|
+
scanAgents,
|
|
461
502
|
compilePattern,
|
|
462
503
|
matchRoute,
|
|
463
504
|
scanServerRoutes,
|
|
@@ -466,4 +507,4 @@ export {
|
|
|
466
507
|
writeManifest,
|
|
467
508
|
loadManifest
|
|
468
509
|
};
|
|
469
|
-
//# sourceMappingURL=chunk-
|
|
510
|
+
//# sourceMappingURL=chunk-2J7XU3PW.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server/scan/action-scan.ts","../src/server/scan/agent-scan.ts","../src/server/scan/match.ts","../src/server/scan/scan.ts","../src/server/scan/detect-http-methods.ts","../src/server/scan/errors.ts","../src/server/scan/ws-scan.ts","../src/server/scan/manifest.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 * P#4 plugin-forms shared-schema convention (per plan p4-plugin-forms v1.1 T1.1).\n * When present, points to an isomorphic schema file at\n * `<serverDir>/actions/schemas/<basename>.ts` exporting `export const schema = z.object(...)`.\n * Virtual module emits `import {schema} from '<schemaFilePath>'` + attaches as\n * `actions.X.__zodSchema` so client-side <TheoForm> can drive zodResolver.\n * Undefined when convention not followed (graceful degrade — TheoForm still\n * works via explicit `schema={...}` prop escape hatch).\n */\n schemaFilePath?: string\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 // T4.1 fix: identify class by name so serverErrorToEnvelope boundary\n // translator can route via class-name lookup (without this, runtime\n // `err.name` defaults to 'Error' and the meta.name diagnostic is wrong).\n this.name = 'ActionScanError'\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 * `.theokit/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 // P#4 plugin-forms — `schemas/` subdir holds isomorphic zod schemas\n // for the shared-schema convention (T1.1). NOT executable actions; skip.\n if (rel.startsWith('schemas/')) return\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 // P#4 plugin-forms shared-schema convention: check for\n // `<actionsDir>/schemas/<basename>.ts` (or .tsx/.js/.jsx).\n // Skip when actions live in subdirs (`schemas/` is flat by convention).\n const schemaFilePath = name.includes('/') ? undefined : detectSchemaFile(actionsDir, basename)\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 const entry: ActionManifestEntry = {\n name: proxyKey,\n filePath: absPath,\n urlPath: `/api/__actions/${name}/${exportName}`,\n accept,\n hasInput,\n }\n if (schemaFilePath !== undefined) {\n entry.schemaFilePath = schemaFilePath\n }\n entries.push(entry)\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\n/**\n * P#4 plugin-forms shared-schema convention helper (per plan p4-plugin-forms v1.1 T1.1).\n * Returns the resolved path to `<actionsDir>/schemas/<basename>.<ext>` if it exists.\n * Tries `.ts`, `.tsx`, `.js`, `.jsx` in that order. Returns undefined when no match.\n */\nfunction detectSchemaFile(actionsDir: string, basename: string): string | undefined {\n const schemasDir = join(actionsDir, 'schemas')\n if (!existsSync(schemasDir)) return undefined\n for (const ext of ['.ts', '.tsx', '.js', '.jsx']) {\n const candidate = join(schemasDir, `${basename}${ext}`)\n if (existsSync(candidate)) return candidate\n }\n return undefined\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 `<projectRoot>/agents/` derived from cwd.\n * No HTTP input ever reaches these fs calls.\n */\nimport { existsSync, statSync } from 'node:fs'\nimport { extname, join, relative } from 'node:path'\n\nimport { walkSourceFiles } from '../_internal/scan-walker.js'\n\nconst AGENT_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx'])\n// Convention § 5: a co-located test file is not an agent.\nconst TEST_FILE = /\\.(test|spec)$/\n\n/**\n * A discovered agent file. `name` is the client-facing key; `agentPath` is the mounted\n * SSE route (M0/M1 `UIMessageStream`).\n */\nexport interface AgentNode {\n filePath: string\n agentPath: string\n name: string\n}\n\n/**\n * M2 — scan the TOP-LEVEL `agents/` convention (sibling of `server/`, per the LOCKED naming\n * decision). Mirrors `scanWebSocketRoutes`: one file → one endpoint, `index` stripped.\n */\nexport function scanAgents(projectRoot: string): AgentNode[] {\n const agentsDir = join(projectRoot, 'agents')\n if (!existsSync(agentsDir) || !statSync(agentsDir).isDirectory()) {\n return []\n }\n\n const results: AgentNode[] = []\n walkSourceFiles(agentsDir, { extensions: AGENT_EXTENSIONS }, (absPath) => {\n let rel = relative(agentsDir, absPath)\n rel = rel.replace(/\\\\/g, '/')\n rel = rel.slice(0, -extname(rel).length)\n if (TEST_FILE.test(rel)) return\n // Unlike routes/ws, an agent needs an explicit name — a bare `agents/index.ts`\n // (name `''` → `/api/agents/`) is nonsensical for a typed `useAgent(name)` binding.\n // `agents/foo/index.ts` still collapses to `foo` (a named nested agent).\n if (rel.endsWith('/index')) rel = rel.slice(0, -6)\n if (rel === 'index' || rel === '') return\n results.push({\n filePath: absPath,\n agentPath: `/api/agents/${rel}`,\n name: rel,\n })\n })\n return results\n}\n","export interface ServerRouteNode {\n filePath: string\n routePath: string\n paramNames: string[]\n pattern: RegExp\n /** HTTP methods (uppercase) the route file exports. Optional for backward\n * compatibility with manifests generated before G1. Empty array means the\n * file has no HTTP exports (util-only); undefined means \"not detected\". */\n methods?: string[]\n}\n\nexport function compilePattern(routePath: string): {\n pattern: RegExp\n paramNames: string[]\n} {\n const paramNames: string[] = []\n // Single pass: handle both catch-all (:...name) and regular (:name) params\n const regexStr = routePath.replace(/:(?:\\.\\.\\.)?([^/]+)/g, (match: string, name: string) => {\n paramNames.push(name)\n // Catch-all matches across slashes, regular matches single segment\n return match.startsWith(':...') ? '(.+)' : '([^/]+)'\n })\n // `regexStr` is derived from a developer-authored route path (build-time\n // input, not HTTP-controlled). The `security/detect-non-literal-regexp`\n // rule cannot see this constraint — disable narrowly.\n // eslint-disable-next-line security/detect-non-literal-regexp -- route pattern from build-time scan, never HTTP input\n return { pattern: new RegExp(`^${regexStr}$`), paramNames }\n}\n\nexport function matchRoute(\n url: string,\n routes: ServerRouteNode[],\n): { route: ServerRouteNode; params: Record<string, string> } | null {\n // Strip query string and trailing slash\n let path = url.split('?')[0]\n if (path.length > 1 && path.endsWith('/')) {\n path = path.slice(0, -1)\n }\n\n for (const route of routes) {\n const match = route.pattern.exec(path)\n if (match) {\n const params: Record<string, string> = {}\n route.paramNames.forEach((name, i) => {\n params[name] = match[i + 1]\n })\n return { route, params }\n }\n }\n return null\n}\n","/* eslint-disable security/detect-non-literal-fs-filename --\n * Build-time scanner: walks `serverDir/routes/` derived from cwd.\n * No HTTP input ever reaches these fs calls.\n */\nimport { existsSync, statSync } from 'node:fs'\nimport { basename, extname, join, relative } from 'node:path'\n\nimport { walkSourceFiles } from '../_internal/scan-walker.js'\n\nimport { detectExportedHttpMethods } from './detect-http-methods.js'\nimport { RouterConventionError } from './errors.js'\nimport { compilePattern, type ServerRouteNode } from './match.js'\n\nconst ROUTE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx'])\n\n// EC-4: Co-located unit/spec tests must be silently skipped by the scanner,\n// BEFORE the dotted-basename check fires. Matches `*.test.ts|tsx|js|jsx`\n// and `*.spec.ts|tsx|js|jsx`.\nconst TEST_OR_SPEC_RE = /\\.(test|spec)\\.[jt]sx?$/\n\nfunction isTestOrSpecFile(filePath: string): boolean {\n return TEST_OR_SPEC_RE.test(basename(filePath))\n}\n\nfunction hasDotOutsideBrackets(segment: string): boolean {\n let depth = 0\n for (const ch of segment) {\n if (ch === '[') depth++\n else if (ch === ']') depth--\n else if (ch === '.' && depth === 0) return true\n }\n return false\n}\n\nfunction splitDottedSegmentOutsideBrackets(segment: string): string[] {\n const parts: string[] = []\n let current = ''\n let depth = 0\n for (const ch of segment) {\n if (ch === '[') {\n depth++\n current += ch\n } else if (ch === ']') {\n depth--\n current += ch\n } else if (ch === '.' && depth === 0) {\n if (current) parts.push(current)\n current = ''\n } else {\n current += ch\n }\n }\n if (current) parts.push(current)\n return parts\n}\n\nfunction buildDirectoryNestedSuggestion(filePath: string, routesDir: string): string {\n const rel = relative(routesDir, filePath).replace(/\\\\/g, '/')\n const ext = extname(rel)\n const withoutExt = rel.slice(0, -ext.length)\n const segments = withoutExt.split('/').flatMap(splitDottedSegmentOutsideBrackets)\n return `routes/${segments.join('/')}${ext}`\n}\n\nfunction assertNoDottedSegment(filePath: string, routesDir: string): void {\n const rel = relative(routesDir, filePath).replace(/\\\\/g, '/')\n const ext = extname(rel)\n const withoutExt = rel.slice(0, -ext.length)\n const segments = withoutExt.split('/')\n for (const seg of segments) {\n if (hasDotOutsideBrackets(seg)) {\n throw new RouterConventionError({\n file: filePath,\n suggestion: buildDirectoryNestedSuggestion(filePath, routesDir),\n })\n }\n }\n}\n\nfunction fileToRoutePath(filePath: string, routesDir: string): string {\n let rel = relative(routesDir, filePath)\n // Strip extension\n const ext = extname(rel)\n rel = rel.slice(0, -ext.length)\n // Normalize separators\n rel = rel.replace(/\\\\/g, '/')\n // Strip index suffix\n if (rel.endsWith('/index')) {\n rel = rel.slice(0, -6)\n } else if (rel === 'index') {\n rel = ''\n }\n // Replace [...param] with :...param (catch-all, before regular params).\n // Replace [param] with :param. Inputs are file paths bounded by the\n // OS filename limit; the bracket capture is bounded by `]`.\n rel = rel.replace(/\\[\\.\\.\\.([^\\]]+)\\]/g, ':...$1')\n // eslint-disable-next-line sonarjs/slow-regex -- bounded by `]`; input is a single filename\n rel = rel.replace(/\\[([^\\]]+)\\]/g, ':$1')\n return `/api/${rel}`\n}\n\nexport function scanServerRoutes(serverDir: string): ServerRouteNode[] {\n const routesDir = join(serverDir, 'routes')\n if (!existsSync(routesDir) || !statSync(routesDir).isDirectory()) {\n return []\n }\n\n const results: ServerRouteNode[] = []\n walkSourceFiles(routesDir, { extensions: ROUTE_EXTENSIONS }, (absPath) => {\n // EC-4: skip co-located test/spec files BEFORE the dotted-basename check\n if (isTestOrSpecFile(absPath)) return\n\n // G6 T1.1: reject dotted basenames (legacy convention that produced wrong\n // paramNames due to greedy `:(?:\\.\\.\\.)?([^/]+)` regex in compilePattern).\n assertNoDottedSegment(absPath, routesDir)\n\n const routePath = fileToRoutePath(absPath, routesDir)\n const { pattern, paramNames } = compilePattern(routePath)\n const methods = detectExportedHttpMethods(absPath)\n results.push({\n filePath: absPath,\n routePath,\n paramNames,\n pattern,\n methods,\n })\n })\n\n // T1.4 / EC-2 — refuse to scan if a user route collides with the reserved\n // batch endpoint path. User must rename or disable batching.\n const conflicting = results.find((r) => r.routePath === '/api/__theo_batch__')\n if (conflicting) {\n throw new Error(\n `Server route ${conflicting.filePath} resolves to '/api/__theo_batch__' which is reserved for the batch endpoint. Rename the route or disable batching in theo.config.ts.`,\n )\n }\n\n // Sort: static first, then dynamic, then catch-all last\n const isCatchAll = (route: ServerRouteNode) => route.routePath.includes(':...')\n results.sort((a, b) => {\n const aStatic = a.paramNames.length === 0\n const bStatic = b.paramNames.length === 0\n const aCatchAll = isCatchAll(a)\n const bCatchAll = isCatchAll(b)\n\n // Static routes first\n if (aStatic && !bStatic) return -1\n if (!aStatic && bStatic) return 1\n // Catch-all routes last\n if (aCatchAll && !bCatchAll) return 1\n if (!aCatchAll && bCatchAll) return -1\n return a.routePath.localeCompare(b.routePath)\n })\n\n return results\n}\n","/**\n * Detect which HTTP-method named exports a route file declares.\n *\n * Uses the TypeScript compiler API (not regex) per G1 edge-case review EC-4:\n * regex over file content emits false positives for `// export const GET = ...`\n * in comments and `` `export const GET = ...` `` in template literals. AST\n * walking avoids both classes of bug.\n *\n * `typescript` ships as CommonJS with internal dynamic `require('fs')`.\n * When loaded via ESM `import`, the dynamic requires fail at module-bootstrap.\n * We use `createRequire(import.meta.url)` to keep the package on its native\n * CJS path. The type-only namespace import gives us the AST helpers shape.\n *\n * Returns the set of HTTP methods (uppercase) the file exports. Empty array\n * means the file has no HTTP exports (the route file is util-only).\n */\n\nimport { readFileSync } from 'node:fs'\nimport { createRequire } from 'node:module'\n\nimport type * as TS from 'typescript'\n\nimport { HTTP_METHODS, type HttpMethod } from '../../core/contracts/http-methods.js'\n\nconst require_ = createRequire(import.meta.url)\n// eslint-disable-next-line @typescript-eslint/no-require-imports\nconst ts = require_('typescript') as typeof TS\n\nconst HTTP_METHOD_NAMES = new Set<string>(HTTP_METHODS)\n\nfunction hasExportModifier(modifiers: readonly TS.Modifier[] | undefined): boolean {\n if (!modifiers) return false\n for (const m of modifiers) {\n if (m.kind === ts.SyntaxKind.ExportKeyword) return true\n }\n return false\n}\n\nfunction collectFromStatement(stmt: TS.Statement, found: Set<HttpMethod>): void {\n // `export const GET = ...` / `export function GET ...` / `export async function GET ...`\n if (ts.isVariableStatement(stmt) && hasExportModifier(ts.getModifiers(stmt))) {\n for (const decl of stmt.declarationList.declarations) {\n if (ts.isIdentifier(decl.name) && HTTP_METHOD_NAMES.has(decl.name.text)) {\n found.add(decl.name.text as HttpMethod)\n }\n }\n return\n }\n\n if (\n (ts.isFunctionDeclaration(stmt) || ts.isClassDeclaration(stmt)) &&\n hasExportModifier(ts.getModifiers(stmt))\n ) {\n if (stmt.name && HTTP_METHOD_NAMES.has(stmt.name.text)) {\n found.add(stmt.name.text as HttpMethod)\n }\n return\n }\n\n // `export { GET }` / `export { handler as GET } from './shared'` (EC-5)\n if (ts.isExportDeclaration(stmt) && stmt.exportClause && ts.isNamedExports(stmt.exportClause)) {\n for (const spec of stmt.exportClause.elements) {\n // spec.name is the exported (re-)name; spec.propertyName is the original (when renamed)\n if (HTTP_METHOD_NAMES.has(spec.name.text)) {\n found.add(spec.name.text as HttpMethod)\n }\n }\n }\n}\n\nexport function detectExportedHttpMethods(filePath: string, content?: string): HttpMethod[] {\n const src = content ?? readFileSync(filePath, 'utf-8')\n const sourceFile = ts.createSourceFile(\n filePath,\n src,\n ts.ScriptTarget.Latest,\n /* setParentNodes */ false,\n ts.ScriptKind.TS,\n )\n const found = new Set<HttpMethod>()\n for (const stmt of sourceFile.statements) {\n collectFromStatement(stmt, found)\n }\n return [...found].sort()\n}\n","/**\n * Router-convention errors thrown by the server-route scanner.\n *\n * Plan: .claude/knowledge-base/plans/g6-router-convention-plan.md v1.1\n *\n * theokit 0.4.0+ enforces directory-nested file-system routing\n * (`auth/[provider]/login.ts`) and REJECTS dotted-basename routes\n * (`auth.[provider].login.ts`) because the legacy regex extracted the\n * dotted basename incorrectly — `params.provider` was undefined at request\n * time. See ADR-XXX (router-convention-decision) in CHANGELOG 0.4.0.\n */\n\n/**\n * Canonical migration guide URL. T4.2 establishes this as the authoritative\n * landing page. EC-3: error message uses this constant so the URL never\n * drifts from the doc location.\n */\nexport const ROUTER_MIGRATION_GUIDE_URL = 'https://theokit.dev/migration/0.3-to-0.4-router'\n\nexport interface RouterConventionErrorOptions {\n /** Absolute path of the offending route file. */\n file: string\n /** Suggested directory-nested replacement path (relative, e.g. `routes/auth/[provider]/login.ts`). */\n suggestion: string\n /** Migration guide URL (defaults to `ROUTER_MIGRATION_GUIDE_URL`). */\n migrationUrl?: string\n}\n\n/**\n * Thrown by `scanServerRoutes` when a route file uses the legacy\n * dotted-basename convention (`auth.[provider].login.ts`).\n *\n * The error is FAIL-FAST by design — running with a route that has wrong\n * `paramNames` produces silent 404s at request time, which is strictly\n * worse than a build-time error.\n */\nexport class RouterConventionError extends Error {\n override readonly name = 'RouterConventionError'\n readonly file: string\n readonly suggestion: string\n readonly migrationUrl: string\n\n constructor(opts: RouterConventionErrorOptions) {\n const migrationUrl = opts.migrationUrl ?? ROUTER_MIGRATION_GUIDE_URL\n const message = [\n `Router convention violation: dotted route basename is not supported in theokit 0.4+.`,\n ``,\n ` File: ${opts.file}`,\n ` Use directory-nested form: ${opts.suggestion}`,\n ``,\n `Migration guide: ${migrationUrl}`,\n `Run \\`theokit migrate router\\` to convert all dotted basenames automatically.`,\n ].join('\\n')\n super(message)\n this.file = opts.file\n this.suggestion = opts.suggestion\n this.migrationUrl = migrationUrl\n }\n}\n","/* eslint-disable security/detect-non-literal-fs-filename --\n * Build-time scanner: walks `serverDir/ws/` derived from cwd.\n * No HTTP input ever reaches these fs calls.\n */\nimport { existsSync, statSync } from 'node:fs'\nimport { extname, join, relative } from 'node:path'\n\nimport { walkSourceFiles } from '../_internal/scan-walker.js'\n\nconst WS_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx'])\n\nexport interface WebSocketRouteNode {\n filePath: string\n wsPath: string\n}\n\nexport function scanWebSocketRoutes(serverDir: string): WebSocketRouteNode[] {\n const wsDir = join(serverDir, 'ws')\n if (!existsSync(wsDir) || !statSync(wsDir).isDirectory()) {\n return []\n }\n\n const results: WebSocketRouteNode[] = []\n walkSourceFiles(wsDir, { extensions: WS_EXTENSIONS }, (absPath) => {\n let rel = relative(wsDir, absPath)\n rel = rel.replace(/\\\\/g, '/')\n rel = rel.slice(0, -extname(rel).length)\n if (rel.endsWith('/index')) rel = rel.slice(0, -6)\n else if (rel === 'index') rel = ''\n results.push({\n filePath: absPath,\n wsPath: `/ws/${rel}`,\n })\n })\n return results\n}\n","/* eslint-disable security/detect-non-literal-fs-filename --\n * Build-time manifest emitter / loader. All paths derived from `distDir`\n * + `serverDir`, themselves resolved from `process.cwd()`. No HTTP input.\n */\nimport { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'\nimport { join, resolve, relative, dirname } from 'node:path'\n\nimport { scanServerActions } from './action-scan.js'\nimport type { ActionNode } from './action-scan.js'\nimport { scanAgents } from './agent-scan.js'\nimport type { AgentNode } from './agent-scan.js'\nimport { compilePattern } from './match.js'\nimport type { ServerRouteNode } from './match.js'\nimport { scanServerRoutes } from './scan.js'\nimport { scanWebSocketRoutes } from './ws-scan.js'\nimport type { WebSocketRouteNode } from './ws-scan.js'\n\n// --- Manifest Types ---\n\nexport interface ManifestRoute {\n filePath: string\n routePath: string\n paramNames: string[]\n /** HTTP methods (uppercase) the route file exports. Optional — manifests\n * generated before G1 omit this; loaders treat absence as \"unknown\". */\n methods?: string[]\n}\n\nexport interface ManifestAction {\n filePath: string\n actionPath: string\n}\n\nexport interface ManifestWebSocket {\n filePath: string\n wsPath: string\n}\n\n/** M2 — a top-level `agents/*.ts` convention entry. `filePath` is relative to the\n * project root (agents live OUTSIDE `serverDir`), unlike routes/actions/ws. */\nexport interface ManifestAgent {\n filePath: string\n agentPath: string\n name: string\n}\n\nexport interface TheoManifest {\n version: 1\n generatedAt: string\n routes: ManifestRoute[]\n actions: ManifestAction[]\n websockets: ManifestWebSocket[]\n /** M2 — optional for backward compat: manifests generated before M2 omit it. */\n agents?: ManifestAgent[]\n}\n\nexport interface LoadedManifest {\n routes: ServerRouteNode[]\n actions: ActionNode[]\n websockets: WebSocketRouteNode[]\n agents: AgentNode[]\n}\n\n// --- Generate ---\n\nexport function generateManifest(\n serverDir: string,\n // Agents live at `<projectRoot>/agents`, a sibling of `serverDir` (LOCKED naming).\n // Defaults to the server dir's parent; overridable for tests / non-standard layouts.\n projectRoot: string = dirname(serverDir),\n): TheoManifest {\n const routes = scanServerRoutes(serverDir)\n const actions = scanServerActions(serverDir)\n const websockets = scanWebSocketRoutes(serverDir)\n const agents = scanAgents(projectRoot)\n\n return {\n version: 1,\n generatedAt: new Date().toISOString(),\n routes: routes.map((r) => ({\n filePath: relative(serverDir, r.filePath),\n routePath: r.routePath,\n paramNames: r.paramNames,\n ...(r.methods !== undefined ? { methods: r.methods } : {}),\n })),\n actions: actions.map((a) => ({\n filePath: relative(serverDir, a.filePath),\n actionPath: a.actionPath,\n })),\n websockets: websockets.map((w) => ({\n filePath: relative(serverDir, w.filePath),\n wsPath: w.wsPath,\n })),\n agents: agents.map((a) => ({\n // Relative to projectRoot (agents/ is outside serverDir).\n filePath: relative(projectRoot, a.filePath),\n agentPath: a.agentPath,\n name: a.name,\n })),\n }\n}\n\n// --- Write ---\n\nexport function writeManifest(manifest: TheoManifest, outputDir: string): void {\n mkdirSync(outputDir, { recursive: true })\n const manifestPath = join(outputDir, 'manifest.json')\n writeFileSync(manifestPath, JSON.stringify(manifest, null, 2))\n}\n\n// --- Load ---\n\nexport function loadManifest(distDir: string, serverDir: string): LoadedManifest {\n const manifestPath = join(distDir, 'manifest.json')\n\n if (!existsSync(manifestPath)) {\n throw new Error(`No manifest found at ${manifestPath}. Run \"theo build\" first.`)\n }\n\n const raw = JSON.parse(readFileSync(manifestPath, 'utf-8')) as TheoManifest\n\n const routes: ServerRouteNode[] = raw.routes.map((r) => {\n const { pattern, paramNames } = compilePattern(r.routePath)\n return {\n filePath: resolve(serverDir, r.filePath),\n routePath: r.routePath,\n paramNames,\n pattern,\n ...(r.methods !== undefined ? { methods: r.methods } : {}),\n }\n })\n\n const actions: ActionNode[] = raw.actions.map((a) => ({\n filePath: resolve(serverDir, a.filePath),\n actionPath: a.actionPath,\n }))\n\n const websockets: WebSocketRouteNode[] = raw.websockets.map((w) => ({\n filePath: resolve(serverDir, w.filePath),\n wsPath: w.wsPath,\n }))\n\n // M2 — agents resolve relative to the project root (sibling of serverDir).\n // `?? []` keeps pre-M2 manifests (no `agents` field) loadable (fail-safe).\n const projectRoot = dirname(serverDir)\n const agents: AgentNode[] = (raw.agents ?? []).map((a) => ({\n filePath: resolve(projectRoot, a.filePath),\n agentPath: a.agentPath,\n name: a.name,\n }))\n\n return { routes, actions, websockets, agents }\n}\n"],"mappings":"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","names":["basename","existsSync","statSync","extname","join","relative","join","existsSync","statSync","relative","extname","existsSync","statSync","extname","join","relative","readFileSync","readFileSync","relative","extname","join","existsSync","statSync","existsSync","statSync","extname","join","relative","join","existsSync","statSync","relative","extname","existsSync","readFileSync","join","relative","relative","join","existsSync","readFileSync"]}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
tryResolveProvider
|
|
3
|
+
} from "./chunk-EXP56GFQ.js";
|
|
4
|
+
|
|
1
5
|
// src/server/agent/stream-agent-run.ts
|
|
2
6
|
function safeJsonStringify(value) {
|
|
3
7
|
try {
|
|
@@ -94,53 +98,6 @@ async function* streamAgentRun(run) {
|
|
|
94
98
|
}
|
|
95
99
|
}
|
|
96
100
|
|
|
97
|
-
// src/server/agent/provider-resolver.ts
|
|
98
|
-
var DEFAULT_REGISTRY = [
|
|
99
|
-
{
|
|
100
|
-
name: "openrouter",
|
|
101
|
-
envKey: "OPENROUTER_API_KEY",
|
|
102
|
-
baseUrl: "https://openrouter.ai/api/v1",
|
|
103
|
-
priority: 1
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
name: "openai",
|
|
107
|
-
envKey: "OPENAI_API_KEY",
|
|
108
|
-
baseUrl: "https://api.openai.com/v1",
|
|
109
|
-
priority: 2
|
|
110
|
-
},
|
|
111
|
-
{
|
|
112
|
-
name: "anthropic",
|
|
113
|
-
envKey: "ANTHROPIC_API_KEY",
|
|
114
|
-
baseUrl: "https://api.anthropic.com",
|
|
115
|
-
priority: 3
|
|
116
|
-
}
|
|
117
|
-
];
|
|
118
|
-
var registry = [...DEFAULT_REGISTRY];
|
|
119
|
-
function resolveProvider() {
|
|
120
|
-
const sorted = [...registry].sort((a, b) => a.priority - b.priority);
|
|
121
|
-
for (const desc of sorted) {
|
|
122
|
-
const apiKey = process.env[desc.envKey];
|
|
123
|
-
if (apiKey && apiKey.length > 0) {
|
|
124
|
-
return {
|
|
125
|
-
name: desc.name,
|
|
126
|
-
apiKey,
|
|
127
|
-
baseUrl: desc.baseUrl
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
const envKeys = sorted.map((p) => p.envKey).join(" OR ");
|
|
132
|
-
throw new Error(
|
|
133
|
-
`No LLM provider API key found in environment. Set one of: ${envKeys}. Get a free OpenRouter key at https://openrouter.ai/keys (recommended \u2014 one key, many models).`
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
function tryResolveProvider() {
|
|
137
|
-
try {
|
|
138
|
-
return resolveProvider();
|
|
139
|
-
} catch {
|
|
140
|
-
return null;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
101
|
// src/server/agent/create-conversation-history.ts
|
|
145
102
|
var sdkOverride = void 0;
|
|
146
103
|
function __setSdkForTests(sdk) {
|
|
@@ -266,4 +223,4 @@ export {
|
|
|
266
223
|
__resetSdkForTests,
|
|
267
224
|
createConversationHistory
|
|
268
225
|
};
|
|
269
|
-
//# sourceMappingURL=chunk-
|
|
226
|
+
//# sourceMappingURL=chunk-2KZQPDYR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server/agent/stream-agent-run.ts","../src/server/agent/create-conversation-history.ts"],"sourcesContent":["import type { AgentEvent, AgentErrorEvent } from './agent-types.js'\n\n/**\n * Item #4 — `streamAgentRun`\n *\n * Adapter that consumes the `@theokit/sdk` `Run.stream()` async generator\n * (SDKMessage variants) and yields TheoKit `AgentEvent`s suitable for SSE\n * via `defineAgentEndpoint`. One line at the consumer side:\n *\n * ```ts\n * const run = await agent.send(message)\n * yield* streamAgentRun(run)\n * ```\n *\n * Mapping table (SDK → AgentEvent):\n *\n * | SDK message | AgentEvent yielded |\n * |---|---|\n * | assistant.content[].type==='text' | { type: 'message', content } |\n * | assistant.content[].type==='tool_use'| (none — covered by tool_call below) |\n * | tool_call(status='running') | { type: 'tool_call', name, args, id } |\n * | tool_call(status='completed') | { type: 'tool_result', name, data, id }|\n * | tool_call(status='error') | { type: 'error', message, id } |\n * | run.wait() status==='error' | { type: 'error', message } |\n * | run.wait() status==='cancelled' | (none — cancel ≠ error) |\n * | system / user / thinking / status / | (none — internal SDK telemetry) |\n * | task / request / object_delta | |\n *\n * Abort semantics: when the consumer calls `generator.return()` on the\n * outer `defineAgentEndpoint` generator, `streamAgentRun` exits the\n * `for await` loop and does NOT call `run.wait()`. Cleanup of in-flight\n * SDK resources is the SDK consumer's responsibility (`run.cancel()`).\n */\n\n/**\n * Local mirror of the SDK's `Run` interface — only the surfaces we consume.\n * The message contract is minimal (just `{ type: string }`) so the SDK's\n * `SDKMessage` discriminated union IS structurally assignable without any\n * cast at the consumer site (covariant — TS accepts the SDK's typed message\n * as the wider `{ type: string }`). Property access inside `streamAgentRun`\n * narrows via runtime type guards.\n *\n * `import type` from `@theokit/sdk` would couple TheoKit to the SDK at type-\n * resolution time even for consumers who never use the agent surface.\n */\nexport interface AgentRunLike {\n stream: () => AsyncIterable<{ type: string }>\n wait: () => Promise<AgentRunResult>\n}\n\n/**\n * Re-exported as a convenience for test fixtures. Production code typically\n * passes an SDK `Run` directly (structural match via `{ type: string }`).\n */\nexport type AgentRunStreamMessage =\n | {\n type: 'assistant'\n message: { role: 'assistant'; content: { type: string; text?: string }[] }\n }\n | {\n type: 'tool_call'\n name: string\n status: 'running' | 'completed' | 'error'\n args?: unknown\n result?: unknown\n call_id: string\n }\n | { type: string; [k: string]: unknown }\n\n/** Subset of the SDK `RunResult` we consume. */\nexport interface AgentRunResult {\n status: 'finished' | 'error' | 'cancelled'\n error?: { message: string; code?: string; cause?: unknown }\n}\n\n/**\n * EC-1 (edge case review): tool result may be a Date, bigint, circular ref,\n * etc. `JSON.stringify` in `encodeSSE` would throw — uncaught inside the\n * outer generator — and `defineAgentEndpoint` would replace the legitimate\n * `tool_result` with a generic `error`. Coerce here so the wire stays honest.\n */\nfunction safeJsonStringify(value: unknown): string {\n try {\n return JSON.stringify(value)\n } catch {\n return '[Unserializable]'\n }\n}\n\n/**\n * EC-3 (edge case review): SDK types `args?: unknown`. A raw `as` cast hides\n * the possibility that `msg.args` is an array, primitive, or `null` (null\n * survives `??`). Type-guard BEFORE narrowing to `Record<string, unknown>`.\n */\nfunction safeArgs(args: unknown): Record<string, unknown> {\n if (typeof args === 'object' && args !== null && !Array.isArray(args)) {\n return args as Record<string, unknown>\n }\n return {}\n}\n\n/**\n * Yield AgentEvents derived from the SDK Run lifecycle.\n *\n * Consumer pattern:\n *\n * ```ts\n * export const POST = defineAgentEndpoint({\n * async *handler({ body }) {\n * const agent = await Agent.create({ apiKey, model, tools: [...] })\n * try {\n * const run = await agent.send(body.message)\n * yield* streamAgentRun(run)\n * } finally {\n * try { await agent.dispose() } catch (e) { console.warn(e) }\n * }\n * },\n * })\n * ```\n *\n * @public\n */\ninterface AssistantLike {\n type: 'assistant'\n message: { role: 'assistant'; content: { type: string; text?: string }[] }\n}\ninterface ToolCallLike {\n type: 'tool_call'\n name: string\n status: 'running' | 'completed' | 'error'\n args?: unknown\n result?: unknown\n call_id: string\n}\n\nfunction isAssistant(msg: { type: string }): msg is AssistantLike {\n // Wide cast to `unknown` first so runtime null guard survives ESLint\n // narrowing complaints — the SDK contract permits `null` even when its\n // TS type does not.\n const m = msg as unknown as {\n type: string\n message?: { content?: unknown } | null\n }\n return (\n m.type === 'assistant' &&\n m.message != null &&\n typeof m.message === 'object' &&\n Array.isArray(m.message.content)\n )\n}\nfunction isToolCall(msg: { type: string }): msg is ToolCallLike {\n const t = msg as unknown as {\n type: string\n name?: unknown\n status?: unknown\n call_id?: unknown\n }\n return (\n t.type === 'tool_call' &&\n typeof t.name === 'string' &&\n typeof t.call_id === 'string' &&\n (t.status === 'running' || t.status === 'completed' || t.status === 'error')\n )\n}\n\n/**\n * Phase 4 — Production-Readiness #3: structural mirror of SDK's AgentRunError.\n *\n * EC-6 (SHOULD TEST): we only require `code: string` to discriminate.\n * Provider, retriable, retryAfterMs, requestId are all optional — SDK error\n * paths may omit them (e.g., aborted before request, tool runtime error).\n */\ninterface AgentRunErrorLike {\n message: string\n code: string\n provider?: string\n retriable?: boolean\n retryAfterMs?: number\n requestId?: string\n /**\n * EC-15 (DOCUMENT) + invariant: `providerError` is QUARANTINED. We read it\n * for type-narrowing but NEVER serialize into the AgentEvent — leaking the\n * raw provider response could leak API keys, internal endpoints, or PII.\n * Only sanitized fields above flow to the SSE wire. `error.message` is\n * trusted to not contain secrets (SDK's responsibility per the v1.1.0\n * release contract).\n */\n providerError?: unknown\n}\n\n/**\n * EC-6 (SHOULD TEST): minimal type guard — only requires `code: string`.\n * Does NOT require `'provider' in err` because the SDK throws AgentRunErrors\n * without `provider` in local error paths (timeout, tool runtime error,\n * aborted-before-call).\n */\nfunction isAgentRunError(err: unknown): err is AgentRunErrorLike {\n if (!(err instanceof Error)) return false\n const e = err as { code?: unknown }\n return 'code' in e && typeof e.code === 'string'\n}\n\n/**\n * Map an SDK error to the AgentErrorEvent shape. Pure function — easy to test.\n *\n * Backward compat (D4): non-AgentRunError throws yield only `message` field\n * (legacy shape); discriminated fields stay `undefined`.\n *\n * Return type is the specific `AgentErrorEvent` (not the union) for ergonomic\n * call-site access — `errorToEvent(err).code` works without narrowing.\n */\nexport function errorToEvent(err: unknown, id?: string): AgentErrorEvent {\n if (isAgentRunError(err)) {\n const event: AgentErrorEvent = {\n type: 'error',\n message: err.message,\n code: err.code,\n }\n if (err.provider !== undefined) event.provider = err.provider\n if (err.retriable !== undefined) event.retriable = err.retriable\n if (err.retryAfterMs !== undefined) event.retryAfterMs = err.retryAfterMs\n if (id !== undefined) event.id = id\n return event\n }\n // Fallback for non-AgentRunError throws (plain Error, string, plain object with message)\n let message: string\n if (err instanceof Error) {\n message = err.message\n } else if (typeof err === 'string') {\n message = err\n } else if (err !== null && typeof err === 'object' && 'message' in err) {\n const candidate: unknown = (err as Record<string, unknown>).message\n // Plain object with `message: string` — common for SDK status:error payloads\n message = typeof candidate === 'string' ? candidate : '[object Object]'\n } else if (err === null || err === undefined) {\n message = String(err)\n } else {\n message = '[non-stringifiable error]'\n }\n const event: AgentErrorEvent = { type: 'error', message }\n if (id !== undefined) event.id = id\n return event\n}\n\nfunction yieldFromToolCall(msg: ToolCallLike): AgentEvent {\n if (msg.status === 'running') {\n return {\n type: 'tool_call',\n name: msg.name,\n args: safeArgs(msg.args),\n id: msg.call_id,\n }\n }\n if (msg.status === 'completed') {\n const data = typeof msg.result === 'string' ? msg.result : safeJsonStringify(msg.result)\n return {\n type: 'tool_result',\n name: msg.name,\n data,\n id: msg.call_id,\n }\n }\n // status === 'error' — exhaustive by type\n const message = typeof msg.result === 'string' ? msg.result : `Tool ${msg.name} failed`\n return { type: 'error', message, id: msg.call_id }\n}\n\nexport async function* streamAgentRun(\n run: AgentRunLike,\n): AsyncGenerator<AgentEvent, void, unknown> {\n for await (const msg of run.stream()) {\n if (isAssistant(msg)) {\n for (const block of msg.message.content) {\n if (block.type === 'text' && typeof block.text === 'string' && block.text.length > 0) {\n yield { type: 'message', content: block.text }\n }\n }\n } else if (isToolCall(msg)) {\n yield yieldFromToolCall(msg)\n }\n // SDK-internal variants (system, user, thinking, status, task,\n // request, object_delta) are intentionally not yielded.\n }\n\n const result = await run.wait()\n if (result.status === 'error' && result.error !== undefined) {\n yield errorToEvent(result.error)\n }\n}\n","/**\n * Item #5 — `createConversationHistory`\n *\n * Orchestrator that resolves a stable `agentId` from (explicit → session →\n * cookie → fresh UUID), then returns an `@theokit/sdk` `Agent` via\n * `Agent.getOrCreate(agentId, options)`. Conversation turns auto-persist in\n * `<cwd>/.theokit/agents/<agentId>/messages.jsonl` — SDK owns the storage\n * (ADR D1). `MemorySettings` (facts recall layer) is opt-in passthrough via\n * `options.memory` (ADR D2).\n *\n * Security: agentId from cookie/explicit is attacker-controlled. The SDK\n * uses it as a filesystem path component AND we serialize it into Set-Cookie.\n * EC-1 enforces a strict regex `^[a-zA-Z0-9_-]{1,128}$` at every entry point;\n * invalid values fall through (treated as \"missing\") rather than throw.\n */\n\nimport { tryResolveProvider } from './provider-resolver.js'\n\n// ──────────────────────────────────────────────────────────────────────────\n// Structural types — mirrors of SDK shapes we don't want to hard-import\n// (the SDK is an OPTIONAL peer per item #4's stance).\n// ──────────────────────────────────────────────────────────────────────────\n\n/**\n * Minimum surface of an SDK Agent that consumers care about post-creation.\n * Structural match — any object with `send` + `dispose` of compatible\n * shape works. `send` returns a `SdkRunLike` (a Run-shaped object) that\n * `streamAgentRun` can consume. Permissive `unknown` for now; consumers\n * who want stricter types can cast to the SDK's own types.\n *\n * **T5.2 (architecture-cleanup) — DP-7 decision: KEEP (Opt B).** The 5 duck-typed\n * mirrors below were flagged as \"over-engineered\" by the 2026-05-27 architecture\n * review. Decision: KEEP them because `@theokit/sdk` is `devDependency` (not\n * required at runtime for consumers that don't use agent features). Removing\n * the mirrors would force a hard runtime dep on the SDK, breaking consumers\n * who build TheoKit apps without the agent layer.\n *\n * @kept Defensive duck-type. Do NOT replace with direct SDK type imports unless\n * `@theokit/sdk` is promoted from devDependency to dependency. If promoting,\n * ALSO drop the mirrors (Opt A from the architecture-cleanup plan T5.2).\n */\nexport interface SdkRunLike {\n stream: () => AsyncIterable<{ type: string }>\n wait: () => Promise<{ status: 'finished' | 'error' | 'cancelled'; error?: { message: string } }>\n}\n\nexport interface SdkAgent {\n send: (message: string, options?: unknown) => Promise<SdkRunLike>\n dispose: () => Promise<void>\n}\n\n/**\n * Phase 2 — structural duck-type of `@theokit/sdk`'s `ConversationStorageAdapter`.\n *\n * D2 (decoupling): we mirror the SDK's shape locally rather than hard-import\n * the SDK type. This lets consumers pass any object matching the structural\n * contract (own implementation, SDK's `FileSystemConversationStorage`,\n * `InMemoryConversationStorage`, or a Postgres/Redis recipe).\n *\n * EC-5 (SHOULD TEST — sync drift detection): the SDK type MUST be assignable\n * to this interface AND vice-versa. A contract test asserts both directions.\n *\n * `unknown` for the message payload avoids coupling to the SDK's `SDKMessage`\n * shape. Real consumers cast at the call site if they need stricter types.\n */\nexport interface ConversationStorageLike {\n getMessages(conversationId: string): Promise<readonly unknown[]>\n appendMessage(conversationId: string, message: unknown): Promise<void>\n deleteConversation(conversationId: string): Promise<void>\n listConversationIds?(opts?: { limit?: number }): Promise<readonly string[] | undefined>\n dispose?(): Promise<void>\n}\n\n/**\n * Minimum surface of `AgentOptions` accepted by `Agent.getOrCreate`. Forward-\n * compatible: callers pass whatever the SDK supports (memory, tools, etc.).\n *\n * Phase 2 adds typed `conversationStorage` slot. The index signature still\n * passes everything else opaquely.\n */\nexport interface SdkAgentOptions {\n apiKey?: string\n model?: { id: string }\n tools?: readonly unknown[]\n memory?: Record<string, unknown>\n /**\n * Phase 2 (Production-Readiness #1) — pluggable conversation persistence.\n * Default (when omitted): SDK falls back to `FileSystemConversationStorage`.\n * Required for serverless / multi-host deploys.\n */\n conversationStorage?: ConversationStorageLike\n [key: string]: unknown\n}\n\ninterface SdkModule {\n Agent: {\n getOrCreate: (agentId: string, options: SdkAgentOptions) => Promise<SdkAgent>\n }\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// Public API\n// ──────────────────────────────────────────────────────────────────────────\n\nexport interface ConversationHistoryArgs {\n /** Request — for reading the conversation cookie. */\n request: Request | { headers?: { cookie?: string } | Headers }\n /**\n * Response-like surface that accepts a `Set-Cookie` header. The primitive\n * appends a Set-Cookie line when issuing a new conversation id. If absent,\n * the primitive still reads the existing cookie but cannot issue a new\n * one — useful for read-only contexts.\n */\n response?: { headers: Headers }\n /** Explicit override — wins over session/cookie/uuid (ADR D3 step 1). */\n agentId?: string\n /** Auth session containing a `conversationId` field — ADR D3 step 2. */\n session?: { conversationId?: string } | null\n /** SDK AgentOptions forwarded to Agent.getOrCreate. apiKey + model required. */\n options: SdkAgentOptions\n /** Cookie name override. Default: 'theo_conversation'. */\n cookieName?: string\n /** Cookie max-age in seconds. Default + min: 30 days. Non-positive coerced to default (EC-4). */\n cookieMaxAge?: number\n}\n\nexport interface ConversationHistoryResult {\n /** The SDK Agent, ready to receive `agent.send(message)`. */\n agent: SdkAgent\n /** The resolved conversation id (useful for logging / debugging). */\n conversationId: string\n /** True when the id was newly generated (no prior cookie / session). */\n isNew: boolean\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// Test seam — allows unit tests to swap the SDK without dynamic import flake.\n// Underscore-prefixed: NOT part of the public contract.\n// ──────────────────────────────────────────────────────────────────────────\n\nlet sdkOverride: SdkModule | null | undefined = undefined\n\n/** @internal */\nexport function __setSdkForTests(sdk: SdkModule | null): void {\n sdkOverride = sdk\n}\n\n/** @internal */\nexport function __resetSdkForTests(): void {\n sdkOverride = undefined\n}\n\nasync function loadSdk(): Promise<SdkModule> {\n // EC-2 (edge case review — MUST FIX): the SDK is an optional peer; if the\n // consumer never installed it, `import('@theokit/sdk')` throws\n // ERR_MODULE_NOT_FOUND. Re-throw with an actionable message.\n if (sdkOverride === null) {\n throw new Error(\n 'createConversationHistory requires @theokit/sdk. Install: pnpm add @theokit/sdk',\n )\n }\n if (sdkOverride !== undefined) return sdkOverride\n try {\n // Use `createRequire` — Node's CJS-style require resolves against the\n // process's actual node_modules tree, bypassing Vite's SSR import-\n // analysis pipeline. The dynamic ESM `import()` path got intercepted\n // by Vite's `vite:import-analysis` plugin and failed to find the SDK\n // even when it was installed; createRequire goes straight to Node.\n //\n // The SDK ships dual ESM+CJS (per its tsup build), so `require` yields\n // the same module the ESM `import` would.\n const { createRequire } = await import('node:module')\n const requireFn = createRequire(import.meta.url)\n const spec = '@theokit/sdk'\n const mod = requireFn(spec) as unknown as SdkModule\n return mod\n } catch (cause) {\n throw new Error(\n 'createConversationHistory requires @theokit/sdk. Install: pnpm add @theokit/sdk',\n { cause },\n )\n }\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// Implementation\n// ──────────────────────────────────────────────────────────────────────────\n\nconst DEFAULT_COOKIE_NAME = 'theo_conversation'\nconst DEFAULT_COOKIE_MAX_AGE = 60 * 60 * 24 * 30 // 30 days\n\n/**\n * EC-1 (edge case review — MUST FIX): the agentId becomes a filesystem path\n * component (`<cwd>/.theokit/agents/<agentId>/messages.jsonl`) inside the SDK\n * AND is serialized into a Set-Cookie header. The SDK does NOT validate the\n * char set. An attacker setting `Cookie: theo_conversation=../../../etc/passwd`\n * could trigger arbitrary-path writes; an `agentId` with CRLF would inject\n * HTTP headers. The same regex kills both attacks.\n */\nconst AGENT_ID_REGEX = /^[a-zA-Z0-9_-]{1,128}$/\n\nfunction isValidAgentId(s: string | undefined | null): s is string {\n return typeof s === 'string' && AGENT_ID_REGEX.test(s)\n}\n\n/**\n * Minimal RFC 6265 cookie parser. Returns the FIRST occurrence of `name`\n * (EC-5 — pins first-wins behavior).\n */\nfunction readCookieValue(\n request: ConversationHistoryArgs['request'],\n name: string,\n): string | undefined {\n let raw: string | undefined\n const h = request.headers\n if (h === undefined) return undefined\n if (typeof (h as Headers).get === 'function') {\n raw = (h as Headers).get('cookie') ?? undefined\n } else {\n raw = (h as { cookie?: string }).cookie\n }\n if (raw === undefined || raw.length === 0) return undefined\n const pairs = raw.split(/[;,]/)\n for (const pair of pairs) {\n const eq = pair.indexOf('=')\n if (eq < 0) continue\n const k = pair.slice(0, eq).trim()\n if (k === name) return pair.slice(eq + 1).trim()\n }\n return undefined\n}\n\ninterface SerializeCookieOptions {\n httpOnly?: boolean\n sameSite?: 'lax' | 'strict' | 'none'\n maxAge?: number\n path?: string\n}\n\nfunction serializeCookie(name: string, value: string, options: SerializeCookieOptions): string {\n const parts: string[] = [`${name}=${value}`]\n if (options.path !== undefined) parts.push(`Path=${options.path}`)\n if (options.maxAge !== undefined) parts.push(`Max-Age=${options.maxAge}`)\n if (options.sameSite !== undefined) {\n const v = options.sameSite.charAt(0).toUpperCase() + options.sameSite.slice(1)\n parts.push(`SameSite=${v}`)\n }\n if (options.httpOnly === true) parts.push('HttpOnly')\n return parts.join('; ')\n}\n\nexport async function createConversationHistory(\n args: ConversationHistoryArgs,\n): Promise<ConversationHistoryResult> {\n const cookieName = args.cookieName ?? DEFAULT_COOKIE_NAME\n // EC-4 (edge case review — SHOULD TEST): coerce non-positive to default.\n // The `??` operator alone would let `0` through, producing `Max-Age=0`\n // which means \"delete cookie immediately\" — almost certainly not intent.\n const rawMaxAge = args.cookieMaxAge\n const cookieMaxAge =\n typeof rawMaxAge === 'number' && rawMaxAge > 0 ? rawMaxAge : DEFAULT_COOKIE_MAX_AGE\n\n // 1. Resolve agentId per ADR D3 — sources are validated; invalid values\n // fall through (treated as \"missing\") rather than throw.\n let conversationId: string | undefined\n let isNew = false\n const cookieOnRequest = readCookieValue(args.request, cookieName)\n\n if (isValidAgentId(args.agentId)) {\n conversationId = args.agentId\n } else if (args.session !== null && args.session !== undefined) {\n const sId = args.session.conversationId\n if (isValidAgentId(sId)) conversationId = sId\n }\n if (conversationId === undefined && isValidAgentId(cookieOnRequest)) {\n conversationId = cookieOnRequest\n }\n\n if (conversationId === undefined) {\n conversationId = crypto.randomUUID()\n isNew = true\n }\n\n // Issue (or refresh) the cookie when:\n // 1. The id was newly generated (no source had it), OR\n // 2. The request's cookie does NOT match the resolved id (explicit\n // `agentId` override, session-derived id, etc.). Without this,\n // callers that pre-probe the id (e.g. to build agentId-scoped tools)\n // and pass it via `agentId` would never get a Set-Cookie even on\n // first visit. Net effect: every response with a `response` slot\n // ensures the browser ends up with a cookie that matches the\n // resolved id.\n const shouldIssueCookie = isNew || cookieOnRequest !== conversationId\n if (shouldIssueCookie && args.response !== undefined) {\n const cookie = serializeCookie(cookieName, conversationId, {\n httpOnly: true,\n sameSite: 'lax',\n maxAge: cookieMaxAge,\n path: '/',\n })\n args.response.headers.append('set-cookie', cookie)\n }\n\n // 2. Auto-resolve provider via Strategy pattern (FAANG-grade — zero\n // conditionals no consumer). If `options.apiKey` ausente, lê env vars\n // priorizadas (OPENROUTER > OPENAI > ANTHROPIC) e injeta apiKey +\n // providers.baseUrl. Consumer override: passar `options.apiKey` explícito\n // SOBREPÕE o auto-resolve (escape hatch). Ver `provider-resolver.ts` +\n // Dapr Conversation Registry pattern (`referencias/dapr/pkg/components/conversation/`).\n const resolvedOptions = autoResolveProviderIfNeeded(args.options)\n\n // 3. Resolve the agent via SDK.\n const sdk = await loadSdk()\n const agent = await sdk.Agent.getOrCreate(conversationId, resolvedOptions)\n\n return { agent, conversationId, isNew }\n}\n\n/**\n * Auto-inject provider config (apiKey + baseUrl) via Strategy pattern when\n * consumer didn't pass apiKey explicit. Idempotent — se apiKey já set, noop.\n *\n * Output shape:\n * {\n * ...originalOptions,\n * apiKey: env, // resolved from process.env\n * providers: { routes: [{ capability: 'chat', provider: name, ... }] }\n * }\n */\nfunction autoResolveProviderIfNeeded(options: SdkAgentOptions): SdkAgentOptions {\n // Escape hatch — consumer override wins.\n if (typeof options.apiKey === 'string' && options.apiKey.length > 0) {\n return options\n }\n // Auto-resolve via Strategy registry.\n //\n // Finding A workaround (sdk-residual-behavior-2026-05-28): SDK silently\n // returns canned \"Hello! How can I assist you today?\" content when no\n // apiKey AND no provider env vars are present. Fail-fast HERE with an\n // actionable error — template's try/catch yields `{type:'error'}` SSE\n // event so consumer sees what's wrong.\n const resolved = tryResolveProvider()\n if (resolved === null) {\n throw new Error(\n 'No LLM provider API key found in environment. ' +\n 'Set OPENROUTER_API_KEY (recommended — gateway to many models) OR ' +\n 'OPENAI_API_KEY OR ANTHROPIC_API_KEY in your .env. ' +\n 'Get a free OpenRouter key at https://openrouter.ai/keys. ' +\n '(Pass `options.apiKey` explicitly to bypass auto-resolution.)',\n )\n }\n // Inject apiKey + provider routing. SDK ProviderRoutingSettings shape.\n // Note: we use `chat` capability since this is the conversation primitive;\n // SDK may extend to embeddings/etc — those routes are separate concerns.\n return {\n ...options,\n apiKey: resolved.apiKey,\n providers: {\n routes: [{ capability: 'chat', provider: resolved.name, baseUrl: resolved.baseUrl }],\n },\n }\n}\n"],"mappings":"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","names":["event"]}
|