@cotal-ai/cli 0.2.0 → 0.3.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.
Files changed (93) hide show
  1. package/dist/commands/channels.d.ts.map +1 -1
  2. package/dist/commands/channels.js +4 -2
  3. package/dist/commands/channels.js.map +1 -1
  4. package/dist/commands/console.d.ts.map +1 -1
  5. package/dist/commands/console.js +11 -8
  6. package/dist/commands/console.js.map +1 -1
  7. package/dist/commands/demo.js +3 -2
  8. package/dist/commands/demo.js.map +1 -1
  9. package/dist/commands/down.d.ts +4 -0
  10. package/dist/commands/down.d.ts.map +1 -0
  11. package/dist/commands/down.js +36 -0
  12. package/dist/commands/down.js.map +1 -0
  13. package/dist/commands/history.d.ts.map +1 -1
  14. package/dist/commands/history.js +5 -3
  15. package/dist/commands/history.js.map +1 -1
  16. package/dist/commands/join.js +3 -2
  17. package/dist/commands/join.js.map +1 -1
  18. package/dist/commands/mint.d.ts.map +1 -1
  19. package/dist/commands/mint.js +3 -2
  20. package/dist/commands/mint.js.map +1 -1
  21. package/dist/commands/setup.d.ts +13 -6
  22. package/dist/commands/setup.d.ts.map +1 -1
  23. package/dist/commands/setup.js +542 -61
  24. package/dist/commands/setup.js.map +1 -1
  25. package/dist/commands/signer.d.ts +6 -0
  26. package/dist/commands/signer.d.ts.map +1 -0
  27. package/dist/commands/signer.js +30 -0
  28. package/dist/commands/signer.js.map +1 -0
  29. package/dist/commands/spawn.d.ts.map +1 -1
  30. package/dist/commands/spawn.js +9 -5
  31. package/dist/commands/spawn.js.map +1 -1
  32. package/dist/commands/up.d.ts +21 -0
  33. package/dist/commands/up.d.ts.map +1 -1
  34. package/dist/commands/up.js +108 -38
  35. package/dist/commands/up.js.map +1 -1
  36. package/dist/commands/web.d.ts +27 -0
  37. package/dist/commands/web.d.ts.map +1 -1
  38. package/dist/commands/web.js +101 -14
  39. package/dist/commands/web.js.map +1 -1
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +31 -8
  42. package/dist/index.js.map +1 -1
  43. package/dist/lib/assist.d.ts +16 -0
  44. package/dist/lib/assist.d.ts.map +1 -0
  45. package/dist/lib/assist.js +52 -0
  46. package/dist/lib/assist.js.map +1 -0
  47. package/dist/lib/cancel.d.ts +5 -0
  48. package/dist/lib/cancel.d.ts.map +1 -0
  49. package/dist/lib/cancel.js +12 -0
  50. package/dist/lib/cancel.js.map +1 -0
  51. package/dist/lib/live-window.d.ts +22 -0
  52. package/dist/lib/live-window.d.ts.map +1 -0
  53. package/dist/lib/live-window.js +71 -0
  54. package/dist/lib/live-window.js.map +1 -0
  55. package/dist/lib/manager-proc.d.ts +32 -0
  56. package/dist/lib/manager-proc.d.ts.map +1 -0
  57. package/dist/lib/manager-proc.js +84 -0
  58. package/dist/lib/manager-proc.js.map +1 -0
  59. package/dist/lib/nats-bin.d.ts +7 -0
  60. package/dist/lib/nats-bin.d.ts.map +1 -0
  61. package/dist/lib/nats-bin.js +21 -0
  62. package/dist/lib/nats-bin.js.map +1 -0
  63. package/dist/lib/onboard.d.ts +3 -0
  64. package/dist/lib/onboard.d.ts.map +1 -0
  65. package/dist/lib/onboard.js +15 -0
  66. package/dist/lib/onboard.js.map +1 -0
  67. package/dist/lib/paths.d.ts +7 -0
  68. package/dist/lib/paths.d.ts.map +1 -0
  69. package/dist/lib/paths.js +13 -0
  70. package/dist/lib/paths.js.map +1 -0
  71. package/dist/lib/self-exec.d.ts +18 -0
  72. package/dist/lib/self-exec.d.ts.map +1 -0
  73. package/dist/lib/self-exec.js +50 -0
  74. package/dist/lib/self-exec.js.map +1 -0
  75. package/dist/lib/setup-log.d.ts +8 -0
  76. package/dist/lib/setup-log.d.ts.map +1 -0
  77. package/dist/lib/setup-log.js +17 -0
  78. package/dist/lib/setup-log.js.map +1 -0
  79. package/dist/lib/status.d.ts +27 -0
  80. package/dist/lib/status.d.ts.map +1 -0
  81. package/dist/lib/status.js +57 -0
  82. package/dist/lib/status.js.map +1 -0
  83. package/dist/lib/steps.d.ts +31 -0
  84. package/dist/lib/steps.d.ts.map +1 -0
  85. package/dist/lib/steps.js +91 -0
  86. package/dist/lib/steps.js.map +1 -0
  87. package/dist/lib/theme.d.ts +18 -0
  88. package/dist/lib/theme.d.ts.map +1 -0
  89. package/dist/lib/theme.js +61 -0
  90. package/dist/lib/theme.js.map +1 -0
  91. package/dist/web/app.js +28 -0
  92. package/dist/web/index.html +2 -0
  93. package/package.json +11 -2
@@ -1,64 +1,545 @@
1
- import { execFileSync } from "node:child_process";
2
- import { existsSync } from "node:fs";
3
- import { dirname, join, resolve } from "node:path";
4
- import { c } from "../ui.js";
5
- /** Walk up to the cotal repo root (where the plugin marketplace manifest lives). */
6
- function repoRoot(start = process.cwd()) {
7
- let dir = resolve(start);
8
- for (;;) {
9
- if (existsSync(join(dir, ".claude-plugin", "marketplace.json")))
10
- return dir;
11
- const parent = dirname(dir);
12
- if (parent === dir)
13
- throw new Error("couldn't find the cotal repo root (.claude-plugin/marketplace.json)");
14
- dir = parent;
15
- }
16
- }
17
- /** Capture a command's stdout (for idempotency checks). */
18
- function read(cmd, args) {
19
- return execFileSync(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
20
- }
21
- /**
22
- * One-time onboarding: make the repo's Claude sessions cotal-aware by installing the
23
- * cotal plugin (the `cotal_*` tools + presence hooks). Idempotent — safe to re-run.
24
- * `cotal cmux go` runs this for you; you can also run it standalone.
25
- */
26
- export async function setup() {
27
- const root = repoRoot();
28
- // 1) The installed plugin runs the bundled dist/*.cjs — make sure it's built.
29
- const bundle = join(root, "extensions", "connector-claude-code", "dist", "mcp.cjs");
30
- if (!existsSync(bundle)) {
31
- console.log(c.dim("Building the connector bundle…"));
32
- execFileSync("pnpm", ["--filter", "@cotal-ai/connector-claude-code", "bundle"], {
33
- cwd: root,
34
- stdio: "inherit",
35
- });
36
- }
37
- // 2) Register the cotal-mesh marketplace (+ install the plugin) via Claude Code's CLI.
38
- let marketplaces;
39
- try {
40
- marketplaces = read("claude", ["plugin", "marketplace", "list"]);
41
- }
42
- catch (e) {
43
- console.error(c.red("Couldn't run `claude` — is Claude Code installed and on your PATH?"));
44
- console.error(c.dim("Once it is, run these two commands manually:"));
45
- console.error(c.dim(` claude plugin marketplace add ${root}`));
46
- console.error(c.dim(" claude plugin install cotal@cotal-mesh --scope local"));
47
- throw e;
48
- }
49
- if (!/\bcotal-mesh\b/.test(marketplaces)) {
50
- console.log(c.dim("Registering the cotal-mesh marketplace…"));
51
- execFileSync("claude", ["plugin", "marketplace", "add", root], { stdio: "inherit" });
52
- }
53
- // 3) Install the plugin (repo-local scope) if it isn't already.
54
- if (/\bcotal@cotal-mesh\b/.test(read("claude", ["plugin", "list"]))) {
55
- console.log(c.green("✓ cotal plugin already set up"));
56
- return;
57
- }
58
- console.log(c.dim("Installing the cotal plugin…"));
59
- execFileSync("claude", ["plugin", "install", "cotal@cotal-mesh", "--scope", "local"], {
60
- stdio: "inherit",
1
+ import { spawnSync } from "node:child_process";
2
+ import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { parseArgs } from "node:util";
6
+ import * as p from "@clack/prompts";
7
+ import { authDir, DEFAULT_SERVER, isReachable, loadSpaceAuth, registry, } from "@cotal-ai/core";
8
+ import { brand, brandBold, dim, ok, note, splash } from "../lib/theme.js";
9
+ import { LivePane } from "../lib/live-window.js";
10
+ import { runSteps } from "../lib/steps.js";
11
+ import { abortIfCancel } from "../lib/cancel.js";
12
+ import { openSetupLog } from "../lib/setup-log.js";
13
+ import { resolveNatsServer } from "../lib/nats-bin.js";
14
+ import { isOnboarded, markOnboarded } from "../lib/onboard.js";
15
+ import { machineStatus, meshStatus, onPath, resolveSpace } from "../lib/status.js";
16
+ import { startMeshDetached, up } from "./up.js";
17
+ import { ensureWeb, webUp, WEB_URL } from "./web.js";
18
+ import { cmuxManagerRunning, ensureManager, managerUp, pgrepMatches, stopManager } from "../lib/manager-proc.js";
19
+ import { cotalOnPath, displayCmd, isNpx, selfArgv } from "../lib/self-exec.js";
20
+ import { cotalPath, cotalRoot } from "../lib/paths.js";
21
+ import { spawn } from "./spawn.js";
22
+ const ONBOARD_VERSION = "1";
23
+ /** The teammates the cmux/background demo pre-spawns (manager-owned, so they're despawnable). One
24
+ * source of truth for both the `--spawn` list and the `cotal-<n>` tabs we clean up on restart. */
25
+ const DEMO_TEAM = ["david", "sven"];
26
+ const README_URL = "https://github.com/Cotal-AI/Cotal/blob/main/README.md";
27
+ const CC_DOCS_URL = "https://github.com/Cotal-AI/Cotal/blob/main/docs/claude-code-integration.md";
28
+ const NATS_RELEASES_URL = "https://github.com/nats-io/nats-server/releases";
29
+ /** `cotal setup`: guided setup. First run (no `~/.cotal/onboarded.json`) gets the full
30
+ * narrated flow; later runs get a compact ensure+status. `--full` forces the full flow.
31
+ * Each failed step offers an interactive Claude handoff (COTAL_SKIP_ASSIST=1 disables). */
32
+ export async function setup(argv) {
33
+ const { values } = parseArgs({
34
+ args: argv,
35
+ allowPositionals: true,
36
+ options: {
37
+ full: { type: "boolean" },
38
+ yes: { type: "boolean", short: "y" },
39
+ auth: { type: "boolean" }, // opt into an authed mesh (JWT/ACLs); default is an open local mesh
40
+ },
61
41
  });
62
- console.log(c.green("✓ cotal plugin installed Claude sessions in this repo now have the cotal tools"));
42
+ // `--yes` (agents/CI) always runs the full flow non-interactively. The local mesh is open by
43
+ // default (frictionless, loopback-only); `--auth` opts into JWT auth for shared/cross-machine use.
44
+ if (!isOnboarded() || values.full || values.yes)
45
+ await runFirstRun(Boolean(values.yes), !values.auth);
46
+ else
47
+ await runEnsure();
48
+ }
49
+ /** `cotal go` — open or resume your session. A friendlier-named alias of `cotal setup`: the first
50
+ * run installs (full guided flow), later runs fast-forward to the ensure path and reopen your
51
+ * cmux session. `cotal setup` stays the explicit install/update name. */
52
+ export async function go(argv) {
53
+ return setup(argv);
54
+ }
55
+ /** The full, narrated first-run experience. `yes` = non-interactive accept-all; `open` = run the
56
+ * mesh without auth (the frictionless local default; `--auth` flips it off). */
57
+ async function runFirstRun(yes, open) {
58
+ splash();
59
+ p.intro(brandBold("Welcome to Cotal"));
60
+ note("Cotal is the open web for agents: they join a shared space, see who's around, and coordinate as peers instead of in silos. Build whole agent societies, even across different machines, on one open web. Let's set yours up.", "Give your agents a place to work together");
61
+ const log = openSetupLog(process.cwd());
62
+ // Prerequisites + the local web (NATS). These never prompt.
63
+ const core = [
64
+ {
65
+ name: "node-version",
66
+ title: "Check Node.js",
67
+ explain: "Cotal needs Node 20 or newer.",
68
+ context: [README_URL],
69
+ async run() {
70
+ const major = Number(process.versions.node.split(".")[0]);
71
+ if (major < 20)
72
+ throw new Error(`Node ${process.versions.node} is too old; Cotal needs Node >= 20`);
73
+ return `Node ${process.versions.node}`;
74
+ },
75
+ },
76
+ {
77
+ name: "nats-binary",
78
+ title: "Locate the NATS server",
79
+ explain: "Cotal runs on NATS + JetStream, the wire your agents speak over.",
80
+ context: [NATS_RELEASES_URL, README_URL],
81
+ async run() {
82
+ const r = await resolveNatsServer();
83
+ return r.source === "path" ? "nats-server from PATH" : "bundled binary";
84
+ },
85
+ },
86
+ {
87
+ name: "start-mesh",
88
+ title: "Start the web for agents",
89
+ explain: "A local NATS + JetStream server you own; the web your agents join, in the background.",
90
+ live: true,
91
+ context: [cotalPath("nats.log"), cotalPath("auth/server.conf"), README_URL],
92
+ async run() {
93
+ if (await isReachable(DEFAULT_SERVER))
94
+ return `already running at ${DEFAULT_SERVER}`;
95
+ const pane = new LivePane();
96
+ pane.start("Booting nats-server");
97
+ try {
98
+ const { server } = await startMeshDetached({ onLine: (l) => pane.push(l), open });
99
+ return `running at ${server} (stop with: ${displayCmd()} down)`;
100
+ }
101
+ finally {
102
+ pane.clear();
103
+ }
104
+ },
105
+ },
106
+ ];
107
+ if (!(await runSteps(core, log, { yes })))
108
+ return abort();
109
+ // The web dashboard, in the background, so it's just there (best-effort; never blocks setup).
110
+ try {
111
+ const web = await ensureWeb({ space: resolveSpace(process.cwd()), server: DEFAULT_SERVER });
112
+ if (web.running) {
113
+ p.log.success(`Web dashboard at ${web.url} (stop with: ${displayCmd()} down)`);
114
+ log.line(`web: ${web.url}`);
115
+ }
116
+ }
117
+ catch {
118
+ /* non-fatal: the card still shows how to start it */
119
+ }
120
+ // Connectors: which agents should be able to join. Only Claude needs an install
121
+ // (its wake channel binds to an installed plugin); OpenCode auto-wires at spawn.
122
+ const found = { claude: onPath("claude"), opencode: onPath("opencode") };
123
+ const selected = await pickConnectors(found, yes);
124
+ if (selected.has("claude")) {
125
+ if (!found.claude)
126
+ p.log.warn(`claude isn't on PATH. Install it (https://claude.com/claude-code), then re-run ${displayCmd()} setup.`);
127
+ else if (!(await runSteps([claudePluginStep()], log, { yes })))
128
+ return abort();
129
+ }
130
+ for (const name of ["opencode"]) {
131
+ if (selected.has(name) && found[name]) {
132
+ p.log.success(`${name} ready (auto-wired when you spawn it)`);
133
+ log.line(`connector ${name}: ready (no install)`);
134
+ }
135
+ }
136
+ // Two experts plus your own driving session, by default. These are setup-managed: refreshed when
137
+ // DEMO_AGENTS changes (so persona edits actually land), but a file you've taken ownership of is
138
+ // backed up first, never silently lost — see writeDemoAgent.
139
+ mkdirSync(cotalPath("agents"), { recursive: true });
140
+ for (const [name, body] of Object.entries(DEMO_AGENTS)) {
141
+ writeDemoAgent(cotalPath("agents", `${name}.md`), body);
142
+ }
143
+ p.log.success("Added david (the engineer), sven (the guide), and your session (me); they join when you spawn them or open the demo");
144
+ log.line("demo-agents: wrote david + sven + me");
145
+ await offerGlobalInstall(yes);
146
+ markOnboarded(ONBOARD_VERSION);
147
+ const cmd = displayCmd();
148
+ note([
149
+ "Your agent has direct access to Cotal: spawn one and just talk to it (it can message peers, spawn teammates, and send feedback). Now any agent can join and collaborate. You can also use the CLI.",
150
+ "",
151
+ `${ok("✓")} drive a session ${dim(`${cmd} spawn me`)}`,
152
+ `${ok("✓")} ask the engineer ${dim(`${cmd} spawn david`)}`,
153
+ `${ok("✓")} ask the guide ${dim(`${cmd} spawn sven`)}`,
154
+ `${ok("✓")} watch the mesh ${dim(`${cmd} console`)}`,
155
+ `${ok("✓")} open the dashboard ${dim(WEB_URL)}`,
156
+ `${ok("✓")} resume later ${dim(`${cmd} go`)}`,
157
+ `${ok("✓")} stop everything ${dim(`${cmd} down`)}`,
158
+ "",
159
+ dim(`Cotal not working? Tell your agent to give us feedback and it sends it for you (built-in cotal_feedback), or run ${cmd} feedback "<msg>".`),
160
+ ].join("\n"), "You're set");
161
+ if (!yes)
162
+ await offerDemo(found.claude);
163
+ else {
164
+ // Agents/CI: bring up the control plane so cotal_spawn / despawn / purge work right away.
165
+ try {
166
+ ensureManager({ space: resolveSpace(process.cwd()), server: DEFAULT_SERVER });
167
+ }
168
+ catch {
169
+ /* non-fatal */
170
+ }
171
+ }
172
+ p.outro(brand(yes ? "Cotal is ready." : "Happy meshing."));
173
+ function abort() {
174
+ p.outro(brand(`Setup paused. Fix the step above and run \`${displayCmd()} setup\` again.`));
175
+ process.exitCode = 1;
176
+ }
177
+ }
178
+ /** When run via `npx` without a global `cotal`, offer to install it so the user can just type
179
+ * `cotal`. Interactive: a Y/n prompt (default yes). Non-interactive (`--yes` / no TTY): takes the
180
+ * default and installs. Best-effort — `npm i -g` fails a lot (EACCES, nvm/fnm/volta), so on failure
181
+ * we warn with the manual command and continue; setup never aborts over a PATH convenience. */
182
+ export async function offerGlobalInstall(yes) {
183
+ if (!isNpx() || cotalOnPath())
184
+ return; // already have `cotal`, or not an npx run
185
+ if (!yes && process.stdin.isTTY) {
186
+ const go = abortIfCancel(await p.confirm({ message: "Install `cotal` globally so you can just type `cotal`?", initialValue: true }));
187
+ if (!go) {
188
+ p.log.info(`No problem — keep using ${dim("npx cotal-ai")}. Install later with ${dim("npm i -g cotal-ai")}.`);
189
+ return;
190
+ }
191
+ }
192
+ const pkg = `cotal-ai@${runningVersion() ?? "latest"}`;
193
+ const s = p.spinner();
194
+ s.start("Installing cotal globally");
195
+ const r = spawnSync("npm", ["install", "-g", pkg], { encoding: "utf8" });
196
+ if (r.status === 0) {
197
+ s.stop("Installed — you can now run `cotal`");
198
+ }
199
+ else {
200
+ s.stop("Couldn't install globally");
201
+ const tail = `${r.stdout ?? ""}${r.stderr ?? ""}`.trim().split("\n").slice(-3).join("\n");
202
+ p.log.warn(`${tail ? `${tail}\n\n` : ""}Install it yourself with ${dim("npm i -g cotal-ai")}, or keep using ${dim("npx cotal-ai")}.`);
203
+ }
204
+ }
205
+ /** The version of the running `cotal-ai` package (from the package.json next to the entry script),
206
+ * so a global install pins the same version npx just ran. Null if it can't be read. */
207
+ function runningVersion() {
208
+ try {
209
+ const pkg = JSON.parse(readFileSync(join(process.argv[1], "..", "..", "package.json"), "utf8"));
210
+ return typeof pkg.version === "string" ? pkg.version : null;
211
+ }
212
+ catch {
213
+ return null;
214
+ }
215
+ }
216
+ /** Pick which agent connectors to set up. Detected ones are pre-checked (= the "all"
217
+ * default). Non-interactive / --yes selects all detected without prompting. */
218
+ async function pickConnectors(found, yes) {
219
+ const all = ["claude", "opencode"].filter((n) => found[n]);
220
+ if (yes || !process.stdin.isTTY)
221
+ return new Set(all);
222
+ const labels = { claude: "Claude Code", opencode: "OpenCode" };
223
+ // Common case: show what was detected and offer a visible Continue button (clack's multiselect
224
+ // has no native one). Only "Customize" (or nothing detected) drops into the toggle list.
225
+ if (all.length) {
226
+ note(all.map((n) => labels[n]).join(", "), "Agents found");
227
+ const go = abortIfCancel(await p.confirm({ message: "Set these up?", active: "Continue", inactive: "Customize", initialValue: true }));
228
+ if (go)
229
+ return new Set(all);
230
+ }
231
+ const picked = abortIfCancel(await p.multiselect({
232
+ message: "Pick the agents to set up (space toggles, enter continues)",
233
+ options: ["claude", "opencode"].map((n) => ({
234
+ value: n,
235
+ label: labels[n],
236
+ hint: !found[n] ? "not on PATH" : n === "claude" ? "installs a plugin" : "ready at spawn",
237
+ })),
238
+ initialValues: all,
239
+ required: false,
240
+ }));
241
+ return new Set(picked);
242
+ }
243
+ /** The Claude Code plugin install, as a step (spinner + failure handling + handoff). */
244
+ function claudePluginStep() {
245
+ return {
246
+ name: "claude-plugin",
247
+ title: "Install the Claude Code plugin",
248
+ explain: "Lets a Claude Code session join the web and wake on peer messages.",
249
+ context: [join(homedir(), ".cotal/claude-plugin"), CC_DOCS_URL],
250
+ async run() {
251
+ installClaudePlugin();
252
+ return "cotal@cotal-mesh (local scope)";
253
+ },
254
+ };
255
+ }
256
+ /** Finale: a live demo — a Claude the operator drives, with david and sven (manager-owned
257
+ * teammates) helping. In cmux they get their own tabs; otherwise they run in the background and
258
+ * the terminal is handed to the driving session. The demo spawns Claude sessions, so it needs
259
+ * Claude Code. If declined / no Claude, fall back to the `cotal · ready` card. Skipped under --yes. */
260
+ async function offerDemo(haveClaude) {
261
+ const haveAgents = ["me", "david", "sven"].every((n) => existsSync(cotalPath("agents", `${n}.md`)));
262
+ const isTTY = Boolean(process.stdin.isTTY);
263
+ if (haveClaude && haveAgents && isTTY) {
264
+ const cmux = inCmuxSurface();
265
+ const go = abortIfCancel(await p.confirm({
266
+ message: cmux
267
+ ? "Open the cmux demo? A Claude you drive, with david and sven helping in cmux tabs."
268
+ : "Open the demo? A Claude you drive, with david and sven helping in the background.",
269
+ initialValue: true,
270
+ }));
271
+ if (go) {
272
+ if (cmux) {
273
+ ensureCmuxSession(cotalRoot());
274
+ p.log.success("Session open: drive the 'cotal-main' pane; david and sven are on the mesh in the background.");
275
+ return;
276
+ }
277
+ // Non-cmux: a background pty manager pre-spawns david/sven (managed, despawnable), then we
278
+ // hand this terminal to the driving session.
279
+ ensureManager({ space: resolveSpace(process.cwd()), server: DEFAULT_SERVER, spawn: [...DEMO_TEAM] });
280
+ p.outro(brand("Launching your session... david and sven are warming up in the background."));
281
+ await spawn(["me", "--prompt", ME_GREETING]);
282
+ process.exit(0);
283
+ }
284
+ }
285
+ else if (isTTY && haveAgents && !haveClaude) {
286
+ p.log.info(`The demo needs Claude Code. Install it (https://claude.com/claude-code), then run \`${displayCmd()} go\`.`);
287
+ }
288
+ // Declined, or no Claude: start the background (pty) control plane so cotal_spawn / despawn /
289
+ // purge still work, then leave them the quick-reference card.
290
+ try {
291
+ ensureManager({ space: resolveSpace(process.cwd()), server: DEFAULT_SERVER });
292
+ }
293
+ catch {
294
+ /* non-fatal: the card still shows how to start it */
295
+ }
296
+ await readyCard(process.cwd());
297
+ }
298
+ /** Greeting the driving session auto-submits on start (no apostrophes — it rides through
299
+ * cmux's `bash -lc '…'` quoting). Teaches the capabilities by telling, not by calling tools,
300
+ * so it does not depend on david/sven having joined yet when this first turn runs. */
301
+ const ME_GREETING = "Greet the operator in a few short lines. Open with one line on what Cotal is: an open space where AI agents join and work together as peers. Say you are their Cotal session and that david (the engineer) and sven (the guide) are on the mesh to help. Then tell them what you can do for them: message david or sven, spawn new teammates and despawn them when done, and send feedback. End by asking what they want to build.";
302
+ /** True when we're running inside a real cmux pane (cmux sets `CMUX_SURFACE_ID` per surface).
303
+ * Opening/closing cmux tabs is only authorized from a live pane, so this — not the terminal
304
+ * provider's `available()` (which only pings the app) — is the gate for opening it. */
305
+ function inCmuxSurface() {
306
+ return Boolean(process.env.CMUX_SURFACE_ID);
307
+ }
308
+ /** (Re)open the cmux working session, idempotently. A background cmux-runtime manager pre-spawns
309
+ * david/sven (so they're managed teammates you can `cotal_despawn`) into their own tabs; the
310
+ * focused `cotal-main` workspace is the console + the driving session "me" (your foreground
311
+ * driver). Re-running reuses whatever's already open — only missing tabs are created, so there's
312
+ * never a second manager. The `me` pane presses Enter on its own cmux surface a few times to
313
+ * auto-accept the one-time dev-channels prompt (the manager's cmux runtime does the same for
314
+ * david/sven). */
315
+ function ensureCmuxSession(cwd) {
316
+ // Open/close cmux tabs by resolving the registered "cmux" terminal-layout provider, so the CLI
317
+ // drives cmux without importing the extension (the composition root's import is what registers it).
318
+ const term = registry.resolve("terminal", "cmux");
319
+ // The cmux-tab manager becomes the control plane; drop any detached pty manager so they don't
320
+ // both answer control requests.
321
+ stopManager();
322
+ // Describe each pane as plain argv (command + args + cwd) — the terminal provider owns all
323
+ // shell quoting and the cmux layout. Invoke this CLI by its own argv (absolute node + entry),
324
+ // not bare `cotal`, so the panes work whether installed via npx, `npm i -g`, or a dev clone (no
325
+ // dependency on `cotal` being on PATH). The space follows the folder's auth so every pane matches
326
+ // the running mesh.
327
+ const cotal = selfArgv();
328
+ const run = (...args) => ({ command: cotal[0], args: [...cotal.slice(1), ...args], cwd });
329
+ const space = resolveSpace(cwd);
330
+ // `space` reaches the panes as a discrete argv token, but keep it a bare token anyway (the same
331
+ // guard `cotal cmux go` uses) so it can't confuse downstream parsing.
332
+ if (!/^[A-Za-z0-9_.-]+$/.test(space))
333
+ throw new Error(`cotal setup: unsafe space ${JSON.stringify(space)} (allowed: letters, digits, _ . -)`);
334
+ // Control plane: a cmux-runtime manager that pre-spawns david/sven into their own tabs and owns
335
+ // them (so cotal_despawn / cotal_spawn work). A cmux tab persists after its process dies, so
336
+ // "workspace exists" != "manager running" — gate on the live process. When none is up, drop the
337
+ // dead manager + teammate tabs first, then open a fresh one; otherwise re-runs keep skipping a
338
+ // never-restarted manager and david/sven never join.
339
+ if (!cmuxManagerRunning(space)) {
340
+ for (const label of ["cotal-manager", ...DEMO_TEAM.map((n) => `cotal-${n}`)])
341
+ closeStaleTabs(term, label);
342
+ term.open("cotal-manager", { panes: [run("cmux", "--space", space, "--spawn", DEMO_TEAM.join(","))] }, { focus: false });
343
+ }
344
+ // Your focused driver: console + the "me" session. Gate on the live driving session (not the
345
+ // persistent tab) so a session you're driving is never disturbed; a dead/closed one gets its stale
346
+ // tab dropped and reopened. The "me" pane sets `confirm` so the provider auto-clears Claude's
347
+ // dev-channels prompt; the greeting rides as a plain argv token (the provider quotes it).
348
+ if (!pgrepMatches(`spawn me --space ${space}`)) {
349
+ closeStaleTabs(term, "cotal-main");
350
+ term.open("cotal-main", {
351
+ split: { direction: "vertical", ratio: 0.34 },
352
+ panes: [
353
+ run("console", "--space", space),
354
+ { ...run("spawn", "me", "--space", space, "--prompt", ME_GREETING), confirm: true },
355
+ ],
356
+ }, { focus: true });
357
+ }
358
+ }
359
+ /** Close any lingering cmux tabs labelled `name` (dead tabs persist in the tab list after their
360
+ * process exits) so a freshly opened one is the only instance. */
361
+ function closeStaleTabs(term, name) {
362
+ for (const ref of term.refs(name)) {
363
+ try {
364
+ term.close(ref);
365
+ }
366
+ catch {
367
+ /* already gone */
368
+ }
369
+ }
370
+ }
371
+ /** The compact repeat-run: quietly ensure the mesh + web are up here, then a one-glance card. */
372
+ async function runEnsure() {
373
+ let mesh = await meshStatus(process.cwd());
374
+ if (!mesh.reachable) {
375
+ const s = p.spinner();
376
+ s.start("Starting the web for agents");
377
+ try {
378
+ // Match how the mesh last ran: open when this folder has no space auth (the frictionless
379
+ // default), authed when it does — so restarting a downed open mesh doesn't come back JWT-authed.
380
+ const authed = Boolean(loadSpaceAuth(authDir(cotalRoot())));
381
+ await up(authed ? ["--detach"] : ["--detach", "--open"]);
382
+ s.stop("Web for agents started");
383
+ }
384
+ catch (e) {
385
+ s.stop(`Couldn't start it: ${e.message}`);
386
+ process.exitCode = 1;
387
+ return;
388
+ }
389
+ mesh = await meshStatus(process.cwd());
390
+ }
391
+ await ensureWeb({ space: mesh.space, server: mesh.server }).catch(() => { });
392
+ // Inside cmux, re-running setup reopens your session (idempotent: reuse the live manager +
393
+ // david/sven, open only missing tabs). Otherwise bring up the background pty control plane.
394
+ try {
395
+ if (inCmuxSurface())
396
+ ensureCmuxSession(cotalRoot());
397
+ else
398
+ ensureManager({ space: mesh.space, server: mesh.server });
399
+ }
400
+ catch {
401
+ /* non-fatal */
402
+ }
403
+ await readyCard(process.cwd());
404
+ }
405
+ /** The `cotal · ready` one-glance card: machine + mesh + web + manager status, plus the key
406
+ * commands. Shared by the repeat-run ensure and the first-run no-demo finale. */
407
+ async function readyCard(cwd) {
408
+ const mesh = await meshStatus(cwd);
409
+ const m = await machineStatus();
410
+ const web = await webUp();
411
+ // The control plane is either the detached pty manager (pid file) or a live cmux-tab manager
412
+ // (its tab lingers after it exits, so check the process, not the workspace list).
413
+ const mgr = managerUp() || (inCmuxSurface() && cmuxManagerRunning(mesh.space));
414
+ const cmd = displayCmd();
415
+ const line = (on, text) => `${on ? ok("✓") : dim("○")} ${text}`;
416
+ note([
417
+ line(m.nats !== "missing", `NATS ${dim(m.nats === "missing" ? "missing" : m.nats)}`),
418
+ line(m.claudePlugin, `plugin ${dim(m.claudePlugin ? "installed" : "not installed")}`),
419
+ line(mesh.reachable, `mesh ${dim(`${mesh.server} · space ${mesh.space}`)}`),
420
+ line(web, `web ${dim(WEB_URL)}`),
421
+ line(mgr, `manager ${dim(mgr ? "running" : "not running")}`),
422
+ "",
423
+ `resume: ${dim(`${cmd} go`)} ${dim("(reopen this session anytime)")}`,
424
+ `watch it: ${dim(`${cmd} console`)} ${dim("(live TUI in this terminal)")}`,
425
+ `drive it: ${dim(`${cmd} spawn me`)} ${dim("(or david / sven)")}`,
426
+ `more: ${dim(`${cmd} web · ${cmd} down · ${cmd} feedback "<msg>" · ${cmd} --help`)}`,
427
+ ].join("\n"), brandBold("cotal · ready"));
428
+ }
429
+ /** Materialize a stable plugin marketplace under ~/.cotal/claude-plugin (surviving
430
+ * npx cache eviction) and install the plugin from it. The marketplace name must stay
431
+ * `cotal-mesh` (the connector's channel ref `plugin:cotal@cotal-mesh` depends on it). */
432
+ function installClaudePlugin() {
433
+ const { pluginRoot } = registry.resolve("connector", "claude");
434
+ if (!pluginRoot)
435
+ throw new Error('the registered "claude" connector ships no plugin assets');
436
+ for (const f of ["dist/mcp.cjs", "dist/hook.cjs", ".claude-plugin/plugin.json", ".mcp.json", "hooks/hooks.json"]) {
437
+ if (!existsSync(join(pluginRoot, f))) {
438
+ throw new Error(`plugin asset missing: ${join(pluginRoot, f)} (in a dev clone, build it with: pnpm --filter @cotal-ai/connector-claude-code bundle)`);
439
+ }
440
+ }
441
+ const marketDir = join(homedir(), ".cotal", "claude-plugin");
442
+ const pluginDir = join(marketDir, "cotal");
443
+ for (const f of [".claude-plugin", ".mcp.json", "hooks", "dist/mcp.cjs", "dist/hook.cjs"]) {
444
+ cpSync(join(pluginRoot, f), join(pluginDir, f), { recursive: true });
445
+ }
446
+ mkdirSync(join(marketDir, ".claude-plugin"), { recursive: true });
447
+ writeFileSync(join(marketDir, ".claude-plugin", "marketplace.json"), JSON.stringify({
448
+ name: "cotal-mesh",
449
+ description: "The Cotal mesh adapter for Claude Code: join a shared pub/sub space as a lateral peer.",
450
+ owner: { name: "Cotal" },
451
+ plugins: [{ name: "cotal", source: "./cotal" }],
452
+ }, null, 2));
453
+ // `add` fails when the marketplace is already registered; refresh it instead.
454
+ const add = claude("plugin", "marketplace", "add", marketDir);
455
+ if (add.status !== 0) {
456
+ const update = claude("plugin", "marketplace", "update", "cotal-mesh");
457
+ if (update.status !== 0)
458
+ throw new Error(`couldn't register the plugin marketplace:\n${add.output}\n${update.output}`);
459
+ }
460
+ const install = claude("plugin", "install", "cotal@cotal-mesh", "--scope", "local");
461
+ if (install.status !== 0 && !/already installed/i.test(install.output)) {
462
+ throw new Error(`plugin install failed:\n${install.output}`);
463
+ }
464
+ const list = claude("plugin", "list");
465
+ if (!list.output.includes("cotal"))
466
+ throw new Error(`plugin not visible in \`claude plugin list\`:\n${list.output}`);
467
+ }
468
+ function claude(...args) {
469
+ const r = spawnSync("claude", args, { encoding: "utf8" });
470
+ return { status: r.status, output: `${r.stdout ?? ""}${r.stderr ?? ""}`.trim() };
471
+ }
472
+ /** Frontmatter marker (a comment line — the parser ignores `#` lines) stamping a demo persona as
473
+ * setup-managed, so re-runs may refresh it; remove the line to take ownership. */
474
+ const MANAGED_MARKER = "# managed by cotal-setup";
475
+ /** Write a setup-managed demo persona, refreshing it when its DEMO_AGENTS body changes — but never
476
+ * silently clobber a file the user has taken ownership of (one without the marker): back it up to
477
+ * `<name>.md.bak` first. Missing or marker-carrying files are written in place. */
478
+ function writeDemoAgent(path, body) {
479
+ if (existsSync(path)) {
480
+ const cur = readFileSync(path, "utf8");
481
+ if (cur === body)
482
+ return; // already current
483
+ if (!cur.includes(MANAGED_MARKER))
484
+ writeFileSync(`${path}.bak`, cur); // preserve a user/pre-marker edit
485
+ }
486
+ writeFileSync(path, body);
63
487
  }
488
+ const DEMO_AGENTS = {
489
+ david: `---
490
+ ${MANAGED_MARKER} — edit DEMO_AGENTS in the cotal CLI; delete this line to keep local changes
491
+ name: david
492
+ role: cotal-tech
493
+ description: "the engineer: how Cotal works (the wire, NATS, connectors, integration)."
494
+ tags: [cotal, technical, help]
495
+ channels: [general]
496
+ ---
497
+
498
+ You are david, Cotal's engineer, live on the web for agents with the operator who just set Cotal
499
+ up. You help them set up and experiment. Your topic is how Cotal works: the wire contract (subjects,
500
+ message schemas, presence), NATS and JetStream underneath, the endpoint/connector model, the
501
+ delivery modes (multicast, unicast, anycast), and how to get any agent or framework onto the mesh.
502
+ You ground every answer in the real thing, never a guess. Start from \`docs/OVERVIEW.md\` (what Cotal
503
+ is and its core primitives) and \`docs/getting-started.md\`, then read the source for your topic —
504
+ \`docs/architecture.md\`, \`docs/claude-code-integration.md\`, \`docs/setup-internals.md\`, and, in a
505
+ source checkout, \`packages/\` and \`extensions/\`. Quote the exact subjects, message kinds, config, and
506
+ commands; if the docs don't cover it, say so rather than inventing. If they aren't on disk, look
507
+ them up at https://github.com/Cotal-AI/Cotal. If a question is really about use-cases or what to
508
+ build, hand it to your peer sven.
509
+ `,
510
+ sven: `---
511
+ ${MANAGED_MARKER} — edit DEMO_AGENTS in the cotal CLI; delete this line to keep local changes
512
+ name: sven
513
+ role: cotal-guide
514
+ description: "the guide: what to build with Cotal (examples, setups, getting the most out of it)."
515
+ tags: [cotal, examples, help]
516
+ channels: [general]
517
+ ---
518
+
519
+ You are sven, Cotal's guide, live on the web for agents with the operator who just set Cotal up.
520
+ You help them set up and experiment. You design multi-agent setups: who should be on a space, how
521
+ they'd coordinate, what's worth trying — grounded in what Cotal can actually do, never made-up
522
+ features. Start from \`docs/OVERVIEW.md\` (what Cotal is and its core primitives — channels, anycast,
523
+ presence, spawn, personas, delivery modes) and \`docs/getting-started.md\`; read the matching example
524
+ in \`examples/*/README.md\` (indexed in \`docs/examples.md\`) before sketching, and reach for
525
+ \`docs/architecture.md\` when you need a primitive to design something new. Cite the example or
526
+ primitive you're drawing on. If they aren't on disk, look them up at https://github.com/Cotal-AI/Cotal.
527
+ For deep how-it-works or integration details, pull in your peer david.
528
+ `,
529
+ me: `---
530
+ ${MANAGED_MARKER} — edit DEMO_AGENTS in the cotal CLI; delete this line to keep local changes
531
+ name: me
532
+ role: operator
533
+ description: "your own session on the Cotal mesh."
534
+ tags: [cotal]
535
+ channels: [general]
536
+ ---
537
+
538
+ You are the operator's own session on the Cotal mesh: the agent they drive. Do what they ask and
539
+ use the mesh to get it done. Two experts are here to help you set up and experiment: david (the
540
+ engineer, how Cotal works) and sven (the guide, what to build). Reach them with cotal_dm or
541
+ cotal_anycast, grow the team with cotal_spawn, and if Cotal misbehaves send a report with
542
+ cotal_feedback. Docs: https://github.com/Cotal-AI/Cotal
543
+ `,
544
+ };
64
545
  //# sourceMappingURL=setup.js.map