@kybernesis/create 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -14,10 +14,24 @@ import { init } from "./init.js";
14
14
  import { doctor } from "./doctor.js";
15
15
  import { upgrade } from "./upgrade.js";
16
16
  import { installSkills } from "./skills.js";
17
+ function flag(rest, key) {
18
+ const hit = rest.find((a) => a.startsWith(`--${key}=`));
19
+ return hit ? hit.slice(key.length + 3) : undefined;
20
+ }
21
+ function initOptions(rest) {
22
+ const subs = flag(rest, 'subagents');
23
+ return {
24
+ engineer: rest.includes('--engineer'),
25
+ channel: flag(rest, "channel"),
26
+ host: flag(rest, "host"),
27
+ subagents: subs === undefined ? undefined : subs.split(',').map((s) => s.trim()).filter(Boolean),
28
+ yes: rest.includes('--yes') || rest.includes('-y'),
29
+ };
30
+ }
17
31
  const [, , command, ...rest] = process.argv;
18
32
  switch (command) {
19
33
  case "init":
20
- await init(rest.find((a) => !a.startsWith("-")), { engineer: rest.includes("--engineer") });
34
+ await init(rest.find((a) => !a.startsWith("-")), initOptions(rest));
21
35
  break;
22
36
  case "doctor":
23
37
  await doctor();
@@ -29,19 +43,23 @@ switch (command) {
29
43
  await upgrade(rest.includes("--skip-eval"));
30
44
  break;
31
45
  case undefined:
32
- await init(undefined, { engineer: false });
46
+ await init(undefined, initOptions(rest));
33
47
  break;
34
48
  default:
35
49
  if (!command.startsWith("-")) {
36
50
  // `npm create @kybernesis acme-agent` → argv[2] is the name.
37
- await init(command, { engineer: rest.includes("--engineer") });
51
+ await init(command, initOptions(rest));
38
52
  break;
39
53
  }
40
54
  console.log(`
41
55
  ${bold("kyb")} — Kybernesis agent scaffolder & FDE toolkit
42
56
 
43
- ${bold("kyb init [name]")} scaffold a full Kybernesis eve agent
57
+ ${bold("kyb init [name]")} scaffold a Kybernesis eve agent (core: enterprise + arcana + evals)
58
+ --channel=<kind> ${dim("none|slack|imessage|telegram|discord|web (default: none)")}
59
+ --host=<kind> ${dim("vercel|exe (default: vercel)")}
60
+ --subagents=a,b ${dim("department subagents (default: none)")}
44
61
  --engineer ${dim("add the engineer layer: workshop sandbox + vision dev loop")}
62
+ --yes ${dim("no prompts; take flags and defaults")}
45
63
  ${bold("kyb doctor")} preflight checks (keys, issuer, envs, discovery)
46
64
  ${bold("kyb skills")} install/refresh the FDE skill suite for Claude Code
47
65
  --global ${dim("install to ~/.claude/skills instead of this repo")}
package/dist/init.d.ts CHANGED
@@ -1,3 +1,13 @@
1
- export declare function init(rawName: string | undefined, options?: {
1
+ import { type ChannelKind, type HostKind } from "./templates.js";
2
+ export interface InitOptions {
2
3
  engineer?: boolean;
3
- }): Promise<void>;
4
+ /** Chat surface. Default "none" — add later with `kyb add channel`. */
5
+ channel?: ChannelKind;
6
+ /** Where the agent runs. Default "vercel". */
7
+ host?: HostKind;
8
+ /** Department subagents. Default NONE. */
9
+ subagents?: string[];
10
+ /** Skip prompts and take the flags/defaults as given. */
11
+ yes?: boolean;
12
+ }
13
+ export declare function init(rawName: string | undefined, options?: InitOptions): Promise<void>;
package/dist/init.js CHANGED
@@ -1,86 +1,128 @@
1
1
  import { cpSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
- import { DEFAULT_ISSUER, EVE_VERSION, REGISTRY_URL, ask, bold, closePrompts, green, run, slug, yellow, } from "./util.js";
4
- import { envExample, evalFileTs, evalScript, identityMd, rootArcanaTs, subagentAgentTs, subagentArcanaTs, subagentInstructionsMd, } from "./templates.js";
3
+ import { DEFAULT_ISSUER, EVE_VERSION, REGISTRY_URL, ask, bold, closePrompts, dim, green, run, slug, yellow, } from "./util.js";
4
+ import { CHANNEL_KINDS, channelPlan, envExample, evalFileTs, evalScript, hostAgentTs, hostSteps, identityMd, rootArcanaTs, subagentAgentTs, subagentArcanaTs, subagentInstructionsMd, } from "./templates.js";
5
5
  import { suiteDir } from "./skills.js";
6
- const ITEMS = ["enterprise", "arcana", "multiplayer", "evals"];
7
- // Official eve-registry limbs installed alongside the engineer layer.
8
- const ENGINEER_OFFICIAL_ITEMS = ["extension/agent-browser", "extension/github-tools", "connection/vercel"];
9
- export async function init(rawName, options) {
10
- const engineer = options?.engineer === true;
6
+ /**
7
+ * The always-installed core. Everything else channels, subagents, engineer,
8
+ * host bindings — is opt-in, because assuming them means the FDE deletes files
9
+ * AND undoes real setup work (an Arcana workspace + scoped key per subagent).
10
+ */
11
+ const CORE_ITEMS = ["enterprise", "arcana", "evals"];
12
+ const ENGINEER_OFFICIAL_ITEMS = [
13
+ "extension/agent-browser",
14
+ "extension/github-tools",
15
+ "connection/vercel",
16
+ ];
17
+ const DEFAULT_MODEL = "anthropic/claude-sonnet-5";
18
+ export async function init(rawName, options = {}) {
19
+ const engineer = options.engineer === true;
20
+ const nonInteractive = options.yes === true;
11
21
  const name = slug(rawName ?? (await ask("Agent name (kebab-case)?", "acme-agent")));
12
22
  if (!name) {
13
23
  console.error("An agent name is required.");
14
24
  process.exit(1);
15
25
  }
16
- const displayName = await ask("Display name (what employees call it)?", name);
17
- const deptsRaw = await ask("Department subagents (comma-separated, empty for none)?", "finance,marketing,engineering");
18
- const depts = deptsRaw
19
- .split(",")
20
- .map((d) => slug(d))
21
- .filter(Boolean);
22
- const issuer = await ask("Control-plane issuer?", DEFAULT_ISSUER);
26
+ const displayName = nonInteractive
27
+ ? name
28
+ : await ask("Display name (what employees call it)?", name);
29
+ // Channel: default NONE. A client on iMessage should never be handed Slack.
30
+ let channel = options.channel ?? "none";
31
+ if (!options.channel && !nonInteractive) {
32
+ const answer = await ask(`Chat surface? (${CHANNEL_KINDS.join(" | ")})`, "none");
33
+ const picked = answer.trim().toLowerCase();
34
+ channel = CHANNEL_KINDS.includes(picked) ? picked : "none";
35
+ }
36
+ // Host: where it runs. Vercel unless told otherwise.
37
+ let host = options.host ?? "vercel";
38
+ if (!options.host && !nonInteractive) {
39
+ const answer = await ask("Host? (vercel | exe)", "vercel");
40
+ host = answer.trim().toLowerCase() === "exe" ? "exe" : "vercel";
41
+ }
42
+ // Subagents: default NONE. Each one costs a workspace + scoped key.
43
+ let depts = options.subagents ?? [];
44
+ if (!options.subagents && !nonInteractive) {
45
+ const raw = await ask("Department subagents (comma-separated, empty for none)?", "");
46
+ depts = raw.split(",").map((d) => slug(d)).filter(Boolean);
47
+ }
48
+ const issuer = nonInteractive
49
+ ? DEFAULT_ISSUER
50
+ : await ask("Control-plane issuer?", DEFAULT_ISSUER);
23
51
  closePrompts();
24
52
  const dir = resolve(process.cwd(), name);
25
53
  if (existsSync(dir)) {
26
54
  console.error(`Directory ${name}/ already exists.`);
27
55
  process.exit(1);
28
56
  }
57
+ const plan = channelPlan(channel, name, host);
29
58
  console.log(bold(`\n1/6 Scaffolding eve agent (eve@${EVE_VERSION}) …`));
30
59
  run("npx", [`eve@${EVE_VERSION}`, "init", name]);
31
- console.log(bold("\n2/6 Adding the Kybernesis registry + packages …"));
60
+ console.log(bold("\n2/6 Adding the Kybernesis registry + core packages …"));
32
61
  run("npx", ["eve", "registry", "add", `@kybernesis=${REGISTRY_URL}`], { cwd: dir });
33
- for (const item of ITEMS) {
62
+ for (const item of CORE_ITEMS) {
34
63
  run("npx", ["eve", "add", `@kybernesis/${item}`, "--overwrite"], { cwd: dir });
35
64
  }
65
+ const extraDeps = [...plan.deps, ...(host === "exe" ? ["@kybernesis/exe", "@ai-sdk/openai"] : [])];
66
+ if (extraDeps.length) {
67
+ console.log(bold(`\n2b Installing for ${channel}/${host}: ${extraDeps.join(", ")} …`));
68
+ run("npm", ["install", ...extraDeps, "--no-audit", "--no-fund"], { cwd: dir, allowFail: true });
69
+ }
70
+ for (const item of plan.registryItems) {
71
+ run("npx", ["eve", "add", item, "--overwrite"], { cwd: dir, allowFail: true });
72
+ }
36
73
  if (engineer) {
37
- console.log(bold("\n2b Engineer layer: workshop sandbox + vision dev loop …"));
74
+ console.log(bold("\n2c Engineer layer: workshop sandbox + vision dev loop …"));
38
75
  run("npx", ["eve", "add", "@kybernesis/engineer", "--overwrite"], { cwd: dir });
39
76
  for (const item of ENGINEER_OFFICIAL_ITEMS) {
40
- // Official items may carry their own interactive setup; a failure here
41
- // shouldn't kill the scaffold — the FDE can re-run `eve add <item>`.
42
77
  const ok = run("npx", ["eve", "add", item, "--overwrite"], { cwd: dir, allowFail: true });
43
78
  if (!ok)
44
79
  console.log(yellow(` ! ${item} did not install cleanly — re-run: npx eve add ${item}`));
45
80
  }
46
81
  }
47
- console.log(bold("\n2c Seeding the FDE Claude Code skill suite (.claude/skills) …"));
82
+ console.log(bold("\n2d Seeding the FDE Claude Code skill suite (.claude/skills) …"));
48
83
  try {
49
84
  cpSync(suiteDir(), join(dir, ".claude/skills"), { recursive: true });
50
85
  }
51
86
  catch {
52
87
  console.log(yellow(" ! skill suite not found — run kyb skills inside the repo later"));
53
88
  }
54
- console.log(bold("\n3/6 Writing agent identity, memory mount, and eval wiring …"));
89
+ console.log(bold("\n3/6 Writing identity, model config, memory mount, and evals …"));
55
90
  mkdirSync(join(dir, "agent/instructions"), { recursive: true });
56
91
  writeFileSync(join(dir, "agent/instructions/identity.md"), identityMd(displayName, depts));
57
- // The scaffold ships a flat agent/instructions.md; the directory form wins,
58
- // so remove the flat file to avoid ambiguity.
59
92
  try {
60
93
  unlinkSync(join(dir, "agent/instructions.md"));
61
94
  }
62
95
  catch { }
96
+ writeFileSync(join(dir, "agent/agent.ts"), hostAgentTs(host, DEFAULT_MODEL));
63
97
  writeFileSync(join(dir, "agent/extensions/arcana.ts"), rootArcanaTs());
64
98
  writeFileSync(join(dir, "evals/kybernesis.eval.ts"), evalFileTs(displayName, depts));
65
- console.log(bold(`\n4/6 Generating ${depts.length} department subagent(s) …`));
66
- const skillsSource = join(dir, "node_modules/@kybernesis/arcana/dist/extension/skills");
67
- for (const dept of depts) {
68
- const base = join(dir, "agent/subagents", dept);
69
- mkdirSync(join(base, "connections"), { recursive: true });
70
- writeFileSync(join(base, "agent.ts"), subagentAgentTs(dept));
71
- writeFileSync(join(base, "instructions.md"), subagentInstructionsMd(dept));
72
- writeFileSync(join(base, "connections/arcana.ts"), subagentArcanaTs(dept));
73
- // Subagents inherit nothing: copy the memory skills from the installed
74
- // arcana package so each specialist carries the playbooks.
75
- if (existsSync(skillsSource)) {
76
- cpSync(skillsSource, join(base, "skills"), { recursive: true });
77
- }
78
- else {
79
- console.log(yellow(` ! arcana skills not found at ${skillsSource} — copy them manually`));
99
+ if (plan.file) {
100
+ console.log(bold(`\n4/6 Channel: ${channel} …`));
101
+ mkdirSync(join(dir, "agent/channels"), { recursive: true });
102
+ writeFileSync(join(dir, "agent/channels", plan.file), plan.content);
103
+ }
104
+ else {
105
+ console.log(bold(`\n4/6 Channel: ${channel} (no channel file) …`));
106
+ }
107
+ if (depts.length) {
108
+ console.log(bold(`\n4b Generating ${depts.length} department subagent(s) …`));
109
+ const skillsSource = join(dir, "node_modules/@kybernesis/arcana/dist/extension/skills");
110
+ for (const dept of depts) {
111
+ const base = join(dir, "agent/subagents", dept);
112
+ mkdirSync(join(base, "connections"), { recursive: true });
113
+ writeFileSync(join(base, "agent.ts"), subagentAgentTs(dept));
114
+ writeFileSync(join(base, "instructions.md"), subagentInstructionsMd(dept));
115
+ writeFileSync(join(base, "connections/arcana.ts"), subagentArcanaTs(dept));
116
+ if (existsSync(skillsSource)) {
117
+ cpSync(skillsSource, join(base, "skills"), { recursive: true });
118
+ }
119
+ else {
120
+ console.log(yellow(` ! arcana skills not found at ${skillsSource} — copy them manually`));
121
+ }
80
122
  }
81
123
  }
82
124
  console.log(bold("\n5/6 Env template + hermetic eval script …"));
83
- writeFileSync(join(dir, ".env.example"), envExample(name, depts, issuer));
125
+ writeFileSync(join(dir, ".env.example"), envExample(name, depts, issuer, plan.env));
84
126
  const pkgPath = join(dir, "package.json");
85
127
  const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
86
128
  pkg.scripts = { ...pkg.scripts, eval: evalScript(name, depts) };
@@ -88,34 +130,32 @@ export async function init(rawName, options) {
88
130
  console.log(bold("\n6/6 Verifying (typecheck + eve discovery) …"));
89
131
  const tsOk = run("npm", ["run", "typecheck"], { cwd: dir, allowFail: true });
90
132
  const infoOk = run("npx", ["eve", "info"], { cwd: dir, allowFail: true, quiet: true });
91
- console.log(tsOk && infoOk ? green(" ✓ typecheck + discovery clean") : yellow(" ! verify manually: npm run typecheck && npx eve info"));
133
+ console.log(tsOk && infoOk
134
+ ? green(" ✓ typecheck + discovery clean")
135
+ : yellow(" ! verify manually: npm run typecheck && npx eve info"));
136
+ const parts = [
137
+ "governed (enterprise)",
138
+ "remembering (arcana)",
139
+ "self-testing (evals)",
140
+ channel === "none" ? null : `${channel} channel`,
141
+ host === "exe" ? "exe.dev host" : null,
142
+ engineer ? "engineer (workshop + vision loop)" : null,
143
+ depts.length ? `${depts.length} dept subagent(s)` : null,
144
+ ].filter(Boolean);
145
+ const steps = [
146
+ `Arcana: create workspaces (${name}-company, ${name}-eval${depts.map((d) => `, ${name}-${d}`).join("")}) + scoped kb_ keys; fill .env.local from .env.example`,
147
+ ...hostSteps(host, name),
148
+ ...plan.steps,
149
+ `Control plane: register agent "${name}" at ${issuer}/agents + grant the pilot cohort`,
150
+ `npm run eval → green → deploy → live smoke + the revoke demo`,
151
+ ];
92
152
  console.log(`
93
- ${green("✓")} ${bold(name)} scaffolded: governed (enterprise) · remembering (arcana) · multiplayer (slack) · self-testing (evals)${engineer ? " · engineer (workshop + vision loop)" : ""}${depts.length ? ` · ${depts.length} dept subagent(s)` : ""}${engineer ? `
94
-
95
- ${bold("Engineer notes:")}
96
- · The workshop sandbox (agent/sandbox/sandbox.ts) bakes Playwright into the
97
- template at DEPLOY time — a broken bootstrap fails the Vercel build loudly.
98
- Deployed sessions run under a domain allowlist; extend it deliberately in
99
- that file when a project needs another host.
100
- · Vercel connection (preview deploys + link-back), after \`vercel link\`:
101
- vercel connect create mcp.vercel.com --name vercel
102
- vercel connect attach mcp.vercel.com/vercel --yes
103
- then set connect("mcp.vercel.com/vercel") — the UID, not the short name —
104
- in agent/connections/vercel.ts. First tool use posts an OAuth link in the
105
- thread (user-scoped); grant "All projects" so the agent can create new ones,
106
- then narrow the grant in the dashboard once the project exists.
107
- · File delivery (the deliver tool needs it):
108
- vercel blob create-store ${name}-deliverables --access public --yes
109
- links the store and injects BLOB_READ_WRITE_TOKEN automatically.
110
- · agent-browser / github-tools may need their Connect setup flows — run
111
- their printed setup commands if tools 401.` : ""}
153
+ ${green("✓")} ${bold(name)} scaffolded: ${parts.join(" · ")}
112
154
 
113
155
  ${bold("Human steps (in order) — the FDE playbook covers each in detail:")}
114
- 1. Arcana: create workspaces (${name}-company, ${name}-eval${depts.map((d) => `, ${name}-${d}`).join("")}) + scoped kb_ keys; fill .env.local from .env.example
115
- 2. Vercel: \`vercel link\` in ${name}/ (client's team), add envs (prod/preview Sensitive)
116
- 3. Slack: \`vercel connect create slack --triggers --name ${name}\` → detach → re-attach with --trigger-path /eve/v1/slack
117
- 4. Control plane: register agent "${name}" (runtime: ▲ eve) at ${issuer}/agents + grant the pilot cohort
118
- 5. \`npm run eval\` → green → \`npx eve deploy\` → live Slack smoke + the revoke demo
156
+ ${steps.map((s, i) => ` ${i + 1}. ${s}`).join("\n")}
157
+
119
158
  Run ${bold("kyb doctor")} inside ${name}/ any time to check the wiring.
159
+ ${dim(" Add more later: kyb add channel <kind> · kyb add subagent <name>")}
120
160
  `);
121
161
  }
@@ -7,5 +7,24 @@ export declare function subagentInstructionsMd(dept: string): string;
7
7
  export declare function subagentArcanaTs(dept: string): string;
8
8
  export declare function rootArcanaTs(): string;
9
9
  export declare function evalFileTs(displayName: string, depts: string[]): string;
10
- export declare function envExample(name: string, depts: string[], issuer: string): string;
10
+ export declare function envExample(name: string, depts: string[], issuer: string, channelEnv?: string[]): string;
11
11
  export declare function evalScript(name: string, depts: string[]): string;
12
+ export type ChannelKind = "none" | "slack" | "imessage" | "telegram" | "discord" | "web";
13
+ export declare const CHANNEL_KINDS: ChannelKind[];
14
+ export interface ChannelPlan {
15
+ /** File written under agent/channels/, or null for `none`. */
16
+ file: string | null;
17
+ content: string;
18
+ /** npm deps this channel needs beyond eve. */
19
+ deps: string[];
20
+ /** Registry items to `eve add` (interactive setup flows live there). */
21
+ registryItems: string[];
22
+ /** Env lines appended to .env.example. */
23
+ env: string[];
24
+ /** Human setup steps printed after scaffolding. */
25
+ steps: string[];
26
+ }
27
+ export declare function channelPlan(kind: ChannelKind, name: string, host: HostKind): ChannelPlan;
28
+ export type HostKind = "vercel" | "exe";
29
+ export declare function hostAgentTs(host: HostKind, model: string): string;
30
+ export declare function hostSteps(host: HostKind, name: string): string[];
package/dist/templates.js CHANGED
@@ -120,7 +120,7 @@ export default kybernesisBaseline({
120
120
  ${depts.length ? ` routing: [\n${routing}\n ],\n` : ""}});
121
121
  `;
122
122
  }
123
- export function envExample(name, depts, issuer) {
123
+ export function envExample(name, depts, issuer, channelEnv = []) {
124
124
  const deptVars = depts
125
125
  .map((d) => {
126
126
  const upper = d.toUpperCase().replace(/[^A-Z0-9]+/g, "_");
@@ -135,10 +135,7 @@ export function envExample(name, depts, issuer) {
135
135
  KYBERNESIS_ISSUER="${issuer}"
136
136
  KYBERNESIS_AGENT="${name}"
137
137
 
138
- # Slack (@kybernesis/multiplayer) — Vercel Connect connector UID
139
- # SLACK_CONNECTOR_UID="slack/${name}"
140
-
141
- # Arcana memory (@kybernesis/arcana) — one workspace + scoped kb_ key per brain
138
+ ${channelEnv.length ? `# Channel\n${channelEnv.join("\n")}\n\n` : ""}# Arcana memory (@kybernesis/arcana) — one workspace + scoped kb_ key per brain
142
139
  # ARCANA_API_KEY="kb_..."
143
140
  # ARCANA_COMPANY_WORKSPACE="${name}-company"
144
141
  # ARCANA_DM_WORKSPACE="${name}-company"
@@ -154,3 +151,186 @@ export function evalScript(name, depts) {
154
151
  ];
155
152
  return `${overrides.join(" ")} eve eval`;
156
153
  }
154
+ export const CHANNEL_KINDS = [
155
+ "none",
156
+ "slack",
157
+ "imessage",
158
+ "telegram",
159
+ "discord",
160
+ "web",
161
+ ];
162
+ export function channelPlan(kind, name, host) {
163
+ const onExe = host === "exe";
164
+ switch (kind) {
165
+ case "slack":
166
+ return {
167
+ file: "slack.ts",
168
+ content: onExe
169
+ ? `import { multiplayerSlackChannel } from "@kybernesis/multiplayer/slack";
170
+ import { forwardedSocketVerifier } from "@kybernesis/exe/slack";
171
+
172
+ // Slack on a self-hosted (exe.dev) host.
173
+ // Inbound: a forwarder holds the exe-brokered Socket Mode connection and POSTs
174
+ // events here; the verifier authenticates them on SLACK_SOCKET_FORWARDING_SECRET.
175
+ // Outbound: SLACK_BOT_TOKEN must be on the host — eve's SlackChannelCredentials
176
+ // has no apiUrl, so calls can't route through the exe integration yet.
177
+ export default multiplayerSlackChannel({
178
+ credentials: {
179
+ botToken: process.env.SLACK_BOT_TOKEN!,
180
+ webhookVerifier: forwardedSocketVerifier(),
181
+ },
182
+ });
183
+ `
184
+ : `import { connectSlackCredentials } from "@vercel/connect/eve";
185
+ import { multiplayerSlackChannel } from "@kybernesis/multiplayer/slack";
186
+
187
+ // Shared threads with per-speaker verified identity, attributed context, and
188
+ // dual surface (channel vs DM). Credentials are brokered by Vercel Connect.
189
+ export default multiplayerSlackChannel({
190
+ credentials: connectSlackCredentials(process.env.SLACK_CONNECTOR_UID!),
191
+ });
192
+ `,
193
+ deps: ["@kybernesis/multiplayer", ...(onExe ? ["@kybernesis/exe"] : ["@vercel/connect"])],
194
+ registryItems: [],
195
+ env: onExe
196
+ ? [
197
+ `SLACK_BOT_TOKEN="xoxb-..." # outbound; from Slack OAuth & Permissions`,
198
+ `SLACK_SOCKET_FORWARDING_SECRET="$(openssl rand -hex 24)"`,
199
+ ]
200
+ : [`SLACK_CONNECTOR_UID="slack/${name}"`],
201
+ steps: onExe
202
+ ? [
203
+ `Create a Slack app (Socket Mode on; scopes: app_mentions:read, chat:write, channels:history, groups:history, im:history, users:read)`,
204
+ `Hold its tokens off-host: ssh exe.dev integrations add slack --name ${name} --bot-token=- --app-token=-`,
205
+ `Run the forwarder (scripts/slack-forwarder.py in @kybernesis/exe) with EXE_SLACK_GW set`,
206
+ `After ANY scope change, reinstall the app — a stale token silently stops receiving events`,
207
+ ]
208
+ : [
209
+ `vercel connect create slack --triggers --name ${name}`,
210
+ `vercel connect detach <uid> --yes && vercel connect attach <uid> --triggers --trigger-path /eve/v1/slack --yes`,
211
+ ],
212
+ };
213
+ case "imessage":
214
+ return {
215
+ file: "photon.ts",
216
+ content: `import { photonIMessageChannel } from "eve/channels/photon";
217
+ ${onExe ? `import { photonEnvCredentials } from "@kybernesis/exe/photon";\n` : ""}
218
+ // iMessage via Photon. Inbound is a plain webhook at /eve/v1/photon; the Photon
219
+ // signing secret takes precedence over the default Vercel-OIDC verifier, so this
220
+ // works on any host with a public HTTPS URL.
221
+ export default photonIMessageChannel({
222
+ ${onExe
223
+ ? ` credentials: photonEnvCredentials(),`
224
+ : ` async credentials() {
225
+ const projectId = process.env.IMESSAGE_PROJECT_ID;
226
+ const projectSecret = process.env.IMESSAGE_PROJECT_SECRET;
227
+ if (!projectId || !projectSecret) throw new Error("Photon project credentials are required.");
228
+ return { projectId, projectSecret };
229
+ },`}
230
+ webhookSecret: process.env.IMESSAGE_WEBHOOK_SECRET,
231
+ });
232
+ `,
233
+ deps: onExe ? ["@kybernesis/exe"] : [],
234
+ registryItems: [],
235
+ env: [
236
+ `IMESSAGE_PROJECT_ID="..."`,
237
+ `IMESSAGE_PROJECT_SECRET="..."`,
238
+ `IMESSAGE_WEBHOOK_SECRET="..." # Photon webhook signing secret`,
239
+ ],
240
+ steps: [
241
+ `Create a Photon project and register the phone number (npx eve add channel/photon-imessage walks it)`,
242
+ onExe
243
+ ? `Make the host public FIRST — webhooks need anonymous access:\n ssh exe.dev share port <vm> 8000 && ssh exe.dev share set-public <vm>`
244
+ : `Deploy so the public URL exists`,
245
+ `Register a Photon webhook for https://<your-host>/eve/v1/photon and copy its signing secret to IMESSAGE_WEBHOOK_SECRET`,
246
+ ],
247
+ };
248
+ case "telegram":
249
+ return {
250
+ file: "telegram.ts",
251
+ content: `import { telegramChannel } from "eve/channels/telegram";
252
+
253
+ // Telegram. Register the webhook against your public URL after deploying.
254
+ export default telegramChannel({ botToken: process.env.TELEGRAM_BOT_TOKEN! });
255
+ `,
256
+ deps: [],
257
+ registryItems: [],
258
+ env: [`TELEGRAM_BOT_TOKEN="..." # from @BotFather`],
259
+ steps: [
260
+ `Create a bot with @BotFather and copy its token`,
261
+ `After deploy: curl -X POST "https://api.telegram.org/bot<TOKEN>/setWebhook" -d "url=https://<host>/eve/v1/telegram"`,
262
+ ],
263
+ };
264
+ case "discord":
265
+ return {
266
+ file: "discord.ts",
267
+ content: `import { discordChannel } from "eve/channels/discord";
268
+
269
+ export default discordChannel({ botToken: process.env.DISCORD_BOT_TOKEN! });
270
+ `,
271
+ deps: [],
272
+ registryItems: [],
273
+ env: [`DISCORD_BOT_TOKEN="..."`],
274
+ steps: [`Create a Discord application + bot, invite it, set DISCORD_BOT_TOKEN`],
275
+ };
276
+ case "web":
277
+ return {
278
+ file: null,
279
+ content: "",
280
+ deps: [],
281
+ registryItems: [],
282
+ env: [],
283
+ steps: [
284
+ `The eve channel (agent/channels/eve.ts) already serves HTTP; build a frontend with useEveAgent`,
285
+ ],
286
+ };
287
+ case "none":
288
+ default:
289
+ return {
290
+ file: null,
291
+ content: "",
292
+ deps: [],
293
+ registryItems: [],
294
+ env: [],
295
+ steps: [`No chat surface yet — add one later with: kyb add channel <slack|imessage|telegram|discord|web>`],
296
+ };
297
+ }
298
+ }
299
+ export function hostAgentTs(host, model) {
300
+ if (host === "exe") {
301
+ return `import { defineAgent } from "eve";
302
+ import { createOpenAI } from "@ai-sdk/openai";
303
+ import { exeModel } from "@kybernesis/exe";
304
+
305
+ // Model served by the exe.dev LLM integration — no provider key on the host.
306
+ // exe injects the credential (managed gateway, your API key, or a connected
307
+ // ChatGPT subscription) server-side.
308
+ export default defineAgent({
309
+ model: exeModel({ model: process.env.EXE_MODEL ?? ${JSON.stringify(model)}, createOpenAI }),
310
+ modelContextWindowTokens: 200_000,
311
+ });
312
+ `;
313
+ }
314
+ return `import { defineAgent } from "eve";
315
+
316
+ export default defineAgent({
317
+ model: ${JSON.stringify(model)},
318
+ });
319
+ `;
320
+ }
321
+ export function hostSteps(host, name) {
322
+ if (host === "exe") {
323
+ return [
324
+ `Create the VM: ssh exe.dev new --name ${name}`,
325
+ `Attach an LLM integration (ChatGPT subscription, your API key, or exe's gateway):`,
326
+ ` ssh exe.dev integrations setup chatgpt --name work # once, device-code`,
327
+ ` ssh exe.dev integrations edit llm --openai=chatgpt --openai-account=work`,
328
+ `Install Node 24 + deps on the VM, then: npx eve build && bash scripts/eve-server.sh start`,
329
+ `\`eve start\` does NOT read .env.local — scripts/eve-server.sh loads it for you`,
330
+ ];
331
+ }
332
+ return [
333
+ `vercel link (the client's team), then set envs (prod/preview Sensitive)`,
334
+ `npx eve deploy`,
335
+ ];
336
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kybernesis/create",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
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",
@@ -904,6 +904,21 @@ Client-conversation rules of thumb:
904
904
  product conversation — purpose-scoped grants, §2.5 disclosures. Don't wire
905
905
  one as if it were internal.
906
906
 
907
+ **Governed mode (dispatch ≥0.2.1 + enterprise ≥0.2.0 + the client's control
908
+ plane) — the preferred form.** Edges become GRANTS in the admin instead of
909
+ code: register both agents (/agents, OPEN production alias, health 200), grant
910
+ the edge on the CALLEE's panel (caller + purpose + optional expiry), mint each
911
+ agent's credential (shown once) into KYBERNESIS_AGENT_CREDENTIAL on its
912
+ deployment. Code shrinks to remotePeer({ callee: "<EXACT registered name —
913
+ case-sensitive>", governed: { issuer }, envVar, fallbackUrl }) and
914
+ dispatchChannel({ governed: { issuer, agent } }). Outbound auth is a 300 s A2A
915
+ token minted per edge; the callee URL comes from the registry (discovery), env
916
+ var still wins. THE DEMO: revoke the edge in the admin → the caller is refused
917
+ (edge_not_granted) within 5 minutes, no redeploy; re-grant → restored. Run it
918
+ for the client — it's the whole governance story in one minute. Full lifecycle
919
+ proven live 2026-08-07 (kyber ↔ eve-gtm). Budget note: the deployed agent and
920
+ local eval runs share the project's AI Gateway budget — size it for both.
921
+
907
922
  ### 4.4 Author the agent's identity and instructions
908
923
 
909
924
  How instructions work in eve (30 seconds of mechanics): a flat