@kybernesis/create 0.7.3 → 0.7.8

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/cli.js CHANGED
@@ -53,7 +53,7 @@ switch (command) {
53
53
  await register({ name: flag(rest, "name"), url: flag(rest, "url") });
54
54
  break;
55
55
  case "deploy":
56
- await deploy({ host: flag(rest, "host") });
56
+ await deploy({ host: flag(rest, "host"), noEnv: rest.includes("--no-env") });
57
57
  break;
58
58
  case "upgrade":
59
59
  await upgrade(rest.includes("--skip-eval"));
@@ -86,6 +86,7 @@ ${bold("kyb")} — Kybernesis agent scaffolder & FDE toolkit
86
86
  --url=<url> ${dim("defaults to https://$EXE_VM_NAME.exe.xyz")}
87
87
  ${bold("kyb deploy")} copy to the host, install, restart, prove it took
88
88
  --host=<target> ${dim("ssh target; defaults to $EXE_VM_NAME.exe.xyz")}
89
+ --no-env ${dim("do not send .env.local (host manages its own secrets)")}
89
90
  ${bold("kyb upgrade")} bump @kybernesis/* packages, gated on evals
90
91
  --skip-eval ${dim("skip the eval gate")}
91
92
 
package/dist/deploy.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export declare function deploy(options: {
2
2
  host?: string;
3
3
  dir?: string;
4
+ noEnv?: boolean;
4
5
  }): Promise<void>;
package/dist/deploy.js CHANGED
@@ -77,11 +77,14 @@ export async function deploy(options) {
77
77
  * not run on the host. `.eve` is the durable store — conversations, turn
78
78
  * history, the workflow queue — and overwriting it with a local copy is how
79
79
  * a deployment eats its own production state.
80
+ *
81
+ * `.env.local` is excluded from the SOURCE sync and sent separately below.
82
+ * It is not source: rsync --delete would remove a host-only file, and a
83
+ * half-matching copy is worse than a deliberate one.
80
84
  */
81
- console.log(bold("1/3 Copying source …"));
85
+ console.log(bold("1/4 Copying source …"));
82
86
  const ok = run("rsync", [
83
87
  "-az",
84
- "--delete",
85
88
  "--exclude",
86
89
  "node_modules",
87
90
  "--exclude",
@@ -100,7 +103,50 @@ export async function deploy(options) {
100
103
  process.exitCode = 1;
101
104
  return;
102
105
  }
103
- console.log(bold("\n2/3 Installing dependencies on the host …"));
106
+ /**
107
+ * The agent cannot boot without its environment.
108
+ *
109
+ * This step was missing, and the failure was exactly as opaque as it sounds:
110
+ * source copied, dependencies installed, build succeeded, and the server died
111
+ * on `Invalid extension config: apiKey: expected string, received undefined`
112
+ * — an error about a mount, thirty lines into a log, when the actual cause
113
+ * was that no configuration had ever reached the machine.
114
+ *
115
+ * Sent separately from the source sync so `--delete` cannot touch it, and
116
+ * skippable for deployments whose secrets are managed on the host.
117
+ */
118
+ if (!options.noEnv) {
119
+ const envFile = join(dir, ".env.local");
120
+ if (existsSync(envFile)) {
121
+ console.log(bold("\n2/4 Environment …"));
122
+ /**
123
+ * Only when the host has none.
124
+ *
125
+ * The host's copy is authoritative: it holds credentials minted ON the
126
+ * host, values a laptop never had, and secrets nobody keeps in a working
127
+ * tree. A local stub overwriting it does not misconfigure a deployment,
128
+ * it destroys one — that happened here, to a production agent, and
129
+ * `rsync --delete` took the backup with it in the same run.
130
+ */
131
+ const hasRemote = spawnSync("ssh", [target, `test -s ${remote}/.env.local`]).status === 0;
132
+ if (hasRemote) {
133
+ console.log(dim(" host has its own — left untouched"));
134
+ }
135
+ else if (!run("scp", ["-q", envFile, `${target}:${remote}/.env.local`])) {
136
+ console.log(red(" Could not copy .env.local — the agent will not start without it."));
137
+ process.exitCode = 1;
138
+ return;
139
+ }
140
+ else {
141
+ console.log(dim(" sent (host had none)"));
142
+ }
143
+ }
144
+ else {
145
+ console.log(yellow("\n ! No .env.local here. The host needs one, or the agent will fail to boot\n" +
146
+ " on the first config it dereferences. Run kyb arcana, or create it there."));
147
+ }
148
+ }
149
+ console.log(bold("\n3/4 Installing dependencies on the host …"));
104
150
  if (!run("ssh", [target, `cd ${remote} && npm install --no-audit --no-fund`])) {
105
151
  console.log(red(" npm install failed on the host."));
106
152
  process.exitCode = 1;
@@ -114,7 +160,7 @@ export async function deploy(options) {
114
160
  * exists to prevent. setsid + nohup + no stdin is the difference between a
115
161
  * deploy and an outage.
116
162
  */
117
- console.log(bold("\n3/3 Restarting (detached) …"));
163
+ console.log(bold("\n4/4 Restarting (detached) …"));
118
164
  /**
119
165
  * Find the restart script rather than assuming its name.
120
166
  *
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Set values in a project's .env.local, replacing in place.
3
+ *
4
+ * Its own module because two commands need it and both were getting it wrong
5
+ * in the same way: a registry item's `envVars` only appends NAMES, so a
6
+ * scaffold could finish with `KYBERNESIS_AGENT=` empty and every later command
7
+ * reading it would report the value as missing — which reads as a step the
8
+ * person skipped rather than one they were never offered.
9
+ */
10
+ export declare function upsertEnv(dir: string, values: Record<string, string>): void;
@@ -0,0 +1,30 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ /**
4
+ * Set values in a project's .env.local, replacing in place.
5
+ *
6
+ * Its own module because two commands need it and both were getting it wrong
7
+ * in the same way: a registry item's `envVars` only appends NAMES, so a
8
+ * scaffold could finish with `KYBERNESIS_AGENT=` empty and every later command
9
+ * reading it would report the value as missing — which reads as a step the
10
+ * person skipped rather than one they were never offered.
11
+ */
12
+ export function upsertEnv(dir, values) {
13
+ const path = join(dir, ".env.local");
14
+ let text = existsSync(path) ? readFileSync(path, "utf8") : "";
15
+ for (const [key, value] of Object.entries(values)) {
16
+ const line = `${key}="${value}"`;
17
+ const existing = new RegExp(`^${key}=.*$`, "m");
18
+ if (existing.test(text)) {
19
+ // A function replacer, never a string: `$&` and `` $` `` are special in a
20
+ // replacement string, and a value containing either silently splices in
21
+ // part of the file. That has happened twice, and both times it produced
22
+ // a file that looked plausible and did not parse.
23
+ text = text.replace(existing, () => line);
24
+ }
25
+ else {
26
+ text += (text.endsWith("\n") || text === "" ? "" : "\n") + line + "\n";
27
+ }
28
+ }
29
+ writeFileSync(path, text);
30
+ }
package/dist/init.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { chmodSync, copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
3
  import { DEFAULT_ISSUER, EVE_VERSION, REGISTRY_URL, ask, bold, closePrompts, dim, green, run, slug, yellow, } from "./util.js";
4
- import { CHANNEL_KINDS, channelPlan, engineerPlan, envExample, evalFileTs, evalScript, hostAgentTs, hostSteps, identityMd, rootArcanaTs, subagentAgentTs, subagentArcanaTs, subagentInstructionsMd, } from "./templates.js";
4
+ import { CHANNEL_KINDS, channelPlan, engineerPlan, envExample, evalFileTs, exeEvalConfigTs, evalScript, hostAgentTs, hostSteps, identityMd, rootArcanaTs, subagentAgentTs, subagentArcanaTs, subagentInstructionsMd, } from "./templates.js";
5
5
  import { suiteDir } from "./skills.js";
6
6
  import { configureArcana } from "./arcana.js";
7
+ import { upsertEnv } from "./envfile.js";
7
8
  /**
8
9
  * The always-installed core. Everything else — channels, subagents, engineer,
9
10
  * host bindings — is opt-in, because assuming them means the FDE deletes files
@@ -64,7 +65,12 @@ export async function init(rawName, options = {}) {
64
65
  for (const item of CORE_ITEMS) {
65
66
  run("npx", ["eve", "add", `@kybernesis/${item}`, "--overwrite"], { cwd: dir });
66
67
  }
67
- const extraDeps = [...plan.deps, ...(host === "exe" ? ["@kybernesis/exe", "@ai-sdk/openai"] : [])];
68
+ // @ai-sdk/anthropic is for the EVAL JUDGE, not the agent: an agent judged
69
+ // by the model it runs on is a weak test.
70
+ const extraDeps = [
71
+ ...plan.deps,
72
+ ...(host === "exe" ? ["@kybernesis/exe", "@ai-sdk/openai", "@ai-sdk/anthropic"] : []),
73
+ ];
68
74
  if (extraDeps.length) {
69
75
  console.log(bold(`\n2b Installing for ${channel}/${host}: ${extraDeps.join(", ")} …`));
70
76
  run("npm", ["install", ...extraDeps, "--no-audit", "--no-fund"], { cwd: dir, allowFail: true });
@@ -78,10 +84,28 @@ export async function init(rawName, options = {}) {
78
84
  // client change THIS AGENT — its dependencies and its source. Different
79
85
  // blast radius, so an agent can have one without the other.
80
86
  console.log(bold("\n2b2 KYBER Studio: local execution + management routes …"));
87
+ const studioFailures = [];
81
88
  for (const item of ["local", "manage"]) {
82
- const ok = run("npx", ["eve", "add", item, "--overwrite"], { cwd: dir, allowFail: true });
83
- if (!ok)
84
- console.log(yellow(` ! ${item} did not install cleanly re-run: npx eve add ${item}`));
89
+ // The @kybernesis/ prefix is load-bearing: a bare name resolves against
90
+ // eve OWN registry, which has no such item, so both installs failed with
91
+ // "not found" and allowFail swallowed it. --studio therefore did
92
+ // nothing at all, silently, and the first sign was Studio refusing to
93
+ // connect an agent that looked correctly scaffolded.
94
+ const ok = run("npx", ["eve", "add", `@kybernesis/${item}`, "--overwrite"], {
95
+ cwd: dir,
96
+ allowFail: true,
97
+ });
98
+ if (!ok) {
99
+ studioFailures.push(item);
100
+ console.log(yellow(` ! ${item} did not install — re-run: npx eve add @kybernesis/${item}`));
101
+ }
102
+ }
103
+ // Said again, loudly, at the end. A warning printed sixty lines before a
104
+ // green summary is a warning nobody reads — and the agent that results
105
+ // looks correctly scaffolded right up until KYBER Studio refuses it.
106
+ if (studioFailures.length) {
107
+ console.log(yellow(`\n ! --studio did NOT complete: ${studioFailures.join(", ")} missing.\n` +
108
+ ` This agent cannot be connected to a desktop until they install.`));
85
109
  }
86
110
  }
87
111
  const engPlan = engineer ? engineerPlan(host, DEFAULT_MODEL) : null;
@@ -112,9 +136,38 @@ export async function init(rawName, options = {}) {
112
136
  writeFileSync(join(dir, "agent/agent.ts"), hostAgentTs(host, DEFAULT_MODEL));
113
137
  writeFileSync(join(dir, "agent/extensions/arcana.ts"), rootArcanaTs());
114
138
  writeFileSync(join(dir, "evals/kybernesis.eval.ts"), evalFileTs(displayName, depts));
139
+ // A self-hosted agent judges through its own integration; the default
140
+ // resolves through a gateway it has no key for, and only the judged gates
141
+ // fail — which reads as a half-broken agent rather than a missing config.
142
+ if (host === "exe")
143
+ writeFileSync(join(dir, "evals/evals.config.ts"), exeEvalConfigTs());
115
144
  // Ask for the memory keys HERE, while the person is still standing in the
116
145
  // scaffold — not in a printed next-step they will read after the context has
117
146
  // gone. Skipped with --yes, which is for CI and takes no input by design.
147
+ /**
148
+ * Write the values this command already knows.
149
+ *
150
+ * These were left blank, so the first thing a person met after a clean
151
+ * scaffold was `kyb register` reporting "No agent name" — a step they were
152
+ * never offered, reading as one they had skipped.
153
+ */
154
+ const known = { KYBERNESIS_ISSUER: issuer, KYBERNESIS_AGENT: name };
155
+ if (host === "exe") {
156
+ // Usually the agent's name, and usually not. Everything downstream — the
157
+ // deploy target, the registered URL — derives from this one value.
158
+ known.EXE_VM_NAME = options.yes ? name : await ask("exe.dev VM name?", name);
159
+ /**
160
+ * Left empty on purpose rather than guessed.
161
+ *
162
+ * Model ids are per-integration and carry a provider prefix. A hardcoded
163
+ * default put one host's id on another, where an unknown id answers
164
+ * `404 unsupported endpoint` — an error about the endpoint for a problem
165
+ * with the model. Empty fails honestly; the host lists its own with
166
+ * curl https://llm.int.exe.xyz/models.json
167
+ */
168
+ known.EXE_MODEL = "";
169
+ }
170
+ upsertEnv(dir, known);
118
171
  if (!options.yes)
119
172
  await configureArcana({ dir, suggest: name, depts });
120
173
  /**
@@ -37,3 +37,16 @@ export interface EngineerPlan {
37
37
  steps: string[];
38
38
  }
39
39
  export declare function engineerPlan(host: HostKind, model: string): EngineerPlan;
40
+ /**
41
+ * The eval judge, for an exe.dev host.
42
+ *
43
+ * Without this the judge resolves through Vercel's AI Gateway and fails with
44
+ * "Unauthenticated request to AI Gateway" on a host that has no gateway key
45
+ * and does not need one — the agent passes every gate the judge is not part
46
+ * of, which reads as a half-broken agent rather than a missing config.
47
+ *
48
+ * Judged by a DIFFERENT provider from the one under test. A model grading its
49
+ * own output is a weak test, and the Anthropic path is a plain Messages
50
+ * endpoint, so none of the subscription backend's constraints apply.
51
+ */
52
+ export declare function exeEvalConfigTs(): string;
package/dist/templates.js CHANGED
@@ -69,7 +69,15 @@ export function subagentArcanaTs(dept) {
69
69
  const upper = dept.toUpperCase().replace(/[^A-Z0-9]+/g, "_");
70
70
  return `import { defineMcpClientConnection } from "eve/connections";
71
71
 
72
- const workspace = process.env.ARCANA_${upper}_WORKSPACE ?? "REPLACE-${dept}";
72
+ // Same reasoning as the root brain: no placeholder. A subagent quietly
73
+ // addressing a workspace nobody owns is amnesia that looks like a model
74
+ // problem rather than a missing value. Set it with \`kyb arcana\`.
75
+ const workspace = process.env.ARCANA_${upper}_WORKSPACE;
76
+ if (!workspace) {
77
+ throw new Error(
78
+ "ARCANA_${upper}_WORKSPACE is not set — run \`kyb arcana\` to set this subagent workspace and key.",
79
+ );
80
+ }
73
81
 
74
82
  export default defineMcpClientConnection({
75
83
  url: "https://mcp.arcana.kybernesis.ai/mcp",
@@ -97,7 +105,16 @@ export function rootArcanaTs() {
97
105
  // One brain for shared surfaces, optionally a second for DMs. Hermetic eval
98
106
  // runs override ARCANA_COMPANY_WORKSPACE to "<client>-eval", which switches
99
107
  // to the eval workspace's own key automatically.
100
- const COMPANY = process.env.ARCANA_COMPANY_WORKSPACE ?? "REPLACE-company";
108
+ // No placeholder fallback. A default here does not prevent a mistake, it
109
+ // hides one: memory silently addresses a workspace nobody owns, and the agent
110
+ // looks like it has amnesia rather than like it is misconfigured. Set with
111
+ // \`kyb arcana\`.
112
+ const COMPANY = process.env.ARCANA_COMPANY_WORKSPACE;
113
+ if (!COMPANY) {
114
+ throw new Error(
115
+ "ARCANA_COMPANY_WORKSPACE is not set — run \`kyb arcana\` to set the workspace and its key.",
116
+ );
117
+ }
101
118
  const DM = process.env.ARCANA_DM_WORKSPACE ?? COMPANY;
102
119
 
103
120
  export default arcana({
@@ -501,8 +518,13 @@ export default defineAgent({
501
518
  {
502
519
  path: "agent/subagents/builder/extensions/engineer.ts",
503
520
  content: `// Engineer layer mounted LOCALLY on this subagent (eve >=0.30): screenshot,
504
- // deliver, and the trade-school skills belong to \`builder\` alone. The root
505
- // agent never gets shell or a browser.
521
+ // deliver, and the trade-school skills belong to \`builder\` alone, so the root
522
+ // agent has no shell.
523
+ //
524
+ // The root DOES get a browser and GitHub tools — the engineer layer installs
525
+ // extension/agent-browser and extension/github-tools at the root, deliberately,
526
+ // because reading a page is not the same blast radius as running a command.
527
+ // This comment used to claim otherwise, which is worse than saying nothing.
506
528
  export { default } from "@kybernesis/engineer";
507
529
  `,
508
530
  },
@@ -534,3 +556,33 @@ export { default } from "@kybernesis/engineer";
534
556
  ],
535
557
  };
536
558
  }
559
+ /**
560
+ * The eval judge, for an exe.dev host.
561
+ *
562
+ * Without this the judge resolves through Vercel's AI Gateway and fails with
563
+ * "Unauthenticated request to AI Gateway" on a host that has no gateway key
564
+ * and does not need one — the agent passes every gate the judge is not part
565
+ * of, which reads as a half-broken agent rather than a missing config.
566
+ *
567
+ * Judged by a DIFFERENT provider from the one under test. A model grading its
568
+ * own output is a weak test, and the Anthropic path is a plain Messages
569
+ * endpoint, so none of the subscription backend's constraints apply.
570
+ */
571
+ export function exeEvalConfigTs() {
572
+ return `import { defineEvalConfig } from "eve/evals";
573
+ import { createAnthropic } from "@ai-sdk/anthropic";
574
+
575
+ const exe = createAnthropic({
576
+ baseURL: process.env.EXE_LLM_URL ?? "https://llm.int.exe.xyz/v1",
577
+ apiKey: "exe-integration",
578
+ });
579
+
580
+ export default defineEvalConfig({
581
+ // A different provider from the agent under test, on purpose.
582
+ judge: { model: exe(process.env.EXE_JUDGE_MODEL ?? "claude-sonnet-4-6") },
583
+ // Real model and real memory on every turn: generous timeout, gentle concurrency.
584
+ timeoutMs: 300_000,
585
+ maxConcurrency: 1,
586
+ });
587
+ `;
588
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kybernesis/create",
3
- "version": "0.7.3",
3
+ "version": "0.7.8",
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",
@@ -88,6 +88,25 @@ What this arrangement costs you, and it is worth saying to the client:
88
88
 
89
89
  ## The failure modes, each of which cost a real session
90
90
 
91
+ - **`EXE_MODEL` is deliberately empty after `kyb init`.** Set it from the
92
+ host: `curl https://llm.int.exe.xyz/models.json`. Nothing can guess it —
93
+ ids are per-integration.
94
+ - **Model ids carry a provider prefix**: `openai/gpt-5.6-sol`, not
95
+ `gpt-5.6-sol`. Get them from `curl https://llm.int.exe.xyz/models.json` on
96
+ the host, and note its `preferred_model`.
97
+ - **An unknown model id on the responses surface returns**
98
+ `404 unsupported endpoint: /v1/responses` — an error about the ENDPOINT for
99
+ a problem with the MODEL. The endpoint is fine. Check the prefix before
100
+ believing that 404; chasing it cost hours and produced two wrong fixes.
101
+ - **Subscription-backed models are Responses-API only.** models.json reports
102
+ `"apis": ["openai_responses"]`, and chat-completions answers `Model … is
103
+ not in this integration's model list` for a model that plainly is listed.
104
+ - **Evals must run ON the host.** `llm.int.exe.xyz` is internal to exe.dev, so
105
+ a laptop cannot reach it — every model call fails with a connection error to
106
+ the cloud metadata address, which looks like a broken agent and is not.
107
+ - **An integration has to be attached to the VM** (`integrations attach llm
108
+ <vm>`, or `auto:all`). Check with `ssh exe.dev integrations list`.
109
+
91
110
  - **Docker ships disabled on some images.** exe.dev's exeuntu runs
92
111
  `systemctl disable docker.service`, so `docker --version` works while nothing
93
112
  can run. Every sandbox call fails with `SandboxTemplateNotProvisionedError`.