@stackbone/cli 0.1.0-alpha.2 → 0.1.0-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/main.js CHANGED
@@ -82,6 +82,7 @@ var installationStatusSchema = z.enum([
82
82
  "deprovisioning",
83
83
  "deprovisioned"
84
84
  ]);
85
+ var installationKindSchema = z.enum(["cloud", "local"]);
85
86
  var installationProviderSchema = z.enum([
86
87
  "neon",
87
88
  "r2",
@@ -109,17 +110,30 @@ z.object({
109
110
  slug: organizationSlugSchema,
110
111
  id: z.uuid()
111
112
  });
112
- z.object({
113
+ var installationResponseSchema = z.object({
113
114
  id: z.uuid(),
114
115
  organizationId: z.uuid(),
115
116
  agentId: z.uuid(),
116
117
  agentVersion: z.string(),
117
118
  status: installationStatusSchema,
119
+ kind: installationKindSchema.default("cloud"),
120
+ localTunnelUrl: z.url().nullable().optional(),
121
+ lastActiveAt: z.iso.datetime().nullable().optional(),
118
122
  config: jsonObject(),
119
123
  createdByUserId: z.uuid(),
120
124
  createdAt: z.iso.datetime(),
121
125
  updatedAt: z.iso.datetime()
122
126
  });
127
+ z.object({
128
+ creatorOrganizationSlug: organizationSlugSchema,
129
+ agentTemplateSlug: z.string().min(1).max(80),
130
+ endUserOrganizationSlug: organizationSlugSchema.optional()
131
+ });
132
+ z.object({ localTunnelUrl: z.url().nullable() });
133
+ var localDevInstallationResponseSchema = installationResponseSchema.extend({
134
+ organizationSlug: organizationSlugSchema,
135
+ agentSlug: z.string().min(1).max(80)
136
+ });
123
137
  z.object({
124
138
  id: z.uuid(),
125
139
  installationId: z.uuid(),
@@ -468,6 +482,11 @@ z.object({
468
482
  nextCursor: z.string().nullable(),
469
483
  prevCursor: z.string().nullable()
470
484
  });
485
+ z.object({ fxtId: z.uuid() });
486
+ z.object({
487
+ input: jsonObject().nullable(),
488
+ output: jsonObject().nullable()
489
+ });
471
490
  //#endregion
472
491
  //#region ../../libs/validators/src/lib/auth/session.ts
473
492
  var sessionUserSchema = z.object({
@@ -931,12 +950,30 @@ var credentialsSchema = z.object({
931
950
  * Project-local config is written by `stackbone init`/`link` to
932
951
  * `<cwd>/.stackbone/project.json`. Tracks which agent in which organization this
933
952
  * directory points at. Excluded from git by the same `.gitignore` patch.
953
+ *
954
+ * Feature 42: `localDevInstallationId` + `organizationSlug` + `agentSlug` are
955
+ * captured at `init`/`link` time so `stackbone dev` can build the path-aware
956
+ * deeplink (`/app/<orgSlug>/installations/<id>?stackbone-dev=…`) and patch
957
+ * the tunnel URL without a separate `GET` round-trip. They are optional so
958
+ * old project.json files continue to load — a future `stackbone dev` against
959
+ * a pre-feature-42 directory simply re-registers the local-dev row.
934
960
  */
935
961
  var projectConfigSchema = z.object({
936
962
  schemaVersion: z.literal(1),
937
963
  organizationId: z.uuid(),
938
964
  agentId: z.uuid(),
939
- controlPlaneUrl: z.url()
965
+ controlPlaneUrl: z.url(),
966
+ localDevInstallationId: z.uuid().optional(),
967
+ organizationSlug: z.string().optional(),
968
+ agentSlug: z.string().optional(),
969
+ /** F5: slug of the CLI starter the project was scaffolded from. */
970
+ starter: z.string().optional(),
971
+ /**
972
+ * F5 read-compat: pre-rename CLIs wrote a `template` field. Accepted on
973
+ * read for one release so a re-`init` over an existing project upgrades
974
+ * the file to `starter`; never written by current code.
975
+ */
976
+ template: z.string().optional()
940
977
  });
941
978
  /**
942
979
  * Project-shared config at `<cwd>/stackbone.config.json` (committable, unlike
@@ -1263,7 +1300,8 @@ var createHttpAdapter = (opts) => {
1263
1300
  };
1264
1301
  return {
1265
1302
  get: (path) => request("GET", path),
1266
- post: (path, body) => request("POST", path, body)
1303
+ post: (path, body) => request("POST", path, body),
1304
+ patch: (path, body) => request("PATCH", path, body)
1267
1305
  };
1268
1306
  };
1269
1307
  //#endregion
@@ -1361,7 +1399,6 @@ var flagValue = (argv, flag) => {
1361
1399
  var parseFlags = (argv, env) => ({
1362
1400
  json: detectJsonMode(argv, env),
1363
1401
  yes: flagPresent(argv, "-y") || flagPresent(argv, "--yes"),
1364
- autoLogin: env["STACKBONE_AUTO_LOGIN"] !== "0" && !flagPresent(argv, "--no-auto-login"),
1365
1402
  apiUrlOverride: flagValue(argv, "--api-url") ?? env["STACKBONE_API_URL"] ?? null,
1366
1403
  verbose: flagPresent(argv, "--verbose") || env["STACKBONE_VERBOSE"] === "1"
1367
1404
  });
@@ -1422,7 +1459,7 @@ var isVersionFlag = (rawArgs) => rawArgs.length === 1 && VERSION_FLAGS.has(rawAr
1422
1459
  var formatVersionOutput = (input) => `stackbone-cli ${input.cliVersion}\ncontract ${input.contractVersion}`;
1423
1460
  //#endregion
1424
1461
  //#region src/dev/contract.ts
1425
- var STACKBONE_CLI_BUILD_VERSION = "0.1.0-alpha.2";
1462
+ var STACKBONE_CLI_BUILD_VERSION = "0.1.0-alpha.4";
1426
1463
  /**
1427
1464
  * Capabilities the local emulator advertises on `GET /api/contract`. Typed as
1428
1465
  * `readonly Capability[]` so the compiler refuses any string outside the
@@ -1743,55 +1780,25 @@ var clearSession = async (ctx) => {
1743
1780
  };
1744
1781
  //#endregion
1745
1782
  //#region src/commands/_shared/protected.ts
1746
- var isAuthError = (err) => isStackboneCliError(err) && err.code === "auth";
1747
- var refuseWithAuthError = () => {
1748
- throw new StackboneCliError({
1749
- code: "auth",
1750
- message: "Not authenticated",
1751
- suggestion: "Run `stackbone login`"
1752
- });
1753
- };
1754
- var ensureSession = async (ctx) => {
1755
- const existing = await getCurrentSession(ctx);
1756
- if (existing) return existing;
1757
- if (!ctx.flags.autoLogin) refuseWithAuthError();
1758
- await runLoginFlow(ctx);
1759
- return await getCurrentSession(ctx) ?? refuseWithAuthError();
1760
- };
1761
1783
  /**
1762
1784
  * Wraps a command handler so it always receives a non-null `Session`.
1763
1785
  *
1764
- * If the credentials store is empty or the session expired:
1765
- * - With auto-login enabled (default): runs the device-code flow inline,
1766
- * reloads the session, and runs the handler.
1767
- * - With auto-login disabled (`STACKBONE_AUTO_LOGIN=0` or `--no-auto-login`):
1768
- * refuses immediately with `code: 'auth'` and exit 2.
1769
- *
1770
- * Retries once if the handler itself throws an auth error mid-flight
1771
- * (token expired between the pre-check and the request). If the retry
1772
- * also fails with auth, the corrupt credentials are dropped from disk so
1773
- * the next invocation does not loop through the same broken session.
1786
+ * If the credentials store is empty or the session has expired, refuses
1787
+ * immediately with `code: 'auth'` (exit 2) and points the user at
1788
+ * `stackbone login`. Auth flow and command flow are kept strictly separate
1789
+ * protected commands never trigger the device-code dance on the user's
1790
+ * behalf, and a 401 mid-flight propagates as-is so the same `stackbone
1791
+ * login` instruction is the only path back.
1774
1792
  */
1775
1793
  var protectedCommand = (handler) => {
1776
1794
  return async (ctx) => {
1777
- const session = await ensureSession(ctx);
1778
- try {
1779
- await handler(ctx, session);
1780
- } catch (err) {
1781
- if (!isAuthError(err) || !ctx.flags.autoLogin) throw err;
1782
- ctx.logger.debug("auth error mid-flight, retrying after re-login");
1783
- await runLoginFlow(ctx);
1784
- const refreshed = await getCurrentSession(ctx) ?? refuseWithAuthError();
1785
- try {
1786
- await handler(ctx, refreshed);
1787
- } catch (retryErr) {
1788
- if (isAuthError(retryErr)) {
1789
- ctx.logger.warn("auth still failing after re-login, clearing the stored session");
1790
- await clearSession(ctx);
1791
- }
1792
- throw retryErr;
1793
- }
1794
- }
1795
+ const session = await getCurrentSession(ctx);
1796
+ if (!session) throw new StackboneCliError({
1797
+ code: "auth",
1798
+ message: "Not authenticated",
1799
+ suggestion: "Run `stackbone login`"
1800
+ });
1801
+ await handler(ctx, session);
1795
1802
  };
1796
1803
  };
1797
1804
  //#endregion
@@ -2614,7 +2621,7 @@ var buildMigrateStatusCommand = (ctx) => defineCommand({
2614
2621
  env: ctx.env,
2615
2622
  json: ctx.flags.json
2616
2623
  });
2617
- })({})
2624
+ })()
2618
2625
  });
2619
2626
  /**
2620
2627
  * Pure handler for `stackbone db migrate add-rag`. Reads `agent.yaml` to
@@ -2702,7 +2709,7 @@ var buildStudioCommand = (ctx) => defineCommand({
2702
2709
  env: ctx.env,
2703
2710
  json: ctx.flags.json
2704
2711
  });
2705
- })({})
2712
+ })()
2706
2713
  });
2707
2714
  var createDbCommand = (ctx) => defineCommand({
2708
2715
  meta: {
@@ -2747,7 +2754,6 @@ var createDeployCommand = (ctx) => buildStubCommand(ctx, {
2747
2754
  });
2748
2755
  //#endregion
2749
2756
  //#region src/dev/boot-banner.ts
2750
- var STUDIO_CLOUD_BASE_URL = "https://app.stackbone.ai/app";
2751
2757
  var ANSI = {
2752
2758
  reset: "\x1B[0m",
2753
2759
  bold: "\x1B[1m",
@@ -2757,7 +2763,6 @@ var ANSI = {
2757
2763
  };
2758
2764
  var ANSI_RE = /\x1b\[[0-9;]*m/g;
2759
2765
  var visibleLength = (s) => s.replace(ANSI_RE, "").length;
2760
- var buildDeeplink = (tunnelUrl) => `${STUDIO_CLOUD_BASE_URL}?stackbone-dev=${encodeURIComponent(tunnelUrl)}`;
2761
2766
  var LABEL_WIDTH = 11;
2762
2767
  var renderRow = (row, colors) => {
2763
2768
  const marker = colors && row.marker.trim() ? `${ANSI.cyan}${row.marker}${ANSI.reset}` : row.marker;
@@ -2768,11 +2773,11 @@ var TITLE = "stackbone studio";
2768
2773
  var formatBootBanner = (input) => {
2769
2774
  const colors = input.colors ?? false;
2770
2775
  const rows = [];
2771
- if (input.tunnelUrl) {
2776
+ if (input.deeplink && input.tunnelUrl) {
2772
2777
  rows.push({
2773
2778
  marker: "▸",
2774
2779
  label: "Open Studio",
2775
- value: buildDeeplink(input.tunnelUrl),
2780
+ value: input.deeplink,
2776
2781
  primary: true
2777
2782
  });
2778
2783
  rows.push({
@@ -2790,6 +2795,23 @@ var formatBootBanner = (input) => {
2790
2795
  label: "Local",
2791
2796
  value: input.localUrl
2792
2797
  });
2798
+ } else if (input.tunnelUrl) {
2799
+ rows.push({
2800
+ marker: "▸",
2801
+ label: "Tunnel",
2802
+ value: input.tunnelUrl,
2803
+ primary: true
2804
+ });
2805
+ rows.push({
2806
+ marker: " ",
2807
+ label: "",
2808
+ value: ""
2809
+ });
2810
+ rows.push({
2811
+ marker: " ",
2812
+ label: "Local",
2813
+ value: input.localUrl
2814
+ });
2793
2815
  } else rows.push({
2794
2816
  marker: "▸",
2795
2817
  label: "Local",
@@ -2973,6 +2995,7 @@ var fetchActiveConfigValue = async (client) => {
2973
2995
  //#region src/dev/connections.ts
2974
2996
  var STACKBONE_ENV_KEYS = {
2975
2997
  contractVersion: "STACKBONE_CONTRACT_VERSION",
2998
+ agentId: "STACKBONE_AGENT_ID",
2976
2999
  postgresUrl: "STACKBONE_POSTGRES_URL",
2977
3000
  queueUrl: "STACKBONE_QUEUE_URL",
2978
3001
  s3Endpoint: "STACKBONE_S3_ENDPOINT",
@@ -2987,6 +3010,7 @@ var buildAgentEnv = (base, injection, options = {}) => {
2987
3010
  const env = {
2988
3011
  ...base,
2989
3012
  [STACKBONE_ENV_KEYS.contractVersion]: String(10),
3013
+ [STACKBONE_ENV_KEYS.agentId]: injection.agentId,
2990
3014
  [STACKBONE_ENV_KEYS.postgresUrl]: injection.services.postgresUrl,
2991
3015
  [STACKBONE_ENV_KEYS.queueUrl]: injection.services.queueUrl,
2992
3016
  [STACKBONE_ENV_KEYS.s3Endpoint]: injection.services.s3Endpoint,
@@ -4142,7 +4166,7 @@ var quoteIdent = (raw) => `"${raw.replace(/"/g, "\"\"")}"`;
4142
4166
  //#region src/dev/fixtures-store.ts
4143
4167
  var defaultFixturesDir = () => resolve(homedir(), ".stackbone", "dev", "fixtures");
4144
4168
  var filenameFor = (baseDir, agentSlug) => resolve(baseDir, `${agentSlug}.jsonl`);
4145
- var isSerializedFixture = (value) => !!value && typeof value === "object" && typeof value.id === "string" && typeof value.name === "string" && typeof value.created_at === "string";
4169
+ var isSerializedFixture = (value) => !!value && typeof value === "object" && typeof value.id === "string" && typeof value.name === "string" && typeof value.createdAt === "string";
4146
4170
  var readJsonl = async (path) => {
4147
4171
  let raw;
4148
4172
  try {
@@ -4169,7 +4193,7 @@ var createFixturesStore = (opts) => {
4169
4193
  };
4170
4194
  return {
4171
4195
  async list() {
4172
- return (await readJsonl(path)).sort((a, b) => a.created_at < b.created_at ? 1 : -1);
4196
+ return (await readJsonl(path)).sort((a, b) => a.createdAt < b.createdAt ? 1 : -1);
4173
4197
  },
4174
4198
  async create(input) {
4175
4199
  await ensureDir();
@@ -4179,7 +4203,7 @@ var createFixturesStore = (opts) => {
4179
4203
  name: input.name,
4180
4204
  input: input.input,
4181
4205
  description: input.description ?? null,
4182
- created_at: (/* @__PURE__ */ new Date()).toISOString()
4206
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
4183
4207
  };
4184
4208
  await appendFile(path, `${JSON.stringify(fixture)}\n`, "utf8");
4185
4209
  return fixture;
@@ -5426,22 +5450,25 @@ var buildEmulatorApp = (opts) => {
5426
5450
  app.get("/api/logs/stream", (c) => streamSSE(c, (stream) => {
5427
5451
  return runLogTail(stream, parseLogFilters(new URL(c.req.url).searchParams), (emit) => tailJsonlDirectory(logsDir, emit));
5428
5452
  }));
5429
- const proxyAgentJson = async (path) => {
5453
+ app.get("/api/schema", async (c) => {
5430
5454
  try {
5431
- const resp = await fetch(`${agentBaseUrl}${path}`);
5432
- const body = await resp.text();
5433
- return new Response(body, {
5434
- status: resp.status,
5435
- headers: { "content-type": "application/json" }
5455
+ const resp = await fetch(`${agentBaseUrl}/schema`);
5456
+ if (!resp.ok) return c.json({
5457
+ input: null,
5458
+ output: null
5459
+ });
5460
+ const raw = await resp.json().catch(() => null);
5461
+ return c.json({
5462
+ input: extractSchemaField(raw, "input"),
5463
+ output: extractSchemaField(raw, "output")
5436
5464
  });
5437
5465
  } catch {
5438
- return new Response(JSON.stringify({ error: "agent_unreachable" }), {
5439
- status: 502,
5440
- headers: { "content-type": "application/json" }
5466
+ return c.json({
5467
+ input: null,
5468
+ output: null
5441
5469
  });
5442
5470
  }
5443
- };
5444
- app.get("/api/schema", () => proxyAgentJson("/schema"));
5471
+ });
5445
5472
  const dbExplorerOptions = { connectionString: opts.injection.services.postgresUrl };
5446
5473
  app.get("/api/db/schemas", async (c) => {
5447
5474
  const response = await listSchemas(dbExplorerOptions);
@@ -6044,6 +6071,12 @@ var buildEmulatorApp = (opts) => {
6044
6071
  }));
6045
6072
  return app;
6046
6073
  };
6074
+ var extractSchemaField = (raw, field) => {
6075
+ if (!raw) return null;
6076
+ const value = raw[field];
6077
+ if (value && typeof value === "object" && !Array.isArray(value)) return value;
6078
+ return null;
6079
+ };
6047
6080
  var eventMatchesRunId = (event, runId) => {
6048
6081
  const data = event.data;
6049
6082
  if (!data || typeof data["run_id"] === "undefined") return true;
@@ -6706,7 +6739,7 @@ var requestDevTunnelGrant = async (input) => {
6706
6739
  if (response.status === 503) throw new StackboneCliError({
6707
6740
  code: "tunnel_relay_unhealthy",
6708
6741
  message: "Stackbone tunnel relay is unhealthy and cannot mint grants right now",
6709
- suggestion: `Check ${isError503Body(parsedBody) && typeof parsedBody.statusUrl === "string" ? parsedBody.statusUrl : DEFAULT_STATUS_URL} for relay status and re-run with \`--no-tunnel\` to skip the tunnel.`,
6742
+ suggestion: `Check ${isError503Body(parsedBody) && typeof parsedBody.statusUrl === "string" ? parsedBody.statusUrl : DEFAULT_STATUS_URL} for relay status and re-run once the relay is back. The tunnel is mandatory, so there is no opt-out flag.`,
6710
6743
  cause: parsedBody
6711
6744
  });
6712
6745
  if (!response.ok) throw new StackboneCliError({
@@ -6849,7 +6882,7 @@ var resolveFrpcBin = async (input) => {
6849
6882
  throw new StackboneCliError({
6850
6883
  code: "generic",
6851
6884
  message: "Could not auto-fetch frpc",
6852
- suggestion: "Check your network or install frpc manually (https://github.com/fatedier/frp/releases) and re-run, or pass `--no-tunnel` / `--no-auto-frpc`",
6885
+ suggestion: "Check your network, or install frpc manually from https://github.com/fatedier/frp/releases and point STACKBONE_FRPC_BIN at the binary before re-running.",
6853
6886
  cause: err
6854
6887
  });
6855
6888
  } finally {
@@ -6933,7 +6966,7 @@ var TUNNEL_HOST_SUFFIX = "tun.stackbone.ai";
6933
6966
  var frpcNotInstalledError = (cause) => new StackboneCliError({
6934
6967
  code: "generic",
6935
6968
  message: "`frpc` is not installed or not on PATH",
6936
- suggestion: "Install frpc from https://github.com/fatedier/frp/releases, set STACKBONE_FRPC_BIN to override the binary, or run `stackbone dev --no-tunnel` to skip the tunnel",
6969
+ suggestion: "Install frpc from https://github.com/fatedier/frp/releases, or set STACKBONE_FRPC_BIN to point at an existing binary. The tunnel is mandatory there is no opt-out flag.",
6937
6970
  cause
6938
6971
  });
6939
6972
  var buildFrpcConfig = (grant, targetUrl) => {
@@ -7176,6 +7209,7 @@ var startDevSession = async (input) => {
7176
7209
  const bus = createEventBus();
7177
7210
  const agentPort = await pickFreePort();
7178
7211
  const injection = {
7212
+ agentId: input.agent.slug,
7179
7213
  agentPort,
7180
7214
  services: DEFAULT_DEV_SERVICES
7181
7215
  };
@@ -7503,6 +7537,24 @@ var startDevSession = async (input) => {
7503
7537
  }
7504
7538
  };
7505
7539
  //#endregion
7540
+ //#region src/services/local-dev-installations.service.ts
7541
+ var parse$1 = (raw, context) => {
7542
+ const parsed = localDevInstallationResponseSchema.safeParse(raw);
7543
+ if (!parsed.success) throw new StackboneCliError({
7544
+ code: "generic",
7545
+ message: `Unexpected response shape from ${context}`,
7546
+ cause: parsed.error
7547
+ });
7548
+ return parsed.data;
7549
+ };
7550
+ var upsertLocalDevInstallation = async (ctx, body) => {
7551
+ return parse$1(await ctx.http.post("/api/v1/installations/local-dev", body), "POST /api/v1/installations/local-dev");
7552
+ };
7553
+ var patchLocalDevInstallation = async (ctx, id, body) => {
7554
+ const path = `/api/v1/installations/local-dev/${encodeURIComponent(id)}`;
7555
+ return parse$1(await ctx.http.patch(path, body), `PATCH ${path}`);
7556
+ };
7557
+ //#endregion
7506
7558
  //#region src/commands/dev/print-contract.ts
7507
7559
  var defaultWriteStdout = (chunk) => {
7508
7560
  process.stdout.write(chunk);
@@ -7599,15 +7651,37 @@ var buildHandler$2 = (ctx, flags) => protectedCommand(async (innerCtx) => {
7599
7651
  name: agent.name
7600
7652
  },
7601
7653
  emulatorPort: flags.port,
7602
- tunnel: flags.tunnel,
7654
+ tunnel: true,
7603
7655
  listen: flags.listen,
7604
7656
  verbose: flags.verbose,
7605
7657
  ...renderStage ? { onStage: renderStage } : {}
7606
7658
  });
7659
+ const activeOrg = await getActiveOrganization(innerCtx);
7660
+ let localDevId = config.localDevInstallationId;
7661
+ let installationOrgSlug = config.organizationSlug ?? activeOrg.slug;
7662
+ if (!localDevId) {
7663
+ const upserted = await upsertLocalDevInstallation(innerCtx, {
7664
+ creatorOrganizationSlug: activeOrg.slug,
7665
+ agentTemplateSlug: agent.slug
7666
+ });
7667
+ localDevId = upserted.id;
7668
+ installationOrgSlug = upserted.organizationSlug;
7669
+ }
7670
+ if (session.publicUrl) {
7671
+ const patched = await patchLocalDevInstallation(innerCtx, localDevId, { localTunnelUrl: session.publicUrl }).catch((err) => {
7672
+ innerCtx.logger.warn({ err: err.message }, "failed to PATCH local-dev tunnel URL (continuing)");
7673
+ return null;
7674
+ });
7675
+ if (patched) installationOrgSlug = patched.organizationSlug;
7676
+ }
7677
+ const studioBaseUrl = innerCtx.apiUrl.includes("localhost") ? "http://localhost:4200/app" : "https://app.stackbone.ai/app";
7678
+ const deeplink = session.publicUrl ? `${studioBaseUrl}/${installationOrgSlug}/installations/${localDevId}?stackbone-dev=${encodeURIComponent(session.publicUrl)}` : null;
7607
7679
  if (innerCtx.flags.json) emit(innerCtx.flags, () => "", {
7608
7680
  emulator_url: session.emulatorUrl,
7609
7681
  agent_url: session.agentUrl,
7610
7682
  public_url: session.publicUrl,
7683
+ deeplink,
7684
+ local_dev_installation_id: localDevId,
7611
7685
  services: {
7612
7686
  postgres_url: session.injection.services.postgresUrl,
7613
7687
  queue_url: session.injection.services.queueUrl,
@@ -7619,6 +7693,7 @@ var buildHandler$2 = (ctx, flags) => protectedCommand(async (innerCtx) => {
7619
7693
  const inlineLines = formatBootBanner({
7620
7694
  tunnelUrl: session.publicUrl,
7621
7695
  localUrl: session.emulatorUrl,
7696
+ deeplink,
7622
7697
  contractVersion: 10,
7623
7698
  capabilityCount: EMULATOR_CAPABILITIES.length,
7624
7699
  agentName: agent.name,
@@ -7630,7 +7705,13 @@ var buildHandler$2 = (ctx, flags) => protectedCommand(async (innerCtx) => {
7630
7705
  log.step("Press Ctrl-C to stop");
7631
7706
  }
7632
7707
  const sigintHandler = () => {
7633
- session.stop();
7708
+ (async () => {
7709
+ if (localDevId) await patchLocalDevInstallation(innerCtx, localDevId, { localTunnelUrl: null }).catch((err) => {
7710
+ innerCtx.logger.debug({ err: err.message }, "failed to null local-dev tunnel URL on exit (safety net will catch up)");
7711
+ return null;
7712
+ });
7713
+ await session.stop();
7714
+ })();
7634
7715
  };
7635
7716
  process.once("SIGINT", sigintHandler);
7636
7717
  process.once("SIGTERM", sigintHandler);
@@ -7648,12 +7729,6 @@ var createDevCommand = (ctx) => defineCommand({
7648
7729
  description: `Emulator port. Defaults to ${DEFAULT_EMULATOR_PORT}.`,
7649
7730
  default: String(DEFAULT_EMULATOR_PORT)
7650
7731
  },
7651
- tunnel: {
7652
- type: "boolean",
7653
- description: "Open a public tunnel to the emulator (default: on).",
7654
- negativeDescription: "Skip the public tunnel and serve the emulator only on localhost.",
7655
- default: true
7656
- },
7657
7732
  listen: {
7658
7733
  type: "boolean",
7659
7734
  description: "Bind the HTTP server to 0.0.0.0 (reachable from your LAN). Default binds to 127.0.0.1 only.",
@@ -7663,11 +7738,6 @@ var createDevCommand = (ctx) => defineCommand({
7663
7738
  type: "boolean",
7664
7739
  description: "Print the JSON contract this CLI advertises (the same payload the emulator serves at /api/contract) and exit without booting the dev session.",
7665
7740
  default: false
7666
- },
7667
- verbose: {
7668
- type: "boolean",
7669
- description: "Stream every log line and docker compose output. Default UI uses per-stage spinners; --verbose switches back to the raw firehose.",
7670
- default: false
7671
7741
  }
7672
7742
  },
7673
7743
  run: ({ args }) => safeRun(ctx.flags, async (a) => {
@@ -7683,9 +7753,8 @@ var createDevCommand = (ctx) => defineCommand({
7683
7753
  });
7684
7754
  await buildHandler$2(ctx, {
7685
7755
  port,
7686
- tunnel: Boolean(a.tunnel),
7687
7756
  listen: Boolean(a.listen),
7688
- verbose: Boolean(a.verbose)
7757
+ verbose: ctx.flags.verbose
7689
7758
  });
7690
7759
  })(args)
7691
7760
  });
@@ -7720,10 +7789,6 @@ several exploratory calls.
7720
7789
  - \`-y, --yes\`
7721
7790
  Skip every confirmation prompt. Required in CI / non-TTY contexts.
7722
7791
 
7723
- - \`--no-auto-login\` (or \`STACKBONE_AUTO_LOGIN=0\`)
7724
- When a protected command finds no valid session, fail fast with exit
7725
- code 2 instead of triggering the device-code flow inline.
7726
-
7727
7792
  ## Exit codes
7728
7793
 
7729
7794
  - \`0\` ok
@@ -7735,18 +7800,20 @@ several exploratory calls.
7735
7800
 
7736
7801
  ## Commands shipped today
7737
7802
 
7803
+ - \`stackbone login\` / \`logout\` — device-code flow over magic link
7738
7804
  - \`stackbone whoami\` — show the active user and organization
7739
7805
  - \`stackbone current\` — print the active organization slug
7740
7806
  - \`stackbone list\` — list organizations the current user owns
7741
7807
  - \`stackbone metadata\` — agent-friendly one-shot overview
7808
+ - \`stackbone init\` / \`link\` — scaffold a new project, or link an existing one
7809
+ - \`stackbone dev\` — local emulator of the control plane
7810
+ - \`stackbone db migrate up | create | status | add-rag\` — Drizzle migrations
7811
+ - \`stackbone db studio\` — open Drizzle Studio against the dev Postgres
7742
7812
  - \`stackbone docs <topic>\` — print this kind of help inline
7743
7813
 
7744
- ## Commands shipped by sibling tasks
7814
+ ## Pending
7745
7815
 
7746
- - \`stackbone login\` / \`logout\` device-code flow over magic link
7747
- - \`stackbone init\` / \`link\` — scaffold a new project, or link an existing one
7748
- - \`stackbone dev\` — local emulator of the control plane
7749
- - \`stackbone deploy\` — push to Fly Machines
7816
+ - \`stackbone deploy\` — push to Fly Machines (stub)
7750
7817
 
7751
7818
  ## State on disk
7752
7819
 
@@ -7755,34 +7822,35 @@ several exploratory calls.
7755
7822
  `;
7756
7823
  var sdk = `# @stackbone/sdk
7757
7824
 
7758
- Monolithic SDK for the agent container with six modules and explicit
7759
- escape hatches to the upstream SDKs. Refer to the
7760
- \`Implementar @stackbone-sdk con 6 modulos monoliticos\` task for the canonical
7761
- shape.
7825
+ Monolithic SDK for the agent container with wrapper modules over the
7826
+ underlying data primitives plus control-plane façades. The creator
7827
+ keeps explicit escape hatches to the upstream SDKs.
7762
7828
 
7763
7829
  ## Modules
7764
7830
 
7765
- - \`client.database\` — Drizzle + @neondatabase/serverless (HTTP driver)
7766
- - \`client.storage\` — @aws-sdk/client-s3 + Cloudflare R2
7831
+ - \`client.database\` — Drizzle + postgres-js (any Postgres URL)
7832
+ - \`client.storage\` — @aws-sdk/client-s3 + Cloudflare R2 / MinIO
7767
7833
  - \`client.ai\` — openai SDK with OpenRouter base URL override
7768
- - \`client.queues\` — @upstash/qstash publisher + Hono receiver
7834
+ - \`client.queues\` — pgmq publisher (surface defined, runtime pending)
7769
7835
  - \`client.rag\` — LlamaParse (opt-in) + chunker + embeddings + pgvector + tsvector
7770
- - \`client.observability\` — @opentelemetry/sdk-node with auto-instrumentations
7836
+ - \`client.observability\` — OpenTelemetry span processor + run-cost aggregator + per-run logger
7837
+ - \`client.memory\` — long-term mem0-backed memory (surface defined, runtime pending)
7771
7838
 
7772
7839
  ## Façades on top of the control plane
7773
7840
 
7774
7841
  - \`client.approval.*\` — HITL queue and workflow resume
7775
7842
  - \`client.secrets.*\` — organization-scoped encrypted secrets
7776
7843
  - \`client.connections.*\` — OAuth connections (Notion, GDrive, Slack, …)
7777
- - \`client.config.*\` — runtime config injected via \`AGENT_CONFIG\`
7844
+ - \`client.config.*\` — typed reads of dynamic config from the control plane
7778
7845
  - \`client.events.*\` — emit events to the organization eventbus
7846
+ - \`client.prompts.*\` — typed prompt templates fetched from the control plane
7779
7847
 
7780
7848
  ## Escape hatch
7781
7849
 
7782
- Every upstream env var (\`DATABASE_URL\`, \`AWS_*\`, \`S3_ENDPOINT\`,
7850
+ Every upstream env var (\`STACKBONE_POSTGRES_URL\`, \`STACKBONE_S3_*\`,
7783
7851
  \`OPENROUTER_API_KEY\`, \`OPENROUTER_BASE_URL\`, \`LLAMA_PARSE_API_KEY\` if
7784
- opt-in, \`QSTASH_*\`) is also injected, so the creator can swap any module
7785
- for the upstream SDK without losing access to its data.
7852
+ opt-in, \`MEM0_API_KEY\` if opt-in) is also injected, so the creator can
7853
+ swap any module for the upstream SDK without losing access to its data.
7786
7854
 
7787
7855
  The runtime target is **Hono + Node 24 LTS** (Bun 1.x as opt-in via the
7788
7856
  \`runtime.engine: bun\` field of \`agent.yaml\`). No adapters for Next,
@@ -7796,15 +7864,23 @@ runs your agent under \`@stackbone/sdk\`, and exposes the Studio API at
7796
7864
 
7797
7865
  ## Defaults that matter
7798
7866
 
7799
- - \`--tunnel\` (default on) opens a public tunnel against the
7867
+ - Mandatory tunnel\`stackbone dev\` always opens a tunnel against the
7800
7868
  Stackbone-owned relay (\`*.tun.stackbone.ai\`) so the HTTPS bundle of
7801
- \`app.stackbone.ai/studio\` can reach the emulator from
7802
- Safari/Brave/Chrome (mixed-content / local-network-access
7803
- restrictions). Pass \`--no-tunnel\` to skip it on permissive
7804
- browsers. Override the binary with \`STACKBONE_FRPC_BIN\`.
7869
+ \`app.stackbone.ai\` can reach the emulator from Safari/Brave/Chrome
7870
+ (mixed-content / local-network-access restrictions). There is no
7871
+ opt-out flag the \`local_tunnel_url\` field on the installation row
7872
+ is what makes the session shareable from the dashboard. The CLI
7873
+ auto-fetches \`frpc\` into \`~/.cache/stackbone/bin/\` if missing.
7874
+ Override the binary with \`STACKBONE_FRPC_BIN\` and pin the release
7875
+ with \`STACKBONE_FRPC_VERSION\`.
7805
7876
  - HTTP server bound to \`127.0.0.1\` by default — not reachable from
7806
7877
  the LAN. Pass \`--listen\` to bind to \`0.0.0.0\` instead; the CLI
7807
7878
  prints a visible warning so the exposure isn't silent.
7879
+ - \`--print-contract\` — print the JSON contract this CLI advertises
7880
+ (same payload as the emulator's \`/api/contract\`) and exit without
7881
+ booting the dev session. Useful for cross-version handshake debugging.
7882
+ - \`--verbose\` (also \`STACKBONE_VERBOSE=1\`) — stream every log line
7883
+ and docker compose output. Default UI uses per-stage spinners.
7808
7884
 
7809
7885
  ## CORS allowlist
7810
7886
 
@@ -7910,6 +7986,32 @@ before booting the agent process and re-runs it whenever the migrations
7910
7986
  folder changes. The default is opt-out so a noisy schema cannot silently
7911
7987
  mutate your local database.
7912
7988
 
7989
+ ### \`rag\` (optional)
7990
+
7991
+ rag:
7992
+ embeddingModel: openai/text-embedding-3-small # default
7993
+ autoMigrate: false # default
7994
+
7995
+ The block \`client.rag\` reads. \`embeddingModel\` is the OpenRouter/OpenAI
7996
+ identifier the pipeline embeds chunks with — change it before ingesting
7997
+ the first document, since collections are pinned to the dimensionality of
7998
+ the model that created them. \`autoMigrate: true\` lets \`stackbone dev\`
7999
+ apply the per-agent RAG schema on boot (off by default to keep the
8000
+ migration explicit). Strict — unknown keys fail parse.
8001
+
8002
+ ### \`protocol\` (optional)
8003
+
8004
+ protocol:
8005
+ required: 10
8006
+
8007
+ Creator-side floor on the Stackbone Agent Protocol contract version. The
8008
+ CLI forwards \`protocol.required\` to the SDK as \`protocolRequired\`; the
8009
+ SDK's contract gate fails closed with \`contract_version_unsupported\`
8010
+ whenever the datapath advertises a contract below this floor, before any
8011
+ per-module capability check runs. Omit the block to fall back to the SDK's
8012
+ hard floor (\`MIN_SUPPORTED_CONTRACT_VERSION\`). Strict — unknown keys fail
8013
+ parse.
8014
+
7913
8015
  ## Reference
7914
8016
 
7915
8017
  This file lives in the validators contract (\`agentManifestSchema\`) and is
@@ -7917,7 +8019,7 @@ shared by \`stackbone init\`, \`stackbone dev\`, \`stackbone deploy\` and the
7917
8019
  \`stackbone db migrate ...\` family. Keep it under version control — the
7918
8020
  control plane never writes to it.
7919
8021
  `,
7920
- templates: stubFor("Epic 5 — official templates", "Crear template workflow LangGraph basico"),
8022
+ starters: stubFor("Epic 5 — official starters", "Document the starter catalogue (CLI scaffold sources)"),
7921
8023
  connections: stubFor("Epic 6 — data layer", "Montar docker-compose embebido con Postgres pgvector pgmq y MinIO para stackbone dev")
7922
8024
  };
7923
8025
  var docTopicList = Object.keys(docTopics);
@@ -8016,7 +8118,6 @@ Branch on these instead of parsing stack traces.
8016
8118
 
8017
8119
  - \`STACKBONE_JSON=1\` — same as \`--json\`
8018
8120
  - \`STACKBONE_API_URL\` — same as \`--api-url <url>\`
8019
- - \`STACKBONE_AUTO_LOGIN=0\` — same as \`--no-auto-login\`
8020
8121
  - \`STACKBONE_LOG_LEVEL\` — \`debug\`, \`info\`, \`warn\`, \`error\` (logger goes
8021
8122
  to stderr, never pollutes the JSON payload on stdout)
8022
8123
 
@@ -8045,7 +8146,7 @@ stackbone metadata --json # one-shot overview that replaces 5–7 explorer call
8045
8146
  ### Start a new project
8046
8147
 
8047
8148
  \`\`\`sh
8048
- stackbone init # menu of templates (workflow / autonomous / RAG)
8149
+ stackbone init # menu of starters (hello-world + 9 capability demos)
8049
8150
  stackbone dev # local emulator at http://localhost:4242
8050
8151
  \`\`\`
8051
8152
 
@@ -8072,7 +8173,7 @@ stackbone deploy # build + push + Fly Machines
8072
8173
  - \`stackbone docs cli\` — command reference
8073
8174
  - \`stackbone docs sdk\` — \`@stackbone/sdk\` modules
8074
8175
  - \`stackbone docs agent-yaml\` — manifest reference (Epic 3)
8075
- - \`stackbone docs templates\` — official templates (Epic 5)
8176
+ - \`stackbone docs starters\` — official starters (Epic 5)
8076
8177
  - \`stackbone docs connections\` — data layer (Epic 6)
8077
8178
  `;
8078
8179
  //#endregion
@@ -8155,18 +8256,22 @@ var writeFileSafe = async (path, contents, force) => {
8155
8256
  await mkdir(dirname(path), { recursive: true });
8156
8257
  await writeFile(path, contents);
8157
8258
  };
8158
- var writeTemplateFiles = async (files, input) => {
8259
+ var writeStarterFiles = async (files, input) => {
8159
8260
  for (const file of files) await writeFileSafe(join(input.root, file.path), file.contents, input.force);
8160
8261
  };
8161
8262
  var writeAgentManifest = async (agentName, input) => {
8162
8263
  await writeFileSafe(join(input.root, "agent.yaml"), renderAgentYaml({ name: agentName }), input.force);
8163
8264
  };
8164
- var writeProjectConfig = async (ctx, agent, input) => {
8265
+ var writeProjectConfig = async (ctx, agent, input, extras = {}) => {
8165
8266
  const config = {
8166
8267
  schemaVersion: 1,
8167
8268
  organizationId: agent.organizationId,
8168
8269
  agentId: agent.id,
8169
- controlPlaneUrl: ctx.apiUrl
8270
+ controlPlaneUrl: ctx.apiUrl,
8271
+ ...extras.localDevInstallationId !== void 0 ? { localDevInstallationId: extras.localDevInstallationId } : {},
8272
+ ...extras.organizationSlug !== void 0 ? { organizationSlug: extras.organizationSlug } : {},
8273
+ ...extras.agentSlug !== void 0 ? { agentSlug: extras.agentSlug } : {},
8274
+ ...extras.starter !== void 0 ? { starter: extras.starter } : {}
8170
8275
  };
8171
8276
  await mkdir(join(input.root, ".stackbone"), { recursive: true });
8172
8277
  await writeFile(join(input.root, ".stackbone", "project.json"), JSON.stringify(config, null, 2));
@@ -8198,209 +8303,779 @@ var detectExistingProjectFiles = async (root, paths) => {
8198
8303
  return found;
8199
8304
  };
8200
8305
  //#endregion
8201
- //#region src/templates/autonomous/template.yaml?raw
8202
- var template_default$2 = "id: autonomous\nlabel: autonomous (mock)\nhint: ReAct loop with two tools. Mock LLM output until @stackbone/sdk lands.\norder: 3\nvariables:\n - agentName\n - agentSlug\n";
8306
+ //#region ../../starters/ai/package.json?raw
8307
+ var package_default$9 = "{\n \"name\": \"starter-ai\",\n \"version\": \"0.0.1\",\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"node --watch src/index.ts\",\n \"start\": \"node src/index.ts\"\n },\n \"engines\": {\n \"node\": \">=24\"\n },\n \"dependencies\": {\n \"@hono/node-server\": \"^1.13.0\",\n \"@stackbone/sdk\": \"^0.1.0-alpha.1\",\n \"hono\": \"^4.6.0\"\n },\n \"stackbone\": {\n \"label\": \"AI (LLM via OpenRouter)\",\n \"hint\": \"Demo of `client.ai` — chat completions and embeddings against any OpenRouter model.\",\n \"order\": 1,\n \"category\": \"Capabilities\"\n }\n}\n";
8203
8308
  //#endregion
8204
- //#region src/templates/hello-world/template.yaml?raw
8205
- var template_default$1 = "id: hello-world\nlabel: hello-world\nhint: Minimal Hono /invoke that echoes its input. Best to learn the contract.\norder: 1\nvariables:\n - agentName\n - agentSlug\n";
8309
+ //#region ../../starters/config/package.json?raw
8310
+ var package_default$8 = "{\n \"name\": \"starter-config\",\n \"version\": \"0.0.1\",\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"node --watch src/index.ts\",\n \"start\": \"node src/index.ts\"\n },\n \"engines\": {\n \"node\": \">=24\"\n },\n \"dependencies\": {\n \"@hono/node-server\": \"^1.13.0\",\n \"@stackbone/sdk\": \"^0.1.0-alpha.1\",\n \"hono\": \"^4.6.0\"\n },\n \"stackbone\": {\n \"label\": \"Config\",\n \"hint\": \"Demo of `client.config` — runtime config injected via `AGENT_CONFIG`.\",\n \"order\": 9,\n \"category\": \"Capabilities\"\n }\n}\n";
8206
8311
  //#endregion
8207
- //#region src/templates/workflow/template.yaml?raw
8208
- var template_default = "id: workflow\nlabel: workflow (mock)\nhint: Multi-node state machine. Mock LLM output until @stackbone/sdk lands.\norder: 2\nvariables:\n - agentName\n - agentSlug\n";
8312
+ //#region ../../starters/db/package.json?raw
8313
+ var package_default$7 = "{\n \"name\": \"starter-db\",\n \"version\": \"0.0.1\",\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"node --watch src/index.ts\",\n \"start\": \"node src/index.ts\"\n },\n \"engines\": {\n \"node\": \">=24\"\n },\n \"dependencies\": {\n \"@hono/node-server\": \"^1.13.0\",\n \"@stackbone/sdk\": \"^0.1.0-alpha.1\",\n \"hono\": \"^4.6.0\"\n },\n \"stackbone\": {\n \"label\": \"Database (Drizzle + Postgres)\",\n \"hint\": \"Demo of `client.database` — Drizzle ORM against per-agent Postgres.\",\n \"order\": 3,\n \"category\": \"Capabilities\"\n }\n}\n";
8209
8314
  //#endregion
8210
- //#region src/templates/autonomous/.stackbone/migrations/0000_init.sql?raw
8211
- var _0000_init_default$2 = "CREATE TABLE \"examples\" (\n \"id\" serial PRIMARY KEY NOT NULL,\n \"name\" text NOT NULL,\n \"created_at\" timestamp DEFAULT now() NOT NULL\n);\n";
8315
+ //#region ../../starters/hello-world/package.json?raw
8316
+ var package_default$6 = "{\n \"name\": \"starter-hello-world\",\n \"version\": \"0.0.1\",\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"node --watch src/index.ts\",\n \"start\": \"node src/index.ts\"\n },\n \"engines\": {\n \"node\": \">=24\"\n },\n \"dependencies\": {\n \"@hono/node-server\": \"^1.13.0\",\n \"@stackbone/sdk\": \"^0.1.0-alpha.1\",\n \"hono\": \"^4.6.0\"\n },\n \"stackbone\": {\n \"label\": \"Hello world\",\n \"hint\": \"Minimal Hono /invoke that echoes its input. Best place to learn the contract.\",\n \"order\": 1,\n \"category\": \"Core\"\n }\n}\n";
8212
8317
  //#endregion
8213
- //#region src/templates/autonomous/.stackbone/migrations/meta/0000_snapshot.json?raw
8214
- var _0000_snapshot_default$2 = "{\n \"id\": \"00000000-0000-4000-8000-000000000001\",\n \"prevId\": \"00000000-0000-0000-0000-000000000000\",\n \"version\": \"7\",\n \"dialect\": \"postgresql\",\n \"tables\": {\n \"public.examples\": {\n \"name\": \"examples\",\n \"schema\": \"\",\n \"columns\": {\n \"id\": {\n \"name\": \"id\",\n \"type\": \"serial\",\n \"primaryKey\": true,\n \"notNull\": true\n },\n \"name\": {\n \"name\": \"name\",\n \"type\": \"text\",\n \"primaryKey\": false,\n \"notNull\": true\n },\n \"created_at\": {\n \"name\": \"created_at\",\n \"type\": \"timestamp\",\n \"primaryKey\": false,\n \"notNull\": true,\n \"default\": \"now()\"\n }\n },\n \"indexes\": {},\n \"foreignKeys\": {},\n \"compositePrimaryKeys\": {},\n \"uniqueConstraints\": {},\n \"policies\": {},\n \"checkConstraints\": {},\n \"isRLSEnabled\": false\n }\n },\n \"enums\": {},\n \"schemas\": {},\n \"sequences\": {},\n \"roles\": {},\n \"policies\": {},\n \"views\": {},\n \"_meta\": {\n \"columns\": {},\n \"schemas\": {},\n \"tables\": {}\n }\n}\n";
8318
+ //#region ../../starters/hitl/package.json?raw
8319
+ var package_default$5 = "{\n \"name\": \"starter-hitl\",\n \"version\": \"0.0.1\",\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"node --watch src/index.ts\",\n \"start\": \"node src/index.ts\"\n },\n \"engines\": {\n \"node\": \">=24\"\n },\n \"dependencies\": {\n \"@hono/node-server\": \"^1.13.0\",\n \"@stackbone/sdk\": \"^0.1.0-alpha.1\",\n \"hono\": \"^4.6.0\"\n },\n \"stackbone\": {\n \"label\": \"Human-in-the-loop\",\n \"hint\": \"Demo of `client.approval` pause and wait for human approval before continuing.\",\n \"order\": 6,\n \"category\": \"Capabilities\"\n }\n}\n";
8215
8320
  //#endregion
8216
- //#region src/templates/autonomous/.stackbone/migrations/meta/_journal.json?raw
8217
- var _journal_default$2 = "{\n \"version\": \"7\",\n \"dialect\": \"postgresql\",\n \"entries\": [\n {\n \"idx\": 0,\n \"version\": \"7\",\n \"when\": 1778889700000,\n \"tag\": \"0000_init\",\n \"breakpoints\": true\n }\n ]\n}\n";
8321
+ //#region ../../starters/prompts/package.json?raw
8322
+ var package_default$4 = "{\n \"name\": \"starter-prompts\",\n \"version\": \"0.0.1\",\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"node --watch src/index.ts\",\n \"start\": \"node src/index.ts\"\n },\n \"engines\": {\n \"node\": \">=24\"\n },\n \"dependencies\": {\n \"@hono/node-server\": \"^1.13.0\",\n \"@stackbone/sdk\": \"^0.1.0-alpha.1\",\n \"hono\": \"^4.6.0\"\n },\n \"stackbone\": {\n \"label\": \"Prompts\",\n \"hint\": \"Demo of `client.prompts` — managed prompts with versioned templates.\",\n \"order\": 8,\n \"category\": \"Capabilities\"\n }\n}\n";
8218
8323
  //#endregion
8219
- //#region src/templates/autonomous/Dockerfile?raw
8220
- var Dockerfile_raw_default$2 = "# Distroless Node 24 image. Multi-stage so the runtime stays minimal.\nFROM node:24-alpine AS deps\nWORKDIR /app\nCOPY package.json ./\nRUN npm install --omit=dev\n\nFROM gcr.io/distroless/nodejs24-debian12\nWORKDIR /app\nCOPY --from=deps /app/node_modules ./node_modules\nCOPY src ./src\nCOPY agent.yaml ./\nENV PORT=8080\nEXPOSE 8080\nCMD [\"src/index.ts\"]\n";
8324
+ //#region ../../starters/queues/package.json?raw
8325
+ var package_default$3 = "{\n \"name\": \"starter-queues\",\n \"version\": \"0.0.1\",\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"node --watch src/index.ts\",\n \"start\": \"node src/index.ts\"\n },\n \"engines\": {\n \"node\": \">=24\"\n },\n \"dependencies\": {\n \"@hono/node-server\": \"^1.13.0\",\n \"@stackbone/sdk\": \"^0.1.0-alpha.1\",\n \"hono\": \"^4.6.0\"\n },\n \"stackbone\": {\n \"label\": \"Queues (QStash)\",\n \"hint\": \"Demo of `client.queues` QStash publisher + signed webhook receiver.\",\n \"order\": 5,\n \"category\": \"Capabilities\"\n }\n}\n";
8221
8326
  //#endregion
8222
- //#region src/templates/autonomous/README.md?raw
8223
- var README_default$2 = "# {{agentName}}\n\nStackbone agent scaffolded by `stackbone init`. The runtime contract is the\nHTTP surface in `src/index.ts`: `/invoke`, `/health`, `/schema`.\n\n## Develop\n\n```sh\npnpm install\npnpm dev # node --watch src/index.ts\n```\n\n## Deploy\n\n```sh\nstackbone deploy # builds the image and pushes it to Fly Machines\n```\n\nSee `stackbone docs sdk` for the SDK reference and `stackbone docs cli`\nfor the full command surface.\n";
8327
+ //#region ../../starters/rag/package.json?raw
8328
+ var package_default$2 = "{\n \"name\": \"starter-rag\",\n \"version\": \"0.0.1\",\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"node --watch src/index.ts\",\n \"start\": \"node src/index.ts\"\n },\n \"engines\": {\n \"node\": \">=24\"\n },\n \"dependencies\": {\n \"@hono/node-server\": \"^1.13.0\",\n \"@stackbone/sdk\": \"^0.1.0-alpha.1\",\n \"hono\": \"^4.6.0\"\n },\n \"stackbone\": {\n \"label\": \"RAG (pgvector)\",\n \"hint\": \"Demo of `client.rag` pgvector-backed retrieval-augmented generation.\",\n \"order\": 2,\n \"category\": \"Capabilities\"\n }\n}\n";
8224
8329
  //#endregion
8225
- //#region src/templates/autonomous/package.json?raw
8226
- var package_default$2 = "{\n \"name\": \"{{agentSlug}}\",\n \"version\": \"0.0.1\",\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"node --watch src/index.ts\",\n \"start\": \"node src/index.ts\"\n },\n \"engines\": {\n \"node\": \">=24\"\n },\n \"dependencies\": {\n \"@hono/node-server\": \"^1.13.0\",\n \"@stackbone/sdk\": \"^0.0.1\",\n \"hono\": \"^4.6.0\"\n }\n}\n";
8330
+ //#region ../../starters/secrets/package.json?raw
8331
+ var package_default$1 = "{\n \"name\": \"starter-secrets\",\n \"version\": \"0.0.1\",\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"node --watch src/index.ts\",\n \"start\": \"node src/index.ts\"\n },\n \"engines\": {\n \"node\": \">=24\"\n },\n \"dependencies\": {\n \"@hono/node-server\": \"^1.13.0\",\n \"@stackbone/sdk\": \"^0.1.0-alpha.1\",\n \"hono\": \"^4.6.0\"\n },\n \"stackbone\": {\n \"label\": \"Secrets\",\n \"hint\": \"Demo of `client.secrets` — workspace-scoped encrypted values.\",\n \"order\": 7,\n \"category\": \"Capabilities\"\n }\n}\n";
8227
8332
  //#endregion
8228
- //#region src/templates/autonomous/src/index.ts?raw
8229
- var src_default$2 = "import { serve } from '@hono/node-server';\nimport { Hono } from 'hono';\n\n// MOCK autonomous loop. In MVP this is a real ReAct loop driven by\n// `client.ai.completion` from `@stackbone/sdk`. For POC the agent runs a\n// fixed-iteration loop with two example tools so the contract works and\n// `stackbone dev` has something to invoke.\n\nconst MAX_ITERATIONS = 4;\n\nconst tools = {\n search: (query: string): string => `mocked search result for \"${query}\"`,\n summarise: (text: string): string => `summary: ${text.slice(0, 80)}`,\n};\n\ninterface Step {\n tool: keyof typeof tools;\n input: string;\n output: string;\n}\n\nconst runLoop = (goal: string): { goal: string; steps: Step[]; answer: string } => {\n const steps: Step[] = [];\n for (let i = 0; i < MAX_ITERATIONS && steps.length < 2; i++) {\n if (steps.length === 0) {\n const output = tools.search(goal);\n steps.push({ tool: 'search', input: goal, output });\n } else {\n const lastOutput = steps[steps.length - 1]?.output ?? '';\n const output = tools.summarise(lastOutput);\n steps.push({ tool: 'summarise', input: lastOutput, output });\n }\n }\n return {\n goal,\n steps,\n answer: steps[steps.length - 1]?.output ?? 'no steps executed',\n };\n};\n\nconst app = new Hono();\n\napp.post('/invoke', async (c) => {\n const body = await c.req.json().catch(() => ({ goal: '' }));\n return c.json(runLoop(typeof body.goal === 'string' ? body.goal : ''));\n});\n\napp.get('/health', (c) => c.json({ status: 'ok' }));\napp.get('/schema', (c) =>\n c.json({\n input: { type: 'object', properties: { goal: { type: 'string' } }, required: ['goal'] },\n output: {\n type: 'object',\n properties: {\n goal: { type: 'string' },\n steps: { type: 'array' },\n answer: { type: 'string' },\n },\n },\n }),\n);\n\nconst port = Number(process.env.PORT ?? 8080);\nserve({ fetch: app.fetch, port });\nconsole.log(`autonomous agent listening on :${port}`);\n";
8333
+ //#region ../../starters/storage/package.json?raw
8334
+ var package_default = "{\n \"name\": \"starter-storage\",\n \"version\": \"0.0.1\",\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"node --watch src/index.ts\",\n \"start\": \"node src/index.ts\"\n },\n \"engines\": {\n \"node\": \">=24\"\n },\n \"dependencies\": {\n \"@hono/node-server\": \"^1.13.0\",\n \"@stackbone/sdk\": \"^0.1.0-alpha.1\",\n \"hono\": \"^4.6.0\"\n },\n \"stackbone\": {\n \"label\": \"Object storage (S3)\",\n \"hint\": \"Demo of `client.storage` S3 / MinIO object storage scoped to the agent.\",\n \"order\": 4,\n \"category\": \"Capabilities\"\n }\n}\n";
8230
8335
  //#endregion
8231
- //#region src/templates/autonomous/src/schema.ts?raw
8232
- var schema_default$2 = "// Drizzle schema for {{agentName}}.\n//\n// Tables declared here are managed by `stackbone db migrate up`. Edit this\n// file, then run `stackbone db migrate create <name>` to emit the next SQL\n// migration under `.stackbone/migrations/`. The first migration shipped with\n// the template (`0000_init.sql`) creates the `examples` table below so the\n// agent has something queryable on its first `stackbone dev` boot.\n//\n// Drizzle column helpers re-export through `@stackbone/sdk/db` never import\n// from `drizzle-orm/pg-core` directly. The SDK barrel is the single contract\n// the platform supports.\nimport { pgTable, serial, text, timestamp } from '@stackbone/sdk/db';\n\nexport const examples = pgTable('examples', {\n id: serial('id').primaryKey(),\n name: text('name').notNull(),\n createdAt: timestamp('created_at').defaultNow().notNull(),\n});\n";
8336
+ //#region ../../starters/ai/.claude/skills/stackbone/SKILL.md?raw
8337
+ var SKILL_default$8 = "---\nname: stackbone\ndescription: Use this skill when the user is working with the Stackbone CLI, the `@stackbone/sdk` package, the `agent.yaml` manifest, official templates, or connections to the Stackbone control plane. Triggers on commands like `stackbone init`, `stackbone dev`, `stackbone deploy`, on questions about organization state, exit codes, or environment variables, and on any reference to `@stackbone/*` imports in TypeScript code. Provides the command surface, output contract, exit codes, environment variables and idiomatic workflows.\n---\n\n# Stackbone agent operating manual\n\nThis skill is regenerated by `stackbone init` / `stackbone link` and ships\ninside the CLI release 0.1.0-alpha.1. Edit the upstream template in\n`apps/cli/src/skills/templates.ts` if something here is wrong.\n\n## CLI surface\n\nThe CLI binary is `stackbone` (also reachable as `npx @stackbone/cli@alpha ...`). Run\n`stackbone docs cli` for the canonical command/flag reference, or\n`stackbone docs sdk` for the `@stackbone/sdk` overview neither command does\nnetwork IO.\n\n## Output contract\n\nEvery command supports `--json` (or `STACKBONE_JSON=1`). Successful JSON\nresponses look like:\n\n```json\n{ \"schema_version\": 1, \"...\": \"...\" }\n```\n\nErrors look like:\n\n```json\n{\n \"schema_version\": 1,\n \"error\": { \"code\": \"auth\", \"message\": \"...\", \"suggestion\": \"Run \\`stackbone login\\`\" },\n \"exit_code\": 2\n}\n```\n\n`schema_version` only bumps on backward-incompatible shape changes. If you\nsee a different value, refuse to parse and ask the user to upgrade.\n\n## Exit codes\n\n- `0` ok\n- `1` generic\n- `2` auth (no session, expired, refused login)\n- `3` no project linked (no `.stackbone/project.json` in cwd)\n- `4` not found (organization, agent, deployment)\n- `5` permission denied\n\nBranch on these instead of parsing stack traces.\n\n## Environment variables\n\n- `STACKBONE_JSON=1` — same as `--json`\n- `STACKBONE_API_URL` — same as `--api-url <url>`\n- `STACKBONE_AUTO_LOGIN=0` same as `--no-auto-login`\n- `STACKBONE_LOG_LEVEL` `debug`, `info`, `warn`, `error` (logger goes\n to stderr, never pollutes the JSON payload on stdout)\n\n## State on disk\n\n- `<cwd>/.stackbone/project.json` — organization + agent linked to this folder.\n Created by `stackbone init` / `link`.\n- `~/.stackbone/credentials.json` (chmod 600) — sessions keyed by control\n plane URL.\n- `~/.stackbone/config.json` — global preferences (default API URL,\n telemetry opt-in).\n\nThe control plane this skill points at: `https://api.stackbone.ai`.\nActive organization slug: `template1-2`.\n\n## Common workflows\n\n### Authenticate\n\n```sh\nstackbone login # interactive (device-code flow)\nstackbone whoami # confirm session\nstackbone metadata --json # one-shot overview that replaces 5–7 explorer calls\n```\n\n### Start a new project\n\n```sh\nstackbone init # menu of templates (workflow / autonomous / RAG)\nstackbone dev # local emulator at http://localhost:4242\n```\n\n### Deploy\n\n```sh\nstackbone deploy # build + push + Fly Machines\n```\n\n## Anti-patterns\n\n- Do not hardcode organization IDs, credentials or control plane URLs into\n agent code — the SDK reads them from the environment at runtime.\n- Do not parse human output. Re-run the command with `--json` and use\n `JSON.parse`.\n- Do not call the control plane HTTP API directly from a creator's agent.\n Use `@stackbone/sdk` so credential rotation, observability and metering\n stay correct.\n- Do not commit `.stackbone/` or `~/.stackbone/` — the entries are in the\n project's `.gitignore` for a reason.\n\n## Where to learn more\n\n- `stackbone docs cli` — command reference\n- `stackbone docs sdk` — `@stackbone/sdk` modules\n- `stackbone docs agent-yaml` — manifest reference (Epic 3)\n- `stackbone docs templates` — official templates (Epic 5)\n- `stackbone docs connections` — data layer (Epic 6)\n";
8233
8338
  //#endregion
8234
- //#region src/templates/hello-world/.stackbone/migrations/0000_init.sql?raw
8235
- var _0000_init_default$1 = "CREATE TABLE \"examples\" (\n \"id\" serial PRIMARY KEY NOT NULL,\n \"name\" text NOT NULL,\n \"created_at\" timestamp DEFAULT now() NOT NULL\n);\n";
8339
+ //#region ../../starters/ai/.gitignore?raw
8340
+ var _gitignore_raw_default$8 = "# >>> stackbone >>>\n.stackbone/\nnode_modules/\ndist/\n# <<< stackbone <<<\n";
8236
8341
  //#endregion
8237
- //#region src/templates/hello-world/.stackbone/migrations/meta/0000_snapshot.json?raw
8238
- var _0000_snapshot_default$1 = "{\n \"id\": \"00000000-0000-4000-8000-000000000001\",\n \"prevId\": \"00000000-0000-0000-0000-000000000000\",\n \"version\": \"7\",\n \"dialect\": \"postgresql\",\n \"tables\": {\n \"public.examples\": {\n \"name\": \"examples\",\n \"schema\": \"\",\n \"columns\": {\n \"id\": {\n \"name\": \"id\",\n \"type\": \"serial\",\n \"primaryKey\": true,\n \"notNull\": true\n },\n \"name\": {\n \"name\": \"name\",\n \"type\": \"text\",\n \"primaryKey\": false,\n \"notNull\": true\n },\n \"created_at\": {\n \"name\": \"created_at\",\n \"type\": \"timestamp\",\n \"primaryKey\": false,\n \"notNull\": true,\n \"default\": \"now()\"\n }\n },\n \"indexes\": {},\n \"foreignKeys\": {},\n \"compositePrimaryKeys\": {},\n \"uniqueConstraints\": {},\n \"policies\": {},\n \"checkConstraints\": {},\n \"isRLSEnabled\": false\n }\n },\n \"enums\": {},\n \"schemas\": {},\n \"sequences\": {},\n \"roles\": {},\n \"policies\": {},\n \"views\": {},\n \"_meta\": {\n \"columns\": {},\n \"schemas\": {},\n \"tables\": {}\n }\n}\n";
8342
+ //#region ../../starters/ai/Dockerfile?raw
8343
+ var Dockerfile_raw_default$9 = "# Distroless Node 24 image. Multi-stage so the runtime stays minimal.\nFROM node:24-alpine AS deps\nWORKDIR /app\nCOPY package.json ./\nRUN npm install --omit=dev\n\nFROM gcr.io/distroless/nodejs24-debian12\nWORKDIR /app\nCOPY --from=deps /app/node_modules ./node_modules\nCOPY src ./src\nCOPY agent.yaml ./\nENV PORT=8080\nEXPOSE 8080\nCMD [\"src/index.ts\"]\n";
8239
8344
  //#endregion
8240
- //#region src/templates/hello-world/.stackbone/migrations/meta/_journal.json?raw
8241
- var _journal_default$1 = "{\n \"version\": \"7\",\n \"dialect\": \"postgresql\",\n \"entries\": [\n {\n \"idx\": 0,\n \"version\": \"7\",\n \"when\": 1778889700000,\n \"tag\": \"0000_init\",\n \"breakpoints\": true\n }\n ]\n}\n";
8345
+ //#region ../../starters/ai/README.md?raw
8346
+ var README_default$9 = "# starter-ai\n\nDemo of `client.ai` (chat completions, embeddings, models) from `@stackbone/sdk`.\n\n`client.ai` proxies the OpenAI SDK against OpenRouter, so any model id\n(`openai/gpt-4o-mini`, `anthropic/claude-3.5-sonnet`, …) works.\n\n## Layout\n\n```\nsrc/\n index.ts # Hono server, mounts routes, logs every request\n lib/\n client.ts # Shared Stackbone client\n logger.ts # JSON line logger\n routes/\n health.ts # GET /health\n schema-info.ts # GET /schema\n invoke.ts # POST /invoke -> chat completion\n embeddings.ts # POST /embeddings -> create embedding(s)\n models.ts # GET /models -> list available models\n```\n\n## Run\n\n```sh\nnpm install\nstackbone dev\n```\n\nThen:\n\n```sh\ncurl http://127.0.0.1:8080/health\ncurl -X POST http://127.0.0.1:8080/invoke -H 'content-type: application/json' \\\n -d '{\"prompt\":\"Say hi in one short sentence.\"}'\ncurl -X POST http://127.0.0.1:8080/embeddings -H 'content-type: application/json' \\\n -d '{\"input\":\"the quick brown fox\"}'\ncurl http://127.0.0.1:8080/models\n```\n\nRequires `OPENROUTER_API_KEY` (or whichever upstream env the SDK falls back to)\nin the agent environment.\n";
8242
8347
  //#endregion
8243
- //#region src/templates/hello-world/Dockerfile?raw
8244
- var Dockerfile_raw_default$1 = "# Distroless Node 24 image. Multi-stage so the runtime stays minimal.\nFROM node:24-alpine AS deps\nWORKDIR /app\nCOPY package.json ./\nRUN npm install --omit=dev\n\nFROM gcr.io/distroless/nodejs24-debian12\nWORKDIR /app\nCOPY --from=deps /app/node_modules ./node_modules\nCOPY src ./src\nCOPY agent.yaml ./\nENV PORT=8080\nEXPOSE 8080\nCMD [\"src/index.ts\"]\n";
8348
+ //#region ../../starters/ai/agent.yaml?raw
8349
+ var agent_default$9 = "# Stackbone agent manifest. Schema: stackbone.ai/v1.\n# Run `stackbone docs agent-yaml` for the full reference.\n\napiVersion: stackbone.ai/v1\nname: starter-ai\nruntime:\n engine: node\n entry: src/index.ts\ndatabase:\n schema: ./src/schema.ts\n migrations: ./.stackbone/migrations\ndev:\n autoMigrate: false\nrag:\n embeddingModel: openai/text-embedding-3-small\n autoMigrate: false\n";
8350
+ //#endregion
8351
+ //#region ../../starters/ai/src/index.ts?raw
8352
+ var src_default$9 = "import { serve } from '@hono/node-server';\nimport { Hono } from 'hono';\n\nimport { log } from './lib/logger.ts';\nimport embeddings from './routes/embeddings.ts';\nimport health from './routes/health.ts';\nimport invoke from './routes/invoke.ts';\nimport models from './routes/models.ts';\nimport schemaInfo from './routes/schema-info.ts';\n\nconst app = new Hono();\n\napp.use('*', async (c, next) => {\n const start = Date.now();\n await next();\n log('info', 'request', {\n method: c.req.method,\n path: c.req.path,\n status: c.res.status,\n durationMs: Date.now() - start,\n });\n});\n\napp.route('/', health);\napp.route('/', schemaInfo);\napp.route('/', invoke);\napp.route('/', embeddings);\napp.route('/', models);\n\nconst port = Number(process.env.PORT ?? 8080);\nserve({ fetch: app.fetch, port });\nlog('info', 'starter-ai listening', { port });\n";
8353
+ //#endregion
8354
+ //#region ../../starters/ai/src/lib/client.ts?raw
8355
+ var client_default$8 = "import { createClient } from '@stackbone/sdk';\n\nexport const client = createClient({\n stackboneApiUrl: process.env.STACKBONE_DEV_API_URL ?? 'http://127.0.0.1:4242',\n});\n";
8356
+ //#endregion
8357
+ //#region ../../starters/ai/src/lib/logger.ts?raw
8358
+ var logger_default$8 = "type Level = 'info' | 'warn' | 'error';\n\nexport function log(level: Level, msg: string, fields?: Record<string, unknown>): void {\n const line = JSON.stringify({ ts: new Date().toISOString(), level, msg, ...fields });\n if (level === 'error') console.error(line);\n else console.log(line);\n}\n";
8359
+ //#endregion
8360
+ //#region ../../starters/ai/src/routes/embeddings.ts?raw
8361
+ var embeddings_default = "import { Hono } from 'hono';\n\nimport { client } from '../lib/client.ts';\nimport { log } from '../lib/logger.ts';\n\nconst route = new Hono();\n\nroute.post('/embeddings', async (c) => {\n const body = await c.req.json().catch(() => ({}) as { input?: string | string[]; model?: string });\n const input = body.input ?? '';\n const model = typeof body.model === 'string' ? body.model : 'openai/text-embedding-3-small';\n\n log('info', 'embeddings', { model, count: Array.isArray(input) ? input.length : 1 });\n\n const result = await client.ai.embeddings.create({ model, input });\n\n if (result.error) {\n log('error', 'embeddings failed', { code: result.error.code });\n return c.json({ error: result.error }, 502);\n }\n\n const vectors = result.data.data.map((d) => ({ index: d.index, dimensions: d.embedding.length }));\n log('info', 'embeddings done', { model, vectors: vectors.length });\n return c.json({ model, vectors });\n});\n\nexport default route;\n";
8362
+ //#endregion
8363
+ //#region ../../starters/ai/src/routes/health.ts?raw
8364
+ var health_default$8 = "import { Hono } from 'hono';\n\nconst route = new Hono();\nroute.get('/health', (c) => c.json({ status: 'ok' }));\nexport default route;\n";
8365
+ //#endregion
8366
+ //#region ../../starters/ai/src/routes/invoke.ts?raw
8367
+ var invoke_default$8 = "import { Hono } from 'hono';\n\nimport { client } from '../lib/client.ts';\nimport { log } from '../lib/logger.ts';\n\nconst route = new Hono();\n\nroute.post('/invoke', async (c) => {\n const body = await c.req.json().catch(() => ({}) as { prompt?: string; model?: string });\n const prompt = typeof body.prompt === 'string' ? body.prompt : 'Say hi in one short sentence.';\n const model = typeof body.model === 'string' ? body.model : 'openai/gpt-4o-mini';\n\n log('info', 'invoke', { model, prompt });\n\n const result = await client.ai.chat.completions.create({\n model,\n messages: [{ role: 'user', content: prompt }],\n });\n\n if (result.error) {\n log('error', 'chat completion failed', { code: result.error.code });\n return c.json({ error: result.error }, 502);\n }\n\n const reply = result.data.choices[0]?.message?.content ?? '';\n log('info', 'invoke reply', { model, length: reply.length });\n return c.json({ reply, model });\n});\n\nexport default route;\n";
8368
+ //#endregion
8369
+ //#region ../../starters/ai/src/routes/models.ts?raw
8370
+ var models_default = "import { Hono } from 'hono';\n\nimport { client } from '../lib/client.ts';\nimport { log } from '../lib/logger.ts';\n\nconst route = new Hono();\n\nroute.get('/models', async (c) => {\n log('info', 'list models');\n\n const result = await client.ai.models.list();\n if (result.error) {\n log('error', 'list models failed', { code: result.error.code });\n return c.json({ error: result.error }, 502);\n }\n\n const ids = result.data.data.slice(0, 50).map((m) => m.id);\n log('info', 'list models done', { total: result.data.data.length, returned: ids.length });\n return c.json({ total: result.data.data.length, ids });\n});\n\nexport default route;\n";
8371
+ //#endregion
8372
+ //#region ../../starters/ai/src/routes/schema-info.ts?raw
8373
+ var schema_info_default$8 = "import { Hono } from 'hono';\n\nconst route = new Hono();\n\nroute.get('/schema', (c) =>\n c.json({\n input: {\n type: 'object',\n properties: {\n prompt: { type: 'string' },\n model: { type: 'string' },\n },\n required: ['prompt'],\n },\n output: {\n type: 'object',\n properties: {\n reply: { type: 'string' },\n model: { type: 'string' },\n },\n },\n }),\n);\n\nexport default route;\n";
8374
+ //#endregion
8375
+ //#region ../../starters/config/.claude/skills/stackbone/SKILL.md?raw
8376
+ var SKILL_default$7 = "---\nname: stackbone\ndescription: Use this skill when the user is working with the Stackbone CLI, the `@stackbone/sdk` package, the `agent.yaml` manifest, official templates, or connections to the Stackbone control plane. Triggers on commands like `stackbone init`, `stackbone dev`, `stackbone deploy`, on questions about organization state, exit codes, or environment variables, and on any reference to `@stackbone/*` imports in TypeScript code. Provides the command surface, output contract, exit codes, environment variables and idiomatic workflows.\n---\n\n# Stackbone — agent operating manual\n\nThis skill is regenerated by `stackbone init` / `stackbone link` and ships\ninside the CLI release 0.1.0-alpha.1. Edit the upstream template in\n`apps/cli/src/skills/templates.ts` if something here is wrong.\n\n## CLI surface\n\nThe CLI binary is `stackbone` (also reachable as `npx @stackbone/cli@alpha ...`). Run\n`stackbone docs cli` for the canonical command/flag reference, or\n`stackbone docs sdk` for the `@stackbone/sdk` overview — neither command does\nnetwork IO.\n\n## Output contract\n\nEvery command supports `--json` (or `STACKBONE_JSON=1`). Successful JSON\nresponses look like:\n\n```json\n{ \"schema_version\": 1, \"...\": \"...\" }\n```\n\nErrors look like:\n\n```json\n{\n \"schema_version\": 1,\n \"error\": { \"code\": \"auth\", \"message\": \"...\", \"suggestion\": \"Run \\`stackbone login\\`\" },\n \"exit_code\": 2\n}\n```\n\n`schema_version` only bumps on backward-incompatible shape changes. If you\nsee a different value, refuse to parse and ask the user to upgrade.\n\n## Exit codes\n\n- `0` ok\n- `1` generic\n- `2` auth (no session, expired, refused login)\n- `3` no project linked (no `.stackbone/project.json` in cwd)\n- `4` not found (organization, agent, deployment)\n- `5` permission denied\n\nBranch on these instead of parsing stack traces.\n\n## Environment variables\n\n- `STACKBONE_JSON=1` — same as `--json`\n- `STACKBONE_API_URL` — same as `--api-url <url>`\n- `STACKBONE_AUTO_LOGIN=0` — same as `--no-auto-login`\n- `STACKBONE_LOG_LEVEL` — `debug`, `info`, `warn`, `error` (logger goes\n to stderr, never pollutes the JSON payload on stdout)\n\n## State on disk\n\n- `<cwd>/.stackbone/project.json` — organization + agent linked to this folder.\n Created by `stackbone init` / `link`.\n- `~/.stackbone/credentials.json` (chmod 600) — sessions keyed by control\n plane URL.\n- `~/.stackbone/config.json` — global preferences (default API URL,\n telemetry opt-in).\n\nThe control plane this skill points at: `https://api.stackbone.ai`.\nActive organization slug: `template1-2`.\n\n## Common workflows\n\n### Authenticate\n\n```sh\nstackbone login # interactive (device-code flow)\nstackbone whoami # confirm session\nstackbone metadata --json # one-shot overview that replaces 5–7 explorer calls\n```\n\n### Start a new project\n\n```sh\nstackbone init # menu of templates (workflow / autonomous / RAG)\nstackbone dev # local emulator at http://localhost:4242\n```\n\n### Deploy\n\n```sh\nstackbone deploy # build + push + Fly Machines\n```\n\n## Anti-patterns\n\n- Do not hardcode organization IDs, credentials or control plane URLs into\n agent code — the SDK reads them from the environment at runtime.\n- Do not parse human output. Re-run the command with `--json` and use\n `JSON.parse`.\n- Do not call the control plane HTTP API directly from a creator's agent.\n Use `@stackbone/sdk` so credential rotation, observability and metering\n stay correct.\n- Do not commit `.stackbone/` or `~/.stackbone/` — the entries are in the\n project's `.gitignore` for a reason.\n\n## Where to learn more\n\n- `stackbone docs cli` — command reference\n- `stackbone docs sdk` — `@stackbone/sdk` modules\n- `stackbone docs agent-yaml` — manifest reference (Epic 3)\n- `stackbone docs templates` — official templates (Epic 5)\n- `stackbone docs connections` — data layer (Epic 6)\n";
8377
+ //#endregion
8378
+ //#region ../../starters/config/.gitignore?raw
8379
+ var _gitignore_raw_default$7 = "# >>> stackbone >>>\n.stackbone/\nnode_modules/\ndist/\n# <<< stackbone <<<\n";
8380
+ //#endregion
8381
+ //#region ../../starters/config/Dockerfile?raw
8382
+ var Dockerfile_raw_default$8 = "# Distroless Node 24 image. Multi-stage so the runtime stays minimal.\nFROM node:24-alpine AS deps\nWORKDIR /app\nCOPY package.json ./\nRUN npm install --omit=dev\n\nFROM gcr.io/distroless/nodejs24-debian12\nWORKDIR /app\nCOPY --from=deps /app/node_modules ./node_modules\nCOPY src ./src\nCOPY agent.yaml ./\nENV PORT=8080\nEXPOSE 8080\nCMD [\"src/index.ts\"]\n";
8383
+ //#endregion
8384
+ //#region ../../starters/config/README.md?raw
8385
+ var README_default$8 = "# starter-config\n\nDemo of `client.config` (runtime config injected via `AGENT_CONFIG`) from\n`@stackbone/sdk`.\n\n## Layout\n\n```\nsrc/\n index.ts # Hono server\n lib/\n client.ts # Shared Stackbone client\n logger.ts # JSON line logger\n routes/\n health.ts # GET /health\n schema-info.ts # GET /schema\n invoke.ts # POST /invoke -> get one key (soft-miss on absence)\n get-config.ts # GET /config/:key\n get-many.ts # POST /config/bulk -> { values: { key: any } }\n```\n\n## Run\n\n```sh\nnpm install\nstackbone dev\n```\n\nThen:\n\n```sh\ncurl -X POST http://127.0.0.1:8080/invoke -H 'content-type: application/json' \\\n -d '{\"key\":\"feature_flags.new_ui\"}'\ncurl http://127.0.0.1:8080/config/api.timeout_ms\ncurl -X POST http://127.0.0.1:8080/config/bulk -H 'content-type: application/json' \\\n -d '{\"keys\":[\"api.timeout_ms\",\"feature_flags.new_ui\"]}'\n```\n";
8386
+ //#endregion
8387
+ //#region ../../starters/config/agent.yaml?raw
8388
+ var agent_default$8 = "# Stackbone agent manifest. Schema: stackbone.ai/v1.\n# Run `stackbone docs agent-yaml` for the full reference.\n\napiVersion: stackbone.ai/v1\nname: starter-config\nruntime:\n engine: node\n entry: src/index.ts\ndatabase:\n schema: ./src/schema.ts\n migrations: ./.stackbone/migrations\ndev:\n autoMigrate: false\nrag:\n embeddingModel: openai/text-embedding-3-small\n autoMigrate: false\n";
8389
+ //#endregion
8390
+ //#region ../../starters/config/src/index.ts?raw
8391
+ var src_default$8 = "import { serve } from '@hono/node-server';\nimport { Hono } from 'hono';\n\nimport { log } from './lib/logger.ts';\nimport getConfig from './routes/get-config.ts';\nimport getMany from './routes/get-many.ts';\nimport health from './routes/health.ts';\nimport invoke from './routes/invoke.ts';\nimport schemaInfo from './routes/schema-info.ts';\n\nconst app = new Hono();\n\napp.use('*', async (c, next) => {\n const start = Date.now();\n await next();\n log('info', 'request', {\n method: c.req.method,\n path: c.req.path,\n status: c.res.status,\n durationMs: Date.now() - start,\n });\n});\n\napp.route('/', health);\napp.route('/', schemaInfo);\napp.route('/', invoke);\napp.route('/', getConfig);\napp.route('/', getMany);\n\nconst port = Number(process.env.PORT ?? 8080);\nserve({ fetch: app.fetch, port });\nlog('info', 'starter-config listening', { port });\n";
8392
+ //#endregion
8393
+ //#region ../../starters/config/src/lib/client.ts?raw
8394
+ var client_default$7 = "import { createClient } from '@stackbone/sdk';\n\nexport const client = createClient({\n stackboneApiUrl: process.env.STACKBONE_DEV_API_URL ?? 'http://127.0.0.1:4242',\n});\n";
8395
+ //#endregion
8396
+ //#region ../../starters/config/src/lib/logger.ts?raw
8397
+ var logger_default$7 = "type Level = 'info' | 'warn' | 'error';\n\nexport function log(level: Level, msg: string, fields?: Record<string, unknown>): void {\n const line = JSON.stringify({ ts: new Date().toISOString(), level, msg, ...fields });\n if (level === 'error') console.error(line);\n else console.log(line);\n}\n";
8398
+ //#endregion
8399
+ //#region ../../starters/config/src/routes/get-config.ts?raw
8400
+ var get_config_default = "import { Hono } from 'hono';\n\nimport { client } from '../lib/client.ts';\nimport { log } from '../lib/logger.ts';\n\nconst route = new Hono();\n\nroute.get('/config/:key', async (c) => {\n const key = c.req.param('key');\n log('info', 'fetch config', { key });\n\n const result = await client.config.get(key);\n if (result.error) {\n log('error', 'fetch config failed', { key, code: result.error.code });\n return c.json({ error: result.error }, 502);\n }\n\n return c.json({ key, value: result.data });\n});\n\nexport default route;\n";
8401
+ //#endregion
8402
+ //#region ../../starters/config/src/routes/get-many.ts?raw
8403
+ var get_many_default$1 = "import { Hono } from 'hono';\n\nimport { client } from '../lib/client.ts';\nimport { log } from '../lib/logger.ts';\n\nconst route = new Hono();\n\nroute.post('/config/bulk', async (c) => {\n const body = await c.req.json().catch(() => ({}) as { keys?: string[] });\n const keys = Array.isArray(body.keys) ? body.keys : [];\n if (keys.length === 0) return c.json({ error: 'missing keys[]' }, 400);\n\n log('info', 'config bulk', { count: keys.length });\n\n const result = await client.config.getMany(keys);\n if (result.error) {\n log('error', 'config bulk failed', { code: result.error.code });\n return c.json({ error: result.error }, 502);\n }\n\n log('info', 'config bulk done', { resolved: Object.keys(result.data).length });\n return c.json({ values: result.data });\n});\n\nexport default route;\n";
8404
+ //#endregion
8405
+ //#region ../../starters/config/src/routes/health.ts?raw
8406
+ var health_default$7 = "import { Hono } from 'hono';\n\nconst route = new Hono();\nroute.get('/health', (c) => c.json({ status: 'ok' }));\nexport default route;\n";
8407
+ //#endregion
8408
+ //#region ../../starters/config/src/routes/invoke.ts?raw
8409
+ var invoke_default$7 = "import { Hono } from 'hono';\n\nimport { client } from '../lib/client.ts';\nimport { log } from '../lib/logger.ts';\n\nconst route = new Hono();\n\nroute.post('/invoke', async (c) => {\n const body = await c.req.json().catch(() => ({}) as { key?: string });\n const key = typeof body.key === 'string' ? body.key : '';\n if (!key) return c.json({ error: 'missing key' }, 400);\n\n log('info', 'config get', { key });\n const result = await client.config.get(key);\n\n if (result.error) {\n log('warn', 'config miss', { key, code: result.error.code });\n return c.json({ key, value: null, code: result.error.code });\n }\n\n log('info', 'config hit', { key });\n return c.json({ key, value: result.data });\n});\n\nexport default route;\n";
8410
+ //#endregion
8411
+ //#region ../../starters/config/src/routes/schema-info.ts?raw
8412
+ var schema_info_default$7 = "import { Hono } from 'hono';\n\nconst route = new Hono();\n\nroute.get('/schema', (c) =>\n c.json({\n input: { type: 'object', properties: { key: { type: 'string' } }, required: ['key'] },\n output: { type: 'object', properties: { key: { type: 'string' }, value: {} } },\n }),\n);\n\nexport default route;\n";
8413
+ //#endregion
8414
+ //#region ../../starters/db/.claude/skills/stackbone/SKILL.md?raw
8415
+ var SKILL_default$6 = "---\nname: stackbone\ndescription: Use this skill when the user is working with the Stackbone CLI, the `@stackbone/sdk` package, the `agent.yaml` manifest, official templates, or connections to the Stackbone control plane. Triggers on commands like `stackbone init`, `stackbone dev`, `stackbone deploy`, on questions about organization state, exit codes, or environment variables, and on any reference to `@stackbone/*` imports in TypeScript code. Provides the command surface, output contract, exit codes, environment variables and idiomatic workflows.\n---\n\n# Stackbone — agent operating manual\n\nThis skill is regenerated by `stackbone init` / `stackbone link` and ships\ninside the CLI release 0.1.0-alpha.1. Edit the upstream template in\n`apps/cli/src/skills/templates.ts` if something here is wrong.\n\n## CLI surface\n\nThe CLI binary is `stackbone` (also reachable as `npx @stackbone/cli@alpha ...`). Run\n`stackbone docs cli` for the canonical command/flag reference, or\n`stackbone docs sdk` for the `@stackbone/sdk` overview — neither command does\nnetwork IO.\n\n## Output contract\n\nEvery command supports `--json` (or `STACKBONE_JSON=1`). Successful JSON\nresponses look like:\n\n```json\n{ \"schema_version\": 1, \"...\": \"...\" }\n```\n\nErrors look like:\n\n```json\n{\n \"schema_version\": 1,\n \"error\": { \"code\": \"auth\", \"message\": \"...\", \"suggestion\": \"Run \\`stackbone login\\`\" },\n \"exit_code\": 2\n}\n```\n\n`schema_version` only bumps on backward-incompatible shape changes. If you\nsee a different value, refuse to parse and ask the user to upgrade.\n\n## Exit codes\n\n- `0` ok\n- `1` generic\n- `2` auth (no session, expired, refused login)\n- `3` no project linked (no `.stackbone/project.json` in cwd)\n- `4` not found (organization, agent, deployment)\n- `5` permission denied\n\nBranch on these instead of parsing stack traces.\n\n## Environment variables\n\n- `STACKBONE_JSON=1` — same as `--json`\n- `STACKBONE_API_URL` — same as `--api-url <url>`\n- `STACKBONE_AUTO_LOGIN=0` — same as `--no-auto-login`\n- `STACKBONE_LOG_LEVEL` — `debug`, `info`, `warn`, `error` (logger goes\n to stderr, never pollutes the JSON payload on stdout)\n\n## State on disk\n\n- `<cwd>/.stackbone/project.json` — organization + agent linked to this folder.\n Created by `stackbone init` / `link`.\n- `~/.stackbone/credentials.json` (chmod 600) — sessions keyed by control\n plane URL.\n- `~/.stackbone/config.json` — global preferences (default API URL,\n telemetry opt-in).\n\nThe control plane this skill points at: `https://api.stackbone.ai`.\nActive organization slug: `template1-2`.\n\n## Common workflows\n\n### Authenticate\n\n```sh\nstackbone login # interactive (device-code flow)\nstackbone whoami # confirm session\nstackbone metadata --json # one-shot overview that replaces 5–7 explorer calls\n```\n\n### Start a new project\n\n```sh\nstackbone init # menu of templates (workflow / autonomous / RAG)\nstackbone dev # local emulator at http://localhost:4242\n```\n\n### Deploy\n\n```sh\nstackbone deploy # build + push + Fly Machines\n```\n\n## Anti-patterns\n\n- Do not hardcode organization IDs, credentials or control plane URLs into\n agent code — the SDK reads them from the environment at runtime.\n- Do not parse human output. Re-run the command with `--json` and use\n `JSON.parse`.\n- Do not call the control plane HTTP API directly from a creator's agent.\n Use `@stackbone/sdk` so credential rotation, observability and metering\n stay correct.\n- Do not commit `.stackbone/` or `~/.stackbone/` — the entries are in the\n project's `.gitignore` for a reason.\n\n## Where to learn more\n\n- `stackbone docs cli` — command reference\n- `stackbone docs sdk` — `@stackbone/sdk` modules\n- `stackbone docs agent-yaml` — manifest reference (Epic 3)\n- `stackbone docs templates` — official templates (Epic 5)\n- `stackbone docs connections` — data layer (Epic 6)\n";
8416
+ //#endregion
8417
+ //#region ../../starters/db/.gitignore?raw
8418
+ var _gitignore_raw_default$6 = "# >>> stackbone >>>\n.stackbone/\nnode_modules/\ndist/\n# <<< stackbone <<<\n";
8419
+ //#endregion
8420
+ //#region ../../starters/db/Dockerfile?raw
8421
+ var Dockerfile_raw_default$7 = "# Distroless Node 24 image. Multi-stage so the runtime stays minimal.\nFROM node:24-alpine AS deps\nWORKDIR /app\nCOPY package.json ./\nRUN npm install --omit=dev\n\nFROM gcr.io/distroless/nodejs24-debian12\nWORKDIR /app\nCOPY --from=deps /app/node_modules ./node_modules\nCOPY src ./src\nCOPY agent.yaml ./\nENV PORT=8080\nEXPOSE 8080\nCMD [\"src/index.ts\"]\n";
8422
+ //#endregion
8423
+ //#region ../../starters/db/README.md?raw
8424
+ var README_default$7 = "# starter-db\n\nDemo of `client.database` (Drizzle + Postgres) from `@stackbone/sdk`.\n\n## Layout\n\n```\nsrc/\n index.ts # Hono server, mounts routes, logs every request\n schema.ts # Drizzle table definitions\n lib/\n client.ts # Shared Stackbone client\n logger.ts # JSON line logger (captured by stackbone dev)\n routes/\n health.ts # GET /health\n schema-info.ts # GET /schema\n invoke.ts # POST /invoke -> insert example\n list-examples.ts # GET /examples -> list latest 20\n delete-example.ts # DELETE /examples/:id\n```\n\n## Run\n\n```sh\nnpm install\nstackbone dev\n```\n\nThen:\n\n```sh\ncurl http://127.0.0.1:8080/health\ncurl -X POST http://127.0.0.1:8080/invoke -d '{\"name\":\"luis\"}' -H 'content-type: application/json'\ncurl http://127.0.0.1:8080/examples\ncurl -X DELETE http://127.0.0.1:8080/examples/1\n```\n";
8425
+ //#endregion
8426
+ //#region ../../starters/db/agent.yaml?raw
8427
+ var agent_default$7 = "# Stackbone agent manifest. Schema: stackbone.ai/v1.\n# Run `stackbone docs agent-yaml` for the full reference.\n\napiVersion: stackbone.ai/v1\nname: starter-db\nruntime:\n engine: node\n entry: src/index.ts\ndatabase:\n schema: ./src/schema.ts\n migrations: ./.stackbone/migrations\ndev:\n autoMigrate: false\nrag:\n embeddingModel: openai/text-embedding-3-small\n autoMigrate: false\n";
8428
+ //#endregion
8429
+ //#region ../../starters/db/src/index.ts?raw
8430
+ var src_default$7 = "import { serve } from '@hono/node-server';\nimport { Hono } from 'hono';\n\nimport { log } from './lib/logger.ts';\nimport deleteExample from './routes/delete-example.ts';\nimport health from './routes/health.ts';\nimport invoke from './routes/invoke.ts';\nimport listExamples from './routes/list-examples.ts';\nimport schemaInfo from './routes/schema-info.ts';\n\nconst app = new Hono();\n\napp.use('*', async (c, next) => {\n const start = Date.now();\n await next();\n log('info', 'request', {\n method: c.req.method,\n path: c.req.path,\n status: c.res.status,\n durationMs: Date.now() - start,\n });\n});\n\napp.route('/', health);\napp.route('/', schemaInfo);\napp.route('/', invoke);\napp.route('/', listExamples);\napp.route('/', deleteExample);\n\nconst port = Number(process.env.PORT ?? 8080);\nserve({ fetch: app.fetch, port });\nlog('info', 'starter-db listening', { port });\n";
8431
+ //#endregion
8432
+ //#region ../../starters/db/src/lib/client.ts?raw
8433
+ var client_default$6 = "import { createClient } from '@stackbone/sdk';\n\n// Shared Stackbone client. Every route imports it from here so we open one\n// pool against STACKBONE_POSTGRES_URL and reuse the same datapath handle.\nexport const client = createClient({\n stackboneApiUrl: process.env.STACKBONE_DEV_API_URL ?? 'http://127.0.0.1:4242',\n});\n";
8245
8434
  //#endregion
8246
- //#region src/templates/hello-world/README.md?raw
8247
- var README_default$1 = "# {{agentName}}\n\nStackbone agent scaffolded by `stackbone init`. The runtime contract is the\nHTTP surface in `src/index.ts`: `/invoke`, `/health`, `/schema`.\n\n## Develop\n\n```sh\npnpm install\npnpm dev # node --watch src/index.ts\n```\n\n## Deploy\n\n```sh\nstackbone deploy # builds the image and pushes it to Fly Machines\n```\n\nSee `stackbone docs sdk` for the SDK reference and `stackbone docs cli`\nfor the full command surface.\n";
8435
+ //#region ../../starters/db/src/lib/logger.ts?raw
8436
+ var logger_default$6 = "// Minimal JSON logger. Every line is captured by `stackbone dev` and surfaced\n// in Stackbone Studio. Keep one log per route entry/exit so traces are easy\n// to follow.\n\ntype Level = 'info' | 'warn' | 'error';\n\nexport function log(level: Level, msg: string, fields?: Record<string, unknown>): void {\n const record = { ts: new Date().toISOString(), level, msg, ...fields };\n const line = JSON.stringify(record);\n if (level === 'error') console.error(line);\n else console.log(line);\n}\n";
8248
8437
  //#endregion
8249
- //#region src/templates/hello-world/package.json?raw
8250
- var package_default$1 = "{\n \"name\": \"{{agentSlug}}\",\n \"version\": \"0.0.1\",\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"node --watch src/index.ts\",\n \"start\": \"node src/index.ts\"\n },\n \"engines\": {\n \"node\": \">=24\"\n },\n \"dependencies\": {\n \"@hono/node-server\": \"^1.13.0\",\n \"@stackbone/sdk\": \"^0.0.1\",\n \"hono\": \"^4.6.0\"\n }\n}\n";
8438
+ //#region ../../starters/db/src/routes/delete-example.ts?raw
8439
+ var delete_example_default = "import { eq } from '@stackbone/sdk/db';\nimport { Hono } from 'hono';\n\nimport { client } from '../lib/client.ts';\nimport { log } from '../lib/logger.ts';\nimport { examples } from '../schema.ts';\n\nconst route = new Hono();\n\nroute.delete('/examples/:id', async (c) => {\n const id = Number(c.req.param('id'));\n if (!Number.isInteger(id)) {\n log('warn', 'delete example: invalid id', { id: c.req.param('id') });\n return c.json({ error: 'invalid id' }, 400);\n }\n\n log('info', 'delete example', { id });\n const deleted = await client.database\n .delete(examples)\n .where(eq(examples.id, id))\n .returning();\n\n return c.json({ deleted });\n});\n\nexport default route;\n";
8251
8440
  //#endregion
8252
- //#region src/templates/hello-world/src/index.ts?raw
8253
- var src_default$1 = "import { serve } from '@hono/node-server';\nimport { Hono } from 'hono';\n\nconst app = new Hono();\n\napp.post('/invoke', async (c) => {\n const body = await c.req.json().catch(() => ({}));\n return c.json({ hello: body.who ?? 'world' });\n});\n\napp.get('/health', (c) => c.json({ status: 'ok' }));\n\napp.get('/schema', (c) =>\n c.json({\n input: { type: 'object', properties: { who: { type: 'string' } } },\n output: { type: 'object', properties: { hello: { type: 'string' } } },\n }),\n);\n\nconst port = Number(process.env.PORT ?? 8080);\nserve({ fetch: app.fetch, port });\nconsole.log(`hello-world agent listening on :${port}`);\n";
8441
+ //#region ../../starters/db/src/routes/health.ts?raw
8442
+ var health_default$6 = "import { Hono } from 'hono';\n\nimport { log } from '../lib/logger.ts';\n\nconst route = new Hono();\n\nroute.get('/health', (c) => {\n log('info', 'health check');\n return c.json({ status: 'ok' });\n});\n\nexport default route;\n";
8254
8443
  //#endregion
8255
- //#region src/templates/hello-world/src/schema.ts?raw
8256
- var schema_default$1 = "// Drizzle schema for {{agentName}}.\n//\n// Tables declared here are managed by `stackbone db migrate up`. Edit this\n// file, then run `stackbone db migrate create <name>` to emit the next SQL\n// migration under `.stackbone/migrations/`. The first migration shipped with\n// the template (`0000_init.sql`) creates the `examples` table below so the\n// agent has something queryable on its first `stackbone dev` boot.\n//\n// Drizzle column helpers re-export through `@stackbone/sdk/db` — never import\n// from `drizzle-orm/pg-core` directly. The SDK barrel is the single contract\n// the platform supports.\nimport { pgTable, serial, text, timestamp } from '@stackbone/sdk/db';\n\nexport const examples = pgTable('examples', {\n id: serial('id').primaryKey(),\n name: text('name').notNull(),\n createdAt: timestamp('created_at').defaultNow().notNull(),\n});\n";
8444
+ //#region ../../starters/db/src/routes/invoke.ts?raw
8445
+ var invoke_default$6 = "import { Hono } from 'hono';\n\nimport { client } from '../lib/client.ts';\nimport { log } from '../lib/logger.ts';\nimport { examples } from '../schema.ts';\n\nconst route = new Hono();\n\nroute.post('/invoke', async (c) => {\n const body = await c.req.json().catch(() => ({}) as { name?: string });\n const name = typeof body.name === 'string' ? body.name : 'anonymous';\n\n log('info', 'invoke received', { name });\n const [row] = await client.database.insert(examples).values({ name }).returning();\n log('info', 'invoke inserted', { id: row?.id, name });\n\n return c.json({ inserted: row });\n});\n\nexport default route;\n";
8257
8446
  //#endregion
8258
- //#region src/templates/workflow/.stackbone/migrations/0000_init.sql?raw
8447
+ //#region ../../starters/db/src/routes/list-examples.ts?raw
8448
+ var list_examples_default = "import { desc } from '@stackbone/sdk/db';\nimport { Hono } from 'hono';\n\nimport { client } from '../lib/client.ts';\nimport { log } from '../lib/logger.ts';\nimport { examples } from '../schema.ts';\n\nconst route = new Hono();\n\nroute.get('/examples', async (c) => {\n const limit = Number(c.req.query('limit') ?? 20);\n log('info', 'list examples', { limit });\n\n const rows = await client.database\n .select()\n .from(examples)\n .orderBy(desc(examples.id))\n .limit(limit);\n\n return c.json({ rows });\n});\n\nexport default route;\n";
8449
+ //#endregion
8450
+ //#region ../../starters/db/src/routes/schema-info.ts?raw
8451
+ var schema_info_default$6 = "import { Hono } from 'hono';\n\nconst route = new Hono();\n\nroute.get('/schema', (c) =>\n c.json({\n input: {\n type: 'object',\n properties: { name: { type: 'string' } },\n required: ['name'],\n },\n output: {\n type: 'object',\n properties: {\n id: { type: 'number' },\n name: { type: 'string' },\n createdAt: { type: 'string' },\n },\n },\n }),\n);\n\nexport default route;\n";
8452
+ //#endregion
8453
+ //#region ../../starters/db/src/schema.ts?raw
8454
+ var schema_default$1 = "// Drizzle schema for template1.\n//\n// Tables declared here are managed by `stackbone db migrate up`. Edit this\n// file, then run `stackbone db migrate create <name>` to emit the next SQL\n// migration under `.stackbone/migrations/`. The first migration shipped with\n// the template (`0000_init.sql`) creates the `examples` table below so the\n// agent has something queryable on its first `stackbone dev` boot.\n//\n// Drizzle column helpers re-export through `@stackbone/sdk/db` — never import\n// from `drizzle-orm/pg-core` directly. The SDK barrel is the single contract\n// the platform supports.\nimport { pgTable, serial, text, timestamp } from '@stackbone/sdk/db';\n\nexport const examples = pgTable('examples', {\n id: serial('id').primaryKey(),\n name: text('name').notNull(),\n createdAt: timestamp('created_at').defaultNow().notNull(),\n});\n";
8455
+ //#endregion
8456
+ //#region ../../starters/hello-world/.stackbone/migrations/0000_init.sql?raw
8259
8457
  var _0000_init_default = "CREATE TABLE \"examples\" (\n \"id\" serial PRIMARY KEY NOT NULL,\n \"name\" text NOT NULL,\n \"created_at\" timestamp DEFAULT now() NOT NULL\n);\n";
8260
8458
  //#endregion
8261
- //#region src/templates/workflow/.stackbone/migrations/meta/0000_snapshot.json?raw
8459
+ //#region ../../starters/hello-world/.stackbone/migrations/meta/0000_snapshot.json?raw
8262
8460
  var _0000_snapshot_default = "{\n \"id\": \"00000000-0000-4000-8000-000000000001\",\n \"prevId\": \"00000000-0000-0000-0000-000000000000\",\n \"version\": \"7\",\n \"dialect\": \"postgresql\",\n \"tables\": {\n \"public.examples\": {\n \"name\": \"examples\",\n \"schema\": \"\",\n \"columns\": {\n \"id\": {\n \"name\": \"id\",\n \"type\": \"serial\",\n \"primaryKey\": true,\n \"notNull\": true\n },\n \"name\": {\n \"name\": \"name\",\n \"type\": \"text\",\n \"primaryKey\": false,\n \"notNull\": true\n },\n \"created_at\": {\n \"name\": \"created_at\",\n \"type\": \"timestamp\",\n \"primaryKey\": false,\n \"notNull\": true,\n \"default\": \"now()\"\n }\n },\n \"indexes\": {},\n \"foreignKeys\": {},\n \"compositePrimaryKeys\": {},\n \"uniqueConstraints\": {},\n \"policies\": {},\n \"checkConstraints\": {},\n \"isRLSEnabled\": false\n }\n },\n \"enums\": {},\n \"schemas\": {},\n \"sequences\": {},\n \"roles\": {},\n \"policies\": {},\n \"views\": {},\n \"_meta\": {\n \"columns\": {},\n \"schemas\": {},\n \"tables\": {}\n }\n}\n";
8263
8461
  //#endregion
8264
- //#region src/templates/workflow/.stackbone/migrations/meta/_journal.json?raw
8462
+ //#region ../../starters/hello-world/.stackbone/migrations/meta/_journal.json?raw
8265
8463
  var _journal_default = "{\n \"version\": \"7\",\n \"dialect\": \"postgresql\",\n \"entries\": [\n {\n \"idx\": 0,\n \"version\": \"7\",\n \"when\": 1778889700000,\n \"tag\": \"0000_init\",\n \"breakpoints\": true\n }\n ]\n}\n";
8266
8464
  //#endregion
8267
- //#region src/templates/workflow/Dockerfile?raw
8465
+ //#region ../../starters/hello-world/Dockerfile?raw
8466
+ var Dockerfile_raw_default$6 = "# Distroless Node 24 image. Multi-stage so the runtime stays minimal.\nFROM node:24-alpine AS deps\nWORKDIR /app\nCOPY package.json ./\nRUN npm install --omit=dev\n\nFROM gcr.io/distroless/nodejs24-debian12\nWORKDIR /app\nCOPY --from=deps /app/node_modules ./node_modules\nCOPY src ./src\nCOPY agent.yaml ./\nENV PORT=8080\nEXPOSE 8080\nCMD [\"src/index.ts\"]\n";
8467
+ //#endregion
8468
+ //#region ../../starters/hello-world/README.md?raw
8469
+ var README_default$6 = "# starter-hello-world\n\nStackbone agent scaffolded by `stackbone init`. The runtime contract is the\nHTTP surface in `src/index.ts`: `/invoke`, `/health`, `/schema`.\n\n## Develop\n\n```sh\npnpm install\npnpm dev # node --watch src/index.ts\n```\n\n## Deploy\n\n```sh\nstackbone deploy # builds the image and pushes it to Fly Machines\n```\n\nSee `stackbone docs sdk` for the SDK reference and `stackbone docs cli`\nfor the full command surface.\n";
8470
+ //#endregion
8471
+ //#region ../../starters/hello-world/agent.yaml?raw
8472
+ var agent_default$6 = "# Stackbone agent manifest. Schema: stackbone.ai/v1.\n# Run `stackbone docs agent-yaml` for the full reference.\n\napiVersion: stackbone.ai/v1\nname: starter-hello-world\nruntime:\n engine: node\n entry: src/index.ts\n";
8473
+ //#endregion
8474
+ //#region ../../starters/hello-world/src/index.ts?raw
8475
+ var src_default$6 = "import { serve } from '@hono/node-server';\nimport { Hono } from 'hono';\n\nconst app = new Hono();\n\napp.post('/invoke', async (c) => {\n const body = await c.req.json().catch(() => ({}));\n return c.json({ hello: body.who ?? 'world' });\n});\n\napp.get('/health', (c) => c.json({ status: 'ok' }));\n\napp.get('/schema', (c) =>\n c.json({\n input: { type: 'object', properties: { who: { type: 'string' } } },\n output: { type: 'object', properties: { hello: { type: 'string' } } },\n }),\n);\n\nconst port = Number(process.env.PORT ?? 8080);\nserve({ fetch: app.fetch, port });\nconsole.log(`hello-world agent listening on :${port}`);\n";
8476
+ //#endregion
8477
+ //#region ../../starters/hello-world/src/schema.ts?raw
8478
+ var schema_default = "// Drizzle schema for starter-hello-world.\n//\n// Tables declared here are managed by `stackbone db migrate up`. Edit this\n// file, then run `stackbone db migrate create <name>` to emit the next SQL\n// migration under `.stackbone/migrations/`. The first migration shipped with\n// the template (`0000_init.sql`) creates the `examples` table below so the\n// agent has something queryable on its first `stackbone dev` boot.\n//\n// Drizzle column helpers re-export through `@stackbone/sdk/db` — never import\n// from `drizzle-orm/pg-core` directly. The SDK barrel is the single contract\n// the platform supports.\nimport { pgTable, serial, text, timestamp } from '@stackbone/sdk/db';\n\nexport const examples = pgTable('examples', {\n id: serial('id').primaryKey(),\n name: text('name').notNull(),\n createdAt: timestamp('created_at').defaultNow().notNull(),\n});\n";
8479
+ //#endregion
8480
+ //#region ../../starters/hitl/.claude/skills/stackbone/SKILL.md?raw
8481
+ var SKILL_default$5 = "---\nname: stackbone\ndescription: Use this skill when the user is working with the Stackbone CLI, the `@stackbone/sdk` package, the `agent.yaml` manifest, official templates, or connections to the Stackbone control plane. Triggers on commands like `stackbone init`, `stackbone dev`, `stackbone deploy`, on questions about organization state, exit codes, or environment variables, and on any reference to `@stackbone/*` imports in TypeScript code. Provides the command surface, output contract, exit codes, environment variables and idiomatic workflows.\n---\n\n# Stackbone — agent operating manual\n\nThis skill is regenerated by `stackbone init` / `stackbone link` and ships\ninside the CLI release 0.1.0-alpha.1. Edit the upstream template in\n`apps/cli/src/skills/templates.ts` if something here is wrong.\n\n## CLI surface\n\nThe CLI binary is `stackbone` (also reachable as `npx @stackbone/cli@alpha ...`). Run\n`stackbone docs cli` for the canonical command/flag reference, or\n`stackbone docs sdk` for the `@stackbone/sdk` overview — neither command does\nnetwork IO.\n\n## Output contract\n\nEvery command supports `--json` (or `STACKBONE_JSON=1`). Successful JSON\nresponses look like:\n\n```json\n{ \"schema_version\": 1, \"...\": \"...\" }\n```\n\nErrors look like:\n\n```json\n{\n \"schema_version\": 1,\n \"error\": { \"code\": \"auth\", \"message\": \"...\", \"suggestion\": \"Run \\`stackbone login\\`\" },\n \"exit_code\": 2\n}\n```\n\n`schema_version` only bumps on backward-incompatible shape changes. If you\nsee a different value, refuse to parse and ask the user to upgrade.\n\n## Exit codes\n\n- `0` ok\n- `1` generic\n- `2` auth (no session, expired, refused login)\n- `3` no project linked (no `.stackbone/project.json` in cwd)\n- `4` not found (organization, agent, deployment)\n- `5` permission denied\n\nBranch on these instead of parsing stack traces.\n\n## Environment variables\n\n- `STACKBONE_JSON=1` — same as `--json`\n- `STACKBONE_API_URL` — same as `--api-url <url>`\n- `STACKBONE_AUTO_LOGIN=0` — same as `--no-auto-login`\n- `STACKBONE_LOG_LEVEL` — `debug`, `info`, `warn`, `error` (logger goes\n to stderr, never pollutes the JSON payload on stdout)\n\n## State on disk\n\n- `<cwd>/.stackbone/project.json` — organization + agent linked to this folder.\n Created by `stackbone init` / `link`.\n- `~/.stackbone/credentials.json` (chmod 600) — sessions keyed by control\n plane URL.\n- `~/.stackbone/config.json` — global preferences (default API URL,\n telemetry opt-in).\n\nThe control plane this skill points at: `https://api.stackbone.ai`.\nActive organization slug: `template1-2`.\n\n## Common workflows\n\n### Authenticate\n\n```sh\nstackbone login # interactive (device-code flow)\nstackbone whoami # confirm session\nstackbone metadata --json # one-shot overview that replaces 5–7 explorer calls\n```\n\n### Start a new project\n\n```sh\nstackbone init # menu of templates (workflow / autonomous / RAG)\nstackbone dev # local emulator at http://localhost:4242\n```\n\n### Deploy\n\n```sh\nstackbone deploy # build + push + Fly Machines\n```\n\n## Anti-patterns\n\n- Do not hardcode organization IDs, credentials or control plane URLs into\n agent code — the SDK reads them from the environment at runtime.\n- Do not parse human output. Re-run the command with `--json` and use\n `JSON.parse`.\n- Do not call the control plane HTTP API directly from a creator's agent.\n Use `@stackbone/sdk` so credential rotation, observability and metering\n stay correct.\n- Do not commit `.stackbone/` or `~/.stackbone/` — the entries are in the\n project's `.gitignore` for a reason.\n\n## Where to learn more\n\n- `stackbone docs cli` — command reference\n- `stackbone docs sdk` — `@stackbone/sdk` modules\n- `stackbone docs agent-yaml` — manifest reference (Epic 3)\n- `stackbone docs templates` — official templates (Epic 5)\n- `stackbone docs connections` — data layer (Epic 6)\n";
8482
+ //#endregion
8483
+ //#region ../../starters/hitl/.gitignore?raw
8484
+ var _gitignore_raw_default$5 = "# >>> stackbone >>>\n.stackbone/\nnode_modules/\ndist/\n# <<< stackbone <<<\n";
8485
+ //#endregion
8486
+ //#region ../../starters/hitl/Dockerfile?raw
8487
+ var Dockerfile_raw_default$5 = "# Distroless Node 24 image. Multi-stage so the runtime stays minimal.\nFROM node:24-alpine AS deps\nWORKDIR /app\nCOPY package.json ./\nRUN npm install --omit=dev\n\nFROM gcr.io/distroless/nodejs24-debian12\nWORKDIR /app\nCOPY --from=deps /app/node_modules ./node_modules\nCOPY src ./src\nCOPY agent.yaml ./\nENV PORT=8080\nEXPOSE 8080\nCMD [\"src/index.ts\"]\n";
8488
+ //#endregion
8489
+ //#region ../../starters/hitl/README.md?raw
8490
+ var README_default$5 = "# starter-hitl\n\nDemo of `client.approval` (Human-in-the-Loop) from `@stackbone/sdk`.\n\nThe agent opens an approval request on `POST /invoke`. The control plane\nnotifies the approver out-of-band, then POSTs the signed decision back to\n`POST /decide`. The agent verifies the signature with the workspace key and\nrecords the outcome.\n\n## Layout\n\n```\nsrc/\n index.ts # Hono server\n lib/\n client.ts # Shared Stackbone client\n logger.ts # JSON line logger\n routes/\n health.ts # GET /health\n schema-info.ts # GET /schema\n invoke.ts # POST /invoke -> open approval\n decide.ts # POST /decide -> signed decision callback\n get-approval.ts # GET /approvals/:id\n list-approvals.ts # GET /approvals -> ?status&topic\n cancel.ts # DELETE /approvals/:id -> cancel pending request\n```\n\n## Run\n\n```sh\nnpm install\nstackbone dev\n```\n\nThen:\n\n```sh\ncurl -X POST http://127.0.0.1:8080/invoke -H 'content-type: application/json' -d '{\n \"topic\": \"refund-request\",\n \"title\": \"Issue $50 refund\",\n \"payload\": {\"order_id\": \"ord_123\", \"amount\": 50}\n}'\n\ncurl http://127.0.0.1:8080/approvals\n```\n\nRequires `STACKBONE_APPROVAL_SIGNING_KEY` so `/decide` can verify the HMAC\nsignature the control plane attaches.\n";
8491
+ //#endregion
8492
+ //#region ../../starters/hitl/agent.yaml?raw
8493
+ var agent_default$5 = "# Stackbone agent manifest. Schema: stackbone.ai/v1.\n# Run `stackbone docs agent-yaml` for the full reference.\n\napiVersion: stackbone.ai/v1\nname: starter-hitl\nruntime:\n engine: node\n entry: src/index.ts\ndatabase:\n schema: ./src/schema.ts\n migrations: ./.stackbone/migrations\ndev:\n autoMigrate: false\nrag:\n embeddingModel: openai/text-embedding-3-small\n autoMigrate: false\n";
8494
+ //#endregion
8495
+ //#region ../../starters/hitl/src/index.ts?raw
8496
+ var src_default$5 = "import { serve } from '@hono/node-server';\nimport { Hono } from 'hono';\n\nimport { log } from './lib/logger.ts';\nimport cancel from './routes/cancel.ts';\nimport decide from './routes/decide.ts';\nimport getApproval from './routes/get-approval.ts';\nimport health from './routes/health.ts';\nimport invoke from './routes/invoke.ts';\nimport listApprovals from './routes/list-approvals.ts';\nimport schemaInfo from './routes/schema-info.ts';\n\nconst app = new Hono();\n\napp.use('*', async (c, next) => {\n const start = Date.now();\n await next();\n log('info', 'request', {\n method: c.req.method,\n path: c.req.path,\n status: c.res.status,\n durationMs: Date.now() - start,\n });\n});\n\napp.route('/', health);\napp.route('/', schemaInfo);\napp.route('/', invoke);\napp.route('/', decide);\napp.route('/', getApproval);\napp.route('/', listApprovals);\napp.route('/', cancel);\n\nconst port = Number(process.env.PORT ?? 8080);\nserve({ fetch: app.fetch, port });\nlog('info', 'starter-hitl listening', { port });\n";
8497
+ //#endregion
8498
+ //#region ../../starters/hitl/src/lib/client.ts?raw
8499
+ var client_default$5 = "import { createClient } from '@stackbone/sdk';\n\nexport const client = createClient({\n stackboneApiUrl: process.env.STACKBONE_DEV_API_URL ?? 'http://127.0.0.1:4242',\n});\n";
8500
+ //#endregion
8501
+ //#region ../../starters/hitl/src/lib/logger.ts?raw
8502
+ var logger_default$5 = "type Level = 'info' | 'warn' | 'error';\n\nexport function log(level: Level, msg: string, fields?: Record<string, unknown>): void {\n const line = JSON.stringify({ ts: new Date().toISOString(), level, msg, ...fields });\n if (level === 'error') console.error(line);\n else console.log(line);\n}\n";
8503
+ //#endregion
8504
+ //#region ../../starters/hitl/src/routes/cancel.ts?raw
8505
+ var cancel_default = "import { Hono } from 'hono';\n\nimport { client } from '../lib/client.ts';\nimport { log } from '../lib/logger.ts';\n\nconst route = new Hono();\n\nroute.delete('/approvals/:id', async (c) => {\n const id = c.req.param('id');\n const reason = c.req.query('reason') ?? undefined;\n log('info', 'cancel approval', { id, reason });\n\n const result = await client.approval.cancel(id, reason);\n if (result.error) {\n log('error', 'cancel failed', { id, code: result.error.code });\n return c.json({ error: result.error }, 502);\n }\n\n return c.json({ status: 'cancelled', id });\n});\n\nexport default route;\n";
8506
+ //#endregion
8507
+ //#region ../../starters/hitl/src/routes/decide.ts?raw
8508
+ var decide_default = "import { Hono } from 'hono';\n\nimport { client } from '../lib/client.ts';\nimport { log } from '../lib/logger.ts';\n\nconst route = new Hono();\n\n// Webhook the control plane POSTs to with the approver's decision.\n// `client.approval.verify` validates the HMAC signature using the workspace's\n// signing key (`STACKBONE_APPROVAL_SIGNING_KEY`).\nroute.post('/decide', async (c) => {\n log('info', 'decide received');\n\n const verified = await client.approval.verify(c.req.raw);\n if (verified.error) {\n log('warn', 'verify failed', { code: verified.error.code });\n return c.json({ error: verified.error }, 401);\n }\n\n log('info', 'decision', { status: verified.data.status });\n return c.json({ status: 'recorded', decision: verified.data });\n});\n\nexport default route;\n";
8509
+ //#endregion
8510
+ //#region ../../starters/hitl/src/routes/get-approval.ts?raw
8511
+ var get_approval_default = "import { Hono } from 'hono';\n\nimport { client } from '../lib/client.ts';\nimport { log } from '../lib/logger.ts';\n\nconst route = new Hono();\n\nroute.get('/approvals/:id', async (c) => {\n const id = c.req.param('id');\n log('info', 'get approval', { id });\n\n const result = await client.approval.get(id);\n if (result.error) {\n log('error', 'get failed', { id, code: result.error.code });\n return c.json({ error: result.error }, 502);\n }\n\n return c.json(result.data);\n});\n\nexport default route;\n";
8512
+ //#endregion
8513
+ //#region ../../starters/hitl/src/routes/health.ts?raw
8514
+ var health_default$5 = "import { Hono } from 'hono';\n\nconst route = new Hono();\nroute.get('/health', (c) => c.json({ status: 'ok' }));\nexport default route;\n";
8515
+ //#endregion
8516
+ //#region ../../starters/hitl/src/routes/invoke.ts?raw
8517
+ var invoke_default$5 = "import { Hono } from 'hono';\n\nimport { client } from '../lib/client.ts';\nimport { log } from '../lib/logger.ts';\n\nconst route = new Hono();\n\n// Opens a HITL approval request. The agent pauses on `/approvals/:id` (or via\n// the SDK's poll/wait helpers); the control plane delivers the decision to\n// the `/decide` route below.\nroute.post('/invoke', async (c) => {\n const body = await c.req.json().catch(\n () =>\n ({}) as {\n topic?: string;\n payload?: unknown;\n title?: string;\n description?: string;\n },\n );\n\n const topic = typeof body.topic === 'string' ? body.topic : 'demo-approval';\n log('info', 'request approval', { topic });\n\n const result = await client.approval.request({\n topic,\n payload: body.payload ?? { reason: 'starter-hitl demo' },\n title: body.title ?? 'Demo approval',\n description: body.description,\n onDecide: '/decide',\n });\n\n if (result.error) {\n log('error', 'request failed', { code: result.error.code });\n return c.json({ error: result.error }, 502);\n }\n\n log('info', 'request done', { approvalId: result.data.approvalId });\n return c.json(result.data);\n});\n\nexport default route;\n";
8518
+ //#endregion
8519
+ //#region ../../starters/hitl/src/routes/list-approvals.ts?raw
8520
+ var list_approvals_default = "import { Hono } from 'hono';\n\nimport { client } from '../lib/client.ts';\nimport { log } from '../lib/logger.ts';\n\nconst route = new Hono();\n\nroute.get('/approvals', async (c) => {\n const status = c.req.query('status');\n const topic = c.req.query('topic');\n\n log('info', 'list approvals', { status, topic });\n\n const result = await client.approval.list({\n status: status as never,\n topic,\n limit: 50,\n });\n\n if (result.error) {\n log('error', 'list failed', { code: result.error.code });\n return c.json({ error: result.error }, 502);\n }\n\n log('info', 'list done', { count: result.data.items.length });\n return c.json(result.data);\n});\n\nexport default route;\n";
8521
+ //#endregion
8522
+ //#region ../../starters/hitl/src/routes/schema-info.ts?raw
8523
+ var schema_info_default$5 = "import { Hono } from 'hono';\n\nconst route = new Hono();\n\nroute.get('/schema', (c) =>\n c.json({\n input: {\n type: 'object',\n properties: {\n topic: { type: 'string' },\n payload: {},\n },\n required: ['topic'],\n },\n output: {\n type: 'object',\n properties: {\n approvalId: { type: 'string' },\n status: { type: 'string' },\n },\n },\n }),\n);\n\nexport default route;\n";
8524
+ //#endregion
8525
+ //#region ../../starters/prompts/.claude/skills/stackbone/SKILL.md?raw
8526
+ var SKILL_default$4 = "---\nname: stackbone\ndescription: Use this skill when the user is working with the Stackbone CLI, the `@stackbone/sdk` package, the `agent.yaml` manifest, official templates, or connections to the Stackbone control plane. Triggers on commands like `stackbone init`, `stackbone dev`, `stackbone deploy`, on questions about organization state, exit codes, or environment variables, and on any reference to `@stackbone/*` imports in TypeScript code. Provides the command surface, output contract, exit codes, environment variables and idiomatic workflows.\n---\n\n# Stackbone — agent operating manual\n\nThis skill is regenerated by `stackbone init` / `stackbone link` and ships\ninside the CLI release 0.1.0-alpha.1. Edit the upstream template in\n`apps/cli/src/skills/templates.ts` if something here is wrong.\n\n## CLI surface\n\nThe CLI binary is `stackbone` (also reachable as `npx @stackbone/cli@alpha ...`). Run\n`stackbone docs cli` for the canonical command/flag reference, or\n`stackbone docs sdk` for the `@stackbone/sdk` overview — neither command does\nnetwork IO.\n\n## Output contract\n\nEvery command supports `--json` (or `STACKBONE_JSON=1`). Successful JSON\nresponses look like:\n\n```json\n{ \"schema_version\": 1, \"...\": \"...\" }\n```\n\nErrors look like:\n\n```json\n{\n \"schema_version\": 1,\n \"error\": { \"code\": \"auth\", \"message\": \"...\", \"suggestion\": \"Run \\`stackbone login\\`\" },\n \"exit_code\": 2\n}\n```\n\n`schema_version` only bumps on backward-incompatible shape changes. If you\nsee a different value, refuse to parse and ask the user to upgrade.\n\n## Exit codes\n\n- `0` ok\n- `1` generic\n- `2` auth (no session, expired, refused login)\n- `3` no project linked (no `.stackbone/project.json` in cwd)\n- `4` not found (organization, agent, deployment)\n- `5` permission denied\n\nBranch on these instead of parsing stack traces.\n\n## Environment variables\n\n- `STACKBONE_JSON=1` — same as `--json`\n- `STACKBONE_API_URL` — same as `--api-url <url>`\n- `STACKBONE_AUTO_LOGIN=0` — same as `--no-auto-login`\n- `STACKBONE_LOG_LEVEL` — `debug`, `info`, `warn`, `error` (logger goes\n to stderr, never pollutes the JSON payload on stdout)\n\n## State on disk\n\n- `<cwd>/.stackbone/project.json` — organization + agent linked to this folder.\n Created by `stackbone init` / `link`.\n- `~/.stackbone/credentials.json` (chmod 600) — sessions keyed by control\n plane URL.\n- `~/.stackbone/config.json` — global preferences (default API URL,\n telemetry opt-in).\n\nThe control plane this skill points at: `https://api.stackbone.ai`.\nActive organization slug: `template1-2`.\n\n## Common workflows\n\n### Authenticate\n\n```sh\nstackbone login # interactive (device-code flow)\nstackbone whoami # confirm session\nstackbone metadata --json # one-shot overview that replaces 5–7 explorer calls\n```\n\n### Start a new project\n\n```sh\nstackbone init # menu of templates (workflow / autonomous / RAG)\nstackbone dev # local emulator at http://localhost:4242\n```\n\n### Deploy\n\n```sh\nstackbone deploy # build + push + Fly Machines\n```\n\n## Anti-patterns\n\n- Do not hardcode organization IDs, credentials or control plane URLs into\n agent code — the SDK reads them from the environment at runtime.\n- Do not parse human output. Re-run the command with `--json` and use\n `JSON.parse`.\n- Do not call the control plane HTTP API directly from a creator's agent.\n Use `@stackbone/sdk` so credential rotation, observability and metering\n stay correct.\n- Do not commit `.stackbone/` or `~/.stackbone/` — the entries are in the\n project's `.gitignore` for a reason.\n\n## Where to learn more\n\n- `stackbone docs cli` — command reference\n- `stackbone docs sdk` — `@stackbone/sdk` modules\n- `stackbone docs agent-yaml` — manifest reference (Epic 3)\n- `stackbone docs templates` — official templates (Epic 5)\n- `stackbone docs connections` — data layer (Epic 6)\n";
8527
+ //#endregion
8528
+ //#region ../../starters/prompts/.gitignore?raw
8529
+ var _gitignore_raw_default$4 = "# >>> stackbone >>>\n.stackbone/\nnode_modules/\ndist/\n# <<< stackbone <<<\n";
8530
+ //#endregion
8531
+ //#region ../../starters/prompts/Dockerfile?raw
8532
+ var Dockerfile_raw_default$4 = "# Distroless Node 24 image. Multi-stage so the runtime stays minimal.\nFROM node:24-alpine AS deps\nWORKDIR /app\nCOPY package.json ./\nRUN npm install --omit=dev\n\nFROM gcr.io/distroless/nodejs24-debian12\nWORKDIR /app\nCOPY --from=deps /app/node_modules ./node_modules\nCOPY src ./src\nCOPY agent.yaml ./\nENV PORT=8080\nEXPOSE 8080\nCMD [\"src/index.ts\"]\n";
8533
+ //#endregion
8534
+ //#region ../../starters/prompts/README.md?raw
8535
+ var README_default$4 = "# starter-prompts\n\nDemo of `client.prompts` (managed prompts with Mustache-style `{{var}}`\nsubstitution, versioned in the control plane) from `@stackbone/sdk`.\n\n## Layout\n\n```\nsrc/\n index.ts # Hono server\n lib/\n client.ts # Shared Stackbone client\n logger.ts # JSON line logger\n routes/\n health.ts # GET /health\n schema-info.ts # GET /schema\n invoke.ts # POST /invoke -> compile a prompt with variables\n list-prompts.ts # GET /prompts -> latest version of each prompt\n get-prompt.ts # GET /prompts/:name -> single prompt (?version=N)\n create-prompt.ts # POST /prompts -> register a new prompt\n```\n\n## Run\n\n```sh\nnpm install\nstackbone dev\n```\n\nThen:\n\n```sh\ncurl -X POST http://127.0.0.1:8080/prompts -H 'content-type: application/json' -d '{\n \"name\": \"greeting\",\n \"template\": \"Hello {{user}}, welcome to {{product}}.\"\n}'\n\ncurl -X POST http://127.0.0.1:8080/invoke -H 'content-type: application/json' -d '{\n \"name\": \"greeting\",\n \"variables\": {\"user\": \"Luis\", \"product\": \"Stackbone\"}\n}'\n\ncurl http://127.0.0.1:8080/prompts\n```\n\nNote: the `client.prompts` facade is a scaffolding placeholder in this SDK\nversion — every call returns `not_implemented` until the control plane lands.\n";
8536
+ //#endregion
8537
+ //#region ../../starters/prompts/agent.yaml?raw
8538
+ var agent_default$4 = "# Stackbone agent manifest. Schema: stackbone.ai/v1.\n# Run `stackbone docs agent-yaml` for the full reference.\n\napiVersion: stackbone.ai/v1\nname: starter-prompts\nruntime:\n engine: node\n entry: src/index.ts\ndatabase:\n schema: ./src/schema.ts\n migrations: ./.stackbone/migrations\ndev:\n autoMigrate: false\nrag:\n embeddingModel: openai/text-embedding-3-small\n autoMigrate: false\n";
8539
+ //#endregion
8540
+ //#region ../../starters/prompts/src/index.ts?raw
8541
+ var src_default$4 = "import { serve } from '@hono/node-server';\nimport { Hono } from 'hono';\n\nimport { log } from './lib/logger.ts';\nimport createPrompt from './routes/create-prompt.ts';\nimport getPrompt from './routes/get-prompt.ts';\nimport health from './routes/health.ts';\nimport invoke from './routes/invoke.ts';\nimport listPrompts from './routes/list-prompts.ts';\nimport schemaInfo from './routes/schema-info.ts';\n\nconst app = new Hono();\n\napp.use('*', async (c, next) => {\n const start = Date.now();\n await next();\n log('info', 'request', {\n method: c.req.method,\n path: c.req.path,\n status: c.res.status,\n durationMs: Date.now() - start,\n });\n});\n\napp.route('/', health);\napp.route('/', schemaInfo);\napp.route('/', invoke);\napp.route('/', listPrompts);\napp.route('/', getPrompt);\napp.route('/', createPrompt);\n\nconst port = Number(process.env.PORT ?? 8080);\nserve({ fetch: app.fetch, port });\nlog('info', 'starter-prompts listening', { port });\n";
8542
+ //#endregion
8543
+ //#region ../../starters/prompts/src/lib/client.ts?raw
8544
+ var client_default$4 = "import { createClient } from '@stackbone/sdk';\n\nexport const client = createClient({\n stackboneApiUrl: process.env.STACKBONE_DEV_API_URL ?? 'http://127.0.0.1:4242',\n});\n";
8545
+ //#endregion
8546
+ //#region ../../starters/prompts/src/lib/logger.ts?raw
8547
+ var logger_default$4 = "type Level = 'info' | 'warn' | 'error';\n\nexport function log(level: Level, msg: string, fields?: Record<string, unknown>): void {\n const line = JSON.stringify({ ts: new Date().toISOString(), level, msg, ...fields });\n if (level === 'error') console.error(line);\n else console.log(line);\n}\n";
8548
+ //#endregion
8549
+ //#region ../../starters/prompts/src/routes/create-prompt.ts?raw
8550
+ var create_prompt_default = "import { Hono } from 'hono';\n\nimport { client } from '../lib/client.ts';\nimport { log } from '../lib/logger.ts';\n\nconst route = new Hono();\n\nroute.post('/prompts', async (c) => {\n const body = await c.req.json().catch(\n () =>\n ({}) as {\n name?: string;\n template?: string;\n metadata?: Record<string, unknown>;\n },\n );\n\n if (!body.name || !body.template) {\n return c.json({ error: 'missing name or template' }, 400);\n }\n\n log('info', 'create prompt', { name: body.name });\n\n const result = await client.prompts.create({\n name: body.name,\n template: body.template,\n metadata: body.metadata,\n });\n\n if (result.error) {\n log('error', 'create failed', { code: result.error.code });\n return c.json({ error: result.error }, 502);\n }\n\n log('info', 'create done', { name: result.data.name, version: result.data.version });\n return c.json(result.data);\n});\n\nexport default route;\n";
8551
+ //#endregion
8552
+ //#region ../../starters/prompts/src/routes/get-prompt.ts?raw
8553
+ var get_prompt_default = "import { Hono } from 'hono';\n\nimport { client } from '../lib/client.ts';\nimport { log } from '../lib/logger.ts';\n\nconst route = new Hono();\n\nroute.get('/prompts/:name', async (c) => {\n const name = c.req.param('name');\n const version = c.req.query('version') ? Number(c.req.query('version')) : undefined;\n\n log('info', 'fetch prompt', { name, version });\n\n const result = await client.prompts.get(name, { version });\n if (result.error) {\n log('error', 'fetch failed', { name, code: result.error.code });\n return c.json({ error: result.error }, 502);\n }\n\n return c.json(result.data);\n});\n\nexport default route;\n";
8554
+ //#endregion
8555
+ //#region ../../starters/prompts/src/routes/health.ts?raw
8556
+ var health_default$4 = "import { Hono } from 'hono';\n\nconst route = new Hono();\nroute.get('/health', (c) => c.json({ status: 'ok' }));\nexport default route;\n";
8557
+ //#endregion
8558
+ //#region ../../starters/prompts/src/routes/invoke.ts?raw
8559
+ var invoke_default$4 = "import { Hono } from 'hono';\n\nimport { client } from '../lib/client.ts';\nimport { log } from '../lib/logger.ts';\n\nconst route = new Hono();\n\n// Compiles a managed prompt with `{{var}}` substitution and returns the\n// resulting string. The agent typically passes this to `client.ai`.\nroute.post('/invoke', async (c) => {\n const body = await c.req.json().catch(\n () => ({}) as { name?: string; variables?: Record<string, unknown> },\n );\n const name = typeof body.name === 'string' ? body.name : '';\n if (!name) return c.json({ error: 'missing name' }, 400);\n\n log('info', 'compile', { name });\n\n const result = await client.prompts.compile(name, body.variables ?? {});\n if (result.error) {\n log('error', 'compile failed', { name, code: result.error.code });\n return c.json({ error: result.error }, 502);\n }\n\n log('info', 'compile done', { name, length: result.data.length });\n return c.json({ name, compiled: result.data });\n});\n\nexport default route;\n";
8560
+ //#endregion
8561
+ //#region ../../starters/prompts/src/routes/list-prompts.ts?raw
8562
+ var list_prompts_default = "import { Hono } from 'hono';\n\nimport { client } from '../lib/client.ts';\nimport { log } from '../lib/logger.ts';\n\nconst route = new Hono();\n\nroute.get('/prompts', async (c) => {\n log('info', 'list prompts');\n\n const result = await client.prompts.list({ limit: 50 });\n if (result.error) {\n log('error', 'list failed', { code: result.error.code });\n return c.json({ error: result.error }, 502);\n }\n\n log('info', 'list done', { count: result.data.items.length });\n return c.json(result.data);\n});\n\nexport default route;\n";
8563
+ //#endregion
8564
+ //#region ../../starters/prompts/src/routes/schema-info.ts?raw
8565
+ var schema_info_default$4 = "import { Hono } from 'hono';\n\nconst route = new Hono();\n\nroute.get('/schema', (c) =>\n c.json({\n input: {\n type: 'object',\n properties: {\n name: { type: 'string' },\n variables: { type: 'object' },\n },\n required: ['name'],\n },\n output: { type: 'object', properties: { compiled: { type: 'string' } } },\n }),\n);\n\nexport default route;\n";
8566
+ //#endregion
8567
+ //#region ../../starters/queues/.claude/skills/stackbone/SKILL.md?raw
8568
+ var SKILL_default$3 = "---\nname: stackbone\ndescription: Use this skill when the user is working with the Stackbone CLI, the `@stackbone/sdk` package, the `agent.yaml` manifest, official templates, or connections to the Stackbone control plane. Triggers on commands like `stackbone init`, `stackbone dev`, `stackbone deploy`, on questions about organization state, exit codes, or environment variables, and on any reference to `@stackbone/*` imports in TypeScript code. Provides the command surface, output contract, exit codes, environment variables and idiomatic workflows.\n---\n\n# Stackbone — agent operating manual\n\nThis skill is regenerated by `stackbone init` / `stackbone link` and ships\ninside the CLI release 0.1.0-alpha.1. Edit the upstream template in\n`apps/cli/src/skills/templates.ts` if something here is wrong.\n\n## CLI surface\n\nThe CLI binary is `stackbone` (also reachable as `npx @stackbone/cli@alpha ...`). Run\n`stackbone docs cli` for the canonical command/flag reference, or\n`stackbone docs sdk` for the `@stackbone/sdk` overview — neither command does\nnetwork IO.\n\n## Output contract\n\nEvery command supports `--json` (or `STACKBONE_JSON=1`). Successful JSON\nresponses look like:\n\n```json\n{ \"schema_version\": 1, \"...\": \"...\" }\n```\n\nErrors look like:\n\n```json\n{\n \"schema_version\": 1,\n \"error\": { \"code\": \"auth\", \"message\": \"...\", \"suggestion\": \"Run \\`stackbone login\\`\" },\n \"exit_code\": 2\n}\n```\n\n`schema_version` only bumps on backward-incompatible shape changes. If you\nsee a different value, refuse to parse and ask the user to upgrade.\n\n## Exit codes\n\n- `0` ok\n- `1` generic\n- `2` auth (no session, expired, refused login)\n- `3` no project linked (no `.stackbone/project.json` in cwd)\n- `4` not found (organization, agent, deployment)\n- `5` permission denied\n\nBranch on these instead of parsing stack traces.\n\n## Environment variables\n\n- `STACKBONE_JSON=1` — same as `--json`\n- `STACKBONE_API_URL` — same as `--api-url <url>`\n- `STACKBONE_AUTO_LOGIN=0` — same as `--no-auto-login`\n- `STACKBONE_LOG_LEVEL` — `debug`, `info`, `warn`, `error` (logger goes\n to stderr, never pollutes the JSON payload on stdout)\n\n## State on disk\n\n- `<cwd>/.stackbone/project.json` — organization + agent linked to this folder.\n Created by `stackbone init` / `link`.\n- `~/.stackbone/credentials.json` (chmod 600) — sessions keyed by control\n plane URL.\n- `~/.stackbone/config.json` — global preferences (default API URL,\n telemetry opt-in).\n\nThe control plane this skill points at: `https://api.stackbone.ai`.\nActive organization slug: `template1-2`.\n\n## Common workflows\n\n### Authenticate\n\n```sh\nstackbone login # interactive (device-code flow)\nstackbone whoami # confirm session\nstackbone metadata --json # one-shot overview that replaces 5–7 explorer calls\n```\n\n### Start a new project\n\n```sh\nstackbone init # menu of templates (workflow / autonomous / RAG)\nstackbone dev # local emulator at http://localhost:4242\n```\n\n### Deploy\n\n```sh\nstackbone deploy # build + push + Fly Machines\n```\n\n## Anti-patterns\n\n- Do not hardcode organization IDs, credentials or control plane URLs into\n agent code — the SDK reads them from the environment at runtime.\n- Do not parse human output. Re-run the command with `--json` and use\n `JSON.parse`.\n- Do not call the control plane HTTP API directly from a creator's agent.\n Use `@stackbone/sdk` so credential rotation, observability and metering\n stay correct.\n- Do not commit `.stackbone/` or `~/.stackbone/` — the entries are in the\n project's `.gitignore` for a reason.\n\n## Where to learn more\n\n- `stackbone docs cli` — command reference\n- `stackbone docs sdk` — `@stackbone/sdk` modules\n- `stackbone docs agent-yaml` — manifest reference (Epic 3)\n- `stackbone docs templates` — official templates (Epic 5)\n- `stackbone docs connections` — data layer (Epic 6)\n";
8569
+ //#endregion
8570
+ //#region ../../starters/queues/.gitignore?raw
8571
+ var _gitignore_raw_default$3 = "# >>> stackbone >>>\n.stackbone/\nnode_modules/\ndist/\n# <<< stackbone <<<\n";
8572
+ //#endregion
8573
+ //#region ../../starters/queues/Dockerfile?raw
8574
+ var Dockerfile_raw_default$3 = "# Distroless Node 24 image. Multi-stage so the runtime stays minimal.\nFROM node:24-alpine AS deps\nWORKDIR /app\nCOPY package.json ./\nRUN npm install --omit=dev\n\nFROM gcr.io/distroless/nodejs24-debian12\nWORKDIR /app\nCOPY --from=deps /app/node_modules ./node_modules\nCOPY src ./src\nCOPY agent.yaml ./\nENV PORT=8080\nEXPOSE 8080\nCMD [\"src/index.ts\"]\n";
8575
+ //#endregion
8576
+ //#region ../../starters/queues/README.md?raw
8577
+ var README_default$3 = "# starter-queues\n\nDemo of `client.queues` (QStash publisher + signed webhook receiver) from\n`@stackbone/sdk`.\n\n## Layout\n\n```\nsrc/\n index.ts # Hono server\n lib/\n client.ts # Shared Stackbone client\n logger.ts # JSON line logger\n routes/\n health.ts # GET /health\n schema-info.ts # GET /schema\n invoke.ts # POST /invoke -> publish to own /receive\n publish.ts # POST /publish -> publish to arbitrary url\n receive.ts # POST /receive -> queue webhook target\n```\n\n## Run\n\n```sh\nnpm install\nstackbone dev\n```\n\nThen:\n\n```sh\ncurl -X POST http://127.0.0.1:8080/publish -H 'content-type: application/json' \\\n -d '{\"url\":\"https://httpbin.org/post\",\"body\":{\"hello\":\"world\"}}'\n\ncurl -X POST http://127.0.0.1:8080/invoke -H 'content-type: application/json' \\\n -d '{\"payload\":{\"hello\":\"from invoke\"}}'\n```\n\nRequires `QSTASH_TOKEN` (and `QSTASH_*_SIGNING_KEY` if you want signature\nverification on `/receive`).\n";
8578
+ //#endregion
8579
+ //#region ../../starters/queues/agent.yaml?raw
8580
+ var agent_default$3 = "# Stackbone agent manifest. Schema: stackbone.ai/v1.\n# Run `stackbone docs agent-yaml` for the full reference.\n\napiVersion: stackbone.ai/v1\nname: starter-queues\nruntime:\n engine: node\n entry: src/index.ts\ndatabase:\n schema: ./src/schema.ts\n migrations: ./.stackbone/migrations\ndev:\n autoMigrate: false\nrag:\n embeddingModel: openai/text-embedding-3-small\n autoMigrate: false\n";
8581
+ //#endregion
8582
+ //#region ../../starters/queues/src/index.ts?raw
8583
+ var src_default$3 = "import { serve } from '@hono/node-server';\nimport { Hono } from 'hono';\n\nimport { log } from './lib/logger.ts';\nimport health from './routes/health.ts';\nimport invoke from './routes/invoke.ts';\nimport publish from './routes/publish.ts';\nimport receive from './routes/receive.ts';\nimport schemaInfo from './routes/schema-info.ts';\n\nconst app = new Hono();\n\napp.use('*', async (c, next) => {\n const start = Date.now();\n await next();\n log('info', 'request', {\n method: c.req.method,\n path: c.req.path,\n status: c.res.status,\n durationMs: Date.now() - start,\n });\n});\n\napp.route('/', health);\napp.route('/', schemaInfo);\napp.route('/', invoke);\napp.route('/', publish);\napp.route('/', receive);\n\nconst port = Number(process.env.PORT ?? 8080);\nserve({ fetch: app.fetch, port });\nlog('info', 'starter-queues listening', { port });\n";
8584
+ //#endregion
8585
+ //#region ../../starters/queues/src/lib/client.ts?raw
8586
+ var client_default$3 = "import { createClient } from '@stackbone/sdk';\n\nexport const client = createClient({\n stackboneApiUrl: process.env.STACKBONE_DEV_API_URL ?? 'http://127.0.0.1:4242',\n});\n";
8587
+ //#endregion
8588
+ //#region ../../starters/queues/src/lib/logger.ts?raw
8589
+ var logger_default$3 = "type Level = 'info' | 'warn' | 'error';\n\nexport function log(level: Level, msg: string, fields?: Record<string, unknown>): void {\n const line = JSON.stringify({ ts: new Date().toISOString(), level, msg, ...fields });\n if (level === 'error') console.error(line);\n else console.log(line);\n}\n";
8590
+ //#endregion
8591
+ //#region ../../starters/queues/src/routes/health.ts?raw
8592
+ var health_default$3 = "import { Hono } from 'hono';\n\nconst route = new Hono();\nroute.get('/health', (c) => c.json({ status: 'ok' }));\nexport default route;\n";
8593
+ //#endregion
8594
+ //#region ../../starters/queues/src/routes/invoke.ts?raw
8595
+ var invoke_default$3 = "import { Hono } from 'hono';\n\nimport { client } from '../lib/client.ts';\nimport { log } from '../lib/logger.ts';\n\nconst route = new Hono();\n\n// Convenience entrypoint: publishes a message back to this same agent's\n// `/receive` route, so the round-trip is visible end-to-end.\nroute.post('/invoke', async (c) => {\n const body = await c.req.json().catch(() => ({}) as { selfUrl?: string; payload?: unknown });\n const selfUrl = body.selfUrl ?? 'http://127.0.0.1:8080/receive';\n\n log('info', 'invoke -> queue', { selfUrl });\n\n const result = await client.queues.publish({\n url: selfUrl,\n body: body.payload ?? { hello: 'from invoke' },\n });\n\n if (result.error) {\n log('error', 'invoke publish failed', { code: result.error.code });\n return c.json({ error: result.error }, 502);\n }\n\n return c.json(result.data);\n});\n\nexport default route;\n";
8596
+ //#endregion
8597
+ //#region ../../starters/queues/src/routes/publish.ts?raw
8598
+ var publish_default = "import { Hono } from 'hono';\n\nimport { client } from '../lib/client.ts';\nimport { log } from '../lib/logger.ts';\n\nconst route = new Hono();\n\nroute.post('/publish', async (c) => {\n const body = await c.req.json().catch(\n () =>\n ({}) as {\n url?: string;\n body?: unknown;\n retries?: number;\n delay?: number;\n },\n );\n\n const url = typeof body.url === 'string' ? body.url : '';\n if (!url) return c.json({ error: 'missing url' }, 400);\n\n log('info', 'publish', { url, retries: body.retries, delay: body.delay });\n\n const result = await client.queues.publish({\n url,\n body: body.body ?? {},\n retries: body.retries,\n delay: body.delay,\n });\n\n if (result.error) {\n log('error', 'publish failed', { code: result.error.code });\n return c.json({ error: result.error }, 502);\n }\n\n log('info', 'publish done', { messageId: result.data.messageId });\n return c.json(result.data);\n});\n\nexport default route;\n";
8599
+ //#endregion
8600
+ //#region ../../starters/queues/src/routes/receive.ts?raw
8601
+ var receive_default = "import { Hono } from 'hono';\n\nimport { log } from '../lib/logger.ts';\n\nconst route = new Hono();\n\n// Webhook endpoint that the queue calls back into. In production QStash sends\n// the `Upstash-Signature` header and the SDK exposes a verifier — for the\n// template we just log the body so you can observe the round-trip from\n// `/publish` -> queue -> `/receive`.\nroute.post('/receive', async (c) => {\n const signature = c.req.header('upstash-signature');\n const body = await c.req.json().catch(() => null);\n\n log('info', 'receive', {\n hasSignature: Boolean(signature),\n bodySize: body ? JSON.stringify(body).length : 0,\n });\n\n return c.json({ status: 'received' });\n});\n\nexport default route;\n";
8602
+ //#endregion
8603
+ //#region ../../starters/queues/src/routes/schema-info.ts?raw
8604
+ var schema_info_default$3 = "import { Hono } from 'hono';\n\nconst route = new Hono();\n\nroute.get('/schema', (c) =>\n c.json({\n input: {\n type: 'object',\n properties: { url: { type: 'string' }, body: { type: 'object' } },\n required: ['url'],\n },\n output: {\n type: 'object',\n properties: { messageId: { type: 'string' } },\n },\n }),\n);\n\nexport default route;\n";
8605
+ //#endregion
8606
+ //#region ../../starters/rag/.claude/skills/stackbone/SKILL.md?raw
8607
+ var SKILL_default$2 = "---\nname: stackbone\ndescription: Use this skill when the user is working with the Stackbone CLI, the `@stackbone/sdk` package, the `agent.yaml` manifest, official templates, or connections to the Stackbone control plane. Triggers on commands like `stackbone init`, `stackbone dev`, `stackbone deploy`, on questions about organization state, exit codes, or environment variables, and on any reference to `@stackbone/*` imports in TypeScript code. Provides the command surface, output contract, exit codes, environment variables and idiomatic workflows.\n---\n\n# Stackbone — agent operating manual\n\nThis skill is regenerated by `stackbone init` / `stackbone link` and ships\ninside the CLI release 0.1.0-alpha.1. Edit the upstream template in\n`apps/cli/src/skills/templates.ts` if something here is wrong.\n\n## CLI surface\n\nThe CLI binary is `stackbone` (also reachable as `npx @stackbone/cli@alpha ...`). Run\n`stackbone docs cli` for the canonical command/flag reference, or\n`stackbone docs sdk` for the `@stackbone/sdk` overview — neither command does\nnetwork IO.\n\n## Output contract\n\nEvery command supports `--json` (or `STACKBONE_JSON=1`). Successful JSON\nresponses look like:\n\n```json\n{ \"schema_version\": 1, \"...\": \"...\" }\n```\n\nErrors look like:\n\n```json\n{\n \"schema_version\": 1,\n \"error\": { \"code\": \"auth\", \"message\": \"...\", \"suggestion\": \"Run \\`stackbone login\\`\" },\n \"exit_code\": 2\n}\n```\n\n`schema_version` only bumps on backward-incompatible shape changes. If you\nsee a different value, refuse to parse and ask the user to upgrade.\n\n## Exit codes\n\n- `0` ok\n- `1` generic\n- `2` auth (no session, expired, refused login)\n- `3` no project linked (no `.stackbone/project.json` in cwd)\n- `4` not found (organization, agent, deployment)\n- `5` permission denied\n\nBranch on these instead of parsing stack traces.\n\n## Environment variables\n\n- `STACKBONE_JSON=1` — same as `--json`\n- `STACKBONE_API_URL` — same as `--api-url <url>`\n- `STACKBONE_AUTO_LOGIN=0` — same as `--no-auto-login`\n- `STACKBONE_LOG_LEVEL` — `debug`, `info`, `warn`, `error` (logger goes\n to stderr, never pollutes the JSON payload on stdout)\n\n## State on disk\n\n- `<cwd>/.stackbone/project.json` — organization + agent linked to this folder.\n Created by `stackbone init` / `link`.\n- `~/.stackbone/credentials.json` (chmod 600) — sessions keyed by control\n plane URL.\n- `~/.stackbone/config.json` — global preferences (default API URL,\n telemetry opt-in).\n\nThe control plane this skill points at: `https://api.stackbone.ai`.\nActive organization slug: `template1-2`.\n\n## Common workflows\n\n### Authenticate\n\n```sh\nstackbone login # interactive (device-code flow)\nstackbone whoami # confirm session\nstackbone metadata --json # one-shot overview that replaces 5–7 explorer calls\n```\n\n### Start a new project\n\n```sh\nstackbone init # menu of templates (workflow / autonomous / RAG)\nstackbone dev # local emulator at http://localhost:4242\n```\n\n### Deploy\n\n```sh\nstackbone deploy # build + push + Fly Machines\n```\n\n## Anti-patterns\n\n- Do not hardcode organization IDs, credentials or control plane URLs into\n agent code — the SDK reads them from the environment at runtime.\n- Do not parse human output. Re-run the command with `--json` and use\n `JSON.parse`.\n- Do not call the control plane HTTP API directly from a creator's agent.\n Use `@stackbone/sdk` so credential rotation, observability and metering\n stay correct.\n- Do not commit `.stackbone/` or `~/.stackbone/` — the entries are in the\n project's `.gitignore` for a reason.\n\n## Where to learn more\n\n- `stackbone docs cli` — command reference\n- `stackbone docs sdk` — `@stackbone/sdk` modules\n- `stackbone docs agent-yaml` — manifest reference (Epic 3)\n- `stackbone docs templates` — official templates (Epic 5)\n- `stackbone docs connections` — data layer (Epic 6)\n";
8608
+ //#endregion
8609
+ //#region ../../starters/rag/.gitignore?raw
8610
+ var _gitignore_raw_default$2 = "# >>> stackbone >>>\n.stackbone/\nnode_modules/\ndist/\n# <<< stackbone <<<\n";
8611
+ //#endregion
8612
+ //#region ../../starters/rag/Dockerfile?raw
8613
+ var Dockerfile_raw_default$2 = "# Distroless Node 24 image. Multi-stage so the runtime stays minimal.\nFROM node:24-alpine AS deps\nWORKDIR /app\nCOPY package.json ./\nRUN npm install --omit=dev\n\nFROM gcr.io/distroless/nodejs24-debian12\nWORKDIR /app\nCOPY --from=deps /app/node_modules ./node_modules\nCOPY src ./src\nCOPY agent.yaml ./\nENV PORT=8080\nEXPOSE 8080\nCMD [\"src/index.ts\"]\n";
8614
+ //#endregion
8615
+ //#region ../../starters/rag/README.md?raw
8616
+ var README_default$2 = "# starter-rag\n\nDemo of `client.rag` (pgvector-backed retrieval) from `@stackbone/sdk`.\n\n## Layout\n\n```\nsrc/\n index.ts # Hono server\n lib/\n client.ts # Shared Stackbone client\n logger.ts # JSON line logger\n routes/\n health.ts # GET /health\n schema-info.ts # GET /schema\n invoke.ts # POST /invoke -> action: ingest | retrieve\n chunk.ts # POST /chunk -> preview chunker output\n ingest.ts # POST /ingest -> chunk + embed + write\n retrieve.ts # POST /retrieve -> embed + topK\n delete.ts # DELETE /documents/:id\n```\n\n## Run\n\n```sh\nnpm install\nstackbone dev\n```\n\nThen:\n\n```sh\ncurl -X POST http://127.0.0.1:8080/ingest -H 'content-type: application/json' -d '{\n \"id\": \"doc-1\",\n \"text\": \"Stackbone is a marketplace and hosting platform for AI agents.\"\n}'\n\ncurl -X POST http://127.0.0.1:8080/retrieve -H 'content-type: application/json' -d '{\n \"text\": \"what is stackbone\",\n \"topK\": 3\n}'\n```\n\nRequires `OPENROUTER_API_KEY` for the embedding model.\n";
8617
+ //#endregion
8618
+ //#region ../../starters/rag/agent.yaml?raw
8619
+ var agent_default$2 = "# Stackbone agent manifest. Schema: stackbone.ai/v1.\n# Run `stackbone docs agent-yaml` for the full reference.\n\napiVersion: stackbone.ai/v1\nname: starter-rag\nruntime:\n engine: node\n entry: src/index.ts\ndatabase:\n schema: ./src/schema.ts\n migrations: ./.stackbone/migrations\ndev:\n autoMigrate: false\nrag:\n embeddingModel: openai/text-embedding-3-small\n autoMigrate: true\n";
8620
+ //#endregion
8621
+ //#region ../../starters/rag/src/index.ts?raw
8622
+ var src_default$2 = "import { serve } from '@hono/node-server';\nimport { Hono } from 'hono';\n\nimport { log } from './lib/logger.ts';\nimport chunk from './routes/chunk.ts';\nimport deleteDoc from './routes/delete.ts';\nimport health from './routes/health.ts';\nimport ingest from './routes/ingest.ts';\nimport invoke from './routes/invoke.ts';\nimport retrieve from './routes/retrieve.ts';\nimport schemaInfo from './routes/schema-info.ts';\n\nconst app = new Hono();\n\napp.use('*', async (c, next) => {\n const start = Date.now();\n await next();\n log('info', 'request', {\n method: c.req.method,\n path: c.req.path,\n status: c.res.status,\n durationMs: Date.now() - start,\n });\n});\n\napp.route('/', health);\napp.route('/', schemaInfo);\napp.route('/', invoke);\napp.route('/', chunk);\napp.route('/', ingest);\napp.route('/', retrieve);\napp.route('/', deleteDoc);\n\nconst port = Number(process.env.PORT ?? 8080);\nserve({ fetch: app.fetch, port });\nlog('info', 'starter-rag listening', { port });\n";
8623
+ //#endregion
8624
+ //#region ../../starters/rag/src/lib/client.ts?raw
8625
+ var client_default$2 = "import { createClient } from '@stackbone/sdk';\n\nexport const client = createClient({\n stackboneApiUrl: process.env.STACKBONE_DEV_API_URL ?? 'http://127.0.0.1:4242',\n});\n";
8626
+ //#endregion
8627
+ //#region ../../starters/rag/src/lib/logger.ts?raw
8628
+ var logger_default$2 = "type Level = 'info' | 'warn' | 'error';\n\nexport function log(level: Level, msg: string, fields?: Record<string, unknown>): void {\n const line = JSON.stringify({ ts: new Date().toISOString(), level, msg, ...fields });\n if (level === 'error') console.error(line);\n else console.log(line);\n}\n";
8629
+ //#endregion
8630
+ //#region ../../starters/rag/src/routes/chunk.ts?raw
8631
+ var chunk_default = "import { Hono } from 'hono';\n\nimport { client } from '../lib/client.ts';\nimport { log } from '../lib/logger.ts';\n\nconst route = new Hono();\n\n// Pure helper — splits text into chunks without touching DB or AI. Useful to\n// preview the chunker before paying for embeddings.\nroute.post('/chunk', async (c) => {\n const body = await c.req.json().catch(() => ({}) as { text?: string; size?: number; overlap?: number });\n const text = typeof body.text === 'string' ? body.text : '';\n if (!text) return c.json({ error: 'missing text' }, 400);\n\n log('info', 'chunk', { length: text.length, size: body.size, overlap: body.overlap });\n const chunks = client.rag.chunk(text, { size: body.size, overlap: body.overlap });\n return c.json({ count: chunks.length, chunks });\n});\n\nexport default route;\n";
8632
+ //#endregion
8633
+ //#region ../../starters/rag/src/routes/delete.ts?raw
8634
+ var delete_default = "import { Hono } from 'hono';\n\nimport { client } from '../lib/client.ts';\nimport { log } from '../lib/logger.ts';\n\nconst route = new Hono();\n\nroute.delete('/documents/:id', async (c) => {\n const id = c.req.param('id');\n log('info', 'delete document', { id });\n\n const result = await client.rag.delete(id);\n if (result.error) {\n log('error', 'delete failed', { id, code: result.error.code });\n return c.json({ error: result.error }, 502);\n }\n\n log('info', 'delete done', { id, deleted: result.data.deleted });\n return c.json(result.data);\n});\n\nexport default route;\n";
8635
+ //#endregion
8636
+ //#region ../../starters/rag/src/routes/health.ts?raw
8637
+ var health_default$2 = "import { Hono } from 'hono';\n\nconst route = new Hono();\nroute.get('/health', (c) => c.json({ status: 'ok' }));\nexport default route;\n";
8638
+ //#endregion
8639
+ //#region ../../starters/rag/src/routes/ingest.ts?raw
8640
+ var ingest_default = "import { Hono } from 'hono';\n\nimport { client } from '../lib/client.ts';\nimport { log } from '../lib/logger.ts';\n\nconst route = new Hono();\n\nroute.post('/ingest', async (c) => {\n const body = await c.req.json().catch(\n () =>\n ({}) as {\n id?: string;\n text?: string;\n model?: string;\n metadata?: Record<string, unknown>;\n },\n );\n\n const id = typeof body.id === 'string' ? body.id : crypto.randomUUID();\n const text = typeof body.text === 'string' ? body.text : '';\n const model = typeof body.model === 'string' ? body.model : 'openai/text-embedding-3-small';\n\n if (!text) return c.json({ error: 'missing text' }, 400);\n\n log('info', 'ingest start', { id, model, length: text.length });\n const chunks = client.rag.chunk(text);\n log('info', 'ingest chunked', { id, chunks: chunks.length });\n\n const result = await client.rag.ingest({ id, chunks, model, metadata: body.metadata });\n\n if (result.error) {\n log('error', 'ingest failed', { id, code: result.error.code });\n return c.json({ error: result.error }, 502);\n }\n\n log('info', 'ingest done', { id, chunks: result.data.chunks });\n return c.json(result.data);\n});\n\nexport default route;\n";
8641
+ //#endregion
8642
+ //#region ../../starters/rag/src/routes/invoke.ts?raw
8643
+ var invoke_default$2 = "import { Hono } from 'hono';\n\nimport { client } from '../lib/client.ts';\nimport { log } from '../lib/logger.ts';\n\nconst route = new Hono();\n\n// `/invoke` is the canonical Stackbone agent entrypoint. We dispatch by an\n// `action` field so the agent has a single contract while the dev surface\n// still exposes the granular routes (ingest / retrieve / chunk / delete).\nroute.post('/invoke', async (c) => {\n const body = await c.req.json().catch(\n () => ({}) as { action?: 'ingest' | 'retrieve'; id?: string; text?: string },\n );\n\n log('info', 'invoke', { action: body.action });\n\n if (body.action === 'ingest') {\n const text = body.text ?? '';\n const id = body.id ?? crypto.randomUUID();\n const chunks = client.rag.chunk(text);\n const result = await client.rag.ingest({\n id,\n chunks,\n model: 'openai/text-embedding-3-small',\n });\n return result.error ? c.json({ error: result.error }, 502) : c.json(result.data);\n }\n\n if (body.action === 'retrieve') {\n const result = await client.rag.retrieve({\n text: body.text ?? '',\n model: 'openai/text-embedding-3-small',\n topK: 3,\n includeContent: true,\n });\n return result.error ? c.json({ error: result.error }, 502) : c.json({ hits: result.data });\n }\n\n return c.json({ error: 'unknown action — use \"ingest\" or \"retrieve\"' }, 400);\n});\n\nexport default route;\n";
8644
+ //#endregion
8645
+ //#region ../../starters/rag/src/routes/retrieve.ts?raw
8646
+ var retrieve_default = "import { Hono } from 'hono';\n\nimport { client } from '../lib/client.ts';\nimport { log } from '../lib/logger.ts';\n\nconst route = new Hono();\n\nroute.post('/retrieve', async (c) => {\n const body = await c.req.json().catch(\n () => ({}) as { text?: string; topK?: number; model?: string },\n );\n const text = typeof body.text === 'string' ? body.text : '';\n const model = typeof body.model === 'string' ? body.model : 'openai/text-embedding-3-small';\n const topK = typeof body.topK === 'number' ? body.topK : 5;\n\n if (!text) return c.json({ error: 'missing text' }, 400);\n\n log('info', 'retrieve', { topK, model, length: text.length });\n\n const result = await client.rag.retrieve({ text, model, topK, includeContent: true });\n if (result.error) {\n log('error', 'retrieve failed', { code: result.error.code });\n return c.json({ error: result.error }, 502);\n }\n\n log('info', 'retrieve done', { hits: result.data.length });\n return c.json({ hits: result.data });\n});\n\nexport default route;\n";
8647
+ //#endregion
8648
+ //#region ../../starters/rag/src/routes/schema-info.ts?raw
8649
+ var schema_info_default$2 = "import { Hono } from 'hono';\n\nconst route = new Hono();\n\nroute.get('/schema', (c) =>\n c.json({\n input: {\n type: 'object',\n properties: {\n action: { type: 'string', enum: ['ingest', 'retrieve'] },\n text: { type: 'string' },\n },\n required: ['action'],\n },\n }),\n);\n\nexport default route;\n";
8650
+ //#endregion
8651
+ //#region ../../starters/secrets/.claude/skills/stackbone/SKILL.md?raw
8652
+ var SKILL_default$1 = "---\nname: stackbone\ndescription: Use this skill when the user is working with the Stackbone CLI, the `@stackbone/sdk` package, the `agent.yaml` manifest, official templates, or connections to the Stackbone control plane. Triggers on commands like `stackbone init`, `stackbone dev`, `stackbone deploy`, on questions about organization state, exit codes, or environment variables, and on any reference to `@stackbone/*` imports in TypeScript code. Provides the command surface, output contract, exit codes, environment variables and idiomatic workflows.\n---\n\n# Stackbone — agent operating manual\n\nThis skill is regenerated by `stackbone init` / `stackbone link` and ships\ninside the CLI release 0.1.0-alpha.1. Edit the upstream template in\n`apps/cli/src/skills/templates.ts` if something here is wrong.\n\n## CLI surface\n\nThe CLI binary is `stackbone` (also reachable as `npx @stackbone/cli@alpha ...`). Run\n`stackbone docs cli` for the canonical command/flag reference, or\n`stackbone docs sdk` for the `@stackbone/sdk` overview — neither command does\nnetwork IO.\n\n## Output contract\n\nEvery command supports `--json` (or `STACKBONE_JSON=1`). Successful JSON\nresponses look like:\n\n```json\n{ \"schema_version\": 1, \"...\": \"...\" }\n```\n\nErrors look like:\n\n```json\n{\n \"schema_version\": 1,\n \"error\": { \"code\": \"auth\", \"message\": \"...\", \"suggestion\": \"Run \\`stackbone login\\`\" },\n \"exit_code\": 2\n}\n```\n\n`schema_version` only bumps on backward-incompatible shape changes. If you\nsee a different value, refuse to parse and ask the user to upgrade.\n\n## Exit codes\n\n- `0` ok\n- `1` generic\n- `2` auth (no session, expired, refused login)\n- `3` no project linked (no `.stackbone/project.json` in cwd)\n- `4` not found (organization, agent, deployment)\n- `5` permission denied\n\nBranch on these instead of parsing stack traces.\n\n## Environment variables\n\n- `STACKBONE_JSON=1` — same as `--json`\n- `STACKBONE_API_URL` — same as `--api-url <url>`\n- `STACKBONE_AUTO_LOGIN=0` — same as `--no-auto-login`\n- `STACKBONE_LOG_LEVEL` — `debug`, `info`, `warn`, `error` (logger goes\n to stderr, never pollutes the JSON payload on stdout)\n\n## State on disk\n\n- `<cwd>/.stackbone/project.json` — organization + agent linked to this folder.\n Created by `stackbone init` / `link`.\n- `~/.stackbone/credentials.json` (chmod 600) — sessions keyed by control\n plane URL.\n- `~/.stackbone/config.json` — global preferences (default API URL,\n telemetry opt-in).\n\nThe control plane this skill points at: `https://api.stackbone.ai`.\nActive organization slug: `template1-2`.\n\n## Common workflows\n\n### Authenticate\n\n```sh\nstackbone login # interactive (device-code flow)\nstackbone whoami # confirm session\nstackbone metadata --json # one-shot overview that replaces 5–7 explorer calls\n```\n\n### Start a new project\n\n```sh\nstackbone init # menu of templates (workflow / autonomous / RAG)\nstackbone dev # local emulator at http://localhost:4242\n```\n\n### Deploy\n\n```sh\nstackbone deploy # build + push + Fly Machines\n```\n\n## Anti-patterns\n\n- Do not hardcode organization IDs, credentials or control plane URLs into\n agent code — the SDK reads them from the environment at runtime.\n- Do not parse human output. Re-run the command with `--json` and use\n `JSON.parse`.\n- Do not call the control plane HTTP API directly from a creator's agent.\n Use `@stackbone/sdk` so credential rotation, observability and metering\n stay correct.\n- Do not commit `.stackbone/` or `~/.stackbone/` — the entries are in the\n project's `.gitignore` for a reason.\n\n## Where to learn more\n\n- `stackbone docs cli` — command reference\n- `stackbone docs sdk` — `@stackbone/sdk` modules\n- `stackbone docs agent-yaml` — manifest reference (Epic 3)\n- `stackbone docs templates` — official templates (Epic 5)\n- `stackbone docs connections` — data layer (Epic 6)\n";
8653
+ //#endregion
8654
+ //#region ../../starters/secrets/.gitignore?raw
8655
+ var _gitignore_raw_default$1 = "# >>> stackbone >>>\n.stackbone/\nnode_modules/\ndist/\n# <<< stackbone <<<\n";
8656
+ //#endregion
8657
+ //#region ../../starters/secrets/Dockerfile?raw
8658
+ var Dockerfile_raw_default$1 = "# Distroless Node 24 image. Multi-stage so the runtime stays minimal.\nFROM node:24-alpine AS deps\nWORKDIR /app\nCOPY package.json ./\nRUN npm install --omit=dev\n\nFROM gcr.io/distroless/nodejs24-debian12\nWORKDIR /app\nCOPY --from=deps /app/node_modules ./node_modules\nCOPY src ./src\nCOPY agent.yaml ./\nENV PORT=8080\nEXPOSE 8080\nCMD [\"src/index.ts\"]\n";
8659
+ //#endregion
8660
+ //#region ../../starters/secrets/README.md?raw
8661
+ var README_default$1 = "# starter-secrets\n\nDemo of `client.secrets` (workspace-scoped encrypted values) from\n`@stackbone/sdk`.\n\n## Layout\n\n```\nsrc/\n index.ts # Hono server\n lib/\n client.ts # Shared Stackbone client\n logger.ts # JSON line logger\n routes/\n health.ts # GET /health\n schema-info.ts # GET /schema\n invoke.ts # POST /invoke -> check if secret is present\n get-secret.ts # GET /secrets/:name -> fetch value (demo only)\n get-many.ts # POST /secrets/bulk -> bulk check (names[] -> present/missing)\n```\n\n## Run\n\n```sh\nnpm install\nstackbone dev\n```\n\nThen (after registering `OPENAI_API_KEY` and `STRIPE_KEY` in the control plane):\n\n```sh\ncurl -X POST http://127.0.0.1:8080/invoke -H 'content-type: application/json' \\\n -d '{\"name\":\"OPENAI_API_KEY\"}'\ncurl -X POST http://127.0.0.1:8080/secrets/bulk -H 'content-type: application/json' \\\n -d '{\"names\":[\"OPENAI_API_KEY\",\"STRIPE_KEY\",\"NOT_SET\"]}'\n```\n";
8662
+ //#endregion
8663
+ //#region ../../starters/secrets/agent.yaml?raw
8664
+ var agent_default$1 = "# Stackbone agent manifest. Schema: stackbone.ai/v1.\n# Run `stackbone docs agent-yaml` for the full reference.\n\napiVersion: stackbone.ai/v1\nname: starter-secrets\nruntime:\n engine: node\n entry: src/index.ts\ndatabase:\n schema: ./src/schema.ts\n migrations: ./.stackbone/migrations\ndev:\n autoMigrate: false\nrag:\n embeddingModel: openai/text-embedding-3-small\n autoMigrate: false\n";
8665
+ //#endregion
8666
+ //#region ../../starters/secrets/src/index.ts?raw
8667
+ var src_default$1 = "import { serve } from '@hono/node-server';\nimport { Hono } from 'hono';\n\nimport { log } from './lib/logger.ts';\nimport getMany from './routes/get-many.ts';\nimport getSecret from './routes/get-secret.ts';\nimport health from './routes/health.ts';\nimport invoke from './routes/invoke.ts';\nimport schemaInfo from './routes/schema-info.ts';\n\nconst app = new Hono();\n\napp.use('*', async (c, next) => {\n const start = Date.now();\n await next();\n log('info', 'request', {\n method: c.req.method,\n path: c.req.path,\n status: c.res.status,\n durationMs: Date.now() - start,\n });\n});\n\napp.route('/', health);\napp.route('/', schemaInfo);\napp.route('/', invoke);\napp.route('/', getSecret);\napp.route('/', getMany);\n\nconst port = Number(process.env.PORT ?? 8080);\nserve({ fetch: app.fetch, port });\nlog('info', 'starter-secrets listening', { port });\n";
8668
+ //#endregion
8669
+ //#region ../../starters/secrets/src/lib/client.ts?raw
8670
+ var client_default$1 = "import { createClient } from '@stackbone/sdk';\n\nexport const client = createClient({\n stackboneApiUrl: process.env.STACKBONE_DEV_API_URL ?? 'http://127.0.0.1:4242',\n});\n";
8671
+ //#endregion
8672
+ //#region ../../starters/secrets/src/lib/logger.ts?raw
8673
+ var logger_default$1 = "type Level = 'info' | 'warn' | 'error';\n\nexport function log(level: Level, msg: string, fields?: Record<string, unknown>): void {\n const line = JSON.stringify({ ts: new Date().toISOString(), level, msg, ...fields });\n if (level === 'error') console.error(line);\n else console.log(line);\n}\n";
8674
+ //#endregion
8675
+ //#region ../../starters/secrets/src/routes/get-many.ts?raw
8676
+ var get_many_default = "import { Hono } from 'hono';\n\nimport { client } from '../lib/client.ts';\nimport { log } from '../lib/logger.ts';\n\nconst route = new Hono();\n\nroute.post('/secrets/bulk', async (c) => {\n const body = await c.req.json().catch(() => ({}) as { names?: string[] });\n const names = Array.isArray(body.names) ? body.names : [];\n if (names.length === 0) return c.json({ error: 'missing names[]' }, 400);\n\n log('info', 'fetch secrets bulk', { count: names.length });\n\n const result = await client.secrets.getMany(names);\n if (result.error) {\n log('error', 'bulk failed', { code: result.error.code });\n return c.json({ error: result.error }, 502);\n }\n\n // Mask values — just return which names resolved.\n const present = Object.keys(result.data);\n const missing = names.filter((name) => !present.includes(name));\n log('info', 'bulk done', { present: present.length, missing: missing.length });\n return c.json({ present, missing });\n});\n\nexport default route;\n";
8677
+ //#endregion
8678
+ //#region ../../starters/secrets/src/routes/get-secret.ts?raw
8679
+ var get_secret_default = "import { Hono } from 'hono';\n\nimport { client } from '../lib/client.ts';\nimport { log } from '../lib/logger.ts';\n\nconst route = new Hono();\n\n// Returns the raw secret value. Real agents would consume the value\n// internally — exposing it over HTTP defeats the purpose. This endpoint is\n// scaffolded for the demo only.\nroute.get('/secrets/:name', async (c) => {\n const name = c.req.param('name');\n log('info', 'fetch secret', { name });\n\n const result = await client.secrets.get(name);\n if (result.error) {\n log('error', 'fetch secret failed', { name, code: result.error.code });\n return c.json({ error: result.error }, 502);\n }\n\n log('info', 'fetch secret done', { name, length: result.data.length });\n return c.json({ name, value: result.data });\n});\n\nexport default route;\n";
8680
+ //#endregion
8681
+ //#region ../../starters/secrets/src/routes/health.ts?raw
8682
+ var health_default$1 = "import { Hono } from 'hono';\n\nconst route = new Hono();\nroute.get('/health', (c) => c.json({ status: 'ok' }));\nexport default route;\n";
8683
+ //#endregion
8684
+ //#region ../../starters/secrets/src/routes/invoke.ts?raw
8685
+ var invoke_default$1 = "import { Hono } from 'hono';\n\nimport { client } from '../lib/client.ts';\nimport { log } from '../lib/logger.ts';\n\nconst route = new Hono();\n\n// Checks whether a named secret is present in the agent's workspace without\n// leaking the value. Useful as a probe before running the dependent task.\nroute.post('/invoke', async (c) => {\n const body = await c.req.json().catch(() => ({}) as { name?: string });\n const name = typeof body.name === 'string' ? body.name : '';\n if (!name) return c.json({ error: 'missing name' }, 400);\n\n log('info', 'check secret', { name });\n const result = await client.secrets.get(name);\n\n if (result.error) {\n log('warn', 'secret missing', { name, code: result.error.code });\n return c.json({ name, present: false, code: result.error.code });\n }\n\n log('info', 'secret present', { name, length: result.data.length });\n return c.json({ name, present: true, length: result.data.length });\n});\n\nexport default route;\n";
8686
+ //#endregion
8687
+ //#region ../../starters/secrets/src/routes/schema-info.ts?raw
8688
+ var schema_info_default$1 = "import { Hono } from 'hono';\n\nconst route = new Hono();\n\nroute.get('/schema', (c) =>\n c.json({\n input: {\n type: 'object',\n properties: { name: { type: 'string' } },\n required: ['name'],\n },\n output: {\n type: 'object',\n properties: { name: { type: 'string' }, present: { type: 'boolean' } },\n },\n }),\n);\n\nexport default route;\n";
8689
+ //#endregion
8690
+ //#region ../../starters/storage/.claude/skills/stackbone/SKILL.md?raw
8691
+ var SKILL_default = "---\nname: stackbone\ndescription: Use this skill when the user is working with the Stackbone CLI, the `@stackbone/sdk` package, the `agent.yaml` manifest, official templates, or connections to the Stackbone control plane. Triggers on commands like `stackbone init`, `stackbone dev`, `stackbone deploy`, on questions about organization state, exit codes, or environment variables, and on any reference to `@stackbone/*` imports in TypeScript code. Provides the command surface, output contract, exit codes, environment variables and idiomatic workflows.\n---\n\n# Stackbone — agent operating manual\n\nThis skill is regenerated by `stackbone init` / `stackbone link` and ships\ninside the CLI release 0.1.0-alpha.1. Edit the upstream template in\n`apps/cli/src/skills/templates.ts` if something here is wrong.\n\n## CLI surface\n\nThe CLI binary is `stackbone` (also reachable as `npx @stackbone/cli@alpha ...`). Run\n`stackbone docs cli` for the canonical command/flag reference, or\n`stackbone docs sdk` for the `@stackbone/sdk` overview — neither command does\nnetwork IO.\n\n## Output contract\n\nEvery command supports `--json` (or `STACKBONE_JSON=1`). Successful JSON\nresponses look like:\n\n```json\n{ \"schema_version\": 1, \"...\": \"...\" }\n```\n\nErrors look like:\n\n```json\n{\n \"schema_version\": 1,\n \"error\": { \"code\": \"auth\", \"message\": \"...\", \"suggestion\": \"Run \\`stackbone login\\`\" },\n \"exit_code\": 2\n}\n```\n\n`schema_version` only bumps on backward-incompatible shape changes. If you\nsee a different value, refuse to parse and ask the user to upgrade.\n\n## Exit codes\n\n- `0` ok\n- `1` generic\n- `2` auth (no session, expired, refused login)\n- `3` no project linked (no `.stackbone/project.json` in cwd)\n- `4` not found (organization, agent, deployment)\n- `5` permission denied\n\nBranch on these instead of parsing stack traces.\n\n## Environment variables\n\n- `STACKBONE_JSON=1` — same as `--json`\n- `STACKBONE_API_URL` — same as `--api-url <url>`\n- `STACKBONE_AUTO_LOGIN=0` — same as `--no-auto-login`\n- `STACKBONE_LOG_LEVEL` — `debug`, `info`, `warn`, `error` (logger goes\n to stderr, never pollutes the JSON payload on stdout)\n\n## State on disk\n\n- `<cwd>/.stackbone/project.json` — organization + agent linked to this folder.\n Created by `stackbone init` / `link`.\n- `~/.stackbone/credentials.json` (chmod 600) — sessions keyed by control\n plane URL.\n- `~/.stackbone/config.json` — global preferences (default API URL,\n telemetry opt-in).\n\nThe control plane this skill points at: `https://api.stackbone.ai`.\nActive organization slug: `template1-2`.\n\n## Common workflows\n\n### Authenticate\n\n```sh\nstackbone login # interactive (device-code flow)\nstackbone whoami # confirm session\nstackbone metadata --json # one-shot overview that replaces 5–7 explorer calls\n```\n\n### Start a new project\n\n```sh\nstackbone init # menu of templates (workflow / autonomous / RAG)\nstackbone dev # local emulator at http://localhost:4242\n```\n\n### Deploy\n\n```sh\nstackbone deploy # build + push + Fly Machines\n```\n\n## Anti-patterns\n\n- Do not hardcode organization IDs, credentials or control plane URLs into\n agent code — the SDK reads them from the environment at runtime.\n- Do not parse human output. Re-run the command with `--json` and use\n `JSON.parse`.\n- Do not call the control plane HTTP API directly from a creator's agent.\n Use `@stackbone/sdk` so credential rotation, observability and metering\n stay correct.\n- Do not commit `.stackbone/` or `~/.stackbone/` — the entries are in the\n project's `.gitignore` for a reason.\n\n## Where to learn more\n\n- `stackbone docs cli` — command reference\n- `stackbone docs sdk` — `@stackbone/sdk` modules\n- `stackbone docs agent-yaml` — manifest reference (Epic 3)\n- `stackbone docs templates` — official templates (Epic 5)\n- `stackbone docs connections` — data layer (Epic 6)\n";
8692
+ //#endregion
8693
+ //#region ../../starters/storage/.gitignore?raw
8694
+ var _gitignore_raw_default = "# >>> stackbone >>>\n.stackbone/\nnode_modules/\ndist/\n# <<< stackbone <<<\n";
8695
+ //#endregion
8696
+ //#region ../../starters/storage/Dockerfile?raw
8268
8697
  var Dockerfile_raw_default = "# Distroless Node 24 image. Multi-stage so the runtime stays minimal.\nFROM node:24-alpine AS deps\nWORKDIR /app\nCOPY package.json ./\nRUN npm install --omit=dev\n\nFROM gcr.io/distroless/nodejs24-debian12\nWORKDIR /app\nCOPY --from=deps /app/node_modules ./node_modules\nCOPY src ./src\nCOPY agent.yaml ./\nENV PORT=8080\nEXPOSE 8080\nCMD [\"src/index.ts\"]\n";
8269
8698
  //#endregion
8270
- //#region src/templates/workflow/README.md?raw
8271
- var README_default = "# {{agentName}}\n\nStackbone agent scaffolded by `stackbone init`. The runtime contract is the\nHTTP surface in `src/index.ts`: `/invoke`, `/health`, `/schema`.\n\n## Develop\n\n```sh\npnpm install\npnpm dev # node --watch src/index.ts\n```\n\n## Deploy\n\n```sh\nstackbone deploy # builds the image and pushes it to Fly Machines\n```\n\nSee `stackbone docs sdk` for the SDK reference and `stackbone docs cli`\nfor the full command surface.\n";
8699
+ //#region ../../starters/storage/README.md?raw
8700
+ var README_default = "# starter-storage\n\nDemo of `client.storage` (S3 / MinIO) from `@stackbone/sdk`.\n\nThe template writes into a logical bucket called `demo`. Real keys land in\n`<agentId>/demo/...` inside the physical bucket configured via `S3_BUCKET`.\n\n## Layout\n\n```\nsrc/\n index.ts # Hono server\n lib/\n client.ts # Shared Stackbone client + BUCKET constant\n logger.ts # JSON line logger\n routes/\n health.ts # GET /health\n schema-info.ts # GET /schema\n invoke.ts # POST /invoke -> upload text\n list-objects.ts # GET /objects -> list (prefix?, limit?)\n download.ts # GET /objects/:key -> download bytes\n delete-object.ts # DELETE /objects/:key\n signed-url.ts # GET /signed-download/:key -> presigned GET\n # POST /signed-upload -> presigned PUT\n```\n\n## Run\n\n```sh\nnpm install\nstackbone dev\n```\n\nThen:\n\n```sh\ncurl -X POST http://127.0.0.1:8080/invoke -H 'content-type: application/json' \\\n -d '{\"key\":\"hello.txt\",\"body\":\"hello stackbone\"}'\ncurl http://127.0.0.1:8080/objects\ncurl http://127.0.0.1:8080/objects/hello.txt\ncurl http://127.0.0.1:8080/signed-download/hello.txt\ncurl -X DELETE http://127.0.0.1:8080/objects/hello.txt\n```\n\n`stackbone dev` provisions a local MinIO but does not inject S3 credentials\ninto the agent process export them yourself before booting:\n\n```sh\nexport STACKBONE_AGENT_ID=$(jq -r .agentId .stackbone/project.json)\nexport AWS_ACCESS_KEY_ID=stackbone\nexport AWS_SECRET_ACCESS_KEY=stackbone-secret\nexport S3_ENDPOINT=http://127.0.0.1:9004\nexport S3_BUCKET=stackbone-dev\nstackbone dev\n```\n\nIn production these come from the control plane / Fly secrets.\n";
8272
8701
  //#endregion
8273
- //#region src/templates/workflow/package.json?raw
8274
- var package_default = "{\n \"name\": \"{{agentSlug}}\",\n \"version\": \"0.0.1\",\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"node --watch src/index.ts\",\n \"start\": \"node src/index.ts\"\n },\n \"engines\": {\n \"node\": \">=24\"\n },\n \"dependencies\": {\n \"@hono/node-server\": \"^1.13.0\",\n \"@stackbone/sdk\": \"^0.0.1\",\n \"hono\": \"^4.6.0\"\n }\n}\n";
8702
+ //#region ../../starters/storage/agent.yaml?raw
8703
+ var agent_default = "# Stackbone agent manifest. Schema: stackbone.ai/v1.\n# Run `stackbone docs agent-yaml` for the full reference.\n\napiVersion: stackbone.ai/v1\nname: starter-storage\nruntime:\n engine: node\n entry: src/index.ts\ndatabase:\n schema: ./src/schema.ts\n migrations: ./.stackbone/migrations\ndev:\n autoMigrate: false\nrag:\n embeddingModel: openai/text-embedding-3-small\n autoMigrate: false\n";
8275
8704
  //#endregion
8276
- //#region src/templates/workflow/src/index.ts?raw
8277
- var src_default = "import { serve } from '@hono/node-server';\nimport { Hono } from 'hono';\n\n// MOCK workflow. In MVP this becomes a LangGraph state machine driven by\n// `@stackbone/sdk` (`client.ai.completion`); for POC the nodes are pure JS\n// returning hardcoded structured output so `stackbone dev` has something to\n// iterate on without an LLM bill.\n\ninterface State {\n input: string;\n classified?: 'lead' | 'spam';\n reply?: string;\n}\n\nconst classify = (s: State): State => ({\n ...s,\n classified: s.input.toLowerCase().includes('buy') ? 'lead' : 'spam',\n});\n\nconst draftReply = (s: State): State => ({\n ...s,\n reply:\n s.classified === 'lead'\n ? `Thanks! A human will follow up about: ${s.input}`\n : 'Filtered as spam.',\n});\n\nconst fallback = (s: State): State =>\n s.reply ? s : { ...s, reply: 'Could not produce a reply, escalating.' };\n\nconst runGraph = (input: string): State => fallback(draftReply(classify({ input })));\n\nconst app = new Hono();\n\napp.post('/invoke', async (c) => {\n const body = await c.req.json().catch(() => ({ input: '' }));\n const state = runGraph(typeof body.input === 'string' ? body.input : '');\n return c.json(state);\n});\n\napp.get('/health', (c) => c.json({ status: 'ok' }));\napp.get('/schema', (c) =>\n c.json({\n input: { type: 'object', properties: { input: { type: 'string' } }, required: ['input'] },\n output: {\n type: 'object',\n properties: {\n input: { type: 'string' },\n classified: { type: 'string', enum: ['lead', 'spam'] },\n reply: { type: 'string' },\n },\n },\n }),\n);\n\nconst port = Number(process.env.PORT ?? 8080);\nserve({ fetch: app.fetch, port });\nconsole.log(`workflow agent listening on :${port}`);\n";
8705
+ //#region ../../starters/storage/src/index.ts?raw
8706
+ var src_default = "import { serve } from '@hono/node-server';\nimport { Hono } from 'hono';\n\nimport { log } from './lib/logger.ts';\nimport deleteObject from './routes/delete-object.ts';\nimport download from './routes/download.ts';\nimport health from './routes/health.ts';\nimport invoke from './routes/invoke.ts';\nimport listObjects from './routes/list-objects.ts';\nimport schemaInfo from './routes/schema-info.ts';\nimport signedUrl from './routes/signed-url.ts';\n\nconst app = new Hono();\n\napp.use('*', async (c, next) => {\n const start = Date.now();\n await next();\n log('info', 'request', {\n method: c.req.method,\n path: c.req.path,\n status: c.res.status,\n durationMs: Date.now() - start,\n });\n});\n\napp.route('/', health);\napp.route('/', schemaInfo);\napp.route('/', invoke);\napp.route('/', listObjects);\napp.route('/', download);\napp.route('/', deleteObject);\napp.route('/', signedUrl);\n\nconst port = Number(process.env.PORT ?? 8080);\nserve({ fetch: app.fetch, port });\nlog('info', 'starter-storage listening', { port });\n";
8278
8707
  //#endregion
8279
- //#region src/templates/workflow/src/schema.ts?raw
8280
- var schema_default = "// Drizzle schema for {{agentName}}.\n//\n// Tables declared here are managed by `stackbone db migrate up`. Edit this\n// file, then run `stackbone db migrate create <name>` to emit the next SQL\n// migration under `.stackbone/migrations/`. The first migration shipped with\n// the template (`0000_init.sql`) creates the `examples` table below so the\n// agent has something queryable on its first `stackbone dev` boot.\n//\n// Drizzle column helpers re-export through `@stackbone/sdk/db` — never import\n// from `drizzle-orm/pg-core` directly. The SDK barrel is the single contract\n// the platform supports.\nimport { pgTable, serial, text, timestamp } from '@stackbone/sdk/db';\n\nexport const examples = pgTable('examples', {\n id: serial('id').primaryKey(),\n name: text('name').notNull(),\n createdAt: timestamp('created_at').defaultNow().notNull(),\n});\n";
8708
+ //#region ../../starters/storage/src/lib/client.ts?raw
8709
+ var client_default = "import { createClient } from '@stackbone/sdk';\n\nexport const client = createClient({\n stackboneApiUrl: process.env.STACKBONE_DEV_API_URL ?? 'http://127.0.0.1:4242',\n});\n\n// Single logical bucket the template writes into. Real keys are namespaced\n// underneath by `client.storage` (prefixed with the agent id).\nexport const BUCKET = 'demo';\n";
8281
8710
  //#endregion
8282
- //#region src/services/scaffolding.ts
8283
- var TOKEN_PATTERN = /{{\s*(\w+)\s*}}/g;
8284
- var manifestModules = /* @__PURE__ */ Object.assign({
8285
- "../templates/autonomous/template.yaml": template_default$2,
8286
- "../templates/hello-world/template.yaml": template_default$1,
8287
- "../templates/workflow/template.yaml": template_default
8288
- });
8289
- var fileModules = /* @__PURE__ */ Object.assign({
8290
- "../templates/autonomous/.stackbone/migrations/0000_init.sql": _0000_init_default$2,
8291
- "../templates/autonomous/.stackbone/migrations/meta/0000_snapshot.json": _0000_snapshot_default$2,
8292
- "../templates/autonomous/.stackbone/migrations/meta/_journal.json": _journal_default$2,
8293
- "../templates/autonomous/Dockerfile": Dockerfile_raw_default$2,
8294
- "../templates/autonomous/README.md": README_default$2,
8295
- "../templates/autonomous/package.json": package_default$2,
8296
- "../templates/autonomous/src/index.ts": src_default$2,
8297
- "../templates/autonomous/src/schema.ts": schema_default$2,
8298
- "../templates/hello-world/.stackbone/migrations/0000_init.sql": _0000_init_default$1,
8299
- "../templates/hello-world/.stackbone/migrations/meta/0000_snapshot.json": _0000_snapshot_default$1,
8300
- "../templates/hello-world/.stackbone/migrations/meta/_journal.json": _journal_default$1,
8301
- "../templates/hello-world/Dockerfile": Dockerfile_raw_default$1,
8302
- "../templates/hello-world/README.md": README_default$1,
8303
- "../templates/hello-world/package.json": package_default$1,
8304
- "../templates/hello-world/src/index.ts": src_default$1,
8305
- "../templates/hello-world/src/schema.ts": schema_default$1,
8306
- "../templates/workflow/.stackbone/migrations/0000_init.sql": _0000_init_default,
8307
- "../templates/workflow/.stackbone/migrations/meta/0000_snapshot.json": _0000_snapshot_default,
8308
- "../templates/workflow/.stackbone/migrations/meta/_journal.json": _journal_default,
8309
- "../templates/workflow/Dockerfile": Dockerfile_raw_default,
8310
- "../templates/workflow/README.md": README_default,
8311
- "../templates/workflow/package.json": package_default,
8312
- "../templates/workflow/src/index.ts": src_default,
8313
- "../templates/workflow/src/schema.ts": schema_default
8314
- });
8315
- var requireString = (value, field, id) => {
8316
- if (typeof value !== "string" || value.length === 0) throw new Error(`Template '${id}': field '${field}' must be a non-empty string.`);
8711
+ //#region ../../starters/storage/src/lib/logger.ts?raw
8712
+ var logger_default = "type Level = 'info' | 'warn' | 'error';\n\nexport function log(level: Level, msg: string, fields?: Record<string, unknown>): void {\n const line = JSON.stringify({ ts: new Date().toISOString(), level, msg, ...fields });\n if (level === 'error') console.error(line);\n else console.log(line);\n}\n";
8713
+ //#endregion
8714
+ //#region ../../starters/storage/src/routes/delete-object.ts?raw
8715
+ var delete_object_default = "import { Hono } from 'hono';\n\nimport { BUCKET, client } from '../lib/client.ts';\nimport { log } from '../lib/logger.ts';\n\nconst route = new Hono();\n\nroute.delete('/objects/:key', async (c) => {\n const key = c.req.param('key');\n log('info', 'remove', { key });\n\n const result = await client.storage.from(BUCKET).remove(key);\n if (result.error) {\n log('error', 'remove failed', { key, code: result.error.code });\n return c.json({ error: result.error }, 502);\n }\n\n log('info', 'remove done', { key });\n return c.json(result.data);\n});\n\nexport default route;\n";
8716
+ //#endregion
8717
+ //#region ../../starters/storage/src/routes/download.ts?raw
8718
+ var download_default = "import { Hono } from 'hono';\n\nimport { BUCKET, client } from '../lib/client.ts';\nimport { log } from '../lib/logger.ts';\n\nconst route = new Hono();\n\n// Returns the object body verbatim. Streaming-safe via response cloning so\n// the agent does not have to materialise large objects fully in memory.\nroute.get('/objects/:key', async (c) => {\n const key = c.req.param('key');\n log('info', 'download', { key });\n\n const result = await client.storage.from(BUCKET).download(key);\n if (result.error) {\n log('error', 'download failed', { key, code: result.error.code });\n return c.json({ error: result.error }, 502);\n }\n\n const buffer = await result.data.arrayBuffer();\n log('info', 'download done', { key, bytes: buffer.byteLength });\n return new Response(buffer, {\n headers: { 'content-type': result.data.type || 'application/octet-stream' },\n });\n});\n\nexport default route;\n";
8719
+ //#endregion
8720
+ //#region ../../starters/storage/src/routes/health.ts?raw
8721
+ var health_default = "import { Hono } from 'hono';\n\nconst route = new Hono();\nroute.get('/health', (c) => c.json({ status: 'ok' }));\nexport default route;\n";
8722
+ //#endregion
8723
+ //#region ../../starters/storage/src/routes/invoke.ts?raw
8724
+ var invoke_default = "import { Hono } from 'hono';\n\nimport { BUCKET, client } from '../lib/client.ts';\nimport { log } from '../lib/logger.ts';\n\nconst route = new Hono();\n\n// `/invoke` uploads a small text blob to the demo bucket — the simplest path\n// to validate the agent has working S3 credentials.\nroute.post('/invoke', async (c) => {\n const body = await c.req.json().catch(() => ({}) as { key?: string; body?: string });\n const key = body.key ?? `invoke-${Date.now()}.txt`;\n const content = body.body ?? 'hello from starter-storage';\n\n log('info', 'upload', { key, bytes: content.length });\n\n const result = await client.storage.from(BUCKET).upload(key, content, {\n contentType: 'text/plain',\n });\n\n if (result.error) {\n log('error', 'upload failed', { key, code: result.error.code });\n return c.json({ error: result.error }, 502);\n }\n\n log('info', 'upload done', { key: result.data.key });\n return c.json(result.data);\n});\n\nexport default route;\n";
8725
+ //#endregion
8726
+ //#region ../../starters/storage/src/routes/list-objects.ts?raw
8727
+ var list_objects_default = "import { Hono } from 'hono';\n\nimport { BUCKET, client } from '../lib/client.ts';\nimport { log } from '../lib/logger.ts';\n\nconst route = new Hono();\n\nroute.get('/objects', async (c) => {\n const prefix = c.req.query('prefix');\n const limit = c.req.query('limit') ? Number(c.req.query('limit')) : undefined;\n\n log('info', 'list objects', { prefix, limit });\n\n const result = await client.storage.from(BUCKET).list({ prefix, limit });\n if (result.error) {\n log('error', 'list failed', { code: result.error.code });\n return c.json({ error: result.error }, 502);\n }\n\n log('info', 'list done', { count: result.data.objects.length });\n return c.json(result.data);\n});\n\nexport default route;\n";
8728
+ //#endregion
8729
+ //#region ../../starters/storage/src/routes/schema-info.ts?raw
8730
+ var schema_info_default = "import { Hono } from 'hono';\n\nconst route = new Hono();\n\nroute.get('/schema', (c) =>\n c.json({\n input: {\n type: 'object',\n properties: { key: { type: 'string' }, body: { type: 'string' } },\n required: ['key', 'body'],\n },\n output: {\n type: 'object',\n properties: { key: { type: 'string' }, etag: { type: 'string' } },\n },\n }),\n);\n\nexport default route;\n";
8731
+ //#endregion
8732
+ //#region ../../starters/storage/src/routes/signed-url.ts?raw
8733
+ var signed_url_default = "import { Hono } from 'hono';\n\nimport { BUCKET, client } from '../lib/client.ts';\nimport { log } from '../lib/logger.ts';\n\nconst route = new Hono();\n\n// Pre-signed download URL — used by clients that pull large objects directly\n// from S3/MinIO without going through the agent.\nroute.get('/signed-download/:key', async (c) => {\n const key = c.req.param('key');\n const expiresIn = c.req.query('expiresIn') ? Number(c.req.query('expiresIn')) : undefined;\n\n log('info', 'signed download', { key, expiresIn });\n\n const result = await client.storage.from(BUCKET).getSignedDownloadUrl(key, { expiresIn });\n if (result.error) {\n log('error', 'signed download failed', { key, code: result.error.code });\n return c.json({ error: result.error }, 502);\n }\n\n return c.json(result.data);\n});\n\n// Pre-signed upload URL — clients PUT directly to S3 with a matching\n// content-type to avoid going through the agent on the upload path.\nroute.post('/signed-upload', async (c) => {\n const body = await c.req.json().catch(() => ({}) as { key?: string; contentType?: string });\n const key = body.key ?? `upload-${Date.now()}.bin`;\n\n log('info', 'signed upload', { key, contentType: body.contentType });\n\n const result = await client.storage\n .from(BUCKET)\n .getSignedUploadUrl(key, { contentType: body.contentType });\n if (result.error) {\n log('error', 'signed upload failed', { key, code: result.error.code });\n return c.json({ error: result.error }, 502);\n }\n\n return c.json(result.data);\n});\n\nexport default route;\n";
8734
+ //#endregion
8735
+ //#region src/services/starter-loader.ts
8736
+ var STARTERS_PATH_RE = /(?:^|\/)starters\/([^/]+)\/(.+)$/;
8737
+ var OTHER_CATEGORY = "Other";
8738
+ var slugFromManifestPath$1 = (path) => {
8739
+ const match = STARTERS_PATH_RE.exec(path);
8740
+ if (!match || match[2] !== "package.json") return null;
8741
+ return match[1];
8742
+ };
8743
+ var slugFromFilePath = (path) => {
8744
+ const match = STARTERS_PATH_RE.exec(path);
8745
+ if (!match) return null;
8746
+ const rel = match[2];
8747
+ if (rel === "package.json") return null;
8748
+ return {
8749
+ slug: match[1],
8750
+ rel
8751
+ };
8752
+ };
8753
+ var requireString = (value, field, slug) => {
8754
+ if (typeof value !== "string" || value.length === 0) throw new Error(`Starter '${slug}': field 'stackbone.${field}' must be a non-empty string.`);
8317
8755
  return value;
8318
8756
  };
8319
- var idFromManifestPath = (path) => {
8320
- const match = path.match(/\/templates\/([^/]+)\/template\.yaml$/);
8321
- if (!match || !match[1]) throw new Error(`Unexpected manifest path '${path}' (expected '../templates/<id>/template.yaml').`);
8322
- return match[1];
8757
+ var parseManifest = (slug, raw) => {
8758
+ let parsed;
8759
+ try {
8760
+ parsed = JSON.parse(raw);
8761
+ } catch (error) {
8762
+ const reason = error instanceof Error ? error.message : String(error);
8763
+ throw new Error(`Starter '${slug}': package.json is not valid JSON (${reason}).`);
8764
+ }
8765
+ if (typeof parsed.name !== "string" || parsed.name.length === 0) throw new Error(`Starter '${slug}': package.json 'name' must be a non-empty string.`);
8766
+ const expected = `starter-${slug}`;
8767
+ if (parsed.name !== expected) throw new Error(`Starter '${slug}': package.json 'name' is '${parsed.name}' but must be '${expected}'.`);
8768
+ const stackbone = parsed.stackbone ?? {};
8769
+ const label = requireString(stackbone.label, "label", slug);
8770
+ const hint = requireString(stackbone.hint, "hint", slug);
8771
+ let order = null;
8772
+ if (stackbone.order !== void 0) {
8773
+ if (typeof stackbone.order !== "number" || !Number.isFinite(stackbone.order)) throw new Error(`Starter '${slug}': field 'stackbone.order' must be a finite number.`);
8774
+ order = stackbone.order;
8775
+ }
8776
+ let category = null;
8777
+ if (stackbone.category !== void 0) {
8778
+ if (typeof stackbone.category !== "string" || stackbone.category.length === 0) throw new Error(`Starter '${slug}': field 'stackbone.category' must be a non-empty string when present.`);
8779
+ category = stackbone.category;
8780
+ }
8781
+ return {
8782
+ packageName: parsed.name,
8783
+ meta: {
8784
+ label,
8785
+ hint,
8786
+ order,
8787
+ category
8788
+ }
8789
+ };
8323
8790
  };
8324
- var groupFilesByTemplate = () => {
8791
+ var groupFiles = (fileMap) => {
8325
8792
  const grouped = /* @__PURE__ */ new Map();
8326
- for (const [path, contents] of Object.entries(fileModules)) {
8327
- const match = path.match(/\/templates\/([^/]+)\/(.+)$/);
8328
- if (!match) continue;
8329
- const id = match[1];
8330
- const rel = match[2];
8331
- let bucket = grouped.get(id);
8793
+ for (const [path, contents] of Object.entries(fileMap)) {
8794
+ const parsed = slugFromFilePath(path);
8795
+ if (!parsed) continue;
8796
+ let bucket = grouped.get(parsed.slug);
8332
8797
  if (!bucket) {
8333
8798
  bucket = [];
8334
- grouped.set(id, bucket);
8799
+ grouped.set(parsed.slug, bucket);
8335
8800
  }
8336
8801
  bucket.push({
8337
- path: rel,
8802
+ path: parsed.rel,
8338
8803
  contents
8339
8804
  });
8340
8805
  }
8341
8806
  for (const bucket of grouped.values()) bucket.sort((a, b) => a.path.localeCompare(b.path));
8342
8807
  return grouped;
8343
8808
  };
8344
- var validateVariables = (id, variables, files) => {
8345
- const declared = new Set(variables);
8346
- const undeclared = /* @__PURE__ */ new Set();
8347
- for (const file of files) for (const match of file.contents.matchAll(TOKEN_PATTERN)) {
8348
- const name = match[1];
8349
- if (name && !declared.has(name)) undeclared.add(name);
8809
+ /**
8810
+ * Compare two starters by the documented ordering: `(category, order, slug)`.
8811
+ *
8812
+ * - `category`: starters without a declared category fall into an `Other` bucket
8813
+ * placed after named categories (lex order among named categories).
8814
+ * - `order`: lower numeric values come first; missing `order` is treated as
8815
+ * `+Infinity` so explicit-order starters always win against defaults.
8816
+ * - `slug`: final tiebreaker, lexicographic.
8817
+ */
8818
+ var compareStarters = (a, b) => {
8819
+ const catA = a.metadata.category ?? OTHER_CATEGORY;
8820
+ const catB = b.metadata.category ?? OTHER_CATEGORY;
8821
+ if (catA !== catB) {
8822
+ if (catA === OTHER_CATEGORY) return 1;
8823
+ if (catB === OTHER_CATEGORY) return -1;
8824
+ return catA.localeCompare(catB);
8350
8825
  }
8351
- if (undeclared.size > 0) throw new Error(`Template '${id}' uses undeclared variables: ${[...undeclared].join(", ")}. Add them to template.yaml 'variables'.`);
8352
- };
8353
- var parseManifest = (id, raw, files) => {
8354
- const parsed = parse(raw);
8355
- if (!parsed) throw new Error(`Template '${id}': empty or invalid YAML.`);
8356
- if (parsed.id !== id) throw new Error(`Template '${id}': manifest id '${String(parsed.id)}' does not match directory name.`);
8357
- if (typeof parsed.order !== "number") throw new Error(`Template '${id}': field 'order' must be a number.`);
8358
- const variables = Array.isArray(parsed.variables) ? parsed.variables.map(String) : [];
8359
- validateVariables(id, variables, files);
8360
- return {
8361
- id,
8362
- label: requireString(parsed.label, "label", id),
8363
- hint: requireString(parsed.hint, "hint", id),
8364
- order: parsed.order,
8365
- variables,
8366
- files
8367
- };
8826
+ const orderA = a.metadata.order ?? Number.POSITIVE_INFINITY;
8827
+ const orderB = b.metadata.order ?? Number.POSITIVE_INFINITY;
8828
+ if (orderA !== orderB) return orderA - orderB;
8829
+ return a.slug.localeCompare(b.slug);
8368
8830
  };
8369
- var loadTemplates = () => {
8370
- const filesByTemplate = groupFilesByTemplate();
8371
- const all = [];
8372
- for (const [path, raw] of Object.entries(manifestModules)) {
8373
- const id = idFromManifestPath(path);
8374
- all.push(parseManifest(id, raw, filesByTemplate.get(id) ?? []));
8831
+ /**
8832
+ * Load starters from raw `import.meta.glob` maps.
8833
+ *
8834
+ * @param manifestMap one entry per `starters/<slug>/package.json` (raw JSON contents)
8835
+ * @param fileMap every other file under `starters/<slug>/**` (raw string contents)
8836
+ * @returns the loaded starters ordered by `(category, order, slug)`
8837
+ */
8838
+ var loadStarters = (manifestMap, fileMap) => {
8839
+ const filesBySlug = groupFiles(fileMap);
8840
+ const loaded = [];
8841
+ for (const [path, raw] of Object.entries(manifestMap)) {
8842
+ const slug = slugFromManifestPath$1(path);
8843
+ if (!slug) continue;
8844
+ const { packageName, meta } = parseManifest(slug, raw);
8845
+ loaded.push({
8846
+ slug,
8847
+ packageName,
8848
+ metadata: meta,
8849
+ files: filesBySlug.get(slug) ?? []
8850
+ });
8375
8851
  }
8376
- all.sort((a, b) => a.order - b.order || a.id.localeCompare(b.id));
8377
- return new Map(all.map((t) => [t.id, t]));
8378
- };
8379
- var TEMPLATES_BY_ID = loadTemplates();
8380
- var TEMPLATES = [...TEMPLATES_BY_ID.values()].map((t) => ({
8381
- id: t.id,
8382
- label: t.label,
8383
- hint: t.hint
8384
- }));
8385
- var parseTemplateId = (value) => {
8386
- if (!value) return null;
8387
- return TEMPLATES_BY_ID.has(value) ? value : null;
8852
+ loaded.sort(compareStarters);
8853
+ return loaded;
8388
8854
  };
8389
- var substitute = (contents, input) => contents.replace(TOKEN_PATTERN, (_match, key) => {
8390
- if (key in input) return input[key];
8391
- throw new Error(`Template variable '${key}' is not provided. Known: ${Object.keys(input).join(", ")}.`);
8392
- });
8393
- var renderTemplate = (templateId, input) => {
8394
- const template = TEMPLATES_BY_ID.get(templateId);
8395
- if (!template) throw new Error(`Unknown template id '${templateId}'. Known: ${[...TEMPLATES_BY_ID.keys()].join(", ")}.`);
8396
- return template.files.map((file) => ({
8855
+ //#endregion
8856
+ //#region src/services/starter-renderer.ts
8857
+ /**
8858
+ * Replace every literal occurrence of `needle` in `haystack` with
8859
+ * `replacement`. The needle is treated as a literal string even when it
8860
+ * contains regex-special characters.
8861
+ */
8862
+ var replaceAllLiteral = (haystack, needle, replacement) => {
8863
+ if (needle.length === 0) return haystack;
8864
+ return haystack.split(needle).join(replacement);
8865
+ };
8866
+ /**
8867
+ * Render a loaded starter into a file collection ready to be written to disk.
8868
+ *
8869
+ * The renderer is pure and idempotent: re-running with the same `agentSlug`
8870
+ * on already-rendered contents is a no-op.
8871
+ *
8872
+ * @param starter the loaded starter, carrying its `packageName` source string
8873
+ * @param options `{ agentSlug }` — the target slug to substitute in contents
8874
+ * @returns a new file collection with substituted contents; paths preserved
8875
+ */
8876
+ var renderStarter$1 = (starter, options) => {
8877
+ const { agentSlug } = options;
8878
+ const { packageName } = starter;
8879
+ return starter.files.map((file) => ({
8397
8880
  path: file.path,
8398
- contents: substitute(file.contents, input),
8399
- ...file.executable ? { executable: true } : {}
8881
+ contents: replaceAllLiteral(file.contents, packageName, agentSlug)
8400
8882
  }));
8401
8883
  };
8402
8884
  //#endregion
8885
+ //#region src/services/scaffolding.ts
8886
+ var manifestModules = /* @__PURE__ */ Object.assign({
8887
+ "../../../../starters/ai/package.json": package_default$9,
8888
+ "../../../../starters/config/package.json": package_default$8,
8889
+ "../../../../starters/db/package.json": package_default$7,
8890
+ "../../../../starters/hello-world/package.json": package_default$6,
8891
+ "../../../../starters/hitl/package.json": package_default$5,
8892
+ "../../../../starters/prompts/package.json": package_default$4,
8893
+ "../../../../starters/queues/package.json": package_default$3,
8894
+ "../../../../starters/rag/package.json": package_default$2,
8895
+ "../../../../starters/secrets/package.json": package_default$1,
8896
+ "../../../../starters/storage/package.json": package_default
8897
+ });
8898
+ var fileModules = /* @__PURE__ */ Object.assign({
8899
+ "../../../../starters/ai/.claude/skills/stackbone/SKILL.md": SKILL_default$8,
8900
+ "../../../../starters/ai/.gitignore": _gitignore_raw_default$8,
8901
+ "../../../../starters/ai/Dockerfile": Dockerfile_raw_default$9,
8902
+ "../../../../starters/ai/README.md": README_default$9,
8903
+ "../../../../starters/ai/agent.yaml": agent_default$9,
8904
+ "../../../../starters/ai/src/index.ts": src_default$9,
8905
+ "../../../../starters/ai/src/lib/client.ts": client_default$8,
8906
+ "../../../../starters/ai/src/lib/logger.ts": logger_default$8,
8907
+ "../../../../starters/ai/src/routes/embeddings.ts": embeddings_default,
8908
+ "../../../../starters/ai/src/routes/health.ts": health_default$8,
8909
+ "../../../../starters/ai/src/routes/invoke.ts": invoke_default$8,
8910
+ "../../../../starters/ai/src/routes/models.ts": models_default,
8911
+ "../../../../starters/ai/src/routes/schema-info.ts": schema_info_default$8,
8912
+ "../../../../starters/config/.claude/skills/stackbone/SKILL.md": SKILL_default$7,
8913
+ "../../../../starters/config/.gitignore": _gitignore_raw_default$7,
8914
+ "../../../../starters/config/Dockerfile": Dockerfile_raw_default$8,
8915
+ "../../../../starters/config/README.md": README_default$8,
8916
+ "../../../../starters/config/agent.yaml": agent_default$8,
8917
+ "../../../../starters/config/src/index.ts": src_default$8,
8918
+ "../../../../starters/config/src/lib/client.ts": client_default$7,
8919
+ "../../../../starters/config/src/lib/logger.ts": logger_default$7,
8920
+ "../../../../starters/config/src/routes/get-config.ts": get_config_default,
8921
+ "../../../../starters/config/src/routes/get-many.ts": get_many_default$1,
8922
+ "../../../../starters/config/src/routes/health.ts": health_default$7,
8923
+ "../../../../starters/config/src/routes/invoke.ts": invoke_default$7,
8924
+ "../../../../starters/config/src/routes/schema-info.ts": schema_info_default$7,
8925
+ "../../../../starters/db/.claude/skills/stackbone/SKILL.md": SKILL_default$6,
8926
+ "../../../../starters/db/.gitignore": _gitignore_raw_default$6,
8927
+ "../../../../starters/db/Dockerfile": Dockerfile_raw_default$7,
8928
+ "../../../../starters/db/README.md": README_default$7,
8929
+ "../../../../starters/db/agent.yaml": agent_default$7,
8930
+ "../../../../starters/db/src/index.ts": src_default$7,
8931
+ "../../../../starters/db/src/lib/client.ts": client_default$6,
8932
+ "../../../../starters/db/src/lib/logger.ts": logger_default$6,
8933
+ "../../../../starters/db/src/routes/delete-example.ts": delete_example_default,
8934
+ "../../../../starters/db/src/routes/health.ts": health_default$6,
8935
+ "../../../../starters/db/src/routes/invoke.ts": invoke_default$6,
8936
+ "../../../../starters/db/src/routes/list-examples.ts": list_examples_default,
8937
+ "../../../../starters/db/src/routes/schema-info.ts": schema_info_default$6,
8938
+ "../../../../starters/db/src/schema.ts": schema_default$1,
8939
+ "../../../../starters/hello-world/.stackbone/migrations/0000_init.sql": _0000_init_default,
8940
+ "../../../../starters/hello-world/.stackbone/migrations/meta/0000_snapshot.json": _0000_snapshot_default,
8941
+ "../../../../starters/hello-world/.stackbone/migrations/meta/_journal.json": _journal_default,
8942
+ "../../../../starters/hello-world/Dockerfile": Dockerfile_raw_default$6,
8943
+ "../../../../starters/hello-world/README.md": README_default$6,
8944
+ "../../../../starters/hello-world/agent.yaml": agent_default$6,
8945
+ "../../../../starters/hello-world/src/index.ts": src_default$6,
8946
+ "../../../../starters/hello-world/src/schema.ts": schema_default,
8947
+ "../../../../starters/hitl/.claude/skills/stackbone/SKILL.md": SKILL_default$5,
8948
+ "../../../../starters/hitl/.gitignore": _gitignore_raw_default$5,
8949
+ "../../../../starters/hitl/Dockerfile": Dockerfile_raw_default$5,
8950
+ "../../../../starters/hitl/README.md": README_default$5,
8951
+ "../../../../starters/hitl/agent.yaml": agent_default$5,
8952
+ "../../../../starters/hitl/src/index.ts": src_default$5,
8953
+ "../../../../starters/hitl/src/lib/client.ts": client_default$5,
8954
+ "../../../../starters/hitl/src/lib/logger.ts": logger_default$5,
8955
+ "../../../../starters/hitl/src/routes/cancel.ts": cancel_default,
8956
+ "../../../../starters/hitl/src/routes/decide.ts": decide_default,
8957
+ "../../../../starters/hitl/src/routes/get-approval.ts": get_approval_default,
8958
+ "../../../../starters/hitl/src/routes/health.ts": health_default$5,
8959
+ "../../../../starters/hitl/src/routes/invoke.ts": invoke_default$5,
8960
+ "../../../../starters/hitl/src/routes/list-approvals.ts": list_approvals_default,
8961
+ "../../../../starters/hitl/src/routes/schema-info.ts": schema_info_default$5,
8962
+ "../../../../starters/prompts/.claude/skills/stackbone/SKILL.md": SKILL_default$4,
8963
+ "../../../../starters/prompts/.gitignore": _gitignore_raw_default$4,
8964
+ "../../../../starters/prompts/Dockerfile": Dockerfile_raw_default$4,
8965
+ "../../../../starters/prompts/README.md": README_default$4,
8966
+ "../../../../starters/prompts/agent.yaml": agent_default$4,
8967
+ "../../../../starters/prompts/src/index.ts": src_default$4,
8968
+ "../../../../starters/prompts/src/lib/client.ts": client_default$4,
8969
+ "../../../../starters/prompts/src/lib/logger.ts": logger_default$4,
8970
+ "../../../../starters/prompts/src/routes/create-prompt.ts": create_prompt_default,
8971
+ "../../../../starters/prompts/src/routes/get-prompt.ts": get_prompt_default,
8972
+ "../../../../starters/prompts/src/routes/health.ts": health_default$4,
8973
+ "../../../../starters/prompts/src/routes/invoke.ts": invoke_default$4,
8974
+ "../../../../starters/prompts/src/routes/list-prompts.ts": list_prompts_default,
8975
+ "../../../../starters/prompts/src/routes/schema-info.ts": schema_info_default$4,
8976
+ "../../../../starters/queues/.claude/skills/stackbone/SKILL.md": SKILL_default$3,
8977
+ "../../../../starters/queues/.gitignore": _gitignore_raw_default$3,
8978
+ "../../../../starters/queues/Dockerfile": Dockerfile_raw_default$3,
8979
+ "../../../../starters/queues/README.md": README_default$3,
8980
+ "../../../../starters/queues/agent.yaml": agent_default$3,
8981
+ "../../../../starters/queues/src/index.ts": src_default$3,
8982
+ "../../../../starters/queues/src/lib/client.ts": client_default$3,
8983
+ "../../../../starters/queues/src/lib/logger.ts": logger_default$3,
8984
+ "../../../../starters/queues/src/routes/health.ts": health_default$3,
8985
+ "../../../../starters/queues/src/routes/invoke.ts": invoke_default$3,
8986
+ "../../../../starters/queues/src/routes/publish.ts": publish_default,
8987
+ "../../../../starters/queues/src/routes/receive.ts": receive_default,
8988
+ "../../../../starters/queues/src/routes/schema-info.ts": schema_info_default$3,
8989
+ "../../../../starters/rag/.claude/skills/stackbone/SKILL.md": SKILL_default$2,
8990
+ "../../../../starters/rag/.gitignore": _gitignore_raw_default$2,
8991
+ "../../../../starters/rag/Dockerfile": Dockerfile_raw_default$2,
8992
+ "../../../../starters/rag/README.md": README_default$2,
8993
+ "../../../../starters/rag/agent.yaml": agent_default$2,
8994
+ "../../../../starters/rag/src/index.ts": src_default$2,
8995
+ "../../../../starters/rag/src/lib/client.ts": client_default$2,
8996
+ "../../../../starters/rag/src/lib/logger.ts": logger_default$2,
8997
+ "../../../../starters/rag/src/routes/chunk.ts": chunk_default,
8998
+ "../../../../starters/rag/src/routes/delete.ts": delete_default,
8999
+ "../../../../starters/rag/src/routes/health.ts": health_default$2,
9000
+ "../../../../starters/rag/src/routes/ingest.ts": ingest_default,
9001
+ "../../../../starters/rag/src/routes/invoke.ts": invoke_default$2,
9002
+ "../../../../starters/rag/src/routes/retrieve.ts": retrieve_default,
9003
+ "../../../../starters/rag/src/routes/schema-info.ts": schema_info_default$2,
9004
+ "../../../../starters/secrets/.claude/skills/stackbone/SKILL.md": SKILL_default$1,
9005
+ "../../../../starters/secrets/.gitignore": _gitignore_raw_default$1,
9006
+ "../../../../starters/secrets/Dockerfile": Dockerfile_raw_default$1,
9007
+ "../../../../starters/secrets/README.md": README_default$1,
9008
+ "../../../../starters/secrets/agent.yaml": agent_default$1,
9009
+ "../../../../starters/secrets/src/index.ts": src_default$1,
9010
+ "../../../../starters/secrets/src/lib/client.ts": client_default$1,
9011
+ "../../../../starters/secrets/src/lib/logger.ts": logger_default$1,
9012
+ "../../../../starters/secrets/src/routes/get-many.ts": get_many_default,
9013
+ "../../../../starters/secrets/src/routes/get-secret.ts": get_secret_default,
9014
+ "../../../../starters/secrets/src/routes/health.ts": health_default$1,
9015
+ "../../../../starters/secrets/src/routes/invoke.ts": invoke_default$1,
9016
+ "../../../../starters/secrets/src/routes/schema-info.ts": schema_info_default$1,
9017
+ "../../../../starters/storage/.claude/skills/stackbone/SKILL.md": SKILL_default,
9018
+ "../../../../starters/storage/.gitignore": _gitignore_raw_default,
9019
+ "../../../../starters/storage/Dockerfile": Dockerfile_raw_default,
9020
+ "../../../../starters/storage/README.md": README_default,
9021
+ "../../../../starters/storage/agent.yaml": agent_default,
9022
+ "../../../../starters/storage/src/index.ts": src_default,
9023
+ "../../../../starters/storage/src/lib/client.ts": client_default,
9024
+ "../../../../starters/storage/src/lib/logger.ts": logger_default,
9025
+ "../../../../starters/storage/src/routes/delete-object.ts": delete_object_default,
9026
+ "../../../../starters/storage/src/routes/download.ts": download_default,
9027
+ "../../../../starters/storage/src/routes/health.ts": health_default,
9028
+ "../../../../starters/storage/src/routes/invoke.ts": invoke_default,
9029
+ "../../../../starters/storage/src/routes/list-objects.ts": list_objects_default,
9030
+ "../../../../starters/storage/src/routes/schema-info.ts": schema_info_default,
9031
+ "../../../../starters/storage/src/routes/signed-url.ts": signed_url_default
9032
+ });
9033
+ var slugFromManifestPath = (path) => {
9034
+ const match = /(?:^|\/)starters\/([^/]+)\/package\.json$/.exec(path);
9035
+ return match ? match[1] : null;
9036
+ };
9037
+ var manifestContentsBySlug = new Map(Object.entries(manifestModules).map(([path, raw]) => {
9038
+ const slug = slugFromManifestPath(path);
9039
+ return slug ? [slug, raw] : null;
9040
+ }).filter((entry) => entry !== null));
9041
+ var withManifestFile = (starter) => {
9042
+ const manifestContents = manifestContentsBySlug.get(starter.slug);
9043
+ if (!manifestContents) return starter;
9044
+ const files = [{
9045
+ path: "package.json",
9046
+ contents: manifestContents
9047
+ }, ...starter.files];
9048
+ files.sort((a, b) => a.path.localeCompare(b.path));
9049
+ return {
9050
+ ...starter,
9051
+ files
9052
+ };
9053
+ };
9054
+ var STARTERS_BY_ID = new Map(loadStarters(manifestModules, fileModules).map(withManifestFile).map((s) => [s.slug, s]));
9055
+ var STARTERS = [...STARTERS_BY_ID.values()].map((s) => ({
9056
+ id: s.slug,
9057
+ label: s.metadata.label,
9058
+ hint: s.metadata.hint
9059
+ }));
9060
+ var parseStarterId = (value) => {
9061
+ if (!value) return null;
9062
+ return STARTERS_BY_ID.has(value) ? value : null;
9063
+ };
9064
+ var renderStarter = (starterId, input) => {
9065
+ const starter = STARTERS_BY_ID.get(starterId);
9066
+ if (!starter) throw new Error(`Unknown starter id '${starterId}'. Known: ${[...STARTERS_BY_ID.keys()].join(", ")}.`);
9067
+ return renderStarter$1(starter, { agentSlug: input.agentSlug });
9068
+ };
9069
+ var STARTER_DESCRIPTORS = [...STARTERS_BY_ID.values()].map((s) => ({
9070
+ id: s.slug,
9071
+ label: s.metadata.label,
9072
+ hint: s.metadata.hint,
9073
+ category: s.metadata.category,
9074
+ order: s.metadata.order
9075
+ }));
9076
+ //#endregion
8403
9077
  //#region src/commands/init/init.command.ts
9078
+ var DEFAULT_STARTER_ID = "hello-world";
8404
9079
  var STACKBONE_OWNED_FILES = [
8405
9080
  "agent.yaml",
8406
9081
  ".stackbone/project.json",
@@ -8415,6 +9090,11 @@ var STACKBONE_OWNED_FILES = [
8415
9090
  ".claude/skills/stackbone/SKILL.md"
8416
9091
  ];
8417
9092
  var NAME_REGEX = /^[a-z0-9][a-z0-9-]{0,63}$/i;
9093
+ var CATEGORY_PRIORITY = {
9094
+ Core: 0,
9095
+ Capabilities: 1
9096
+ };
9097
+ var UNCATEGORISED_LABEL = "Other";
8418
9098
  var directoryNameOrFallback = (cwd) => {
8419
9099
  const base = basename(cwd) || "stackbone-agent";
8420
9100
  return NAME_REGEX.test(base) ? base : "stackbone-agent";
@@ -8439,8 +9119,18 @@ var ensureEmptyOrForce = async (targetDir, force) => {
8439
9119
  suggestion: "Re-run with `--force` or pick an empty directory"
8440
9120
  });
8441
9121
  };
9122
+ var bailIfCancelled = (value) => {
9123
+ if (isCancel(value)) {
9124
+ cancel("Init cancelled.");
9125
+ throw new StackboneCliError({
9126
+ code: "generic",
9127
+ message: "Init cancelled by user"
9128
+ });
9129
+ }
9130
+ return value;
9131
+ };
8442
9132
  var promptForName = async (defaultName) => {
8443
- const value = await text({
9133
+ return bailIfCancelled(await text({
8444
9134
  message: "Agent name",
8445
9135
  placeholder: defaultName,
8446
9136
  defaultValue: defaultName,
@@ -8449,42 +9139,58 @@ var promptForName = async (defaultName) => {
8449
9139
  if (v.length === 0) return "Name cannot be empty";
8450
9140
  if (v.length > 120) return "Name is too long";
8451
9141
  }
8452
- });
8453
- if (isCancel(value)) {
8454
- cancel("Init cancelled.");
8455
- throw new StackboneCliError({
8456
- code: "generic",
8457
- message: "Init cancelled by user"
8458
- });
8459
- }
8460
- return value.trim();
8461
- };
8462
- var promptForTemplate = async () => {
8463
- const value = await select({
8464
- message: "Pick a template",
8465
- options: TEMPLATES.map((t) => ({
8466
- value: t.id,
8467
- label: t.label,
8468
- hint: t.hint
8469
- }))
8470
- });
8471
- if (isCancel(value)) {
8472
- cancel("Init cancelled.");
8473
- throw new StackboneCliError({
8474
- code: "generic",
8475
- message: "Init cancelled by user"
9142
+ })).trim();
9143
+ };
9144
+ var buildPickerOptions = () => {
9145
+ const sections = /* @__PURE__ */ new Map();
9146
+ for (const starter of STARTER_DESCRIPTORS) {
9147
+ const category = starter.category ?? UNCATEGORISED_LABEL;
9148
+ let bucket = sections.get(category);
9149
+ if (!bucket) {
9150
+ bucket = [];
9151
+ sections.set(category, bucket);
9152
+ }
9153
+ bucket.push({
9154
+ value: starter.id,
9155
+ label: `[${category}] ${starter.label}`,
9156
+ hint: starter.hint
8476
9157
  });
8477
9158
  }
8478
- return value;
9159
+ const sectionOrder = (name) => {
9160
+ if (name === UNCATEGORISED_LABEL) return [Number.POSITIVE_INFINITY, name];
9161
+ return [CATEGORY_PRIORITY[name] ?? Number.POSITIVE_INFINITY - 1, name];
9162
+ };
9163
+ return [...sections.entries()].sort(([a], [b]) => {
9164
+ const [pa, na] = sectionOrder(a);
9165
+ const [pb, nb] = sectionOrder(b);
9166
+ if (pa !== pb) return pa - pb;
9167
+ return na.localeCompare(nb);
9168
+ }).flatMap(([, options]) => options);
9169
+ };
9170
+ var promptForStarter = async () => {
9171
+ return bailIfCancelled(await select({
9172
+ message: "Pick a starter",
9173
+ options: buildPickerOptions()
9174
+ }));
8479
9175
  };
8480
- var resolveInputs = async (ctx, flags, namePositional) => {
9176
+ var validSlugList = () => STARTERS.map((s) => s.id).join(", ");
9177
+ var resolveInitInputs = async (ctx, flags, namePositional) => {
9178
+ if (flags.legacyTemplate !== void 0) throw new StackboneCliError({
9179
+ code: "generic",
9180
+ message: `--template was renamed to --starter; rerun with --starter ${flags.legacyTemplate}`
9181
+ });
8481
9182
  const targetDir = resolve(ctx.cwd, namePositional ?? ".");
8482
9183
  const fallbackName = namePositional ?? directoryNameOrFallback(targetDir);
8483
9184
  const interactive = isInteractiveStdin(ctx.env) && !ctx.flags.yes && !ctx.flags.json;
8484
9185
  let agentName = flags.name ?? namePositional ?? (interactive ? await promptForName(fallbackName) : fallbackName);
8485
9186
  agentName = agentName.trim();
8486
- let templateId = parseTemplateId(flags.template);
8487
- if (!templateId) templateId = interactive ? await promptForTemplate() : "hello-world";
9187
+ let starterId = parseStarterId(flags.starter);
9188
+ if (flags.starter !== void 0 && starterId === null) throw new StackboneCliError({
9189
+ code: "generic",
9190
+ message: `Unknown starter '${flags.starter}'`,
9191
+ suggestion: `Valid starters: ${validSlugList()}`
9192
+ });
9193
+ if (!starterId) starterId = interactive ? await promptForStarter() : DEFAULT_STARTER_ID;
8488
9194
  if (flags.slug !== void 0) {
8489
9195
  if (!agentSlugSchema.safeParse(flags.slug).success) throw new StackboneCliError({
8490
9196
  code: "generic",
@@ -8496,27 +9202,29 @@ var resolveInputs = async (ctx, flags, namePositional) => {
8496
9202
  agentName,
8497
9203
  agentSlug: flags.slug,
8498
9204
  description: flags.description,
8499
- templateId,
9205
+ starterId,
8500
9206
  targetDir
8501
9207
  };
8502
9208
  };
8503
- var writeProject = async (ctx, resolved, agent, force) => {
9209
+ var writeProject = async (ctx, resolved, agent, force, localDev) => {
8504
9210
  const writeInput = {
8505
9211
  root: resolved.targetDir,
8506
9212
  force
8507
9213
  };
8508
9214
  await mkdir(resolved.targetDir, { recursive: true });
8509
- const templateFiles = renderTemplate(resolved.templateId, {
8510
- agentName: resolved.agentName,
8511
- agentSlug: agent.slug
8512
- });
8513
- await writeTemplateFiles(templateFiles, writeInput);
9215
+ const starterFiles = renderStarter(resolved.starterId, { agentSlug: agent.slug });
9216
+ await writeStarterFiles(starterFiles, writeInput);
8514
9217
  await writeAgentManifest(resolved.agentName, writeInput);
8515
- const projectConfig = await writeProjectConfig(ctx, agent, writeInput);
9218
+ const projectConfig = await writeProjectConfig(ctx, agent, writeInput, {
9219
+ localDevInstallationId: localDev.id,
9220
+ organizationSlug: localDev.organizationSlug,
9221
+ agentSlug: localDev.agentSlug,
9222
+ starter: resolved.starterId
9223
+ });
8516
9224
  await ensureGitignore(resolved.targetDir);
8517
9225
  const skillFiles = await writeSkillFiles(ctx, agent, projectConfig, writeInput);
8518
9226
  return { filesWritten: [
8519
- ...templateFiles.map((f) => f.path),
9227
+ ...starterFiles.map((f) => f.path),
8520
9228
  "agent.yaml",
8521
9229
  ".stackbone/project.json",
8522
9230
  ".gitignore",
@@ -8527,21 +9235,31 @@ var buildHandler$1 = (ctx, args) => protectedCommand(async (innerCtx) => {
8527
9235
  if (!innerCtx.flags.json) intro("stackbone init");
8528
9236
  const flags = {
8529
9237
  name: args.name,
8530
- template: args.template,
9238
+ starter: args.starter,
9239
+ legacyTemplate: args.template,
8531
9240
  slug: args.slug,
8532
9241
  description: args.description,
8533
9242
  force: Boolean(args.force)
8534
9243
  };
8535
- const resolved = await resolveInputs(innerCtx, flags, args.name);
9244
+ const resolved = await resolveInitInputs(innerCtx, flags, args.name);
8536
9245
  await ensureEmptyOrForce(resolved.targetDir, flags.force);
8537
- if (!innerCtx.flags.json) log.step(`Creating "${resolved.agentName}" (template: ${resolved.templateId})`);
9246
+ if (!innerCtx.flags.json) log.step(`Creating "${resolved.agentName}" (starter: ${resolved.starterId})`);
9247
+ const activeOrg = await getActiveOrganization(innerCtx);
8538
9248
  const agent = await createAgent(innerCtx, {
8539
- organizationSlug: (await getActiveOrganization(innerCtx)).slug,
9249
+ organizationSlug: activeOrg.slug,
8540
9250
  name: resolved.agentName,
8541
9251
  ...resolved.agentSlug !== void 0 ? { slug: resolved.agentSlug } : {},
8542
9252
  ...resolved.description !== void 0 ? { description: resolved.description } : {}
8543
9253
  });
8544
- const { filesWritten } = await writeProject(innerCtx, resolved, agent, flags.force);
9254
+ const localDev = await upsertLocalDevInstallation(innerCtx, {
9255
+ creatorOrganizationSlug: activeOrg.slug,
9256
+ agentTemplateSlug: agent.slug
9257
+ });
9258
+ const { filesWritten } = await writeProject(innerCtx, resolved, agent, flags.force, {
9259
+ id: localDev.id,
9260
+ organizationSlug: localDev.organizationSlug,
9261
+ agentSlug: localDev.agentSlug
9262
+ });
8545
9263
  emit(innerCtx.flags, () => "", {
8546
9264
  agent: {
8547
9265
  id: agent.id,
@@ -8549,8 +9267,12 @@ var buildHandler$1 = (ctx, args) => protectedCommand(async (innerCtx) => {
8549
9267
  name: agent.name
8550
9268
  },
8551
9269
  organization_id: agent.organizationId,
9270
+ local_dev_installation: {
9271
+ id: localDev.id,
9272
+ organization_slug: localDev.organizationSlug
9273
+ },
8552
9274
  target_dir: resolved.targetDir,
8553
- template: resolved.templateId,
9275
+ starter: resolved.starterId,
8554
9276
  files_written: filesWritten
8555
9277
  });
8556
9278
  if (!innerCtx.flags.json) outro([
@@ -8566,7 +9288,7 @@ var buildHandler$1 = (ctx, args) => protectedCommand(async (innerCtx) => {
8566
9288
  var createInitCommand = (ctx) => defineCommand({
8567
9289
  meta: {
8568
9290
  name: "init",
8569
- description: "Scaffold a new Stackbone project with templates + Claude skill."
9291
+ description: "Scaffold a new Stackbone project with starters + Claude skill."
8570
9292
  },
8571
9293
  args: {
8572
9294
  name: {
@@ -8574,9 +9296,13 @@ var createInitCommand = (ctx) => defineCommand({
8574
9296
  description: "Subdirectory + initial agent name. Omit to use the current directory.",
8575
9297
  required: false
8576
9298
  },
9299
+ starter: {
9300
+ type: "string",
9301
+ description: `Starter slug. One of ${validSlugList()}. Default: ${DEFAULT_STARTER_ID}.`
9302
+ },
8577
9303
  template: {
8578
9304
  type: "string",
8579
- description: `Template id. One of ${TEMPLATES.map((t) => t.id).join(", ")}. Default: hello-world.`
9305
+ description: "(removed) renamed to --starter; pre-rename flag kept only to emit the migration message."
8580
9306
  },
8581
9307
  slug: {
8582
9308
  type: "string",
@@ -8637,11 +9363,20 @@ var buildHandler = (ctx, args) => protectedCommand(async (innerCtx) => {
8637
9363
  }
8638
9364
  chosenSlug = value;
8639
9365
  }
8640
- const agent = await getAgentByOwnerAndSlug(innerCtx, (await getActiveOrganization(innerCtx)).slug, chosenSlug);
9366
+ const activeOrg = await getActiveOrganization(innerCtx);
9367
+ const agent = await getAgentByOwnerAndSlug(innerCtx, activeOrg.slug, chosenSlug);
8641
9368
  if (!innerCtx.flags.json) log.step(`Linking ${targetDir} to ${agent.slug}`);
9369
+ const localDev = await upsertLocalDevInstallation(innerCtx, {
9370
+ creatorOrganizationSlug: activeOrg.slug,
9371
+ agentTemplateSlug: agent.slug
9372
+ });
8642
9373
  const projectConfig = await writeProjectConfig(innerCtx, agent, {
8643
9374
  root: targetDir,
8644
9375
  force: true
9376
+ }, {
9377
+ localDevInstallationId: localDev.id,
9378
+ organizationSlug: localDev.organizationSlug,
9379
+ agentSlug: localDev.agentSlug
8645
9380
  });
8646
9381
  await ensureGitignore(targetDir);
8647
9382
  const skillFiles = await writeSkillFiles(innerCtx, agent, projectConfig, {
@@ -8655,6 +9390,10 @@ var buildHandler = (ctx, args) => protectedCommand(async (innerCtx) => {
8655
9390
  name: agent.name
8656
9391
  },
8657
9392
  organization_id: agent.organizationId,
9393
+ local_dev_installation: {
9394
+ id: localDev.id,
9395
+ organization_slug: localDev.organizationSlug
9396
+ },
8658
9397
  target_dir: targetDir,
8659
9398
  files_written: [
8660
9399
  ".stackbone/project.json",
@@ -8884,9 +9623,9 @@ var buildRootCommand = async () => {
8884
9623
  type: "string",
8885
9624
  description: "Override the control plane URL."
8886
9625
  },
8887
- "no-auto-login": {
9626
+ verbose: {
8888
9627
  type: "boolean",
8889
- description: "Refuse to start the device-code flow when no session exists.",
9628
+ description: "Stream every log line and docker compose output instead of the per-stage spinner UI.",
8890
9629
  default: false
8891
9630
  }
8892
9631
  },