create-stitchkit 0.4.3 → 0.5.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/CHANGELOG.md +78 -0
- package/README.md +19 -0
- package/UPGRADING.md +65 -0
- package/dist/cli.js +71 -11
- package/examples/repository/packages/backend/src/surface.snapshot.json +2 -2
- package/package.json +25 -1
- package/template/bun.lock +23 -16
- package/template/package.json +4 -4
- 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 +141 -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,84 @@ step is overwritten by the next release.
|
|
|
12
12
|
|
|
13
13
|
## [Unreleased]
|
|
14
14
|
|
|
15
|
+
## [0.5.0] — 2026-09-01
|
|
16
|
+
|
|
17
|
+
### ⚠️ Breaking changes
|
|
18
|
+
|
|
19
|
+
**Who must act:** two audiences. Anyone holding a generated **Agent** project
|
|
20
|
+
changes one key in one object, and nothing reports it when it goes wrong.
|
|
21
|
+
**Everyone** re-reads the Zod item: an API that accepted a timestamp without
|
|
22
|
+
seconds stops accepting it, which is a change in what your endpoints admit, not
|
|
23
|
+
in what your code compiles to.
|
|
24
|
+
|
|
25
|
+
- **The Agent approval policy keys `edit_file`, not `apply_patch`.** Stitchkit
|
|
26
|
+
0.71.0 replaced `apply_patch` with a one-call `edit_file`, and `toolApproval`
|
|
27
|
+
names tools by string. A key matching no tool is not an error — an unlisted
|
|
28
|
+
tool falls through to "no approval required", so the effect of leaving the old
|
|
29
|
+
name is not that editing breaks. It is that editing stops asking: the one tool
|
|
30
|
+
the policy existed to gate now runs unattended, and the only visible sign is an
|
|
31
|
+
approval prompt that never appears.
|
|
32
|
+
`// before: toolApproval: { apply_patch: 'user-approval' }` →
|
|
33
|
+
`// after: toolApproval: { edit_file: 'user-approval' }`
|
|
34
|
+
|
|
35
|
+
- **Zod 4.5 makes seconds mandatory in `z.iso.datetime()`.** The template moves
|
|
36
|
+
to `zod@4.5.4`, and the tightening travels with it: `2026-08-08T00:15Z`
|
|
37
|
+
validated before and is refused now. Nothing in the generated code changes —
|
|
38
|
+
what changes is the set of inputs your API accepts, so a client that omitted
|
|
39
|
+
seconds starts receiving `BAD_REQUEST`. If you need the old latitude, say so
|
|
40
|
+
in the schema rather than by holding the version back:
|
|
41
|
+
`// before: z.iso.datetime()` → `// after: z.iso.datetime() // seconds now required`
|
|
42
|
+
The same release regenerates the committed **surface snapshots**: 4.5 encodes
|
|
43
|
+
a nullable as `type: ['string','null']` where 4.4 wrote `anyOf`, so every
|
|
44
|
+
affected shape fingerprint moves. A project of your own carrying
|
|
45
|
+
`surface.snapshot.json` regenerates it with `bun run surface:snapshot` and
|
|
46
|
+
reviews the diff — the hashes move, the operations do not.
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
|
|
50
|
+
- **The generated project targets the published `stitchkit@0.71.0` line.** The
|
|
51
|
+
catalog target and the frozen lockfile move together, so a fresh scaffold
|
|
52
|
+
receives `edit_file`, `list_directory`, `glob`, the typed coding-tool refusals
|
|
53
|
+
a model can act on, the context usage a step reports, and the peer-free
|
|
54
|
+
`stitchkit/telegram` leaf.
|
|
55
|
+
- **The whole toolchain moves to its current releases** — Zod 4.5.4 (see the
|
|
56
|
+
breaking note above), Biome 2.5.11, `@types/node` 26.4.0, and `ai` 7.0.87 in
|
|
57
|
+
the Agent template. One Zod resolves across the repository and both templates,
|
|
58
|
+
which is now a gate rather than a coincidence: two minors of Zod are two
|
|
59
|
+
incompatible type systems, and the error that surfaces names neither Zod nor
|
|
60
|
+
the file that moved it.
|
|
61
|
+
- **The Agent approval policy is exhaustive again.** 0.71.0 added
|
|
62
|
+
`list_directory` and `glob`; both are read-only and both were therefore
|
|
63
|
+
running under the framework's default rather than under the project's own
|
|
64
|
+
policy. They are now named `'approved'` beside `read_file` and `search_files`.
|
|
65
|
+
Nothing about what happens changes — what changes is that the file says so,
|
|
66
|
+
which is the whole reason the map is written out rather than defaulted.
|
|
67
|
+
|
|
68
|
+
## [0.4.4] — 2026-08-30
|
|
69
|
+
|
|
70
|
+
### Changed
|
|
71
|
+
|
|
72
|
+
- **The generated project now targets the published `stitchkit@0.70.1` line and includes the
|
|
73
|
+
maintained `stitchkit-tui` Agent profile.** The catalog and frozen lockfile move together, so a
|
|
74
|
+
fresh scaffold receives the macOS contained-file fix, bounded realtime shutdown and the terminal
|
|
75
|
+
harness package already validated by the same release train.
|
|
76
|
+
|
|
77
|
+
### Added
|
|
78
|
+
|
|
79
|
+
- **An explicit Agent template opens a durable OpenRouter coding session in the terminal.**
|
|
80
|
+
`bun create stitchkit my-agent --template agent` generates an OpenTUI host over the published
|
|
81
|
+
headless harness, Bun SQLite storage, lazy skills, direct coding tools, signed approvals and
|
|
82
|
+
recovery. The default production application and its repository overlay are unchanged. The
|
|
83
|
+
Agent development source follows framework HEAD locally, while generated projects receive the
|
|
84
|
+
application starter's one canonical Stitchkit catalog target. Startup requires only an API key:
|
|
85
|
+
a bounded picker reads the current popular tool-capable models and their provider-owned context
|
|
86
|
+
windows directly from OpenRouter. → ADR 0132.
|
|
87
|
+
- **The Agent template now composes the official terminal package instead of copying a product
|
|
88
|
+
shell.** `stitchkit.agent.ts` is the editable typed entrypoint; `/model`, durable sessions,
|
|
89
|
+
approvals, scrolling and local `send`/`interrupt` attachment come from `stitchkit-tui`. The
|
|
90
|
+
generated manifest receives portable catalog dependencies while the repository fixture tests
|
|
91
|
+
both packages from source. → ADR 0133.
|
|
92
|
+
|
|
15
93
|
## [0.4.3] — 2026-08-28
|
|
16
94
|
|
|
17
95
|
### 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/UPGRADING.md
CHANGED
|
@@ -60,6 +60,71 @@ the first scaffolder release with a migration channel of its own.
|
|
|
60
60
|
|
|
61
61
|
---
|
|
62
62
|
|
|
63
|
+
## Released migration: 0.5.0
|
|
64
|
+
|
|
65
|
+
### the approval policy names a tool that no longer exists
|
|
66
|
+
|
|
67
|
+
One code edit, and one thing to do to a machine that was left mid-question.
|
|
68
|
+
|
|
69
|
+
1. **Rename the key.** In `src/runtime.ts`, inside `loop.toolApproval`, replace
|
|
70
|
+
`apply_patch: 'user-approval'` with `edit_file: 'user-approval'`. While you
|
|
71
|
+
are in that object, add `list_directory: 'approved'` and `glob: 'approved'`
|
|
72
|
+
beside `read_file` and `search_files` — 0.71.0 added both, they are read-only,
|
|
73
|
+
and an unlisted tool is governed by the framework's default rather than by
|
|
74
|
+
this file.
|
|
75
|
+
|
|
76
|
+
Do this **before** you start the upgraded agent, not after. The failure mode
|
|
77
|
+
is silent in the direction that costs you: a key matching no tool does not
|
|
78
|
+
raise, it simply stops gating, so the first edit after the upgrade is applied
|
|
79
|
+
without asking and looks exactly like an edit you approved.
|
|
80
|
+
|
|
81
|
+
2. **Operator step — a durable session left waiting on the old tool cannot be
|
|
82
|
+
answered.** If an agent was interrupted while a `apply_patch` approval was
|
|
83
|
+
pending, that approval names a call for a tool the framework no longer
|
|
84
|
+
defines: approving it cannot execute anything, and the run will not move.
|
|
85
|
+
Interrupt that run, or drop the local state directory — `.stitchkit/` holds
|
|
86
|
+
`agent.sqlite`, the approval secret and the TUI logs, all of it local and
|
|
87
|
+
regenerated on the next start. There is nothing durable in it that a server
|
|
88
|
+
owns.
|
|
89
|
+
|
|
90
|
+
A session with no pending approval needs none of this and resumes normally.
|
|
91
|
+
|
|
92
|
+
3. **The framework's own move is separate.** Going from a `0.70.x` line to
|
|
93
|
+
`0.71.0` is a Stitchkit upgrade with its own breaking notes — the coding-tool
|
|
94
|
+
surface changed for anyone calling it directly, not only through this
|
|
95
|
+
template. Read
|
|
96
|
+
[`docs/guide/upgrading.md`](../../docs/guide/upgrading.md), section
|
|
97
|
+
`Released migration: 0.71.0`.
|
|
98
|
+
|
|
99
|
+
### the timestamp your API accepts got narrower
|
|
100
|
+
|
|
101
|
+
Zod moved to 4.5, and `z.iso.datetime()` now requires seconds. This is not a
|
|
102
|
+
code change — it is a change in what your endpoints admit, and it arrives the
|
|
103
|
+
moment you install.
|
|
104
|
+
|
|
105
|
+
1. **Decide before you deploy, not after.** If any client sends
|
|
106
|
+
`2026-08-08T00:15Z`, it now receives `BAD_REQUEST` where it used to receive
|
|
107
|
+
`200`. Search your schemas for `z.iso.datetime()` and check who fills those
|
|
108
|
+
fields. A machine-to-machine caller you control is a one-line fix on its
|
|
109
|
+
side; a caller you do not control is a decision, and the honest form of it is
|
|
110
|
+
an explicit schema that accepts what you mean to accept — not a pinned Zod
|
|
111
|
+
version, which only moves the same day to a later one.
|
|
112
|
+
|
|
113
|
+
2. **Regenerate your surface snapshot in the same commit.** 4.5 encodes a
|
|
114
|
+
nullable as `type: ["string","null"]` where 4.4 wrote `anyOf`, so the shape
|
|
115
|
+
fingerprints in `packages/backend/src/surface.snapshot.json` move without any
|
|
116
|
+
operation changing. Run `bun run surface:snapshot` and read the diff: only
|
|
117
|
+
`inputShape` / `outputShape` values may differ. If an operation appeared,
|
|
118
|
+
vanished, or changed its HTTP or tool exposure, that is **your** change and
|
|
119
|
+
Zod did not cause it.
|
|
120
|
+
|
|
121
|
+
3. **Move one Zod, not several.** If your project vendors or links packages that
|
|
122
|
+
depend on Zod themselves, they must resolve the same minor. Two minors are
|
|
123
|
+
two incompatible type systems, and what surfaces is
|
|
124
|
+
`TS2589: Type instantiation is excessively deep` in a file you did not touch.
|
|
125
|
+
No operator step: nothing about a running machine changes.
|
|
126
|
+
|
|
127
|
+
|
|
63
128
|
## Released migration: 0.4.1
|
|
64
129
|
|
|
65
130
|
### a release refuses a stale artifact, and cleanup is bounded
|
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
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"hasInput": false,
|
|
7
7
|
"hasOutput": true,
|
|
8
8
|
"inputShape": null,
|
|
9
|
-
"outputShape": "
|
|
9
|
+
"outputShape": "05d1386c30d82f6a",
|
|
10
10
|
"http": [
|
|
11
11
|
{
|
|
12
12
|
"method": "GET",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"hasInput": false,
|
|
27
27
|
"hasOutput": true,
|
|
28
28
|
"inputShape": null,
|
|
29
|
-
"outputShape": "
|
|
29
|
+
"outputShape": "05d1386c30d82f6a",
|
|
30
30
|
"http": [
|
|
31
31
|
{
|
|
32
32
|
"method": "POST",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-stitchkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
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.71.0",
|
|
63
87
|
"typescript": "^7.0.2"
|
|
64
88
|
},
|
|
65
89
|
"engines": {
|
package/template/bun.lock
CHANGED
|
@@ -11,16 +11,16 @@
|
|
|
11
11
|
"@app/config": "workspace:*",
|
|
12
12
|
"@app/shared": "workspace:*",
|
|
13
13
|
"@axe-core/playwright": "^4.13.0",
|
|
14
|
-
"@biomejs/biome": "^2.5.
|
|
14
|
+
"@biomejs/biome": "^2.5.11",
|
|
15
15
|
"@modelcontextprotocol/client": "^2.0.0",
|
|
16
16
|
"@playwright/test": "^1.62.1",
|
|
17
17
|
"@types/bun": "^1.4.0",
|
|
18
|
-
"@types/node": "^26.
|
|
18
|
+
"@types/node": "^26.4.0",
|
|
19
19
|
"oxc-parser": "^0.147.0",
|
|
20
20
|
"socket.io-client": "^4.8.3",
|
|
21
21
|
"stitchkit": "catalog:",
|
|
22
22
|
"typescript": "^7.0.2",
|
|
23
|
-
"zod": "^4.4
|
|
23
|
+
"zod": "^4.5.4",
|
|
24
24
|
},
|
|
25
25
|
},
|
|
26
26
|
"packages/backend": {
|
|
@@ -139,7 +139,7 @@
|
|
|
139
139
|
},
|
|
140
140
|
},
|
|
141
141
|
"catalog": {
|
|
142
|
-
"stitchkit": "^0.
|
|
142
|
+
"stitchkit": "^0.71.0",
|
|
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=="],
|
|
@@ -168,23 +168,23 @@
|
|
|
168
168
|
|
|
169
169
|
"@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
|
|
170
170
|
|
|
171
|
-
"@biomejs/biome": ["@biomejs/biome@2.5.
|
|
171
|
+
"@biomejs/biome": ["@biomejs/biome@2.5.11", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.11", "@biomejs/cli-darwin-x64": "2.5.11", "@biomejs/cli-linux-arm64": "2.5.11", "@biomejs/cli-linux-arm64-musl": "2.5.11", "@biomejs/cli-linux-x64": "2.5.11", "@biomejs/cli-linux-x64-musl": "2.5.11", "@biomejs/cli-win32-arm64": "2.5.11", "@biomejs/cli-win32-x64": "2.5.11" }, "bin": { "biome": "bin/biome" } }, "sha512-Tj0dnkLPdW0ASjHfj2D/ZkkvPU2wrFmnE1jWTD2xzV1ycapV1DutbYXk4NDnR3rYTi1ZCbNFD4G2gRMEY65WaA=="],
|
|
172
172
|
|
|
173
|
-
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.
|
|
173
|
+
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-6SGZxoKbXvUjMn1t6A98HqWISPnGNbYs0R/Rt2JarmXBSev+lva4QxUMWEBX9lX1Wo1XTJ78uk5xVDtG58SRZg=="],
|
|
174
174
|
|
|
175
|
-
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.
|
|
175
|
+
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-nYkXY7tLBEgnGbYapDKAyKzgt44ZEyG+AKalvTXtCWKYgepI9dw327q+cVgedxm+Udi1ZzHKUyZrIusHi/KQbw=="],
|
|
176
176
|
|
|
177
|
-
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.
|
|
177
|
+
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-3PVLSTD9RR73rvVPt5G3T1gc+ycggWEGfTD7RvzzbtcDPD27NxgxBbAFfpm7DXJKW6VLHWE1lLMGvFt2Qxjcow=="],
|
|
178
178
|
|
|
179
|
-
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.
|
|
179
|
+
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-qhyZUMyCbWYFV2bAwRNVvfMVZ+hv7WYl6mossGrxC+uiQQXhvsuWWU8zz6jYX0mChZd9MgQZbm4vozTmG/5iGw=="],
|
|
180
180
|
|
|
181
|
-
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.
|
|
181
|
+
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.11", "", { "os": "linux", "cpu": "x64" }, "sha512-JOytptlsgM33B2MMFUg8iBrb4IKpbD5JnJrSeYiaFEeAj4vuXx0iQSQZ4qK7sqyMtfjZxxPdNdMZZVL4y/mFyA=="],
|
|
182
182
|
|
|
183
|
-
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.
|
|
183
|
+
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.11", "", { "os": "linux", "cpu": "x64" }, "sha512-oRRlrchG5EfrEL/EmtT1qUjSNHk3/5LGeZhQqADBBAJF1b1ET6964xEKe7aGlGARzDfza8H/seEsFJl7S6Ql9w=="],
|
|
184
184
|
|
|
185
|
-
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.
|
|
185
|
+
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-e49E6K9hzH/ohJNx8Y26mY8HaV4I4ZViIeoqhKsmoXLKHhQnMeBAVqCgsGf2Wa3lXlS7RkporDXMHHWkzvZzFw=="],
|
|
186
186
|
|
|
187
|
-
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.
|
|
187
|
+
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.11", "", { "os": "win32", "cpu": "x64" }, "sha512-QSQr/KjOgXA7OzXJUWS+oguKyAZ3Q0l/lnlDGbu397eKo83atuWUjBPJrsqbKNF6CARGw8XXJLGzpHC8Ryhd4Q=="],
|
|
188
188
|
|
|
189
189
|
"@electric-sql/pglite": ["@electric-sql/pglite@0.4.3", "", {}, "sha512-ichuWTgtd4mOM1G4SpyGJa5trT03lWbMypDV0fUXUCXg5hiHqVAz/bZyV68NqmkLB7WcYmj1RMJVSp8HV/v/ZQ=="],
|
|
190
190
|
|
|
@@ -620,7 +620,7 @@
|
|
|
620
620
|
|
|
621
621
|
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
|
|
622
622
|
|
|
623
|
-
"@types/node": ["@types/node@26.
|
|
623
|
+
"@types/node": ["@types/node@26.4.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ=="],
|
|
624
624
|
|
|
625
625
|
"@types/pg": ["@types/pg@8.23.1", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A=="],
|
|
626
626
|
|
|
@@ -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.71.0", "", { "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-HXqTD1Sv534rWt+KKJV1Gp535NTRzbGFxNMuRAvo9TzuU0kZxBDF18gu+WexF9O8Z4jkhlfvTjXeRy/oQrnHwA=="],
|
|
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
|
|
|
@@ -1186,10 +1186,13 @@
|
|
|
1186
1186
|
|
|
1187
1187
|
"zeptomatch": ["zeptomatch@2.1.0", "", { "dependencies": { "grammex": "^3.1.11", "graphmatch": "^1.1.0" } }, "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA=="],
|
|
1188
1188
|
|
|
1189
|
-
"zod": ["zod@4.4
|
|
1189
|
+
"zod": ["zod@4.5.4", "", {}, "sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA=="],
|
|
1190
1190
|
|
|
1191
1191
|
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
|
1192
1192
|
|
|
1193
|
+
|
|
1194
|
+
|
|
1195
|
+
|
|
1193
1196
|
"@prisma/adapter-pg/@types/pg": ["@types/pg@8.21.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-AYdtudzabjLZgVgRZmAnU8bAnVUXzuJX2IYHeSIiIHm68olD+LgQYCGWdtcNYnP0uq9c4S4NibVG3Ni7VbKW7Q=="],
|
|
1194
1197
|
|
|
1195
1198
|
"@prisma/adapter-pg/pg": ["pg@8.22.0", "", { "dependencies": { "pg-connection-string": "^2.14.0", "pg-pool": "^3.14.0", "pg-protocol": "^1.15.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.4.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA=="],
|
|
@@ -1220,6 +1223,8 @@
|
|
|
1220
1223
|
|
|
1221
1224
|
"@types/cors/@types/node": ["@types/node@26.2.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg=="],
|
|
1222
1225
|
|
|
1226
|
+
"@types/pg/@types/node": ["@types/node@26.3.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-L3fgrnchriRC2ExBflb8j4uZZURHZfQsmQeyVzhjcHW4kkwVyo8/0h1B2MVzMTrYUJYu6G7EWs14hW/L9putqw=="],
|
|
1227
|
+
|
|
1223
1228
|
"@types/ws/@types/node": ["@types/node@26.2.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg=="],
|
|
1224
1229
|
|
|
1225
1230
|
"@visx/vendor/@types/d3-array": ["@types/d3-array@3.0.3", "", {}, "sha512-Reoy+pKnvsksN0lQUlcH6dOGjRZ/3WRwXR//m+/8lt1BXeI4xyaUZoqULNjyXXRuh0Mj4LNpkCvhUpQlY3X5xQ=="],
|
|
@@ -1236,6 +1241,8 @@
|
|
|
1236
1241
|
|
|
1237
1242
|
"accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
|
|
1238
1243
|
|
|
1244
|
+
"bun-types/@types/node": ["@types/node@26.3.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-L3fgrnchriRC2ExBflb8j4uZZURHZfQsmQeyVzhjcHW4kkwVyo8/0h1B2MVzMTrYUJYu6G7EWs14hW/L9putqw=="],
|
|
1245
|
+
|
|
1239
1246
|
"engine.io/@types/node": ["@types/node@26.2.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg=="],
|
|
1240
1247
|
|
|
1241
1248
|
"pg-types/postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
|
package/template/package.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"packages/*"
|
|
8
8
|
],
|
|
9
9
|
"catalog": {
|
|
10
|
-
"stitchkit": "^0.
|
|
10
|
+
"stitchkit": "^0.71.0"
|
|
11
11
|
},
|
|
12
12
|
"scripts": {
|
|
13
13
|
"dev": "bun scripts/dev.ts",
|
|
@@ -42,16 +42,16 @@
|
|
|
42
42
|
"@app/config": "workspace:*",
|
|
43
43
|
"@app/shared": "workspace:*",
|
|
44
44
|
"@axe-core/playwright": "^4.13.0",
|
|
45
|
-
"@biomejs/biome": "^2.5.
|
|
45
|
+
"@biomejs/biome": "^2.5.11",
|
|
46
46
|
"@modelcontextprotocol/client": "^2.0.0",
|
|
47
47
|
"@playwright/test": "^1.62.1",
|
|
48
48
|
"@types/bun": "^1.4.0",
|
|
49
|
-
"@types/node": "^26.
|
|
49
|
+
"@types/node": "^26.4.0",
|
|
50
50
|
"oxc-parser": "^0.147.0",
|
|
51
51
|
"socket.io-client": "^4.8.3",
|
|
52
52
|
"stitchkit": "catalog:",
|
|
53
53
|
"typescript": "^7.0.2",
|
|
54
|
-
"zod": "^4.4
|
|
54
|
+
"zod": "^4.5.4"
|
|
55
55
|
},
|
|
56
56
|
"engines": {
|
|
57
57
|
"bun": ">=1.2.0",
|
|
@@ -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
|
+
"@openrouter/ai-sdk-provider": "^3.0.0",
|
|
20
|
+
"ai": "^7.0.87",
|
|
21
|
+
"stitchkit": "file:../../../core",
|
|
22
|
+
"stitchkit-tui": "file:../../../tui",
|
|
23
|
+
"zod": "4.5.4"
|
|
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,141 @@
|
|
|
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
|
+
list_directory: 'approved',
|
|
122
|
+
glob: 'approved',
|
|
123
|
+
read_resource: 'approved',
|
|
124
|
+
write_file: 'user-approval',
|
|
125
|
+
edit_file: 'user-approval',
|
|
126
|
+
run_command: 'user-approval',
|
|
127
|
+
},
|
|
128
|
+
toolApprovalSecret: await persistentApprovalSecret(stateDirectory),
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
const managedHarness = {
|
|
132
|
+
...harness,
|
|
133
|
+
async close(closeOptions?: Parameters<typeof harness.close>[0]) {
|
|
134
|
+
const result = await harness.close(closeOptions);
|
|
135
|
+
await observability.close();
|
|
136
|
+
await sqlite.close();
|
|
137
|
+
return result;
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
return { harness: managedHarness, conversations: sqlite.conversations };
|
|
141
|
+
}
|
|
@@ -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
|
+
}
|