@tdk-landscape/tdk-cli-core 1.3.170 → 1.3.172

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.
@@ -4,6 +4,7 @@ import { existsSync, mkdirSync } from "node:fs";
4
4
  import { isAbsolute, relative, resolve } from "node:path";
5
5
  import chalk from "chalk";
6
6
  import { Command } from "commander";
7
+ import { DEFAULT_AGENT_RUNTIME, resolveAgentDurability, resolveAgentRuntime, } from "../agent-runtimes/registry.js";
7
8
  import { resolveBackendFramework } from "../backend-frameworks/registry.js";
8
9
  import { getBackendIndexTemplate } from "../backend-languages/bun.js";
9
10
  import { resolveBackendLanguage } from "../backend-languages/registry.js";
@@ -56,6 +57,10 @@ export const TYPE_SPECIFIC = {
56
57
  mcp: {
57
58
  healthCheckPath: "/health",
58
59
  },
60
+ // /ready is the readiness route (checkpoint store reachable); /health is liveness only, so an idle agent stays healthy.
61
+ agent: {
62
+ healthCheckPath: "/health",
63
+ },
59
64
  [BRING_YOUR_OWN_TYPE]: {
60
65
  healthCheckPath: "/health",
61
66
  },
@@ -84,7 +89,7 @@ export function resolveResourcePort(resourceType, value, assignedPort, resources
84
89
  }
85
90
  return port;
86
91
  }
87
- export function createServiceJson(name, type, stack, port, extraFeatures = [], frameworkId, languageId) {
92
+ export function createServiceJson(name, type, stack, port, extraFeatures = [], frameworkId, languageId, agent) {
88
93
  const framework = resolveFrontendFramework(type, frameworkId);
89
94
  // Only an explicit selection is persisted: a missing `language` has always meant Bun.
90
95
  const language = languageId === undefined ? undefined : resolveBackendLanguage(type, languageId);
@@ -123,8 +128,11 @@ export function createServiceJson(name, type, stack, port, extraFeatures = [], f
123
128
  stack,
124
129
  port,
125
130
  ...(framework ? { framework: framework.id } : {}),
126
- ...(backendFramework && type !== "mcp" ? { framework: backendFramework.id } : {}),
131
+ ...(backendFramework && type !== "mcp" && type !== "agent"
132
+ ? { framework: backendFramework.id }
133
+ : {}),
127
134
  ...(language ? { language: language.id } : {}),
135
+ ...(type === "agent" && agent ? { agent } : {}),
128
136
  };
129
137
  }
130
138
  export function createByoServiceJson(name, stack, port, options) {
@@ -154,10 +162,11 @@ function requireGeneratedViteConfig(configPath) {
154
162
  "Run tdk up <stack> once, or point this script at your own Vite config to build on the host.";
155
163
  return `bun -e 'if(!require("fs").existsSync(${JSON.stringify(configPath)})){console.error(${JSON.stringify(message)});process.exit(1)}'`;
156
164
  }
157
- export function createPackageJson(name, type, frameworkId, prismaEnabled = false) {
165
+ export function createPackageJson(name, type, frameworkId, prismaEnabled = false, agentConfig) {
158
166
  const isFrontend = type === "frontend";
159
167
  const framework = resolveFrontendFramework(type, frameworkId);
160
168
  const backendFramework = resolveBackendFramework(type, frameworkId);
169
+ const isPostgresAgent = type === "agent" && agentConfig?.durability === "postgres";
161
170
  return {
162
171
  name: `@project/${name}`,
163
172
  version: "0.0.1",
@@ -184,11 +193,15 @@ export function createPackageJson(name, type, frameworkId, prismaEnabled = false
184
193
  dependencies: {
185
194
  ...(isFrontend ? {} : (backendFramework?.dependencies ?? { hono: "^4.0.0" })),
186
195
  ...(framework?.dependencies ?? {}),
187
- ...(prismaEnabled
196
+ ...(prismaEnabled || isPostgresAgent
188
197
  ? {
189
- prisma: "^7.5.0",
190
- "@prisma/client": "^7.5.0",
191
- "@prisma/adapter-pg": "^7.5.0",
198
+ ...(prismaEnabled
199
+ ? {
200
+ prisma: "^7.5.0",
201
+ "@prisma/client": "^7.5.0",
202
+ "@prisma/adapter-pg": "^7.5.0",
203
+ }
204
+ : {}),
192
205
  pg: "^8.13.0",
193
206
  }
194
207
  : {}),
@@ -272,7 +285,7 @@ export function parseResourceType(type) {
272
285
  export const resourceCommand = new Command("resource")
273
286
  .description("Create a new resource (service) from scratch, or register an existing one")
274
287
  .argument("[name]", "Resource name (kebab-case)")
275
- .option("-t, --type <type>", "Resource type: backend, frontend, worker, mcp, bring-your-own, sdk", "backend")
288
+ .option("-t, --type <type>", "Resource type: backend, frontend, worker, mcp, agent, bring-your-own, sdk", "backend")
276
289
  .option("--framework <id>", "Framework: frontend react (default), vue, svelte, preact, lit, solid, qwik, vanilla, tanstack-router (see --frameworks); backend hono (default), express, elysia, fastify, nestjs, koa, h3")
277
290
  .option("--frameworks", "List registered frontend frameworks and exit")
278
291
  .option("--language <id>", "Backend language: bun (default), python, go, rust")
@@ -289,6 +302,8 @@ export const resourceCommand = new Command("resource")
289
302
  .option("--image <name>", "Docker image name instead of building from Dockerfile (for bring-your-own)")
290
303
  .option("--port <port>", "Port for the resource, within its type's range (default: next free port in that range)")
291
304
  .option("--feature <feature...>", "Enable a resource feature (for example: prisma)")
305
+ .option("--runtime <id>", "Agent runtime (for agent only): custom (default)")
306
+ .option("--durability <kind>", "Where agent sessions are checkpointed (for agent only): files (default, named volume) or postgres")
292
307
  .action(async (name, options) => {
293
308
  if (options.frameworks) {
294
309
  for (const f of listFrontendFrameworks()) {
@@ -322,6 +337,21 @@ export const resourceCommand = new Command("resource")
322
337
  assertValid(validateResourceName(resourceName));
323
338
  }
324
339
  const resourceType = parseResourceType(options.type);
340
+ if (resourceType !== "agent" &&
341
+ (options.runtime !== undefined || options.durability !== undefined)) {
342
+ throw new TdkError("--runtime and --durability can only be used with --type agent.", [
343
+ "Drop --runtime and --durability, or pass --type agent",
344
+ ]);
345
+ }
346
+ // Validate the agent options before anything is written, so a bad flag leaves no half-scaffolded directory.
347
+ let agentConfig;
348
+ if (resourceType === "agent") {
349
+ resolveAgentRuntime(options.runtime);
350
+ agentConfig = {
351
+ runtime: options.runtime ?? DEFAULT_AGENT_RUNTIME,
352
+ durability: resolveAgentDurability(options.durability),
353
+ };
354
+ }
325
355
  let frameworkOption = options.framework;
326
356
  if (resourceType === "frontend" &&
327
357
  frameworkOption === undefined &&
@@ -343,7 +373,9 @@ export const resourceCommand = new Command("resource")
343
373
  const backendFramework = resolveBackendFramework(resourceType, frameworkOption, backendLanguage);
344
374
  // The id passed on to the file generators. An mcp resource has one fixed scaffold and no `framework` setting, so it passes none
345
375
  // (its provider is selected by the resource type itself).
346
- const scaffoldFrameworkId = resourceType === "mcp" ? undefined : (frontendFramework?.id ?? backendFramework?.id);
376
+ const scaffoldFrameworkId = resourceType === "mcp" || resourceType === "agent"
377
+ ? undefined
378
+ : (frontendFramework?.id ?? backendFramework?.id);
347
379
  let dddEnabled = false;
348
380
  if (options.ddd) {
349
381
  if (resourceType !== "backend" && resourceType !== "worker") {
@@ -423,6 +455,7 @@ export const resourceCommand = new Command("resource")
423
455
  frontend: `apps/${resourceName}`,
424
456
  worker: `workers/${resourceName}`,
425
457
  mcp: `services/${stackName}/${resourceName}`,
458
+ agent: `services/${stackName}/${resourceName}`,
426
459
  [BRING_YOUR_OWN_TYPE]: `services/${stackName}/${resourceName}`,
427
460
  sdk: `packages/${resourceName}`,
428
461
  };
@@ -607,8 +640,8 @@ This file contains the resource configuration for TDK.
607
640
  // Prepare file generation tasks
608
641
  const requestedFeatures = Array.isArray(options.feature) ? options.feature : [];
609
642
  const prismaEnabled = requestedFeatures.includes("prisma");
610
- const serviceJson = createServiceJson(resourceName, resourceType, stackName, assignedPort, [...(dddEnabled ? ["ddd"] : []), ...requestedFeatures], scaffoldFrameworkId, options.language === undefined ? undefined : backendLanguage?.id);
611
- const packageJson = createPackageJson(resourceName, resourceType, scaffoldFrameworkId, prismaEnabled);
643
+ const serviceJson = createServiceJson(resourceName, resourceType, stackName, assignedPort, [...(dddEnabled ? ["ddd"] : []), ...requestedFeatures], scaffoldFrameworkId, options.language === undefined ? undefined : backendLanguage?.id, agentConfig);
644
+ const packageJson = createPackageJson(resourceName, resourceType, scaffoldFrameworkId, prismaEnabled, agentConfig);
612
645
  if (prismaEnabled && resourceType === "backend") {
613
646
  serviceJson.dependsOn = [`${resourceName}-migrator`];
614
647
  }
@@ -645,12 +678,19 @@ This file contains the resource configuration for TDK.
645
678
  },
646
679
  ];
647
680
  // Add source files based on resource type
648
- if (resourceType === "backend" || resourceType === "mcp") {
681
+ if (resourceType === "backend" || resourceType === "mcp" || resourceType === "agent") {
682
+ const indexContent = resourceType === "agent"
683
+ ? (backendFramework?.createIndex ?? getBackendIndexTemplate)(resourceName, agentConfig?.durability)
684
+ : (backendFramework?.createIndex ?? getBackendIndexTemplate)(resourceName);
649
685
  tasks.push({
650
686
  type: "text",
651
687
  filename: "src/index.ts",
652
- content: (backendFramework?.createIndex ?? getBackendIndexTemplate)(resourceName),
653
- description: resourceType === "mcp" ? "Generating MCP server source" : "Generating backend source",
688
+ content: indexContent,
689
+ description: resourceType === "mcp"
690
+ ? "Generating MCP server source"
691
+ : resourceType === "agent"
692
+ ? "Generating agent source"
693
+ : "Generating backend source",
654
694
  emoji: "💻",
655
695
  });
656
696
  }
@@ -0,0 +1,79 @@
1
+ // Copyright (c) 2026 TDK Landscape contributors
2
+ // SPDX-License-Identifier: MIT
3
+ import chalk from "chalk";
4
+ import { Command } from "commander";
5
+ import { STANDARD_PORTS } from "../utils/constants.js";
6
+ import { errorFactories, runCommand, TdkError } from "../utils/errors.js";
7
+ import { createMachineEnvelope, writeMachineError } from "../utils/machine-output.js";
8
+ import { findProjectRoot } from "../utils/paths.js";
9
+ import { isTiltAvailable, runTilt } from "../utils/tilt.js";
10
+ import { isTiltConnectionFailure } from "../utils/tilt-logs.js";
11
+ import { tiltUnreachableMessage } from "../utils/tilt-unreachable-message.js";
12
+ export const restartCommand = new Command("restart")
13
+ .description("Restart a running resource in the stack")
14
+ .argument("<resource>", "Name of the resource (service) to restart")
15
+ .option("--port <n>", "Tilt UI port (default: TILT_PORT or 10350)")
16
+ .option("--json", "Output one JSON object and exit", false)
17
+ .action(async (resourceName, options) => {
18
+ const action = async () => {
19
+ const root = findProjectRoot();
20
+ if (!root) {
21
+ const error = errorFactories.notInProject();
22
+ if (options.json)
23
+ writeMachineError(error);
24
+ error.exit();
25
+ }
26
+ const portText = options.port ?? process.env.TILT_PORT ?? String(STANDARD_PORTS.tiltUi);
27
+ let tiltAvailable = false;
28
+ try {
29
+ tiltAvailable = await isTiltAvailable();
30
+ }
31
+ catch {
32
+ tiltAvailable = false;
33
+ }
34
+ if (!tiltAvailable) {
35
+ // isTiltAvailable collapses missing binary and nonzero version into false.
36
+ // Report TILT_NOT_RUNNING for both; a missing binary is rare and still surfaces via doctor.
37
+ const msg = tiltUnreachableMessage(portText);
38
+ if (options.json) {
39
+ console.log(JSON.stringify(createMachineEnvelope(null, [{ code: "TILT_NOT_RUNNING", message: msg }])));
40
+ process.exit(1);
41
+ }
42
+ throw new TdkError(msg, ["Start the stack first with: tdk up"]);
43
+ }
44
+ const result = await runTilt("trigger", [resourceName, "--port", portText], {
45
+ inheritStdio: false,
46
+ });
47
+ if (result.exitCode !== 0) {
48
+ const detail = result.stderr.trim();
49
+ if (!detail || isTiltConnectionFailure(detail)) {
50
+ const msg = tiltUnreachableMessage(portText);
51
+ if (options.json) {
52
+ console.log(JSON.stringify(createMachineEnvelope(null, [{ code: "TILT_NOT_RUNNING", message: msg }])));
53
+ process.exit(1);
54
+ }
55
+ throw new TdkError(msg, ["Start the stack first with: tdk up"]);
56
+ }
57
+ if (options.json) {
58
+ console.log(JSON.stringify(createMachineEnvelope(null, [{ code: "TILT_TRIGGER_FAILED", message: detail }])));
59
+ process.exit(1);
60
+ }
61
+ throw new TdkError(`Failed to restart ${resourceName}: ${detail}`);
62
+ }
63
+ if (options.json) {
64
+ console.log(JSON.stringify(createMachineEnvelope({ resource: resourceName, restarted: true })));
65
+ return;
66
+ }
67
+ console.log(chalk.green(`✓ Triggered restart for ${resourceName}`));
68
+ };
69
+ if (options.json) {
70
+ try {
71
+ await action();
72
+ }
73
+ catch (err) {
74
+ writeMachineError(err);
75
+ }
76
+ return;
77
+ }
78
+ await runCommand(action);
79
+ });
@@ -4,6 +4,7 @@ import { spawn } from "node:child_process";
4
4
  import { closeSync, existsSync, openSync, readFileSync } from "node:fs";
5
5
  import { tmpdir } from "node:os";
6
6
  import { isAbsolute, join } from "node:path";
7
+ import { createMachineEnvelope } from "../utils/machine-output.js";
7
8
  /** A service or stack name. A leading "-" is rejected so a value can never be read as a flag. */
8
9
  const NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
9
10
  const SINCE = /^(\d+(\.\d+)?(ns|us|µs|ms|s|m|h))+$/;
@@ -131,6 +132,8 @@ export function buildUpArgs(args) {
131
132
  out.push(...only.map((name) => `--only=${name}`));
132
133
  if (flag(args.force, "force"))
133
134
  out.push("--force");
135
+ if (flag(args.dryRun, "dryRun"))
136
+ out.push("--dry-run");
134
137
  out.push("--json");
135
138
  return out;
136
139
  }
@@ -168,17 +171,13 @@ export async function startUp(args, deps = defaultUpDeps) {
168
171
  }
169
172
  child.unref();
170
173
  return {
171
- data: {
172
- schemaVersion: 1,
173
- data: {
174
- ok: true,
175
- started: true,
176
- pid: child.pid ?? null,
177
- logFile,
178
- next: "Poll the status tool until data.tilt.readiness.ready is true. Call down to stop it.",
179
- },
180
- errors: [],
181
- },
174
+ data: createMachineEnvelope({
175
+ ok: true,
176
+ started: true,
177
+ pid: child.pid ?? null,
178
+ logFile,
179
+ next: "Poll the status tool until data.tilt.readiness.ready is true. Call down to stop it.",
180
+ }),
182
181
  isError: false,
183
182
  };
184
183
  }
@@ -245,6 +244,7 @@ export function createTdkTools(run = runTdkCli, up = defaultUpDeps) {
245
244
  description: "Start only these services plus what they depend on and shared infrastructure",
246
245
  },
247
246
  force: { type: "boolean", description: "Replace a Tilt that is already running" },
247
+ dryRun: { type: "boolean", description: "Show what would be started without starting" },
248
248
  waitSeconds: {
249
249
  type: "number",
250
250
  description: "How long to watch for an early failure before returning (default 15, max 60)",
@@ -255,8 +255,15 @@ export function createTdkTools(run = runTdkCli, up = defaultUpDeps) {
255
255
  {
256
256
  name: "down",
257
257
  description: "Stop the running stack, optionally selecting its project with projectPath.",
258
- inputSchema: OBJECT({ projectPath: PROJECT_PATH }),
259
- handler: viaCli(() => ["down", "--json"]),
258
+ inputSchema: OBJECT({
259
+ projectPath: PROJECT_PATH,
260
+ dryRun: { type: "boolean", description: "Show what would be stopped without stopping" },
261
+ }),
262
+ handler: viaCli((args) => [
263
+ "down",
264
+ "--json",
265
+ ...(flag(args.dryRun, "dryRun") ? ["--dry-run"] : []),
266
+ ]),
260
267
  },
261
268
  {
262
269
  name: "status",
@@ -290,5 +297,53 @@ export function createTdkTools(run = runTdkCli, up = defaultUpDeps) {
290
297
  return ["resources", "--json", ...(stack ? [`--stack=${stack}`] : [])];
291
298
  }),
292
299
  },
300
+ {
301
+ name: "restart_resource",
302
+ description: "Restart a running resource in the stack (or trigger its rebuild). Set projectPath when the MCP server is not running from the project root.",
303
+ inputSchema: OBJECT({
304
+ projectPath: PROJECT_PATH,
305
+ resource: { type: "string", description: "Name of the resource to restart" },
306
+ }),
307
+ handler: viaCli((args) => {
308
+ const resource = optionalName(args.resource, "resource");
309
+ if (!resource)
310
+ throw new Error("resource is required");
311
+ return ["restart", resource, "--json"];
312
+ }),
313
+ },
314
+ {
315
+ name: "list_agents",
316
+ description: "List AI agent resources (appType: 'agent') in the project with their stack, runtime, durability and port. Set projectPath when the MCP server is not running from the project root.",
317
+ inputSchema: OBJECT({
318
+ projectPath: PROJECT_PATH,
319
+ stack: { type: "string", description: "Only this stack" },
320
+ }),
321
+ handler: viaCli((args) => {
322
+ const stack = optionalName(args.stack, "stack");
323
+ return ["resources", "--type=agent", "--json", ...(stack ? [`--stack=${stack}`] : [])];
324
+ }),
325
+ },
326
+ {
327
+ name: "get_agent_sessions",
328
+ description: "Inspect session checkpoints from a running AI agent resource. Set projectPath when the MCP server is not running from the project root.",
329
+ inputSchema: OBJECT({
330
+ projectPath: PROJECT_PATH,
331
+ resource: { type: "string", description: "Name of the agent resource" },
332
+ sessionId: { type: "string", description: "Optional session ID to fetch" },
333
+ }),
334
+ handler: viaCli((args) => {
335
+ const resource = optionalName(args.resource, "resource");
336
+ if (!resource)
337
+ throw new Error("resource is required");
338
+ const sessionId = optionalName(args.sessionId, "sessionId");
339
+ return [
340
+ "agent",
341
+ "sessions",
342
+ resource,
343
+ ...(sessionId ? [`--session-id=${sessionId}`] : []),
344
+ "--json",
345
+ ];
346
+ }),
347
+ },
293
348
  ];
294
349
  }
@@ -9,6 +9,7 @@ export const RESOURCE_CONFIG_APP_TYPES = [
9
9
  "worker",
10
10
  "migrator",
11
11
  "mcp",
12
+ "agent",
12
13
  "bring-your-own",
13
14
  ];
14
15
  export const CREATABLE_RESOURCE_TYPES = [
@@ -16,6 +17,7 @@ export const CREATABLE_RESOURCE_TYPES = [
16
17
  "frontend",
17
18
  "worker",
18
19
  "mcp",
20
+ "agent",
19
21
  "bring-your-own",
20
22
  ];
21
23
  export function isCreatableResourceType(value) {
@@ -45,6 +45,7 @@ export const VALID_RESOURCE_TYPES = [
45
45
  "worker",
46
46
  "migrator",
47
47
  "mcp",
48
+ "agent",
48
49
  BRING_YOUR_OWN_TYPE,
49
50
  ];
50
51
  // Frontend/library/sdk services are served statically or consumed as
@@ -56,6 +57,7 @@ export const REQUIRED_PACKAGE_SCRIPTS = {
56
57
  worker: ["dev", "build", "start"],
57
58
  migrator: ["dev", "build", "start"],
58
59
  mcp: ["dev", "build", "start"],
60
+ agent: ["dev", "build", "start"],
59
61
  frontend: ["dev", "build"],
60
62
  library: ["dev", "build"],
61
63
  sdk: ["dev", "build"],
@@ -76,6 +78,8 @@ export const PORT_RANGES = {
76
78
  backend: { base: 4000, min: 4000, max: 4999, range: "4000-4999" },
77
79
  // An MCP server is an HTTP service, so it shares the backend-compatible range with bring-your-own.
78
80
  mcp: { base: 4000, min: 4000, max: 5999, range: "4000-5999" },
81
+ // An agent serves /health and /ready over HTTP (see agent-runtimes/custom.ts), so it shares the same range.
82
+ agent: { base: 4000, min: 4000, max: 5999, range: "4000-5999" },
79
83
  [BRING_YOUR_OWN_TYPE]: { base: 4000, min: 4000, max: 5999, range: "4000-5999" },
80
84
  worker: { base: 6000, min: 6000, max: 6999, range: "6000-6999" },
81
85
  health: { base: 5000, min: 5000, max: 5999, range: "5000-5999" },
@@ -1,4 +1,5 @@
1
1
  import { CONTAINER_RUNTIME_CHECK } from "./constants.js";
2
+ import { SCHEMA_VERSION } from "./machine-output.js";
2
3
  /** Wait for concurrent probes to finish even if one throws, so they can release resources. */
3
4
  export async function collectDoctorChecks(machineChecks, projectChecks) {
4
5
  const checks = [];
@@ -28,7 +29,7 @@ function containerRuntimeReachable(checks) {
28
29
  /** Produce the same readiness decision for human and machine consumers. */
29
30
  export function createDoctorReport(checks, inProject, errors = [], portPlan, host) {
30
31
  return {
31
- schemaVersion: 1,
32
+ schemaVersion: SCHEMA_VERSION,
32
33
  data: {
33
34
  ready: errors.length === 0 &&
34
35
  checks.every((check) => check.didPass || check.isSkipped || check.isWarning),
@@ -232,7 +232,7 @@ export function checkDuplicateResourceNames(projectRoot = findProjectRoot() ?? p
232
232
  fix: 'Give each resource its own "appName" in its service.json. The name becomes the Compose service and the Traefik router, so two resources with one name collide',
233
233
  };
234
234
  }
235
- /** A resource that gets a Traefik route to its own `port`: a backend, an mcp server, or a bring-your-own resource that is not opted out. */
235
+ /** A resource that gets a Traefik route to its own `port`: a backend, an mcp server, an agent, or a bring-your-own resource that is not opted out. */
236
236
  function isRoutedToPort(resource) {
237
237
  const appType = resource.config?.appType;
238
238
  if (isApiServiceType(appType))
@@ -1,6 +1,7 @@
1
1
  // Copyright (c) 2026 TDK Landscape contributors
2
2
  // SPDX-License-Identifier: MIT
3
3
  import { writeSync } from "node:fs";
4
+ import { createMachineEnvelope, } from "./machine-output.js";
4
5
  /**
5
6
  * Returns an emitter that writes at most one `{schemaVersion, data, errors}` object to stdout. If the process
6
7
  * exits before anything was emitted (a preflight `process.exit`, Tilt ending early), a failure object is written
@@ -12,7 +13,7 @@ export function createJsonEmitter(failureCode, command) {
12
13
  if (emitted)
13
14
  return;
14
15
  emitted = true;
15
- writeSync(1, `${JSON.stringify({ schemaVersion: 1, data, errors })}\n`);
16
+ writeSync(1, `${JSON.stringify(createMachineEnvelope(data, errors))}\n`);
16
17
  };
17
18
  process.once("exit", (code) => {
18
19
  emit({ ok: false }, [
@@ -1,8 +1,9 @@
1
1
  // Copyright (c) 2026 TDK Landscape contributors
2
2
  // SPDX-License-Identifier: MIT
3
3
  import { TdkError } from "./errors.js";
4
+ export const SCHEMA_VERSION = 1;
4
5
  export function createMachineEnvelope(data, errors = []) {
5
- return { schemaVersion: 1, data, errors };
6
+ return { schemaVersion: SCHEMA_VERSION, data, errors };
6
7
  }
7
8
  export function toMachineError(error) {
8
9
  if (error instanceof TdkError) {
@@ -25,4 +25,8 @@ export const PROJECT_TEMPLATES = {
25
25
  bundledPath: "examples/tdk-example",
26
26
  description: "Hono + Postgres + NATS worker + Vite example with a routed write path",
27
27
  },
28
+ "agent-stack": {
29
+ repo: "https://github.com/tdk-landscape/tdk-agent-stack.git",
30
+ description: "AI agent stack: Bun agent + MCP tool server + Postgres + NATS",
31
+ },
28
32
  };
@@ -5,5 +5,5 @@
5
5
  * Protocol endpoint, which the engine builds, routes and health-checks exactly like a backend).
6
6
  */
7
7
  export function isApiServiceType(appType) {
8
- return appType === "backend" || appType === "mcp";
8
+ return appType === "backend" || appType === "mcp" || appType === "agent";
9
9
  }
@@ -15,6 +15,7 @@ const DEPRECATED_FIELDS = {
15
15
  };
16
16
  const KNOWN_SERVICE_FIELDS = new Set([
17
17
  "$schema",
18
+ "agent",
18
19
  "apiBasePath",
19
20
  "apiPath",
20
21
  "appName",
@@ -9,7 +9,9 @@ export function buildServicePorts(resource, ingressHttp) {
9
9
  const { appBase, apiBase } = resolveSubdomainBases(ingressHttp);
10
10
  url = `${isApiServiceType(appType) ? apiBase : appBase}${resolveServicePath(resource)}`;
11
11
  }
12
- return { url, containerPort: resource.port ?? resource.config?.port ?? null, hostPort: null };
12
+ const containerPort = resource.port ?? resource.config?.port ?? null;
13
+ const directUrl = !routable && containerPort ? `http://localhost:${containerPort}` : null;
14
+ return { url, containerPort, hostPort: null, directUrl };
13
15
  }
14
16
  /** Ports a client should forward or open: ingress, Tilt UI, and the published datastore. Not one per service. */
15
17
  export function buildStackPorts(plan, tiltPort) {