create-stitchkit 0.4.3 → 0.4.4
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/CHANGELOG.md +25 -0
- package/README.md +19 -0
- package/dist/cli.js +71 -11
- package/package.json +25 -1
- package/template/bun.lock +2 -2
- package/template/package.json +1 -1
- package/template/scripts/local-env.test.ts +15 -0
- package/template/scripts/local-env.ts +10 -3
- package/template/scripts/shutdown-budget.test.ts +14 -7
- package/templates/agent/README.md +41 -0
- package/templates/agent/_env.example +4 -0
- package/templates/agent/_gitignore +6 -0
- package/templates/agent/biome.json +30 -0
- package/templates/agent/instructions/AGENTS.md +9 -0
- package/templates/agent/package.json +35 -0
- package/templates/agent/project.json +45 -0
- package/templates/agent/skills/verify/SKILL.md +10 -0
- package/templates/agent/src/config.ts +32 -0
- package/templates/agent/src/index.ts +4 -0
- package/templates/agent/src/runtime.ts +139 -0
- package/templates/agent/stitchkit.agent.ts +15 -0
- package/templates/agent/tests/config.test.ts +17 -0
- package/templates/agent/tests/runtime.test.ts +90 -0
- package/templates/agent/tsconfig.json +16 -0
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,31 @@ step is overwritten by the next release.
|
|
|
12
12
|
|
|
13
13
|
## [Unreleased]
|
|
14
14
|
|
|
15
|
+
## [0.4.4] — 2026-08-30
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **The generated project now targets the published `stitchkit@0.70.1` line and includes the
|
|
20
|
+
maintained `stitchkit-tui` Agent profile.** The catalog and frozen lockfile move together, so a
|
|
21
|
+
fresh scaffold receives the macOS contained-file fix, bounded realtime shutdown and the terminal
|
|
22
|
+
harness package already validated by the same release train.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- **An explicit Agent template opens a durable OpenRouter coding session in the terminal.**
|
|
27
|
+
`bun create stitchkit my-agent --template agent` generates an OpenTUI host over the published
|
|
28
|
+
headless harness, Bun SQLite storage, lazy skills, direct coding tools, signed approvals and
|
|
29
|
+
recovery. The default production application and its repository overlay are unchanged. The
|
|
30
|
+
Agent development source follows framework HEAD locally, while generated projects receive the
|
|
31
|
+
application starter's one canonical Stitchkit catalog target. Startup requires only an API key:
|
|
32
|
+
a bounded picker reads the current popular tool-capable models and their provider-owned context
|
|
33
|
+
windows directly from OpenRouter. → ADR 0132.
|
|
34
|
+
- **The Agent template now composes the official terminal package instead of copying a product
|
|
35
|
+
shell.** `stitchkit.agent.ts` is the editable typed entrypoint; `/model`, durable sessions,
|
|
36
|
+
approvals, scrolling and local `send`/`interrupt` attachment come from `stitchkit-tui`. The
|
|
37
|
+
generated manifest receives portable catalog dependencies while the repository fixture tests
|
|
38
|
+
both packages from source. → ADR 0133.
|
|
39
|
+
|
|
15
40
|
## [0.4.3] — 2026-08-28
|
|
16
41
|
|
|
17
42
|
### Fixed
|
package/README.md
CHANGED
|
@@ -13,6 +13,25 @@ The generated Bun workspace contains a Next.js frontend, a separate Stitchkit
|
|
|
13
13
|
API, Prisma/PostgreSQL, typed shared contracts, Socket.IO, MCP, CLI tools and a
|
|
14
14
|
complete production UI system.
|
|
15
15
|
|
|
16
|
+
To start from a terminal coding agent instead:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
bun create stitchkit my-agent --template agent
|
|
20
|
+
cd my-agent
|
|
21
|
+
cp .env.example .env
|
|
22
|
+
# Set OPENROUTER_API_KEY, then choose a live model in the terminal.
|
|
23
|
+
bun run dev
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
This opt-in profile is deliberately smaller. It composes the published headless
|
|
27
|
+
harness, Bun SQLite store, OpenRouter adapter, lazy filesystem skills and direct
|
|
28
|
+
coding tools behind an OpenTUI shell. Reads/search run directly; writes, edits,
|
|
29
|
+
patches and shell calls require a signed durable `Y`/`N` approval. `bun --watch`
|
|
30
|
+
restarts source while the ignored `.stitchkit/` directory retains conversation
|
|
31
|
+
and recovery state. The bounded startup picker reads current tool-capable models and their context
|
|
32
|
+
windows from OpenRouter instead of duplicating provider metadata in environment variables. The
|
|
33
|
+
workspace path is a containment boundary, not an OS sandbox.
|
|
34
|
+
|
|
16
35
|
It uses one conventional `packages/*` namespace: `backend`, `frontend`,
|
|
17
36
|
`config`, `db` and `shared`. The destination name becomes the generated slug;
|
|
18
37
|
`--display-name` sets the human title. Both are recorded once in
|
package/dist/cli.js
CHANGED
|
@@ -207,10 +207,11 @@ function withIdentity(declaration, identity) {
|
|
|
207
207
|
var HELP = `Create a production-shaped Stitchkit application.
|
|
208
208
|
|
|
209
209
|
Usage:
|
|
210
|
-
bun create stitchkit <directory> [--display-name "Product Name"] [--example repository] [--no-install]
|
|
210
|
+
bun create stitchkit <directory> [--template application|agent] [--display-name "Product Name"] [--example repository] [--no-install]
|
|
211
211
|
|
|
212
212
|
Options:
|
|
213
213
|
--no-install Generate files without installing dependencies
|
|
214
|
+
--template Select the project shape (default: application)
|
|
214
215
|
--example Add an isolated runnable example (supported: repository)
|
|
215
216
|
--display-name Set the initial public application name
|
|
216
217
|
--help Show this help
|
|
@@ -221,7 +222,7 @@ function helpText() {
|
|
|
221
222
|
function parseOptions(args) {
|
|
222
223
|
if (args.includes("--help") || args.includes("-h"))
|
|
223
224
|
return "help";
|
|
224
|
-
const unknown = args.filter((arg) => arg.startsWith("-") && arg !== "--no-install" && arg !== "--example" && arg !== "--display-name");
|
|
225
|
+
const unknown = args.filter((arg) => arg.startsWith("-") && arg !== "--no-install" && arg !== "--template" && arg !== "--example" && arg !== "--display-name");
|
|
225
226
|
if (unknown.length > 0) {
|
|
226
227
|
throw new Error(`Unknown option: ${unknown[0]}`);
|
|
227
228
|
}
|
|
@@ -233,14 +234,26 @@ function parseOptions(args) {
|
|
|
233
234
|
if (example !== undefined && example !== "repository") {
|
|
234
235
|
throw new Error(`Unknown example: ${example}`);
|
|
235
236
|
}
|
|
237
|
+
const templateFlagIndex = args.indexOf("--template");
|
|
238
|
+
const template = templateFlagIndex === -1 ? "application" : args[templateFlagIndex + 1];
|
|
239
|
+
if (templateFlagIndex !== -1 && template === undefined) {
|
|
240
|
+
throw new Error("--template requires a value");
|
|
241
|
+
}
|
|
242
|
+
if (template !== "application" && template !== "agent") {
|
|
243
|
+
throw new Error(`Unknown template: ${template}`);
|
|
244
|
+
}
|
|
245
|
+
if (template === "agent" && example !== undefined) {
|
|
246
|
+
throw new Error("--example is only supported by the application template");
|
|
247
|
+
}
|
|
236
248
|
const displayNameFlagIndex = args.indexOf("--display-name");
|
|
237
249
|
const displayName = displayNameFlagIndex === -1 ? undefined : args[displayNameFlagIndex + 1];
|
|
238
250
|
if (displayNameFlagIndex !== -1 && displayName === undefined) {
|
|
239
251
|
throw new Error("--display-name requires a value");
|
|
240
252
|
}
|
|
241
253
|
const consumedExampleIndex = exampleFlagIndex === -1 ? -1 : exampleFlagIndex + 1;
|
|
254
|
+
const consumedTemplateIndex = templateFlagIndex === -1 ? -1 : templateFlagIndex + 1;
|
|
242
255
|
const consumedDisplayNameIndex = displayNameFlagIndex === -1 ? -1 : displayNameFlagIndex + 1;
|
|
243
|
-
const positionals = args.filter((arg, index) => !arg.startsWith("-") && index !== consumedExampleIndex && index !== consumedDisplayNameIndex);
|
|
256
|
+
const positionals = args.filter((arg, index) => !arg.startsWith("-") && index !== consumedExampleIndex && index !== consumedTemplateIndex && index !== consumedDisplayNameIndex);
|
|
244
257
|
if (positionals.length !== 1) {
|
|
245
258
|
throw new Error("Exactly one destination directory is required");
|
|
246
259
|
}
|
|
@@ -250,6 +263,7 @@ function parseOptions(args) {
|
|
|
250
263
|
return {
|
|
251
264
|
destination,
|
|
252
265
|
install: !args.includes("--no-install"),
|
|
266
|
+
template,
|
|
253
267
|
...example && { example },
|
|
254
268
|
...displayName && { displayName }
|
|
255
269
|
};
|
|
@@ -283,9 +297,15 @@ var TEMPLATE_RENAMES = new Map([
|
|
|
283
297
|
["_env.example.append", ".env.example"],
|
|
284
298
|
["_gitignore", ".gitignore"]
|
|
285
299
|
]);
|
|
286
|
-
var RootManifestSchema = z2.looseObject({
|
|
300
|
+
var RootManifestSchema = z2.looseObject({
|
|
301
|
+
name: z2.string().min(1),
|
|
302
|
+
catalog: z2.record(z2.string(), z2.string()).optional(),
|
|
303
|
+
dependencies: z2.record(z2.string(), z2.string()).optional(),
|
|
304
|
+
devDependencies: z2.record(z2.string(), z2.string()).optional()
|
|
305
|
+
});
|
|
287
306
|
var IGNORED_DIRECTORIES = new Set([
|
|
288
307
|
".next",
|
|
308
|
+
".stitchkit",
|
|
289
309
|
"coverage",
|
|
290
310
|
"dist",
|
|
291
311
|
"node_modules",
|
|
@@ -401,16 +421,43 @@ async function scaffoldProject(templateDirectory, destination, options = {}) {
|
|
|
401
421
|
if (options.overlayDirectory) {
|
|
402
422
|
await writeMaterialisedFiles(resolvedDestination, await materialiseTemplateFiles(options.overlayDirectory));
|
|
403
423
|
}
|
|
424
|
+
if (options.lockfile === false) {
|
|
425
|
+
await rm(join(resolvedDestination, "bun.lock"), { force: true });
|
|
426
|
+
}
|
|
404
427
|
const declarationPath = join(resolvedDestination, "project.json");
|
|
405
428
|
const declaration = withIdentity(JSON.parse(await readFile(declarationPath, "utf8")), identity);
|
|
406
429
|
await writeFile(declarationPath, `${JSON.stringify(declaration, undefined, 2)}
|
|
407
430
|
`);
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
431
|
+
if (options.identityModule !== false) {
|
|
432
|
+
const identityPath = join(resolvedDestination, APP_IDENTITY_PATH);
|
|
433
|
+
await mkdir(dirname(identityPath), { recursive: true });
|
|
434
|
+
await writeFile(identityPath, renderAppIdentityModule(declaration.identity));
|
|
435
|
+
}
|
|
411
436
|
const manifestPath = join(resolvedDestination, "package.json");
|
|
412
437
|
const manifest = RootManifestSchema.parse(JSON.parse(await readFile(manifestPath, "utf8")));
|
|
413
|
-
|
|
438
|
+
const catalog = options.stitchkitCatalogTarget ? { ...manifest.catalog ?? {}, stitchkit: options.stitchkitCatalogTarget } : manifest.catalog;
|
|
439
|
+
const replaceLocalPackages = (dependencies) => {
|
|
440
|
+
if (!dependencies)
|
|
441
|
+
return dependencies;
|
|
442
|
+
return {
|
|
443
|
+
...dependencies,
|
|
444
|
+
...dependencies.stitchkit?.startsWith("file:") && { stitchkit: "catalog:" },
|
|
445
|
+
...dependencies["stitchkit-tui"]?.startsWith("file:") && {
|
|
446
|
+
"stitchkit-tui": "catalog:"
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
};
|
|
450
|
+
await writeFile(manifestPath, `${JSON.stringify({
|
|
451
|
+
...manifest,
|
|
452
|
+
name: identity.slug,
|
|
453
|
+
...catalog && { catalog },
|
|
454
|
+
...manifest.dependencies && {
|
|
455
|
+
dependencies: replaceLocalPackages(manifest.dependencies)
|
|
456
|
+
},
|
|
457
|
+
...manifest.devDependencies && {
|
|
458
|
+
devDependencies: replaceLocalPackages(manifest.devDependencies)
|
|
459
|
+
}
|
|
460
|
+
}, undefined, 2)}
|
|
414
461
|
`);
|
|
415
462
|
} catch (error) {
|
|
416
463
|
if (!destinationExisted) {
|
|
@@ -432,11 +479,17 @@ async function run(args) {
|
|
|
432
479
|
return 0;
|
|
433
480
|
}
|
|
434
481
|
const destination = resolve2(options.destination);
|
|
435
|
-
const
|
|
482
|
+
const applicationTemplateDirectory = resolve2(import.meta.dir, "../template");
|
|
483
|
+
const templateDirectory = options.template === "agent" ? resolve2(import.meta.dir, "../templates/agent") : applicationTemplateDirectory;
|
|
436
484
|
const overlayDirectory = options.example ? resolve2(import.meta.dir, `../examples/${options.example}`) : undefined;
|
|
437
485
|
await scaffoldProject(templateDirectory, destination, {
|
|
438
486
|
...overlayDirectory && { overlayDirectory },
|
|
439
|
-
...options.displayName && { displayName: options.displayName }
|
|
487
|
+
...options.displayName && { displayName: options.displayName },
|
|
488
|
+
...options.template === "agent" && {
|
|
489
|
+
identityModule: false,
|
|
490
|
+
lockfile: false,
|
|
491
|
+
stitchkitCatalogTarget: await readStitchkitCatalogTarget(applicationTemplateDirectory)
|
|
492
|
+
}
|
|
440
493
|
});
|
|
441
494
|
if (options.install) {
|
|
442
495
|
const install = spawn(["bun", "install"], {
|
|
@@ -449,7 +502,7 @@ async function run(args) {
|
|
|
449
502
|
if (exitCode !== 0)
|
|
450
503
|
throw new Error(`bun install failed with exit code ${exitCode}`);
|
|
451
504
|
}
|
|
452
|
-
const mode = options.example ? ` with the ${options.example} example` : "";
|
|
505
|
+
const mode = options.template === "agent" ? " from the agent template" : options.example ? ` with the ${options.example} example` : "";
|
|
453
506
|
process.stdout.write(`
|
|
454
507
|
Created ${options.displayName ?? basename3(destination)}${mode}
|
|
455
508
|
|
|
@@ -471,6 +524,13 @@ Created ${options.displayName ?? basename3(destination)}${mode}
|
|
|
471
524
|
return 1;
|
|
472
525
|
}
|
|
473
526
|
}
|
|
527
|
+
async function readStitchkitCatalogTarget(templateDirectory) {
|
|
528
|
+
const manifest = JSON.parse(await readFile2(join2(templateDirectory, "package.json"), "utf8"));
|
|
529
|
+
if (typeof manifest !== "object" || manifest === null || !("catalog" in manifest) || typeof manifest.catalog !== "object" || manifest.catalog === null || !("stitchkit" in manifest.catalog) || typeof manifest.catalog.stitchkit !== "string") {
|
|
530
|
+
throw new Error("Application template is missing catalog.stitchkit");
|
|
531
|
+
}
|
|
532
|
+
return manifest.catalog.stitchkit;
|
|
533
|
+
}
|
|
474
534
|
if (import.meta.main) {
|
|
475
535
|
process.exitCode = await run(Bun.argv.slice(2));
|
|
476
536
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-stitchkit",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"description": "Create a production-shaped Stitchkit application",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Max Listov <maxlistov@gmail.com>",
|
|
@@ -16,11 +16,13 @@
|
|
|
16
16
|
"files": [
|
|
17
17
|
"dist",
|
|
18
18
|
"template/**/*",
|
|
19
|
+
"templates/**/*",
|
|
19
20
|
"examples/**/*",
|
|
20
21
|
"!template/**/.env",
|
|
21
22
|
"!template/**/.build-stamp.json",
|
|
22
23
|
"!template/**/node_modules/**",
|
|
23
24
|
"!template/**/.next/**",
|
|
25
|
+
"!template/**/.stitchkit/**",
|
|
24
26
|
"!template/**/dist/**",
|
|
25
27
|
"!template/**/playwright-report/**",
|
|
26
28
|
"!template/**/test-results/**",
|
|
@@ -29,10 +31,25 @@
|
|
|
29
31
|
"!template/**/*.log",
|
|
30
32
|
"!template/**/*.tsbuildinfo",
|
|
31
33
|
"!template/**/coverage/**",
|
|
34
|
+
"!templates/**/.env",
|
|
35
|
+
"!templates/**/.build-stamp.json",
|
|
36
|
+
"!templates/**/node_modules/**",
|
|
37
|
+
"!templates/**/.next/**",
|
|
38
|
+
"!templates/**/.stitchkit/**",
|
|
39
|
+
"!templates/**/dist/**",
|
|
40
|
+
"!templates/**/playwright-report/**",
|
|
41
|
+
"!templates/**/test-results/**",
|
|
42
|
+
"!templates/**/next-env.d.ts",
|
|
43
|
+
"!templates/**/src/generated/**",
|
|
44
|
+
"!templates/**/*.log",
|
|
45
|
+
"!templates/**/*.tsbuildinfo",
|
|
46
|
+
"!templates/**/coverage/**",
|
|
47
|
+
"!templates/agent/bun.lock",
|
|
32
48
|
"!examples/**/.env",
|
|
33
49
|
"!examples/**/.build-stamp.json",
|
|
34
50
|
"!examples/**/node_modules/**",
|
|
35
51
|
"!examples/**/.next/**",
|
|
52
|
+
"!examples/**/.stitchkit/**",
|
|
36
53
|
"!examples/**/dist/**",
|
|
37
54
|
"!examples/**/coverage/**",
|
|
38
55
|
"!examples/**/playwright-report/**",
|
|
@@ -59,7 +76,14 @@
|
|
|
59
76
|
"zod": "^4.4.3"
|
|
60
77
|
},
|
|
61
78
|
"devDependencies": {
|
|
79
|
+
"@opentui/core": "^0.5.9",
|
|
80
|
+
"@opentui/react": "^0.5.9",
|
|
81
|
+
"@openrouter/ai-sdk-provider": "^3.0.0",
|
|
62
82
|
"@types/bun": "^1.4.0",
|
|
83
|
+
"@types/react": "^19.2.18",
|
|
84
|
+
"ai": "^7.0.84",
|
|
85
|
+
"react": "^19.2.8",
|
|
86
|
+
"stitchkit": "0.70.1",
|
|
63
87
|
"typescript": "^7.0.2"
|
|
64
88
|
},
|
|
65
89
|
"engines": {
|
package/template/bun.lock
CHANGED
|
@@ -139,7 +139,7 @@
|
|
|
139
139
|
},
|
|
140
140
|
},
|
|
141
141
|
"catalog": {
|
|
142
|
-
"stitchkit": "^0.
|
|
142
|
+
"stitchkit": "^0.70.1",
|
|
143
143
|
},
|
|
144
144
|
"packages": {
|
|
145
145
|
"@ai-sdk/gateway": ["@ai-sdk/gateway@4.0.63", "", { "dependencies": { "@ai-sdk/provider": "4.0.7", "@ai-sdk/provider-utils": "5.0.29", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-D7BogSRg61QfTdr7AEcYn9h0I/e4QHvFXwIV1RW+DZZGJu1wSiX2cH06szZSYyKi7Eat50V4s4J8vggZUEs7eg=="],
|
|
@@ -1118,7 +1118,7 @@
|
|
|
1118
1118
|
|
|
1119
1119
|
"std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
|
|
1120
1120
|
|
|
1121
|
-
"stitchkit": ["stitchkit@0.
|
|
1121
|
+
"stitchkit": ["stitchkit@0.70.1", "", { "dependencies": { "ky": "^2.0.2" }, "peerDependencies": { "@modelcontextprotocol/ext-apps": "^1.7.2", "@modelcontextprotocol/server": "^2.0.0", "@openrouter/ai-sdk-provider": "^3.0.0", "@opentelemetry/api": "^1.9.0", "@socket.io/bun-engine": "^0.1.1", "@socket.io/component-emitter": "^3.1.2", "@tanstack/react-query": ">=5", "@types/bun": "^1.3.14", "ai": "^7.0.0", "grammy": "^1.45.1", "react": ">=18", "react-query-kit": "^3.3.3", "socket.io": "^4.8.3", "socket.io-client": "^4.8.3", "srvx": "^0.12.5", "zod": "^4.4.3" }, "optionalPeers": ["@modelcontextprotocol/ext-apps", "@modelcontextprotocol/server", "@openrouter/ai-sdk-provider", "@opentelemetry/api", "@socket.io/bun-engine", "@socket.io/component-emitter", "@tanstack/react-query", "@types/bun", "ai", "grammy", "react", "react-query-kit", "socket.io", "socket.io-client", "srvx"] }, "sha512-UySE/DO1p7XZDmbISX3+U9RCYpepqsElovnL4IgUu0C9BpsFXGbYpoF0nL38vpY8SLV2frLiZXyy05gmmyrhrg=="],
|
|
1122
1122
|
|
|
1123
1123
|
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
|
|
1124
1124
|
|
package/template/package.json
CHANGED
|
@@ -30,4 +30,19 @@ describe('ensureLocalEnvironment', () => {
|
|
|
30
30
|
await rm(root, { recursive: true, force: true });
|
|
31
31
|
}
|
|
32
32
|
});
|
|
33
|
+
|
|
34
|
+
test('a clean framework source tree reads the pre-scaffold example name', async () => {
|
|
35
|
+
const root = await mkdtemp(join(tmpdir(), 'sk-source-env-'));
|
|
36
|
+
try {
|
|
37
|
+
await writeFile(
|
|
38
|
+
join(root, '_env.example'),
|
|
39
|
+
'DATABASE_URL=postgresql://USER:PASSWORD@127.0.0.1:5432/stitchkit_starter\n',
|
|
40
|
+
);
|
|
41
|
+
ensureLocalEnvironment(root);
|
|
42
|
+
const databaseName = appDeclaration.identity.slug.replaceAll('-', '_');
|
|
43
|
+
expect(await readFile(join(root, '.env'), 'utf8')).toContain(`5432/${databaseName}`);
|
|
44
|
+
} finally {
|
|
45
|
+
await rm(root, { recursive: true, force: true });
|
|
46
|
+
}
|
|
47
|
+
});
|
|
33
48
|
});
|
|
@@ -3,8 +3,11 @@ import { resolve } from 'node:path';
|
|
|
3
3
|
import { appIdentity } from '../packages/config/src/app-identity.generated';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Create `.env` from `.env.example` on first run, rendering the
|
|
7
|
-
* identity into the database name.
|
|
6
|
+
* Create `.env` from the public `.env.example` on first run, rendering the
|
|
7
|
+
* application identity into the database name. The framework repository keeps
|
|
8
|
+
* that same source as `_env.example` until the scaffolder performs its rename,
|
|
9
|
+
* so clean source-tree checks use it directly instead of depending on an
|
|
10
|
+
* ignored developer `.env`.
|
|
8
11
|
*
|
|
9
12
|
* Identity, not the whole declaration: this needs one slug, and the identity
|
|
10
13
|
* module carries no dependencies. That matters here more than elsewhere —
|
|
@@ -19,7 +22,11 @@ import { appIdentity } from '../packages/config/src/app-identity.generated';
|
|
|
19
22
|
export function ensureLocalEnvironment(root: string): void {
|
|
20
23
|
const destination = resolve(root, '.env');
|
|
21
24
|
if (existsSync(destination)) return;
|
|
22
|
-
const
|
|
25
|
+
const publicExample = resolve(root, '.env.example');
|
|
26
|
+
const example = readFileSync(
|
|
27
|
+
existsSync(publicExample) ? publicExample : resolve(root, '_env.example'),
|
|
28
|
+
'utf8',
|
|
29
|
+
);
|
|
23
30
|
const databaseName = appIdentity.slug.replaceAll('-', '_');
|
|
24
31
|
writeFileSync(destination, example.replaceAll('stitchkit_starter', databaseName));
|
|
25
32
|
}
|
|
@@ -43,16 +43,23 @@ describe('the termination budget is an upper bound, not an estimate', () => {
|
|
|
43
43
|
});
|
|
44
44
|
|
|
45
45
|
test('the steps share one budget rather than each getting a full one', async () => {
|
|
46
|
+
const started: string[] = [];
|
|
47
|
+
const hangingClose = (name: string) => () => {
|
|
48
|
+
started.push(name);
|
|
49
|
+
return new Promise<void>(() => undefined);
|
|
50
|
+
};
|
|
51
|
+
const names = Array.from({ length: 10 }, (_, index) => `resource-${index + 1}`);
|
|
46
52
|
const result = await closeWithinBudget(
|
|
47
|
-
|
|
48
|
-
{ name: 'MCP', close: () => new Promise<void>(() => undefined) },
|
|
49
|
-
{ name: 'database', close: () => new Promise<void>(() => undefined) },
|
|
50
|
-
],
|
|
53
|
+
names.map((name) => ({ name, close: hangingClose(name) })),
|
|
51
54
|
25,
|
|
52
55
|
);
|
|
53
|
-
expect(result.unfinished).toEqual(
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
+
expect(result.unfinished).toEqual(names);
|
|
57
|
+
// Timer rounding may let one close start on a sub-millisecond remainder,
|
|
58
|
+
// so no exact boundary is contractual. What a fresh per-step budget would
|
|
59
|
+
// do is start every close; one shared deadline must skip at least one. This
|
|
60
|
+
// proves the state transition directly without treating scheduler latency
|
|
61
|
+
// as a product failure.
|
|
62
|
+
expect(started.length).toBeLessThan(names.length);
|
|
56
63
|
});
|
|
57
64
|
|
|
58
65
|
test('a close that fails is a failure, and keeps its cause', async () => {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Stitchkit Agent
|
|
2
|
+
|
|
3
|
+
A small, real terminal coding agent. The official TUI package owns terminal interaction while
|
|
4
|
+
Stitchkit owns durable messages, runs, direct typed tools, approvals, recovery and resources.
|
|
5
|
+
|
|
6
|
+
## Start
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
cp .env.example .env
|
|
10
|
+
# Fill OPENROUTER_API_KEY.
|
|
11
|
+
bun run dev
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Use `/model` to choose any live tool-capable model. Weekly popularity and benchmark facts stay
|
|
15
|
+
separate in the catalog. File reads and searches run directly. Writes, patches and shell commands
|
|
16
|
+
show an approval card bound to the exact durable tool call; press `Y` or `N`.
|
|
17
|
+
|
|
18
|
+
Source edits restart the terminal host through `bun --watch`, while `.stitchkit/agent.sqlite`
|
|
19
|
+
retains durable conversations and recovery evidence. Every launch opens a fresh conversation;
|
|
20
|
+
use `/resume` to return to an earlier one. `/clear` starts clean without deleting the conversation
|
|
21
|
+
you are leaving.
|
|
22
|
+
|
|
23
|
+
## Shape
|
|
24
|
+
|
|
25
|
+
- `stitchkit.agent.ts` — the small, typed composition point for theme, catalog and runtime policy.
|
|
26
|
+
- `src/runtime.ts` — host policy and composition of published Stitchkit primitives.
|
|
27
|
+
- `stitchkit-tui` — commands, transcript, model/session pickers and local attach protocol.
|
|
28
|
+
- `instructions/` — eager instructions with explicit provenance.
|
|
29
|
+
- `skills/*/SKILL.md` — lazily discoverable skills read through the direct `read_resource` tool.
|
|
30
|
+
- `.stitchkit/` — ignored local durable state, session descriptors, bounded metadata diagnostics
|
|
31
|
+
and approval secret.
|
|
32
|
+
|
|
33
|
+
While the TUI is open, `/status` shows its session ID. Another local process can submit through
|
|
34
|
+
the same controller without racing the runtime:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
bunx stitchkit-agent send --session SESSION_ID -- "Inspect the current project"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The coding root is a path boundary, not an operating-system sandbox. Run the process in a container
|
|
41
|
+
or another isolated environment before granting it access to untrusted projects or executables.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/2.5.10/schema.json",
|
|
3
|
+
"files": {
|
|
4
|
+
"includes": ["**", "!!node_modules", "!!dist", "!!project.json", "!!**/.stitchkit"]
|
|
5
|
+
},
|
|
6
|
+
"formatter": {
|
|
7
|
+
"enabled": true,
|
|
8
|
+
"indentStyle": "space",
|
|
9
|
+
"indentWidth": 2,
|
|
10
|
+
"lineWidth": 96
|
|
11
|
+
},
|
|
12
|
+
"javascript": {
|
|
13
|
+
"formatter": {
|
|
14
|
+
"quoteStyle": "single",
|
|
15
|
+
"jsxQuoteStyle": "single",
|
|
16
|
+
"trailingCommas": "all",
|
|
17
|
+
"semicolons": "always"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"linter": {
|
|
21
|
+
"enabled": true,
|
|
22
|
+
"rules": {
|
|
23
|
+
"preset": "recommended",
|
|
24
|
+
"suspicious": {
|
|
25
|
+
"noExplicitAny": "error",
|
|
26
|
+
"noUnknownAttribute": "off"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Agent workspace
|
|
2
|
+
|
|
3
|
+
You are a coding agent working inside this generated project.
|
|
4
|
+
|
|
5
|
+
- Inspect existing files before changing them.
|
|
6
|
+
- Keep edits inside the workspace root.
|
|
7
|
+
- Use direct file and shell tools; never claim a command succeeded without reading its result.
|
|
8
|
+
- Ask for approval through the tool protocol when a write, patch, edit or shell command requires it.
|
|
9
|
+
- Prefer the smallest coherent implementation and leave the workspace in a checked state.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "stitchkit-agent-starter",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"catalog": {
|
|
7
|
+
"stitchkit-tui": "^0.1.1"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "bun --watch src/index.ts",
|
|
11
|
+
"start": "bun src/index.ts",
|
|
12
|
+
"check": "bun x tsgo --noEmit",
|
|
13
|
+
"lint": "bun x biome check --error-on-warnings .",
|
|
14
|
+
"lint:fix": "bun x biome check --write .",
|
|
15
|
+
"test": "bun test",
|
|
16
|
+
"build": "bun build src/index.ts --outdir dist --target bun --packages external"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"stitchkit-tui": "file:../../../tui",
|
|
20
|
+
"@openrouter/ai-sdk-provider": "^3.0.0",
|
|
21
|
+
"ai": "^7.0.84",
|
|
22
|
+
"stitchkit": "file:../../../core",
|
|
23
|
+
"zod": "4.4.3"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@typescript/native-preview": "7.0.0-dev.20260707.2",
|
|
27
|
+
"@types/bun": "^1.4.0",
|
|
28
|
+
"@types/react": "^19.2.14",
|
|
29
|
+
"typescript": "^7.0.2"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"bun": ">=1.3.0"
|
|
33
|
+
},
|
|
34
|
+
"packageManager": "bun@1.3.14"
|
|
35
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"kind": "application",
|
|
4
|
+
"identity": {
|
|
5
|
+
"slug": "stitchkit-agent-starter",
|
|
6
|
+
"name": "Stitchkit Agent Starter",
|
|
7
|
+
"version": "0.1.0",
|
|
8
|
+
"description": {
|
|
9
|
+
"en": "A durable terminal coding agent built with Stitchkit."
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"roles": [
|
|
13
|
+
{
|
|
14
|
+
"name": "agent",
|
|
15
|
+
"workingDirectory": ".",
|
|
16
|
+
"commands": {
|
|
17
|
+
"development": {
|
|
18
|
+
"executable": "bun",
|
|
19
|
+
"args": ["--watch", "src/index.ts"]
|
|
20
|
+
},
|
|
21
|
+
"production": {
|
|
22
|
+
"executable": "bun",
|
|
23
|
+
"args": ["dist/index.js"]
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"drainFloorMs": 1000
|
|
27
|
+
}
|
|
28
|
+
],
|
|
29
|
+
"requires": [],
|
|
30
|
+
"release": {},
|
|
31
|
+
"env": {
|
|
32
|
+
"variables": [
|
|
33
|
+
{
|
|
34
|
+
"name": "OPENROUTER_API_KEY",
|
|
35
|
+
"shape": "string",
|
|
36
|
+
"required": true
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"name": "OPENROUTER_MODEL",
|
|
40
|
+
"shape": "string",
|
|
41
|
+
"required": false
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: verify
|
|
3
|
+
description: Check a completed change with the project's own Bun scripts before reporting success.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Verify
|
|
7
|
+
|
|
8
|
+
Read `package.json`, choose the narrowest relevant script, run it with the direct shell tool and
|
|
9
|
+
report the exact result. Use `bun run check` and `bun test` when the change affects shared types or
|
|
10
|
+
behavior. Do not invent a green result from code inspection alone.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
const EnvironmentSchema = z
|
|
4
|
+
.object({
|
|
5
|
+
OPENROUTER_API_KEY: z.string().min(1, 'OPENROUTER_API_KEY is required'),
|
|
6
|
+
OPENROUTER_MODEL: z.string().min(1).optional(),
|
|
7
|
+
})
|
|
8
|
+
.loose();
|
|
9
|
+
|
|
10
|
+
export interface AgentConfig {
|
|
11
|
+
apiKey: string;
|
|
12
|
+
preferredModelId?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function readAgentConfig(environment: Record<string, string | undefined>): AgentConfig {
|
|
16
|
+
const result = EnvironmentSchema.safeParse(environment);
|
|
17
|
+
if (!result.success) {
|
|
18
|
+
const fields = [
|
|
19
|
+
...new Set(
|
|
20
|
+
result.error.issues
|
|
21
|
+
.map((issue) => issue.path[0])
|
|
22
|
+
.filter((field) => field !== undefined),
|
|
23
|
+
),
|
|
24
|
+
];
|
|
25
|
+
throw new Error(`Missing or invalid configuration: ${fields.join(', ')}`);
|
|
26
|
+
}
|
|
27
|
+
const parsed = result.data;
|
|
28
|
+
return {
|
|
29
|
+
apiKey: parsed.OPENROUTER_API_KEY,
|
|
30
|
+
...(parsed.OPENROUTER_MODEL && { preferredModelId: parsed.OPENROUTER_MODEL }),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
type AgentLanguageModelProvider,
|
|
5
|
+
type AgentModelCatalog,
|
|
6
|
+
type AgentModelSelectionStore,
|
|
7
|
+
createAgentObservability,
|
|
8
|
+
defineAgentProtocol,
|
|
9
|
+
} from 'stitchkit/agent-runtime';
|
|
10
|
+
import { createAgentCodingTools } from 'stitchkit/agent-runtime/coding-tools';
|
|
11
|
+
import {
|
|
12
|
+
createAgentHarnessFileResources,
|
|
13
|
+
createHeadlessAgentHarness,
|
|
14
|
+
} from 'stitchkit/agent-runtime/harness';
|
|
15
|
+
import { openRouterProvider } from 'stitchkit/agent-runtime/openrouter';
|
|
16
|
+
import { createBunSqliteAgentRuntimeStore } from 'stitchkit/agent-runtime/sqlite/bun';
|
|
17
|
+
import { mountAgent } from 'stitchkit/tools';
|
|
18
|
+
import type { AgentTuiDiagnostics } from 'stitchkit-tui';
|
|
19
|
+
import { z } from 'zod';
|
|
20
|
+
import type { AgentConfig } from './config';
|
|
21
|
+
|
|
22
|
+
const InputMetadataSchema = z.object({ modelId: z.string().min(1) }).strict();
|
|
23
|
+
|
|
24
|
+
async function persistentApprovalSecret(stateDirectory: string): Promise<string> {
|
|
25
|
+
const filename = path.join(stateDirectory, 'approval-secret');
|
|
26
|
+
await mkdir(stateDirectory, { recursive: true });
|
|
27
|
+
try {
|
|
28
|
+
return (await readFile(filename, 'utf8')).trim();
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT')) throw error;
|
|
31
|
+
}
|
|
32
|
+
const secret = crypto.randomUUID();
|
|
33
|
+
try {
|
|
34
|
+
await writeFile(filename, `${secret}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
35
|
+
return secret;
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (!(error instanceof Error && 'code' in error && error.code === 'EEXIST')) throw error;
|
|
38
|
+
return (await readFile(filename, 'utf8')).trim();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function createStarterHarness(
|
|
43
|
+
config: AgentConfig,
|
|
44
|
+
workspace: string,
|
|
45
|
+
options: {
|
|
46
|
+
catalog: AgentModelCatalog;
|
|
47
|
+
selections: AgentModelSelectionStore;
|
|
48
|
+
provider?: AgentLanguageModelProvider;
|
|
49
|
+
diagnostics: AgentTuiDiagnostics;
|
|
50
|
+
},
|
|
51
|
+
) {
|
|
52
|
+
const stateDirectory = path.join(workspace, '.stitchkit');
|
|
53
|
+
await mkdir(stateDirectory, { recursive: true });
|
|
54
|
+
const resources = createAgentHarnessFileResources({
|
|
55
|
+
roots: [
|
|
56
|
+
{ id: 'instructions', path: path.join(workspace, 'instructions'), kind: 'instruction' },
|
|
57
|
+
{ id: 'skills', path: path.join(workspace, 'skills'), kind: 'skill' },
|
|
58
|
+
],
|
|
59
|
+
});
|
|
60
|
+
const codingTools = createAgentCodingTools({
|
|
61
|
+
root: workspace,
|
|
62
|
+
authorize: () => true,
|
|
63
|
+
executables: {
|
|
64
|
+
bun: process.execPath,
|
|
65
|
+
git: Bun.which('git') ?? '/usr/bin/git',
|
|
66
|
+
rg: Bun.which('rg') ?? '/usr/bin/rg',
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
const provider = options.provider ?? openRouterProvider({ apiKey: config.apiKey });
|
|
70
|
+
const sqlite = createBunSqliteAgentRuntimeStore({
|
|
71
|
+
filename: path.join(stateDirectory, 'agent.sqlite'),
|
|
72
|
+
initialize: true,
|
|
73
|
+
});
|
|
74
|
+
const observability = createAgentObservability({
|
|
75
|
+
includeInternalCause: true,
|
|
76
|
+
write: (event) => options.diagnostics.write(event),
|
|
77
|
+
});
|
|
78
|
+
const harness = createHeadlessAgentHarness({
|
|
79
|
+
protocol: defineAgentProtocol({
|
|
80
|
+
context: z.object({}),
|
|
81
|
+
inputMetadata: InputMetadataSchema,
|
|
82
|
+
terminalAcceptance: 'require-output',
|
|
83
|
+
}),
|
|
84
|
+
store: sqlite.store,
|
|
85
|
+
observe: observability,
|
|
86
|
+
models: {
|
|
87
|
+
async resolve({ conversationId, run, snapshot }) {
|
|
88
|
+
const input = snapshot.messages.find(({ id }) => id === run.inputMessageIds[0]);
|
|
89
|
+
const metadata = InputMetadataSchema.safeParse(input?.metadata);
|
|
90
|
+
const selected = metadata.success
|
|
91
|
+
? metadata.data.modelId
|
|
92
|
+
: (await options.selections.load(conversationId))?.modelId;
|
|
93
|
+
const entry = options.catalog.models.find(({ id }) => id === selected);
|
|
94
|
+
if (!entry) throw new Error('The selected model is stale or unavailable');
|
|
95
|
+
return {
|
|
96
|
+
descriptor: entry.descriptor,
|
|
97
|
+
model: provider.create(entry.descriptor.modelId),
|
|
98
|
+
...(provider.normalizeUsage && { normalizeUsage: provider.normalizeUsage }),
|
|
99
|
+
};
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
resources: { load: () => resources.load() },
|
|
103
|
+
promptBudget: ({ contextWindow }) => ({
|
|
104
|
+
contextWindow,
|
|
105
|
+
reservedOutput: Math.min(8_192, Math.floor(contextWindow / 4)),
|
|
106
|
+
toolSchemas: { provenance: 'unavailable' },
|
|
107
|
+
attachments: { value: 0, provenance: 'measured' },
|
|
108
|
+
providerOverhead: { provenance: 'unavailable' },
|
|
109
|
+
}),
|
|
110
|
+
tools: (context) =>
|
|
111
|
+
mountAgent([], {
|
|
112
|
+
runtimeTools: [...codingTools, ...resources.runtimeTools],
|
|
113
|
+
lifecycle: context.toolFenceLifecycle,
|
|
114
|
+
}),
|
|
115
|
+
loop: {
|
|
116
|
+
maxSteps: 50,
|
|
117
|
+
checkpointEveryEvents: 10,
|
|
118
|
+
toolApproval: {
|
|
119
|
+
read_file: 'approved',
|
|
120
|
+
search_files: 'approved',
|
|
121
|
+
read_resource: 'approved',
|
|
122
|
+
write_file: 'user-approval',
|
|
123
|
+
apply_patch: 'user-approval',
|
|
124
|
+
run_command: 'user-approval',
|
|
125
|
+
},
|
|
126
|
+
toolApprovalSecret: await persistentApprovalSecret(stateDirectory),
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
const managedHarness = {
|
|
130
|
+
...harness,
|
|
131
|
+
async close(closeOptions?: Parameters<typeof harness.close>[0]) {
|
|
132
|
+
const result = await harness.close(closeOptions);
|
|
133
|
+
await observability.close();
|
|
134
|
+
await sqlite.close();
|
|
135
|
+
return result;
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
return { harness: managedHarness, conversations: sqlite.conversations };
|
|
139
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { openRouterModelCatalog } from 'stitchkit/agent-runtime/openrouter';
|
|
2
|
+
import { defineAgentTui } from 'stitchkit-tui';
|
|
3
|
+
import { readAgentConfig } from './src/config';
|
|
4
|
+
import { createStarterHarness } from './src/runtime';
|
|
5
|
+
|
|
6
|
+
const environment = readAgentConfig(Bun.env);
|
|
7
|
+
|
|
8
|
+
export default defineAgentTui({
|
|
9
|
+
title: 'Stitchkit agent',
|
|
10
|
+
context: () => ({}),
|
|
11
|
+
modelCatalog: openRouterModelCatalog({ apiKey: environment.apiKey }),
|
|
12
|
+
...(environment.preferredModelId && { preferredModelId: environment.preferredModelId }),
|
|
13
|
+
createRuntime: ({ catalog, selections, diagnostics }) =>
|
|
14
|
+
createStarterHarness(environment, process.cwd(), { catalog, selections, diagnostics }),
|
|
15
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { readAgentConfig } from '../src/config';
|
|
3
|
+
|
|
4
|
+
describe('agent config', () => {
|
|
5
|
+
test('requires only an OpenRouter credential and accepts an optional preferred model', () => {
|
|
6
|
+
expect(() => readAgentConfig({})).toThrow(
|
|
7
|
+
'Missing or invalid configuration: OPENROUTER_API_KEY',
|
|
8
|
+
);
|
|
9
|
+
expect(readAgentConfig({ OPENROUTER_API_KEY: 'secret' })).toEqual({ apiKey: 'secret' });
|
|
10
|
+
expect(
|
|
11
|
+
readAgentConfig({
|
|
12
|
+
OPENROUTER_API_KEY: 'secret',
|
|
13
|
+
OPENROUTER_MODEL: 'provider/model',
|
|
14
|
+
}),
|
|
15
|
+
).toEqual({ apiKey: 'secret', preferredModelId: 'provider/model' });
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from 'bun:test';
|
|
2
|
+
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { simulateReadableStream } from 'ai';
|
|
6
|
+
import { MockLanguageModelV4 } from 'ai/test';
|
|
7
|
+
import {
|
|
8
|
+
AgentModelCatalogSchema,
|
|
9
|
+
createMemoryAgentModelSelectionStore,
|
|
10
|
+
} from 'stitchkit/agent-runtime';
|
|
11
|
+
import { createStarterHarness } from '../src/runtime';
|
|
12
|
+
|
|
13
|
+
const roots: string[] = [];
|
|
14
|
+
|
|
15
|
+
afterEach(async () => {
|
|
16
|
+
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('Agent starter runtime', () => {
|
|
20
|
+
test('runs one model turn and reopens its durable transcript', async () => {
|
|
21
|
+
const workspace = await mkdtemp(path.join(tmpdir(), 'stitchkit-agent-starter-'));
|
|
22
|
+
roots.push(workspace);
|
|
23
|
+
await mkdir(path.join(workspace, 'instructions'));
|
|
24
|
+
await mkdir(path.join(workspace, 'skills'));
|
|
25
|
+
await writeFile(path.join(workspace, 'instructions/AGENTS.md'), 'Answer directly.\n');
|
|
26
|
+
const usage = {
|
|
27
|
+
inputTokens: { total: 1, noCache: 1, cacheRead: undefined, cacheWrite: undefined },
|
|
28
|
+
outputTokens: { total: 1, text: 1, reasoning: undefined },
|
|
29
|
+
};
|
|
30
|
+
const model = new MockLanguageModelV4({
|
|
31
|
+
doStream: {
|
|
32
|
+
stream: simulateReadableStream({
|
|
33
|
+
chunks: [
|
|
34
|
+
{ type: 'text-start', id: 'answer' },
|
|
35
|
+
{ type: 'text-delta', id: 'answer', delta: 'Ready to build.' },
|
|
36
|
+
{ type: 'text-end', id: 'answer' },
|
|
37
|
+
{ type: 'finish', finishReason: { unified: 'stop', raw: undefined }, usage },
|
|
38
|
+
],
|
|
39
|
+
}),
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
const config = { apiKey: 'fixture' };
|
|
43
|
+
const descriptor = {
|
|
44
|
+
provider: 'fixture',
|
|
45
|
+
modelId: 'fixture/model',
|
|
46
|
+
contextWindow: 32_000,
|
|
47
|
+
capabilities: ['tools'],
|
|
48
|
+
};
|
|
49
|
+
const catalog = AgentModelCatalogSchema.parse({
|
|
50
|
+
schemaVersion: 1,
|
|
51
|
+
source: 'fixture',
|
|
52
|
+
observedAt: '2026-08-30T00:00:00.000Z',
|
|
53
|
+
completeness: 'complete',
|
|
54
|
+
diagnostics: [],
|
|
55
|
+
models: [{ id: 'fixture/model', name: 'Fixture', descriptor, metrics: [] }],
|
|
56
|
+
});
|
|
57
|
+
const selections = createMemoryAgentModelSelectionStore();
|
|
58
|
+
await selections.save('main', {
|
|
59
|
+
modelId: 'fixture/model',
|
|
60
|
+
selectedAt: '2026-08-30T00:00:00.000Z',
|
|
61
|
+
});
|
|
62
|
+
const provider = { create: () => model };
|
|
63
|
+
const diagnostics = { write: () => undefined };
|
|
64
|
+
const first = await createStarterHarness(config, workspace, {
|
|
65
|
+
catalog,
|
|
66
|
+
selections,
|
|
67
|
+
provider,
|
|
68
|
+
diagnostics,
|
|
69
|
+
});
|
|
70
|
+
const result = await first.harness.submit({
|
|
71
|
+
conversationId: 'main',
|
|
72
|
+
idempotencyKey: 'first',
|
|
73
|
+
context: {},
|
|
74
|
+
parts: [{ type: 'text', text: 'Hello' }],
|
|
75
|
+
metadata: { modelId: 'fixture/model' },
|
|
76
|
+
}).result;
|
|
77
|
+
expect(result.reason).toBe('success');
|
|
78
|
+
expect(result.message.parts).toContainEqual({ type: 'text', text: 'Ready to build.' });
|
|
79
|
+
await first.harness.close();
|
|
80
|
+
|
|
81
|
+
const reopened = await createStarterHarness(config, workspace, {
|
|
82
|
+
catalog,
|
|
83
|
+
selections,
|
|
84
|
+
provider,
|
|
85
|
+
diagnostics,
|
|
86
|
+
});
|
|
87
|
+
expect((await reopened.harness.snapshot('main')).messages).toHaveLength(2);
|
|
88
|
+
await reopened.harness.close();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"lib": ["ESNext", "DOM"],
|
|
4
|
+
"target": "ESNext",
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"jsxImportSource": "@opentui/react",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"noUncheckedIndexedAccess": true,
|
|
11
|
+
"exactOptionalPropertyTypes": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"types": ["bun"]
|
|
14
|
+
},
|
|
15
|
+
"include": ["src", "tests"]
|
|
16
|
+
}
|