glove-foundry 0.0.0 → 0.2.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.
Files changed (35) hide show
  1. package/dist/{chunk-UNHGCCSA.js → chunk-P65RT7H5.js} +486 -49
  2. package/dist/cli.js +536 -352
  3. package/dist/index.js +1 -1
  4. package/docs/building-with-foundry.md +39 -2
  5. package/docs/inspector.md +35 -2
  6. package/package.json +7 -6
  7. package/templates/minimal/README.md +58 -0
  8. package/templates/minimal/agents/assistant/agent.ts +28 -0
  9. package/templates/minimal/agents/assistant/tools/current-time.tool.ts +17 -0
  10. package/templates/minimal/env.example +2 -0
  11. package/templates/minimal/foundry.application.ts +12 -0
  12. package/templates/minimal/foundry.config.ts +6 -0
  13. package/templates/minimal/gitignore +3 -0
  14. package/templates/travel-concierge/README.md +203 -0
  15. package/templates/travel-concierge/agents/concierge/actions/reply.action.ts +6 -0
  16. package/templates/travel-concierge/agents/concierge/agent.ts +106 -0
  17. package/templates/travel-concierge/agents/concierge/apps/calendar.app.ts +67 -0
  18. package/templates/travel-concierge/agents/concierge/composition.ts +18 -0
  19. package/templates/travel-concierge/agents/concierge/events/message-received.event.ts +7 -0
  20. package/templates/travel-concierge/agents/concierge/events/message-sent.event.ts +7 -0
  21. package/templates/travel-concierge/agents/concierge/layers/trip-context.layer.ts +23 -0
  22. package/templates/travel-concierge/agents/concierge/memory/traveller.memory.ts +19 -0
  23. package/templates/travel-concierge/agents/concierge/predicates/mentions-trip.predicate.ts +17 -0
  24. package/templates/travel-concierge/agents/concierge/schedules/trip-countdown.ts +16 -0
  25. package/templates/travel-concierge/agents/concierge/subscribers/usage.subscriber.ts +16 -0
  26. package/templates/travel-concierge/agents/concierge/tools/find-flights.tool.ts +49 -0
  27. package/templates/travel-concierge/agents/concierge/topology.ts +35 -0
  28. package/templates/travel-concierge/agents/concierge/transmissions/messaging.transmission.ts +57 -0
  29. package/templates/travel-concierge/agents/concierge/workbench.ts +26 -0
  30. package/templates/travel-concierge/env.example +3 -0
  31. package/templates/travel-concierge/foundry.application.ts +18 -0
  32. package/templates/travel-concierge/foundry.config.ts +9 -0
  33. package/templates/travel-concierge/gitignore +3 -0
  34. package/templates/travel-concierge/lib/demo-model.ts +72 -0
  35. package/templates/travel-concierge/src/client.ts +22 -0
package/dist/cli.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  FoundryRuntime,
4
4
  FoundryServer,
5
5
  writeGeneratedTypes
6
- } from "./chunk-UNHGCCSA.js";
6
+ } from "./chunk-P65RT7H5.js";
7
7
  import {
8
8
  DEFAULT_FOUNDRY_CONFIG
9
9
  } from "./chunk-ZFNMFE3T.js";
@@ -16,8 +16,8 @@ import {
16
16
  import { watch } from "node:fs";
17
17
  import { access as access2 } from "node:fs/promises";
18
18
  import { spawn } from "node:child_process";
19
- import { relative, resolve as resolve2, sep } from "node:path";
20
- import { fileURLToPath, pathToFileURL } from "node:url";
19
+ import { relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
20
+ import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
21
21
  import process2 from "node:process";
22
22
 
23
23
  // src/env.ts
@@ -52,8 +52,97 @@ async function loadEnvFile(path) {
52
52
  }
53
53
 
54
54
  // src/scaffold.ts
55
- import { access, mkdir, readdir, writeFile } from "node:fs/promises";
56
- import { basename, dirname, resolve } from "node:path";
55
+ import { access, mkdir, readFile as readFile3, readdir, writeFile } from "node:fs/promises";
56
+ import { basename, dirname as dirname2, join, relative, resolve as resolve2, sep } from "node:path";
57
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
58
+
59
+ // src/scaffold-versions.ts
60
+ import { readFile as readFile2 } from "node:fs/promises";
61
+ import { dirname, resolve } from "node:path";
62
+ import { fileURLToPath } from "node:url";
63
+ var TEMPLATE_DEPENDENCIES = [
64
+ "effect",
65
+ "glove-core",
66
+ "glove-js",
67
+ "glove-lisp",
68
+ "glove-mcp",
69
+ "glove-memory",
70
+ "glove-python",
71
+ "glove-working-environment",
72
+ "zod"
73
+ ];
74
+ var FALLBACK_RANGES = Object.freeze({
75
+ effect: "^3.22.1",
76
+ "glove-core": "^3.6.0",
77
+ "glove-js": "^0.4.0",
78
+ "glove-lisp": "^0.4.0",
79
+ "glove-mcp": "^1.1.0",
80
+ "glove-memory": "^1.1.0",
81
+ "glove-python": "^0.3.0",
82
+ "glove-working-environment": "^0.6.0",
83
+ zod: "^4.3.6"
84
+ });
85
+ var FALLBACK_FOUNDRY_RANGE = "^0.1.0";
86
+ function toRange(value) {
87
+ if (typeof value !== "string" || value.length === 0) return null;
88
+ if (value.startsWith("workspace:") || value.startsWith("link:") || value.startsWith("file:")) {
89
+ return null;
90
+ }
91
+ if (/^[\^~><=]|^\d+\.x|\|\||\s-\s/.test(value)) return value;
92
+ return `^${value}`;
93
+ }
94
+ async function readOwnManifest() {
95
+ const path = resolve(dirname(fileURLToPath(import.meta.url)), "../package.json");
96
+ try {
97
+ return JSON.parse(await readFile2(path, "utf8"));
98
+ } catch {
99
+ return null;
100
+ }
101
+ }
102
+ async function resolveTemplateVersions() {
103
+ const manifest = await readOwnManifest();
104
+ const declared = manifest?.dependencies ?? {};
105
+ const dependencies = { ...FALLBACK_RANGES };
106
+ const fellBack = [];
107
+ for (const name of TEMPLATE_DEPENDENCIES) {
108
+ const range = toRange(declared[name]);
109
+ if (range) dependencies[name] = range;
110
+ else fellBack.push(name);
111
+ }
112
+ const own = toRange(manifest?.version);
113
+ const foundry = own && own !== "^0.0.0" ? own : FALLBACK_FOUNDRY_RANGE;
114
+ return Object.freeze({
115
+ foundry,
116
+ dependencies: Object.freeze(dependencies),
117
+ resolved: fellBack.length === 0,
118
+ fellBack: Object.freeze(fellBack)
119
+ });
120
+ }
121
+
122
+ // src/scaffold.ts
123
+ var FOUNDRY_TEMPLATES = [
124
+ "travel-concierge",
125
+ "minimal"
126
+ ];
127
+ var TEMPLATE_IMPORTS = Object.freeze({
128
+ "travel-concierge": [
129
+ "effect",
130
+ "glove-core",
131
+ "glove-js",
132
+ "glove-memory",
133
+ "glove-working-environment",
134
+ "zod"
135
+ ],
136
+ minimal: ["effect", "glove-core", "zod"]
137
+ });
138
+ var TEMPLATE_AGENT_ROUTE = Object.freeze({
139
+ "travel-concierge": "concierge",
140
+ minimal: "assistant"
141
+ });
142
+ var RENAMED_ON_COPY = Object.freeze({
143
+ gitignore: ".gitignore",
144
+ "env.example": ".env.example"
145
+ });
57
146
  async function exists(path) {
58
147
  try {
59
148
  await access(path);
@@ -62,18 +151,172 @@ async function exists(path) {
62
151
  return false;
63
152
  }
64
153
  }
154
+ function templatesRoot() {
155
+ return resolve2(dirname2(fileURLToPath2(import.meta.url)), "../templates");
156
+ }
157
+ async function walk(root, prefix = "") {
158
+ const entries = await readdir(resolve2(root, prefix), { withFileTypes: true });
159
+ const files = [];
160
+ for (const entry of entries) {
161
+ const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
162
+ if (entry.isDirectory()) files.push(...await walk(root, relativePath));
163
+ else files.push(relativePath);
164
+ }
165
+ return files.sort();
166
+ }
167
+ function projectNameFrom(rootDir) {
168
+ return basename(rootDir).toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-|-$/g, "") || "glove-foundry-app";
169
+ }
170
+ function runner(packageManager, script) {
171
+ if (packageManager === "npm") return `npm run ${script}`;
172
+ return `${packageManager} ${script}`;
173
+ }
174
+ function installCommand(packageManager) {
175
+ return packageManager === "yarn" ? "yarn" : `${packageManager} install`;
176
+ }
177
+ async function detectScaffoldTarget(rootDir) {
178
+ for (const config of [
179
+ "next.config.js",
180
+ "next.config.mjs",
181
+ "next.config.ts",
182
+ "next.config.cjs"
183
+ ]) {
184
+ if (await exists(resolve2(rootDir, config))) return "nextjs";
185
+ }
186
+ try {
187
+ const manifest = JSON.parse(
188
+ await readFile3(resolve2(rootDir, "package.json"), "utf8")
189
+ );
190
+ if (manifest.dependencies?.next ?? manifest.devDependencies?.next) return "nextjs";
191
+ } catch {
192
+ }
193
+ return "standalone";
194
+ }
195
+ async function detectPackageManager(rootDir) {
196
+ if (await exists(resolve2(rootDir, "pnpm-lock.yaml"))) return "pnpm";
197
+ if (await exists(resolve2(rootDir, "yarn.lock"))) return "yarn";
198
+ if (await exists(resolve2(rootDir, "bun.lockb"))) return "bun";
199
+ if (await exists(resolve2(rootDir, "package-lock.json"))) return "npm";
200
+ const agent = process.env.npm_config_user_agent ?? "";
201
+ if (agent.startsWith("yarn")) return "yarn";
202
+ if (agent.startsWith("bun")) return "bun";
203
+ if (agent.startsWith("npm")) return "npm";
204
+ return "pnpm";
205
+ }
206
+ function fill(content, values) {
207
+ return content.replace(
208
+ /\{\{(\w+)\}\}/g,
209
+ (match, key) => Object.hasOwn(values, key) ? values[key] : match
210
+ );
211
+ }
212
+ function dependenciesFor(template, versions) {
213
+ const dependencies = { "glove-foundry": versions.foundry };
214
+ for (const name of TEMPLATE_IMPORTS[template]) {
215
+ dependencies[name] = versions.dependencies[name];
216
+ }
217
+ return Object.fromEntries(
218
+ Object.entries(dependencies).sort(([a], [b]) => a.localeCompare(b))
219
+ );
220
+ }
221
+ var DEV_DEPENDENCIES = Object.freeze({
222
+ "@types/node": "^25.2.3",
223
+ eslint: "^9.39.2",
224
+ typescript: "^5.9.3",
225
+ "typescript-eslint": "^8.54.0"
226
+ });
227
+ function routesDeclaration(agentsDir, route) {
228
+ return `// Generated by Glove Foundry. Do not edit.
229
+ export type FoundryRoutes = {
230
+ readonly "${route}": typeof import("../${agentsDir}/${route}/agent.js").default;
231
+ };
232
+ `;
233
+ }
65
234
  async function scaffoldFoundryProject(options) {
66
- const rootDir = resolve(options.directory);
67
- if (await exists(rootDir)) {
235
+ const rootDir = resolve2(options.directory);
236
+ const template = options.template ?? "travel-concierge";
237
+ if (!FOUNDRY_TEMPLATES.includes(template)) {
238
+ throw new Error(
239
+ `Unknown Foundry template "${template}". Available: ${FOUNDRY_TEMPLATES.join(", ")}.`
240
+ );
241
+ }
242
+ const templateDir = resolve2(templatesRoot(), template);
243
+ if (!await exists(templateDir)) {
244
+ throw new Error(`Foundry template "${template}" is missing from this install.`);
245
+ }
246
+ const target = options.target ?? await detectScaffoldTarget(rootDir);
247
+ const packageManager = options.packageManager ?? await detectPackageManager(rootDir);
248
+ if (target === "standalone" && await exists(rootDir)) {
68
249
  const entries = await readdir(rootDir);
69
250
  if (entries.length > 0) {
70
- throw new Error(`Cannot scaffold into non-empty directory ${rootDir}.`);
251
+ throw new Error(
252
+ `Cannot scaffold into non-empty directory ${rootDir}. Pass --target nextjs to add Foundry to an existing project.`
253
+ );
71
254
  }
72
255
  }
73
256
  await mkdir(rootDir, { recursive: true });
74
- const projectName = basename(rootDir).toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-|-$/g, "") || "glove-foundry-app";
75
- const files = /* @__PURE__ */ new Map([
76
- [
257
+ const versions = await resolveTemplateVersions();
258
+ const projectName = projectNameFrom(rootDir);
259
+ const agentRoute = TEMPLATE_AGENT_ROUTE[template];
260
+ const foundryDir = target === "nextjs" ? "foundry" : "";
261
+ const agentsDir = foundryDir ? `${foundryDir}/agents` : "agents";
262
+ const devScript = target === "nextjs" ? "foundry:dev" : "dev";
263
+ const startScript = target === "nextjs" ? "foundry:start" : "start";
264
+ const placeholders = {
265
+ projectName,
266
+ installCommand: installCommand(packageManager),
267
+ devCommand: runner(packageManager, devScript),
268
+ startCommand: runner(packageManager, startScript),
269
+ lintCommand: runner(packageManager, "lint"),
270
+ typecheckCommand: runner(packageManager, "typecheck")
271
+ };
272
+ const files = /* @__PURE__ */ new Map();
273
+ for (const relativePath of await walk(templateDir)) {
274
+ const source = await readFile3(resolve2(templateDir, relativePath), "utf8");
275
+ const name = basename(relativePath);
276
+ const renamed = RENAMED_ON_COPY[name];
277
+ let destination = renamed ? join(dirname2(relativePath), renamed).split(sep).join("/") : relativePath;
278
+ if (target === "nextjs") {
279
+ if (destination === "README.md" || destination === ".gitignore") continue;
280
+ if (destination.startsWith("src/")) continue;
281
+ if (destination === "foundry.config.ts") continue;
282
+ if (destination === ".env.example") destination = "foundry.env.example";
283
+ else destination = `${foundryDir}/${destination}`;
284
+ }
285
+ files.set(destination, fill(source, placeholders));
286
+ }
287
+ if (target === "nextjs") {
288
+ files.set(
289
+ "foundry.config.mts",
290
+ `import { defineConfig } from "glove-foundry/config";
291
+
292
+ export default defineConfig({
293
+ agentsDir: "${agentsDir}",
294
+ applicationFile: "${foundryDir}/foundry.application.ts",
295
+ server: { port: 4141 },
296
+ execution: { pollIntervalMs: 100, maxConcurrent: 4 },
297
+ });
298
+ `
299
+ );
300
+ files.set(
301
+ `${foundryDir}/package.json`,
302
+ `${JSON.stringify(
303
+ {
304
+ name: `${projectName}-foundry`,
305
+ private: true,
306
+ // Foundry and the Glove packages are ESM. This scopes that to the
307
+ // agents, so the surrounding app keeps its own module system.
308
+ type: "module"
309
+ },
310
+ null,
311
+ 2
312
+ )}
313
+ `
314
+ );
315
+ }
316
+ files.set(".foundry/routes.d.ts", routesDeclaration(agentsDir, agentRoute));
317
+ const dependencies = dependenciesFor(template, versions);
318
+ if (target === "standalone") {
319
+ files.set(
77
320
  "package.json",
78
321
  `${JSON.stringify(
79
322
  {
@@ -87,29 +330,15 @@ async function scaffoldFoundryProject(options) {
87
330
  lint: "eslint .",
88
331
  typecheck: "tsc --noEmit"
89
332
  },
90
- dependencies: {
91
- effect: "^3.22.1",
92
- "glove-core": "^3.5.0",
93
- "glove-foundry": "^0.1.0",
94
- "glove-js": "^0.3.0",
95
- "glove-mcp": "^1.0.1",
96
- "glove-memory": "^1.0.2",
97
- "glove-working-environment": "^0.5.0",
98
- zod: "^4.3.6"
99
- },
100
- devDependencies: {
101
- "@types/node": "^25.2.3",
102
- eslint: "^9.39.2",
103
- "typescript-eslint": "^8.54.0",
104
- typescript: "^5.9.3"
105
- }
333
+ dependencies,
334
+ devDependencies: DEV_DEPENDENCIES
106
335
  },
107
336
  null,
108
337
  2
109
338
  )}
110
339
  `
111
- ],
112
- [
340
+ );
341
+ files.set(
113
342
  "tsconfig.json",
114
343
  `${JSON.stringify(
115
344
  {
@@ -122,324 +351,207 @@ async function scaffoldFoundryProject(options) {
122
351
  noEmit: true,
123
352
  skipLibCheck: true
124
353
  },
125
- include: ["agents", "src", "foundry.application.ts", "foundry.config.ts", ".foundry/routes.d.ts"]
354
+ include: [
355
+ "agents",
356
+ "lib",
357
+ "src",
358
+ "foundry.application.ts",
359
+ "foundry.config.ts",
360
+ ".foundry/routes.d.ts"
361
+ ]
126
362
  },
127
363
  null,
128
364
  2
129
365
  )}
130
366
  `
131
- ],
132
- [
133
- "foundry.config.ts",
134
- `import { defineConfig } from "glove-foundry/config";
135
-
136
- export default defineConfig({
137
- server: { port: 4141 },
138
- execution: {
139
- pollIntervalMs: 100,
140
- maxConcurrent: 4,
141
- },
142
- });
143
- `
144
- ],
145
- [
146
- "agents/assistant/composition.ts",
147
- `import { composeAgent } from "glove-foundry";
148
- import notes from "./apps/notes.app.js";
149
- import requestContext from "./layers/request-context.layer.js";
150
- import notion from "./mcp/notion.mcp.js";
151
- import personalMemory from "./memory/personal.memory.js";
152
- import usage from "./subscribers/usage.subscriber.js";
153
- import currentTime from "./tools/current-time.tool.js";
154
-
155
- export const components = composeAgent(
156
- notes,
157
- requestContext,
158
- notion,
159
- personalMemory,
160
- usage,
161
- currentTime,
162
- );
163
- `
164
- ],
165
- [
166
- "agents/assistant/agent.ts",
167
- `import { MemoryStore } from "glove-core";
168
- import { createAdapter } from "glove-core/models/providers";
169
- import { defineAgent } from "glove-foundry";
170
- import { components } from "./composition.js";
171
- import { loadDefaultInbox } from "./inboxes/default.inbox.js";
172
- import requestContext from "./layers/request-context.layer.js";
173
- import personalMemory from "./memory/personal.memory.js";
174
- import usage from "./subscribers/usage.subscriber.js";
175
- import { assistantRepl, assistantWorkspace } from "./workbench.js";
176
-
177
- export default defineAgent({
178
- description: "A general-purpose Glove assistant",
179
- components,
180
- memory: [personalMemory],
181
- inboxes: (_agent, context) => loadDefaultInbox(context),
182
- workingEnvironment: assistantWorkspace,
183
- repl: (_agent, context) => assistantRepl(context.agentId, context.messageText),
184
- store: ({ conversationId }) => new MemoryStore(\`foundry:\${conversationId}\`),
185
- model: createAdapter({
186
- provider: "openrouter",
187
- model: process.env.OPENROUTER_MODEL ?? "openai/gpt-4.1-mini",
188
- stream: true,
189
- }),
190
- systemPrompt: (_agent, { message, history }) =>
191
- [
192
- "You are a precise, practical assistant.",
193
- \`Current request: \${message.text}\`,
194
- \`Prior messages: \${history.length}\`,
195
- ].join("\\n"),
196
- compactionInstructions: () =>
197
- "Preserve decisions, open work, and important context.",
198
- compactionLimit: (_agent, { messageText }) =>
199
- messageText.length > 2_000 ? 80_000 : 40_000,
200
- layers: [requestContext],
201
- subscribers: [usage],
202
- });
203
- `
204
- ],
205
- [
206
- "agents/assistant/workbench.ts",
207
- `import { JsSession, defineFn } from "glove-js";
208
- import { defineRepl, defineWorkingEnvironment } from "glove-foundry";
209
- import { z } from "zod";
210
-
211
- export const assistantWorkspace = defineWorkingEnvironment({
212
- options: { limits: { maxVfsBytes: 32 * 1024 * 1024 } },
213
- });
214
-
215
- export function assistantRepl(actor: string, message: string) {
216
- const session = JsSession.create({ actor });
217
- session.register(defineFn({
218
- name: "request__current",
219
- description: "Read the current request inside the REPL",
220
- input: z.object({}),
221
- readOnlyHint: true,
222
- handler: () => ({ text: message, length: message.length }),
223
- }));
224
- return defineRepl({ language: "javascript", session });
367
+ );
368
+ files.set(
369
+ "eslint.config.js",
370
+ 'import tseslint from "typescript-eslint";\nimport foundry from "glove-foundry/eslint";\n\nexport default [...tseslint.configs.recommended, foundry];\n'
371
+ );
372
+ } else {
373
+ const [clientPath, clientSource] = nextClientModule(agentRoute, foundryDir);
374
+ files.set(clientPath, clientSource);
375
+ const [handlerPath, handlerSource] = nextRouteHandler(agentRoute);
376
+ files.set(handlerPath, handlerSource);
377
+ files.set(`${foundryDir}/README.md`, nextReadme(projectName, agentRoute, placeholders));
378
+ }
379
+ for (const [relativePath, content] of files) {
380
+ const path = resolve2(rootDir, relativePath);
381
+ await mkdir(dirname2(path), { recursive: true });
382
+ if (target === "nextjs" && await exists(path)) continue;
383
+ await writeFile(path, content, { encoding: "utf8", flag: "wx" });
384
+ }
385
+ if (target === "nextjs") {
386
+ await mergeNextManifest(rootDir, dependencies, files);
387
+ }
388
+ return {
389
+ rootDir,
390
+ files: [...files.keys()].sort(),
391
+ template,
392
+ target,
393
+ packageManager,
394
+ agentsDir,
395
+ versions,
396
+ agentRoute
397
+ };
225
398
  }
226
- `
227
- ],
228
- [
229
- "agents/assistant/layers/request-context.layer.ts",
230
- `import { Effect } from "effect";
231
- import { defineLayer } from "glove-foundry";
232
-
233
- const requestContext = defineLayer({
234
- description: "Expose Foundry request identity as a native Glove skill",
235
- setup: ({ glove, agentId, runId, message, history }) => Effect.sync(() => {
236
- glove.defineSkill({
237
- name: "request-context",
238
- description: "Read the current Foundry agent and run ids",
239
- exposeToAgent: true,
240
- async handler() { return \`agent=\${agentId} run=\${runId} message=\${message.text} prior=\${history.length}\`; },
241
- });
242
- }),
243
- });
244
-
245
- export default requestContext;
246
- `
247
- ],
248
- [
249
- "agents/assistant/subscribers/usage.subscriber.ts",
250
- `import { defineSubscriber } from "glove-foundry";
251
-
252
- const usage = defineSubscriber({
253
- description: "Observe token consumption without changing the agent",
254
- create: {
255
- async record(type, data) {
256
- if (type === "token_consumption") console.log("[tokens]", data);
257
- },
258
- },
259
- });
260
-
261
- export default usage;
262
- `
263
- ],
264
- [
265
- "foundry.application.ts",
266
- `import { MemoryFoundryDataAdapter, defineApplication } from "glove-foundry";
267
-
268
- export const data = new MemoryFoundryDataAdapter();
399
+ function nextClientModule(route, foundryDir) {
400
+ return [
401
+ "lib/foundry.ts",
402
+ `/**
403
+ * The Next.js app talks to the Foundry runtime over HTTP, so nothing from
404
+ * the agent graph is bundled into your app. \`FoundryRoutes\` is a type-only
405
+ * import, erased at build time, and it is regenerated on every dev run \u2014 so
406
+ * agent("${route}") is checked against the files that actually exist.
407
+ */
408
+ import { createFoundryClient } from "glove-foundry/client";
409
+ import type { FoundryRoutes } from "../.foundry/routes.js";
269
410
 
270
- export default defineApplication({
271
- name: "${projectName}",
272
- data,
273
- accounts: [],
274
- routes: [],
275
- bindings: [],
411
+ export const foundry = createFoundryClient<FoundryRoutes>({
412
+ baseUrl: process.env.FOUNDRY_URL ?? "http://127.0.0.1:4141",
276
413
  });
277
- `
278
- ],
279
- [
280
- "agents/assistant/tools/current-time.tool.ts",
281
- `import { defineSharedTool } from "glove-foundry";
282
- import { z } from "zod";
283
414
 
284
- const currentTime = defineSharedTool({
285
- description: "Return the current ISO timestamp",
286
- tool: {
287
- name: "current_time",
288
- description: "Return the current ISO timestamp",
289
- inputSchema: z.object({}),
290
- async do() {
291
- return { status: "success", data: new Date().toISOString() };
292
- },
293
- },
294
- });
295
415
 
296
- export default currentTime;
416
+ // The agents live in ${foundryDir}/agents. Start the runtime with
417
+ // \`foundry:dev\`; it serves the inspector at http://127.0.0.1:4141.
297
418
  `
298
- ],
299
- [
300
- "agents/assistant/apps/notes.app.ts",
301
- `import { Effect } from "effect";
302
- import { defineApp } from "glove-foundry";
303
- import { z } from "zod";
304
-
305
- const notes = defineApp({
306
- description: "An example application installed only when selected",
307
- config: z.object({ namespace: z.string().default("default") }),
308
- inbound: [],
309
- outbound: [],
310
- install: ({ config }) => Effect.sync(() => {
311
- return { tools: [{
312
- name: "notes_namespace",
313
- description: "Return the installed notes namespace",
314
- inputSchema: z.object({}),
315
- async do() { return { status: "success", data: config.namespace }; },
316
- }] };
317
- }),
318
- });
319
-
320
- export default notes;
321
- `
322
- ],
323
- [
324
- "agents/assistant/inboxes/default.inbox.ts",
325
- `import type { InboxItem } from "glove-core";
326
- import type { AgentAssemblyContext } from "glove-foundry";
327
-
328
- export async function loadDefaultInbox(
329
- context: AgentAssemblyContext,
330
- ): Promise<ReadonlyArray<InboxItem>> {
331
- // Load this conversation's inbox from your own adapter or service.
332
- void \`\${context.agentId}:\${context.conversationId}\`;
333
- return [];
419
+ ];
334
420
  }
335
- `
336
- ],
337
- [
338
- "agents/assistant/mcp/notion.mcp.ts",
339
- `import { defineMcp } from "glove-foundry";
340
-
341
- // This remains disconnected until an agent instance installs it.
342
- const notion = defineMcp({
343
- entry: {
344
- name: "Notion",
345
- description: "Search and update a Notion workspace",
346
- url: "https://mcp.notion.com/mcp",
347
- tags: ["notes", "workspace"],
348
- },
349
- });
350
-
351
- export default notion;
352
- `
353
- ],
354
- [
355
- "agents/assistant/memory/personal.memory.ts",
356
- `import { Effect } from "effect";
357
- import { defineMemory } from "glove-foundry";
358
- import { MemorySchema } from "glove-memory/core";
359
- import { InMemoryContextAdapter } from "glove-memory/in-memory";
360
-
361
- const schema = new MemorySchema();
362
- const context = new InMemoryContextAdapter({ schema });
363
-
364
- const personalMemory = defineMemory({
365
- description: "Ambient user context backed by a replaceable memory adapter",
366
- context: { adapter: () => Effect.succeed(context) },
367
- });
368
-
369
- export default personalMemory;
370
- `
371
- ],
372
- [
373
- "src/client.ts",
374
- `import { createFoundryClient } from "glove-foundry/client";
375
- import type { FoundryRoutes } from "../.foundry/routes.js";
376
-
377
- export const foundry = createFoundryClient<FoundryRoutes>();
378
-
379
- const agent = await foundry.agent("assistant").create({ workspaceId: "default" });
380
- const conversation = await foundry.createConversation(agent.id);
381
- const run = await foundry.send(agent.id, conversation.id, "Hello");
382
- const completed = await run.wait();
383
- console.log(completed.output);
384
- `
385
- ],
386
- [
387
- ".foundry/routes.d.ts",
388
- `// Generated by Glove Foundry. Do not edit.
389
- export type FoundryRoutes = {
390
- readonly "assistant": typeof import("../agents/assistant/agent.js").default;
391
- };
392
- `
393
- ],
394
- [".env.example", "OPENROUTER_API_KEY=\nOPENROUTER_MODEL=openai/gpt-4.1-mini\n"],
395
- [
396
- "eslint.config.js",
397
- `import tseslint from "typescript-eslint";
398
- import foundry from "glove-foundry/eslint";
399
-
400
- export default [...tseslint.configs.recommended, foundry];
401
- `
402
- ],
403
- [".gitignore", "node_modules\n.env.local\n.foundry/manifest.json\n"],
404
- [
405
- "README.md",
406
- `# ${projectName}
407
-
408
- A file-routed Glove Foundry application.
421
+ function nextRouteHandler(route) {
422
+ return [
423
+ `app/api/${route}/route.ts`,
424
+ `/**
425
+ * POST /api/${route} \u2014 send a message to the agent and wait for its result.
426
+ *
427
+ * This runs on the server, so the Foundry runtime never has to be public.
428
+ * For a streaming UI, subscribe to the runtime's /api/events instead of
429
+ * awaiting run.wait().
430
+ */
431
+ import { foundry } from "../../../lib/foundry.js";
432
+
433
+ export async function POST(request: Request) {
434
+ const { message, workspaceId = "default" } = (await request.json()) as {
435
+ message: string;
436
+ workspaceId?: string;
437
+ };
409
438
 
410
- 1. Copy \`.env.example\` to \`.env.local\` and add your API key.
411
- 2. Install dependencies with \`pnpm install\`.
412
- 3. Run \`pnpm dev\`.
413
- 4. Open http://127.0.0.1:4141.
439
+ // A real app reuses one instance per user rather than creating one per call.
440
+ const agent = await foundry.agent("${route}").create({ workspaceId });
441
+ const conversation = await foundry.createConversation(agent.id);
442
+ const run = await foundry.send(agent.id, conversation.id, message);
443
+ const completed = await run.wait();
414
444
 
415
- Add agents under \`agents/<route>/agent.ts\`; the file path is the route. Keep that agent's applications, transmissions, tools, MCPs, memory, inboxes, layers, subscribers, and workbench beside it, then combine reusable capabilities with \`composeAgent\`. Code-authored references use imported definition values; Foundry normalizes them to ids only when instance data is persisted. Applications and MCPs remain inert until an agent instance installs them. The scaffold mounts a sandboxed VFS/script environment and a request-aware JavaScript REPL; change or remove them in \`workbench.ts\`. Agents create future and recurring work only through Foundry's scheduling and sleep tools.
445
+ return Response.json({ runId: run.id, output: completed.output });
446
+ }
416
447
  `
417
- ]
418
- ]);
419
- for (const [relativePath, content] of files) {
420
- const path = resolve(rootDir, relativePath);
421
- await mkdir(dirname(path), { recursive: true });
422
- await writeFile(path, content, { encoding: "utf8", flag: "wx" });
448
+ ];
449
+ }
450
+ function nextReadme(projectName, route, placeholders) {
451
+ return [
452
+ `# Foundry agents for ${projectName}`,
453
+ "",
454
+ "The agents live here; the Next.js app calls them over HTTP through",
455
+ "`lib/foundry.ts`. Nothing in this folder is bundled into your app.",
456
+ "",
457
+ "## Running",
458
+ "",
459
+ "Two processes, because they have different lifecycles \u2014 the runtime keeps",
460
+ "durable state and should not restart when a React component changes:",
461
+ "",
462
+ "```bash",
463
+ placeholders.devCommand + " # Foundry runtime + inspector on :4141",
464
+ placeholders.installCommand.split(" ")[0] + " dev # Next.js on :3000",
465
+ "```",
466
+ "",
467
+ "Then POST to your own route:",
468
+ "",
469
+ "```bash",
470
+ `curl -X POST http://localhost:3000/api/${route} \\`,
471
+ " -H 'content-type: application/json' \\",
472
+ ` -d '{"message":"I want to fly from Lisbon to Nairobi in April."}'`,
473
+ "```",
474
+ "",
475
+ "## Layout",
476
+ "",
477
+ "| Path | What it is |",
478
+ "| --- | --- |",
479
+ `| \`foundry/agents/${route}/agent.ts\` | The agent. The file path is its id. |`,
480
+ "| `foundry/foundry.application.ts` | Data adapter, accounts, routes |",
481
+ "| `foundry.config.ts` | Points the runtime at `foundry/agents` |",
482
+ "| `lib/foundry.ts` | Typed HTTP client for your app to import |",
483
+ `| \`app/api/${route}/route.ts\` | An example Next.js route handler |`,
484
+ "| `.foundry/routes.d.ts` | Generated. Do not edit |",
485
+ "",
486
+ "## Deploying",
487
+ "",
488
+ "The runtime is a normal Node process. Deploy it wherever you run servers,",
489
+ "set `FOUNDRY_URL` in your Next.js environment to point at it, and keep it",
490
+ "private to your network \u2014 the inspector is a development surface.",
491
+ "",
492
+ "See the [full Foundry guide](https://github.com/porkytheblack/glove/blob/main/packages/glove-foundry/docs/building-with-foundry.md).",
493
+ ""
494
+ ].join("\n");
495
+ }
496
+ async function mergeNextManifest(rootDir, dependencies, files) {
497
+ const path = resolve2(rootDir, "package.json");
498
+ let manifest = {};
499
+ if (await exists(path)) {
500
+ manifest = JSON.parse(await readFile3(path, "utf8"));
423
501
  }
424
- return { rootDir, files: [...files.keys()] };
502
+ const scripts = { ...manifest.scripts };
503
+ scripts["foundry:dev"] ??= "glove foundry dev";
504
+ scripts["foundry:start"] ??= "glove foundry start";
505
+ const merged = {
506
+ ...manifest,
507
+ scripts,
508
+ dependencies: Object.fromEntries(
509
+ Object.entries({
510
+ ...manifest.dependencies,
511
+ ...dependencies
512
+ }).sort(([a], [b]) => a.localeCompare(b))
513
+ )
514
+ };
515
+ await writeFile(path, `${JSON.stringify(merged, null, 2)}
516
+ `, "utf8");
517
+ files.set("package.json", "(merged)");
518
+ }
519
+ function displayPath(rootDir) {
520
+ return relative(process.cwd(), rootDir) || ".";
425
521
  }
426
522
 
427
523
  // src/cli.ts
428
524
  var HELP = `Glove Foundry \u2014 a file-routed runtime for agents
429
525
 
430
526
  Usage:
431
- glove foundry [directory] Create a Foundry application
432
- glove foundry dev [options] Run the development runtime and DevTools
527
+ glove foundry init [directory] Create a Foundry application
528
+ glove foundry dev [options] Run the development runtime and inspector
433
529
  glove foundry start [options] Run without file watching
434
- glove-foundry init [directory] Create a Foundry application
435
- glove-foundry dev [options] Run the development runtime and DevTools
436
- glove-foundry start [options] Run without file watching
437
530
 
438
- Options:
531
+ The bare form still works: glove foundry my-app
532
+
533
+ Create options:
534
+ --template <name> travel-concierge (default) or minimal
535
+ travel-concierge a worked example: flights, a
536
+ calendar app, chat transport,
537
+ memory, a schedule, a REPL
538
+ minimal one agent and one tool
539
+ --target <name> standalone (default) or nextjs
540
+ Detected automatically: a directory holding a
541
+ Next.js app gets the nextjs target, which adds
542
+ agents under foundry/ and leaves the app alone.
543
+ --package-manager <name> pnpm, npm, yarn, or bun (detected by lockfile)
544
+
545
+ Run options:
439
546
  --root <directory> Project root (default: current directory)
440
547
  --port <number> Override the configured port
441
548
  --host <address> Override the configured host
442
549
  --no-watch Disable agent/config hot restart
550
+
551
+ Examples:
552
+ glove foundry init my-agents
553
+ glove foundry init my-agents --template minimal
554
+ glove foundry init . --target nextjs Add agents to the app in this directory
443
555
  `;
444
556
  function parseArgs(raw) {
445
557
  const args = [...raw];
@@ -476,7 +588,7 @@ async function pathExists(path) {
476
588
  async function loadConfig(rootDir) {
477
589
  const candidates = ["foundry.config.ts", "foundry.config.mts", "foundry.config.js", "foundry.config.mjs"];
478
590
  for (const candidate of candidates) {
479
- const path = resolve2(rootDir, candidate);
591
+ const path = resolve3(rootDir, candidate);
480
592
  if (!await pathExists(path)) continue;
481
593
  const url = pathToFileURL(path);
482
594
  url.searchParams.set("t", String(Date.now()));
@@ -490,7 +602,7 @@ async function loadConfig(rootDir) {
490
602
  }
491
603
  async function loadApplication(rootDir, config) {
492
604
  const relativePath = config.applicationFile ?? DEFAULT_FOUNDRY_CONFIG.applicationFile;
493
- const path = resolve2(rootDir, relativePath);
605
+ const path = resolve3(rootDir, relativePath);
494
606
  if (!await pathExists(path)) return EMPTY_FOUNDRY_APPLICATION;
495
607
  const url = pathToFileURL(path);
496
608
  url.searchParams.set("t", String(Date.now()));
@@ -501,18 +613,18 @@ async function loadApplication(rootDir, config) {
501
613
  return imported.default;
502
614
  }
503
615
  async function runWorker(parsed) {
504
- const rootDir = resolve2(
616
+ const rootDir = resolve3(
505
617
  typeof parsed.flags.root === "string" ? parsed.flags.root : process2.cwd()
506
618
  );
507
- await loadEnvFile(resolve2(rootDir, ".env"));
508
- await loadEnvFile(resolve2(rootDir, ".env.local"));
619
+ await loadEnvFile(resolve3(rootDir, ".env"));
620
+ await loadEnvFile(resolve3(rootDir, ".env.local"));
509
621
  const config = await loadConfig(rootDir);
510
622
  const application = await loadApplication(rootDir, config);
511
- const applicationFilePath = resolve2(
623
+ const applicationFilePath = resolve3(
512
624
  rootDir,
513
625
  config.applicationFile ?? DEFAULT_FOUNDRY_CONFIG.applicationFile
514
626
  );
515
- const agentsDir = resolve2(
627
+ const agentsDir = resolve3(
516
628
  rootDir,
517
629
  config.agentsDir ?? DEFAULT_FOUNDRY_CONFIG.agentsDir
518
630
  );
@@ -558,16 +670,17 @@ async function runWorker(parsed) {
558
670
  process2.once("SIGTERM", () => void close());
559
671
  process2.once("SIGINT", () => void close());
560
672
  }
561
- function shouldRestart(rootDir, changed) {
673
+ function shouldRestart(rootDir, changed, agentsDir) {
562
674
  if (!changed) return false;
563
- const normalized = relative(rootDir, resolve2(rootDir, changed)).split(sep).join("/");
564
- return normalized.startsWith("agents/") || normalized === ".env" || normalized === ".env.local" || /^foundry\.application\.(?:ts|mts|js|mjs)$/.test(normalized) || /^foundry\.config\.(?:ts|mts|js|mjs)$/.test(normalized);
675
+ const normalized = relative2(rootDir, resolve3(rootDir, changed)).split(sep2).join("/");
676
+ return normalized.startsWith(`${agentsDir}/`) || normalized === ".env" || normalized === ".env.local" || /^(?:[\w.-]+\/)*foundry\.application\.(?:ts|mts|js|mjs)$/.test(normalized) || /^foundry\.config\.(?:ts|mts|js|mjs)$/.test(normalized);
565
677
  }
566
678
  async function superviseDev(parsed) {
567
- const rootDir = resolve2(
679
+ const rootDir = resolve3(
568
680
  typeof parsed.flags.root === "string" ? parsed.flags.root : process2.cwd()
569
681
  );
570
- const cliPath = fileURLToPath(import.meta.url);
682
+ const agentsDir = (await loadConfig(rootDir)).agentsDir ?? DEFAULT_FOUNDRY_CONFIG.agentsDir;
683
+ const cliPath = fileURLToPath3(import.meta.url);
571
684
  const tsxImport = import.meta.resolve("tsx");
572
685
  let child = null;
573
686
  const watchers = [];
@@ -618,7 +731,7 @@ async function superviseDev(parsed) {
618
731
  if (!parsed.flags["no-watch"]) {
619
732
  try {
620
733
  const onChange = (filename) => {
621
- if (!shouldRestart(rootDir, filename?.toString() ?? null)) return;
734
+ if (!shouldRestart(rootDir, filename?.toString() ?? null, agentsDir)) return;
622
735
  if (debounce) clearTimeout(debounce);
623
736
  debounce = setTimeout(restart, 120);
624
737
  };
@@ -629,14 +742,14 @@ async function superviseDev(parsed) {
629
742
  (_event, filename) => onChange(filename?.toString() ?? null)
630
743
  )
631
744
  );
632
- const defaultAgentsDir = resolve2(rootDir, "agents");
633
- if (await pathExists(defaultAgentsDir)) {
745
+ const watchedAgentsDir = resolve3(rootDir, agentsDir);
746
+ if (await pathExists(watchedAgentsDir)) {
634
747
  watchers.push(
635
748
  watch(
636
- defaultAgentsDir,
749
+ watchedAgentsDir,
637
750
  { recursive: true },
638
751
  (_event, filename) => onChange(
639
- filename ? `agents/${filename.toString()}` : "agents"
752
+ filename ? `${agentsDir}/${filename.toString()}` : agentsDir
640
753
  )
641
754
  )
642
755
  );
@@ -648,7 +761,7 @@ async function superviseDev(parsed) {
648
761
  "memory",
649
762
  "inboxes"
650
763
  ]) {
651
- const watched = resolve2(rootDir, directory);
764
+ const watched = resolve3(rootDir, directory);
652
765
  if (await pathExists(watched)) {
653
766
  watchers.push(
654
767
  watch(
@@ -678,7 +791,7 @@ async function superviseDev(parsed) {
678
791
  process2.once("SIGTERM", stop);
679
792
  }
680
793
  async function superviseStart(parsed) {
681
- const rootDir = resolve2(
794
+ const rootDir = resolve3(
682
795
  typeof parsed.flags.root === "string" ? parsed.flags.root : process2.cwd()
683
796
  );
684
797
  const child = spawn(
@@ -686,7 +799,7 @@ async function superviseStart(parsed) {
686
799
  [
687
800
  "--import",
688
801
  import.meta.resolve("tsx"),
689
- fileURLToPath(import.meta.url),
802
+ fileURLToPath3(import.meta.url),
690
803
  "__start-worker",
691
804
  "--root",
692
805
  rootDir,
@@ -716,6 +829,77 @@ async function superviseStart(parsed) {
716
829
  throw new Error(`Foundry worker exited with code ${exitCode}.`);
717
830
  }
718
831
  }
832
+ var UsageError = class extends Error {
833
+ };
834
+ function flagValue(parsed, name, allowed) {
835
+ const raw = parsed.flags[name];
836
+ if (raw === void 0) return void 0;
837
+ if (typeof raw !== "string" || !allowed.includes(raw)) {
838
+ throw new UsageError(
839
+ `--${name} must be one of: ${allowed.join(", ")}.` + (typeof raw === "string" ? ` Received "${raw}".` : "")
840
+ );
841
+ }
842
+ return raw;
843
+ }
844
+ async function create(directory, parsed) {
845
+ const template = flagValue(parsed, "template", FOUNDRY_TEMPLATES);
846
+ const target = flagValue(parsed, "target", ["standalone", "nextjs"]);
847
+ const packageManager = flagValue(
848
+ parsed,
849
+ "package-manager",
850
+ ["pnpm", "npm", "yarn", "bun"]
851
+ );
852
+ const result = await scaffoldFoundryProject({
853
+ directory,
854
+ ...template ? { template } : {},
855
+ ...target ? { target } : {},
856
+ ...packageManager ? { packageManager } : {}
857
+ });
858
+ const where = displayPath(result.rootDir);
859
+ const pm = result.packageManager;
860
+ const run = (script) => pm === "npm" ? `npm run ${script}` : `${pm} ${script}`;
861
+ const install = pm === "yarn" ? "yarn" : `${pm} install`;
862
+ const lines = [""];
863
+ if (result.target === "nextjs") {
864
+ lines.push(` Added Glove Foundry to the app in ${result.rootDir}`);
865
+ lines.push("");
866
+ lines.push(` Agents: ${result.agentsDir}/${result.agentRoute}/agent.ts`);
867
+ lines.push(` Client: lib/foundry.ts`);
868
+ lines.push(` Route: app/api/${result.agentRoute}/route.ts`);
869
+ lines.push(` Guide: foundry/README.md`);
870
+ lines.push("");
871
+ lines.push(" Next:");
872
+ if (where !== ".") lines.push(` cd ${where}`);
873
+ lines.push(` ${install}`);
874
+ lines.push(` ${run("foundry:dev")} # runtime + inspector on :4141`);
875
+ lines.push(` ${run("dev")} # your Next.js app`);
876
+ } else {
877
+ lines.push(` Created a Glove Foundry app in ${result.rootDir}`);
878
+ lines.push("");
879
+ lines.push(` Template: ${result.template}`);
880
+ lines.push(` Agent: ${result.agentsDir}/${result.agentRoute}/agent.ts`);
881
+ lines.push(` Guide: README.md`);
882
+ lines.push("");
883
+ lines.push(" Next:");
884
+ if (where !== ".") lines.push(` cd ${where}`);
885
+ lines.push(" cp .env.example .env.local");
886
+ lines.push(` ${install}`);
887
+ lines.push(` ${run("dev")}`);
888
+ lines.push("");
889
+ lines.push(" Then open http://127.0.0.1:4141 and press Start a run.");
890
+ if (result.template === "travel-concierge") {
891
+ lines.push(" It works without an API key \u2014 a demo model answers until you set one.");
892
+ }
893
+ }
894
+ if (!result.versions.resolved) {
895
+ lines.push("");
896
+ lines.push(" Note: some Glove versions came from built-in defaults rather than");
897
+ lines.push(" this install's own manifest. Check before installing:");
898
+ lines.push(` ${result.versions.fellBack.join(", ")}`);
899
+ }
900
+ lines.push("");
901
+ process2.stdout.write(lines.join("\n"));
902
+ }
719
903
  async function main() {
720
904
  const parsed = parseArgs(process2.argv.slice(2));
721
905
  if (parsed.command === "__dev-worker" || parsed.command === "__start-worker") {
@@ -729,18 +913,7 @@ async function main() {
729
913
  const createByGloveSyntax = parsed.gloveSyntax && parsed.command !== "dev" && parsed.command !== "start";
730
914
  if (parsed.command === "init" || createByGloveSyntax || parsed.gloveSyntax && !parsed.command) {
731
915
  const directory = parsed.command === "init" ? parsed.positional[0] ?? "glove-foundry-app" : parsed.command ?? "glove-foundry-app";
732
- const result = await scaffoldFoundryProject({ directory });
733
- process2.stdout.write(`Created Glove Foundry in ${result.rootDir}
734
-
735
- `);
736
- process2.stdout.write(` cd ${relative(process2.cwd(), result.rootDir) || "."}
737
- `);
738
- process2.stdout.write(` cp .env.example .env.local
739
- `);
740
- process2.stdout.write(` pnpm install
741
- `);
742
- process2.stdout.write(` pnpm dev
743
- `);
916
+ await create(directory, parsed);
744
917
  return;
745
918
  }
746
919
  if (parsed.command === "dev") {
@@ -754,7 +927,18 @@ async function main() {
754
927
  process2.stdout.write(HELP);
755
928
  }
756
929
  main().catch((error) => {
757
- process2.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}
930
+ if (error instanceof UsageError) {
931
+ process2.stderr.write(`
932
+ ${error.message}
933
+
934
+ Run "glove foundry help" for usage.
935
+
758
936
  `);
937
+ } else {
938
+ process2.stderr.write(
939
+ `${error instanceof Error ? error.stack ?? error.message : String(error)}
940
+ `
941
+ );
942
+ }
759
943
  process2.exitCode = 1;
760
944
  });