@kybernesis/create 0.7.10 → 0.7.13

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/dist/deploy.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { execFileSync, spawnSync } from "node:child_process";
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
- import { bold, dim, green, red, yellow } from "./util.js";
4
+ import { upsertEnv } from "./envfile.js";
5
+ import { ask, bold, dim, green, red, yellow } from "./util.js";
5
6
  /**
6
7
  * `kyb deploy` — put this repo on its host and restart it, with proof.
7
8
  *
@@ -42,6 +43,59 @@ function sshTarget(dir, explicit) {
42
43
  return `${e.EXE_VM_NAME}.exe.xyz`;
43
44
  return null;
44
45
  }
46
+ /**
47
+ * Make sure EXE_MODEL names a model the integration actually serves.
48
+ *
49
+ * `kyb init` leaves it empty on purpose: the valid ids come from the host's
50
+ * integration, and the laptop cannot see them — `llm.int.exe.xyz` resolves only
51
+ * from an attached VM. But nothing then asked again, so a deployment could
52
+ * complete, report healthy, and fail on the first message with an error that
53
+ * never mentions the empty value.
54
+ *
55
+ * The host CAN see the catalog, and this command is already talking to it. So
56
+ * ask it, and offer the ids. Two details worth keeping: the id must carry its
57
+ * provider prefix (`openai/…`), and an unknown id answers `404 unsupported
58
+ * endpoint: /v1/responses` — an error about the endpoint, for a problem with
59
+ * the model.
60
+ */
61
+ async function ensureModel(dir, target) {
62
+ if (env(dir).EXE_MODEL)
63
+ return;
64
+ console.log(yellow(" EXE_MODEL is empty — the agent has no model to run."));
65
+ console.log(dim(" Asking the host which models its integration serves …"));
66
+ let ids = [];
67
+ try {
68
+ const raw = execFileSync("ssh", [target, "curl -s --max-time 15 https://llm.int.exe.xyz/models.json || true"], { encoding: "utf8" });
69
+ const catalog = JSON.parse(raw);
70
+ // The live catalog is `{ schema_version, models: [...] }`. `data` and a bare
71
+ // array are accepted too rather than assuming one shape forever.
72
+ const entries = Array.isArray(catalog) ? catalog : (catalog.models ?? catalog.data ?? []);
73
+ ids = entries
74
+ .map((m) => m?.id)
75
+ .filter((id) => typeof id === "string" && id.includes("/"))
76
+ .sort();
77
+ }
78
+ catch {
79
+ ids = [];
80
+ }
81
+ if (ids.length === 0) {
82
+ console.log(red("\n Could not read the model catalog from the host."));
83
+ console.log(dim(` Check the integration is attached (ssh exe.dev integrations list), then run on the host:\n` +
84
+ ` ssh ${target} 'curl -s https://llm.int.exe.xyz/models.json'\n` +
85
+ ` Set EXE_MODEL to an id WITH its provider prefix, e.g. openai/gpt-5.6-sol.\n`));
86
+ process.exit(1);
87
+ }
88
+ console.log(dim("\n Models this host can serve:"));
89
+ ids.forEach((id, i) => console.log(` ${i + 1}. ${id}`));
90
+ const answer = await ask(`\n EXE_MODEL (number or full id)?`, ids[0]);
91
+ const chosen = /^\d+$/.test(answer.trim()) ? ids[Number(answer.trim()) - 1] : answer.trim();
92
+ if (!chosen) {
93
+ console.log(red(" No model chosen — stopping before deploying an agent that cannot answer."));
94
+ process.exit(1);
95
+ }
96
+ upsertEnv(dir, { EXE_MODEL: chosen });
97
+ console.log(green(` ✓ EXE_MODEL="${chosen}" written to .env.local\n`));
98
+ }
45
99
  export async function deploy(options) {
46
100
  const dir = options.dir ?? process.cwd();
47
101
  if (!existsSync(join(dir, "agent"))) {
@@ -66,6 +120,7 @@ export async function deploy(options) {
66
120
  const name = JSON.parse(readFileSync(join(dir, "package.json"), "utf8")).name ?? "agent";
67
121
  const remote = `~/${name}`;
68
122
  console.log(dim(` host: ${target} path: ${remote}\n`));
123
+ await ensureModel(dir, target);
69
124
  const run = (cmd, args) => {
70
125
  const r = spawnSync(cmd, args, { cwd: dir, stdio: "inherit" });
71
126
  return (r.status ?? 1) === 0;
@@ -190,15 +245,47 @@ export async function deploy(options) {
190
245
  "echo started",
191
246
  ].join("\n");
192
247
  run("ssh", [target, `cd ${remote}\n${remoteScript}`]);
193
- console.log(dim("\n Waiting for the restart to report …"));
248
+ /**
249
+ * Wait on PROGRESS, not on a clock.
250
+ *
251
+ * A restart takes seconds, so this used to poll for 200s and then declare
252
+ * failure. A FIRST deploy is not a restart: it installs node_modules and
253
+ * builds from cold, which routinely runs longer than that. The deploy would
254
+ * report "did not report health" and exit non-zero while the build was still
255
+ * running perfectly well — and the honest reading of that message is that the
256
+ * thing is broken, so the next move is usually to kill it and start over.
257
+ *
258
+ * So: give up only when the host goes QUIET (nothing new in the log for a few
259
+ * minutes), and echo each new line meanwhile, because a long wait with
260
+ * visible progress is a different experience from a long wait in silence.
261
+ */
262
+ console.log(dim("\n Waiting for the restart to report (first deploys build from cold) …"));
263
+ const QUIET_LIMIT_MS = 4 * 60_000;
264
+ const CEILING_MS = 30 * 60_000;
265
+ const startedAt = Date.now();
194
266
  let last = "";
195
- for (let i = 0; i < 40; i++) {
196
- const out = execFileSync("ssh", [target, `tail -6 /tmp/kyb-deploy.log 2>/dev/null || true`], {
267
+ let seen = "";
268
+ let lastChange = Date.now();
269
+ while (Date.now() - startedAt < CEILING_MS) {
270
+ const out = execFileSync("ssh", [target, `tail -40 /tmp/kyb-deploy.log 2>/dev/null || true`], {
197
271
  encoding: "utf8",
198
272
  });
199
273
  last = out;
274
+ if (out !== seen) {
275
+ for (const line of out.split("\n")) {
276
+ if (line && !seen.includes(line) && /npm|install|build|SOURCE IS NEWER|waiting|pid=|health:|FAILED/i.test(line)) {
277
+ console.log(dim(` ${line.trim().slice(0, 120)}`));
278
+ }
279
+ }
280
+ seen = out;
281
+ lastChange = Date.now();
282
+ }
200
283
  if (/health:|FAILED/.test(out))
201
284
  break;
285
+ if (Date.now() - lastChange > QUIET_LIMIT_MS) {
286
+ console.log(yellow(`\n ! nothing new on the host for ${Math.round(QUIET_LIMIT_MS / 60_000)} minutes — giving up on the wait`));
287
+ break;
288
+ }
202
289
  await new Promise((r) => setTimeout(r, 5000));
203
290
  }
204
291
  const healthy = /health:\s*200/.test(last);
@@ -209,7 +296,8 @@ export async function deploy(options) {
209
296
  .join("\n"));
210
297
  console.log(healthy
211
298
  ? green("\n ✓ deployed and serving the current build")
212
- : yellow("\n ! the restart did not report health — check /tmp/kyb-deploy.log on the host"));
299
+ : yellow("\n ! the restart did not report health. It may still be building this stops watching, it does not stop the host.\n" +
300
+ ` Watch it: ssh ${target} 'tail -f /tmp/kyb-deploy.log'`));
213
301
  if (!healthy)
214
302
  process.exitCode = 1;
215
303
  }
package/dist/doctor.js CHANGED
@@ -26,12 +26,30 @@ export async function doctor() {
26
26
  }
27
27
  const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
28
28
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
29
- for (const p of ["@kybernesis/arcana", "@kybernesis/enterprise", "@kybernesis/multiplayer", "@kybernesis/evals"]) {
29
+ for (const p of ["@kybernesis/arcana", "@kybernesis/enterprise", "@kybernesis/evals"]) {
30
30
  if (deps[p])
31
31
  add("pass", `${p} ${deps[p]}`);
32
32
  else
33
33
  add("warn", `${p} not installed`, `eve add @kybernesis/${p.split("/")[1]}`);
34
34
  }
35
+ /**
36
+ * multiplayer is conversation mechanics for a SHARED channel — threads with
37
+ * per-speaker identity, the public/DM split. An agent reached only through
38
+ * Studio or a direct API has no such channel, so telling its operator to
39
+ * install it is advice that cannot be usefully acted on. A warning nobody can
40
+ * clear is how a checklist stops being read.
41
+ */
42
+ const channelsDir = join(cwd, "agent", "channels");
43
+ const sharedChannel = existsSync(channelsDir) && readdirSync(channelsDir).some((f) => /^(slack|discord|telegram)\./.test(f));
44
+ if (deps["@kybernesis/multiplayer"]) {
45
+ add("pass", `@kybernesis/multiplayer ${deps["@kybernesis/multiplayer"]}`);
46
+ }
47
+ else if (sharedChannel) {
48
+ add("warn", "@kybernesis/multiplayer not installed", "this agent has a shared channel, which needs its thread + per-speaker identity mechanics");
49
+ }
50
+ else {
51
+ add("pass", "no shared channel, so multiplayer is not needed");
52
+ }
35
53
  // ── env ────────────────────────────────────────────────────────────────
36
54
  const envPath = join(cwd, ".env.local");
37
55
  const env = {
@@ -214,10 +232,24 @@ export async function doctor() {
214
232
  else {
215
233
  add("pass", "no Vercel Connect dependencies (correct for a self-hosted agent)");
216
234
  }
217
- // eve start does not read .env.local the way eve dev does.
218
- add("warn", "self-hosted: export .env.local into the server process", "eve start does NOT read it; use the supervision script from @kybernesis/exe (scripts/eve-server.sh)");
219
- // Prewarm runs in the eve CLI, not the built server.
220
- add("warn", "self-hosted: start via `npx eve start`, not `node .output/server/index.mjs`", "sandbox templates are prewarmed by the CLI; starting the server directly skips prewarm and every sandbox tool fails with SandboxTemplateNotProvisionedError");
235
+ /**
236
+ * Both of the next two are real ways a self-hosted agent boots broken:
237
+ * `eve start` does not read .env.local the way `eve dev` does, and sandbox
238
+ * templates are prewarmed by the CLI, so launching the built server
239
+ * directly fails every sandbox tool with SandboxTemplateNotProvisionedError.
240
+ *
241
+ * The supervision script does both correctly. When it is present, saying so
242
+ * is the useful report — repeating the hazard as a warning teaches the
243
+ * operator that warnings here are decoration.
244
+ */
245
+ const supervisor = join(cwd, "scripts/eve-server.sh");
246
+ if (existsSync(supervisor)) {
247
+ add("pass", "scripts/eve-server.sh present (exports .env.local, starts via the eve CLI)");
248
+ }
249
+ else {
250
+ add("warn", "self-hosted: export .env.local into the server process", "eve start does NOT read it; use the supervision script from @kybernesis/exe (scripts/eve-server.sh)");
251
+ add("warn", "self-hosted: start via `npx eve start`, not `node .output/server/index.mjs`", "sandbox templates are prewarmed by the CLI; starting the server directly skips prewarm and every sandbox tool fails with SandboxTemplateNotProvisionedError");
252
+ }
221
253
  // The exe VM sandbox backend needs a credential that cannot be scoped.
222
254
  // Surface the blast radius here, where it is still cheap to change course.
223
255
  const sandboxFile = join(cwd, "agent/sandbox/sandbox.ts");
@@ -267,7 +299,18 @@ export async function doctor() {
267
299
  else {
268
300
  add("fail", "management routes have no KYBERNESIS_AGENT", "KYBER Studio cannot install or write routines here: the agent cannot check a grant for a name it does not know");
269
301
  }
270
- add("warn", "management routes need a writable working copy", "installing edits this repo and rebuilds; on a read-only serverless bundle the routes refuse. Set restartCommand in agent/channels/kyb.ts or an install will not take effect");
302
+ // Installing edits the repo and rebuilds, so it only takes effect where a
303
+ // restart can be triggered. With restartCommand set, that is answered; the
304
+ // remaining requirement (a writable working copy) is a property of the
305
+ // host, and on a read-only bundle the routes refuse with that reason.
306
+ const manageFile = join(cwd, "agent/channels/kyb.ts");
307
+ const restartWired = existsSync(manageFile) && /^\s*restartCommand\s*:/m.test(readFileSync(manageFile, "utf8"));
308
+ if (restartWired) {
309
+ add("pass", "management routes can restart the agent after an install");
310
+ }
311
+ else {
312
+ add("warn", "management routes have no restartCommand", "installing edits this repo and rebuilds; without a restart the response says a restart is still required. Set restartCommand in agent/channels/kyb.ts (self-hosted: \"bash scripts/eve-server.sh restart\")");
313
+ }
271
314
  }
272
315
  // ── engineer subagent (build capability scoped to a subagent) ──────────
273
316
  const builderDir = join(cwd, "agent/subagents/builder");
package/dist/init.js CHANGED
@@ -194,6 +194,26 @@ export async function init(rawName, options = {}) {
194
194
  catch {
195
195
  console.log(yellow(" ! could not install scripts/eve-server.sh — copy it from node_modules/@kybernesis/exe/scripts/"));
196
196
  }
197
+ /**
198
+ * Point the management routes at that script.
199
+ *
200
+ * The registry template ships `restartCommand` commented out next to an
201
+ * example path, because on Vercel there is nothing to restart. On a VM
202
+ * there is, and the correct value is not a guess — it is the script three
203
+ * lines above. Left commented, every install through Studio succeeds and
204
+ * then reports that a restart is still required, which reads as a broken
205
+ * feature rather than one line of config nobody was asked for.
206
+ */
207
+ const manageChannelFile = join(dir, "agent/channels/kyb.ts");
208
+ if (existsSync(manageChannelFile)) {
209
+ const appRoot = `/home/exedev/${name}`;
210
+ const wired = readFileSync(manageChannelFile, "utf8").replace(/export default manageChannel\(\{[\s\S]*?\}\);/, () => `export default manageChannel({\n` +
211
+ ` appRoot: process.env.EVE_APP_DIR ?? ${JSON.stringify(appRoot)},\n` +
212
+ ` restartCommand: "bash scripts/eve-server.sh restart",\n` +
213
+ `});`);
214
+ writeFileSync(manageChannelFile, wired);
215
+ console.log(dim(" agent/channels/kyb.ts — restart wired to that script"));
216
+ }
197
217
  }
198
218
  if (plan.file) {
199
219
  console.log(bold(`\n4/6 Channel: ${channel} …`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kybernesis/create",
3
- "version": "0.7.10",
3
+ "version": "0.7.13",
4
4
  "description": "The Kybernesis agent scaffolder and FDE toolkit: one command to a governed, remembering, multiplayer, self-testing eve agent — plus doctor and upgrade.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",