@cotal-ai/cli 0.9.1 → 0.10.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 (130) hide show
  1. package/dist/command.d.ts +12 -3
  2. package/dist/command.d.ts.map +1 -1
  3. package/dist/command.js +58 -11
  4. package/dist/command.js.map +1 -1
  5. package/dist/commands/agents.d.ts +64 -0
  6. package/dist/commands/agents.d.ts.map +1 -0
  7. package/dist/commands/agents.js +83 -0
  8. package/dist/commands/agents.js.map +1 -0
  9. package/dist/commands/channels.d.ts +2 -1
  10. package/dist/commands/channels.d.ts.map +1 -1
  11. package/dist/commands/channels.js +3 -16
  12. package/dist/commands/channels.js.map +1 -1
  13. package/dist/commands/completion.d.ts +6 -5
  14. package/dist/commands/completion.d.ts.map +1 -1
  15. package/dist/commands/completion.js +37 -14
  16. package/dist/commands/completion.js.map +1 -1
  17. package/dist/commands/console.d.ts +2 -1
  18. package/dist/commands/console.d.ts.map +1 -1
  19. package/dist/commands/console.js +2 -12
  20. package/dist/commands/console.js.map +1 -1
  21. package/dist/commands/down.d.ts +3 -2
  22. package/dist/commands/down.d.ts.map +1 -1
  23. package/dist/commands/down.js +4 -12
  24. package/dist/commands/down.js.map +1 -1
  25. package/dist/commands/ext.d.ts +18 -0
  26. package/dist/commands/ext.d.ts.map +1 -0
  27. package/dist/commands/ext.js +220 -0
  28. package/dist/commands/ext.js.map +1 -0
  29. package/dist/commands/feedback.d.ts +2 -2
  30. package/dist/commands/feedback.d.ts.map +1 -1
  31. package/dist/commands/feedback.js +4 -283
  32. package/dist/commands/feedback.js.map +1 -1
  33. package/dist/commands/history.d.ts +2 -1
  34. package/dist/commands/history.d.ts.map +1 -1
  35. package/dist/commands/history.js +3 -13
  36. package/dist/commands/history.js.map +1 -1
  37. package/dist/commands/join.d.ts +2 -1
  38. package/dist/commands/join.d.ts.map +1 -1
  39. package/dist/commands/join.js +2 -18
  40. package/dist/commands/join.js.map +1 -1
  41. package/dist/commands/meshes.d.ts +2 -1
  42. package/dist/commands/meshes.d.ts.map +1 -1
  43. package/dist/commands/meshes.js +1 -1
  44. package/dist/commands/meshes.js.map +1 -1
  45. package/dist/commands/mint.d.ts +2 -1
  46. package/dist/commands/mint.d.ts.map +1 -1
  47. package/dist/commands/mint.js +3 -14
  48. package/dist/commands/mint.js.map +1 -1
  49. package/dist/commands/personas.d.ts +2 -2
  50. package/dist/commands/personas.d.ts.map +1 -1
  51. package/dist/commands/personas.js +23 -20
  52. package/dist/commands/personas.js.map +1 -1
  53. package/dist/commands/send.d.ts +2 -2
  54. package/dist/commands/send.d.ts.map +1 -1
  55. package/dist/commands/send.js +21 -24
  56. package/dist/commands/send.js.map +1 -1
  57. package/dist/commands/setup.d.ts +30 -8
  58. package/dist/commands/setup.d.ts.map +1 -1
  59. package/dist/commands/setup.js +151 -316
  60. package/dist/commands/setup.js.map +1 -1
  61. package/dist/commands/spawn.d.ts +117 -2
  62. package/dist/commands/spawn.d.ts.map +1 -1
  63. package/dist/commands/spawn.js +137 -48
  64. package/dist/commands/spawn.js.map +1 -1
  65. package/dist/commands/topology.d.ts +2 -1
  66. package/dist/commands/topology.d.ts.map +1 -1
  67. package/dist/commands/topology.js +4 -8
  68. package/dist/commands/topology.js.map +1 -1
  69. package/dist/commands/up.d.ts +9 -4
  70. package/dist/commands/up.d.ts.map +1 -1
  71. package/dist/commands/up.js +96 -56
  72. package/dist/commands/up.js.map +1 -1
  73. package/dist/commands/use.d.ts +2 -2
  74. package/dist/commands/use.d.ts.map +1 -1
  75. package/dist/commands/use.js +2 -2
  76. package/dist/commands/use.js.map +1 -1
  77. package/dist/ext-loader.d.ts +13 -0
  78. package/dist/ext-loader.d.ts.map +1 -0
  79. package/dist/ext-loader.js +121 -0
  80. package/dist/ext-loader.js.map +1 -0
  81. package/dist/index.d.ts +3 -0
  82. package/dist/index.d.ts.map +1 -1
  83. package/dist/index.js +203 -70
  84. package/dist/index.js.map +1 -1
  85. package/dist/lib/attach-client.d.ts +20 -0
  86. package/dist/lib/attach-client.d.ts.map +1 -0
  87. package/dist/lib/attach-client.js +218 -0
  88. package/dist/lib/attach-client.js.map +1 -0
  89. package/dist/lib/completion.d.ts +8 -0
  90. package/dist/lib/completion.d.ts.map +1 -0
  91. package/dist/lib/completion.js +66 -0
  92. package/dist/lib/completion.js.map +1 -0
  93. package/dist/lib/connect.d.ts +1 -73
  94. package/dist/lib/connect.d.ts.map +1 -1
  95. package/dist/lib/connect.js +4 -90
  96. package/dist/lib/connect.js.map +1 -1
  97. package/dist/lib/control.d.ts +34 -0
  98. package/dist/lib/control.d.ts.map +1 -0
  99. package/dist/lib/control.js +65 -0
  100. package/dist/lib/control.js.map +1 -0
  101. package/dist/lib/delivery-proc.d.ts +2 -0
  102. package/dist/lib/delivery-proc.d.ts.map +1 -1
  103. package/dist/lib/manager-proc.d.ts +5 -13
  104. package/dist/lib/manager-proc.d.ts.map +1 -1
  105. package/dist/lib/manager-proc.js +3 -42
  106. package/dist/lib/manager-proc.js.map +1 -1
  107. package/dist/lib/manifest/live.d.ts.map +1 -1
  108. package/dist/lib/manifest/live.js +4 -1
  109. package/dist/lib/manifest/live.js.map +1 -1
  110. package/dist/lib/status.d.ts +8 -4
  111. package/dist/lib/status.d.ts.map +1 -1
  112. package/dist/lib/status.js +21 -5
  113. package/dist/lib/status.js.map +1 -1
  114. package/dist/ui.d.ts +1 -12
  115. package/dist/ui.d.ts.map +1 -1
  116. package/dist/ui.js +4 -12
  117. package/dist/ui.js.map +1 -1
  118. package/package.json +8 -6
  119. package/dist/commands/demo.d.ts +0 -2
  120. package/dist/commands/demo.d.ts.map +0 -1
  121. package/dist/commands/demo.js +0 -163
  122. package/dist/commands/demo.js.map +0 -1
  123. package/dist/commands/web.d.ts +0 -32
  124. package/dist/commands/web.d.ts.map +0 -1
  125. package/dist/commands/web.js +0 -307
  126. package/dist/commands/web.js.map +0 -1
  127. package/dist/web/app.js +0 -823
  128. package/dist/web/graph.html +0 -207
  129. package/dist/web/graph.js +0 -528
  130. package/dist/web/index.html +0 -350
@@ -1,69 +1,59 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
- import { parseArgs } from "node:util";
5
4
  import * as p from "@clack/prompts";
6
- import { DEFAULT_SERVER, isReachable, registry, } from "@cotal-ai/core";
7
- import { authDir, homeCotalDir, loadSpaceAuth } from "@cotal-ai/workspace";
5
+ import { registry } from "@cotal-ai/core";
6
+ import { homeCotalDir, loadExtensionsManifest, provenance } from "@cotal-ai/workspace";
8
7
  import { brand, brandBold, dim, ok, note, splash } from "../lib/theme.js";
9
- import { LivePane } from "../lib/live-window.js";
10
8
  import { runSteps } from "../lib/steps.js";
11
9
  import { abortIfCancel } from "../lib/cancel.js";
12
10
  import { openSetupLog } from "../lib/setup-log.js";
13
11
  import { resolveNatsServer } from "../lib/nats-bin.js";
14
12
  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, tmuxManagerRunning, managerUp, pgrepMatches, stopManager } from "../lib/manager-proc.js";
19
- import { ensureControlPlane } from "../lib/delivery-proc.js";
13
+ import { machineStatus, meshStatus, onPath, webUp, WEB_URL } from "../lib/status.js";
14
+ import { managerUp } from "../lib/manager-proc.js";
20
15
  import { cotalOnPath, displayCmd, isNpx, selfArgv } from "../lib/self-exec.js";
21
- import { cotalPath, cotalRoot } from "../lib/paths.js";
22
- import { spawn } from "./spawn.js";
23
- const ONBOARD_VERSION = "1";
24
- /** The teammates the cmux/background demo pre-spawns (manager-owned, so they're despawnable). One
25
- * source of truth for both the `--spawn` list and the `cotal-<n>` tabs we clean up on restart. */
26
- const DEMO_TEAM = ["david", "sven"];
16
+ import { cotalPath } from "../lib/paths.js";
17
+ const ONBOARD_VERSION = "2";
27
18
  const README_URL = "https://github.com/Cotal-AI/Cotal/blob/main/README.md";
28
19
  const CC_DOCS_URL = "https://github.com/Cotal-AI/Cotal/blob/main/docs/claude-code-integration.md";
29
20
  const NATS_RELEASES_URL = "https://github.com/nats-io/nats-server/releases";
30
- /** `cotal setup`: guided setup. First run (no `~/.cotal/onboarded.json`) gets the full
31
- * narrated flow; later runs get a compact ensure+status. `--full` forces the full flow.
32
- * Each failed step offers an interactive Claude handoff (COTAL_SKIP_ASSIST=1 disables). */
33
- export async function setup(argv) {
34
- const { values } = parseArgs({
35
- args: argv,
36
- allowPositionals: true,
37
- options: {
38
- full: { type: "boolean" },
39
- yes: { type: "boolean", short: "y" },
40
- auth: { type: "boolean" }, // (now the DEFAULT; kept for back-compat / explicitness)
41
- open: { type: "boolean" }, // opt OUT of auth a frictionless loopback-only open mesh (no JWT/ACLs, no durable backstop)
42
- },
43
- });
44
- // `--yes` (agents/CI) always runs the full flow non-interactively. The mesh is AUTHED by default
45
- // (JWT/ACLs the trust-first default, and what the server-side delivery daemon needs to run); `--open`
46
- // opts out to a frictionless loopback-only open mesh with no auth (and so no durable backstop).
21
+ /** `cotal setup`'s grammar — configure-only knobs. The old `--auth`/`--open` flags configured the
22
+ * MESH MODE at setup-launch time; setup no longer launches anything, so both are gone here
23
+ * mesh mode is `cotal up [--open]`'s concern (where `--open` already lived; `--auth` simply died
24
+ * with the launch behavior). An unknown-option error rejects them, nothing silently. */
25
+ export const setupFlags = [
26
+ { name: "full", type: "boolean", description: "redo the full guided flow (implies --demo)" },
27
+ { name: "demo", type: "boolean", description: "also seed the guided expert team (david, sven, me)" },
28
+ { name: "yes", type: "boolean", short: "y", description: "non-interactive accept-all (agents/CI)" },
29
+ ];
30
+ /**
31
+ * `cotal setup`: guided setup CONFIGURE-ONLY, state-independent. It checks prerequisites,
32
+ * installs the Claude Code plugin, and seeds persona files; it NEVER launches anything (no mesh,
33
+ * no web, no manager, no delivery daemon, no cmux/tmux session — launching is `cotal up` /
34
+ * `cotal web` / `cotal supervise`). Every file it writes is announced (`→ wrote …`). First run
35
+ * (no onboarded stamp) gets the full narrated flow; later runs get a status card. `--full`
36
+ * forces the full flow. By default it seeds ONE agent (the `default` persona a bare `cotal spawn`
37
+ * launches); the guided expert team (david/sven/me) is opt-in via `--demo` (and `--full`). Each
38
+ * failed step offers an interactive Claude handoff (COTAL_SKIP_ASSIST=1 disables).
39
+ */
40
+ export async function setup(args) {
41
+ const values = args.values;
42
+ const demo = Boolean(values.demo) || Boolean(values.full); // --full is the whole guided flow ⇒ team
47
43
  if (!isOnboarded() || values.full || values.yes)
48
- await runFirstRun(Boolean(values.yes), Boolean(values.open));
44
+ await runFirstRun(Boolean(values.yes), demo);
49
45
  else
50
- await runEnsure();
51
- }
52
- /** `cotal go` — open or resume your session. A friendlier-named alias of `cotal setup`: the first
53
- * run installs (full guided flow), later runs fast-forward to the ensure path and reopen your
54
- * cmux session. `cotal setup` stays the explicit install/update name. */
55
- export async function go(argv) {
56
- return setup(argv);
46
+ await runEnsure(demo);
57
47
  }
58
- /** The full, narrated first-run experience. `yes` = non-interactive accept-all; `open` = run the mesh
59
- * WITHOUT auth (the `--open` opt-out; auth is the default). Auth mode also brings up the server-side
60
- * delivery daemon (durable backstop); open mode is live-only. */
61
- async function runFirstRun(yes, open) {
48
+ /** The full, narrated first-run experience. `yes` = non-interactive accept-all; `demo` also seeds
49
+ * the guided expert team. Configure-only: prerequisites are CHECKED (never started); the finale
50
+ * tells the user what to run. */
51
+ async function runFirstRun(yes, demo) {
62
52
  splash();
63
53
  p.intro(brandBold("Welcome to Cotal"));
64
54
  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");
65
55
  const log = openSetupLog(process.cwd());
66
- // Prerequisites + the local web (NATS). These never prompt.
56
+ // Prerequisites CHECKS only, no side effects, no prompts.
67
57
  const core = [
68
58
  {
69
59
  name: "node-version",
@@ -80,47 +70,16 @@ async function runFirstRun(yes, open) {
80
70
  {
81
71
  name: "nats-binary",
82
72
  title: "Locate the NATS server",
83
- explain: "Cotal runs on NATS + JetStream, the wire your agents speak over.",
73
+ explain: "Cotal runs on NATS + JetStream, the wire your agents speak over. (Located only — `cotal up` starts it.)",
84
74
  context: [NATS_RELEASES_URL, README_URL],
85
75
  async run() {
86
76
  const r = await resolveNatsServer();
87
77
  return r.source === "path" ? "nats-server from PATH" : "bundled binary";
88
78
  },
89
79
  },
90
- {
91
- name: "start-mesh",
92
- title: "Start the web for agents",
93
- explain: "A local NATS + JetStream server you own; the web your agents join, in the background.",
94
- live: true,
95
- context: [cotalPath("nats.log"), cotalPath("auth/server.conf"), README_URL],
96
- async run() {
97
- if (await isReachable(DEFAULT_SERVER))
98
- return `already running at ${DEFAULT_SERVER}`;
99
- const pane = new LivePane();
100
- pane.start("Booting nats-server");
101
- try {
102
- const { server } = await startMeshDetached({ onLine: (l) => pane.push(l), open });
103
- return `running at ${server} (stop with: ${displayCmd()} down)`;
104
- }
105
- finally {
106
- pane.clear();
107
- }
108
- },
109
- },
110
80
  ];
111
81
  if (!(await runSteps(core, log, { yes })))
112
82
  return abort();
113
- // The web dashboard, in the background, so it's just there (best-effort; never blocks setup).
114
- try {
115
- const web = await ensureWeb({ space: resolveSpace(process.cwd()), server: DEFAULT_SERVER });
116
- if (web.running) {
117
- p.log.success(`Web dashboard at ${web.url} (stop with: ${displayCmd()} down)`);
118
- log.line(`web: ${web.url}`);
119
- }
120
- }
121
- catch {
122
- /* non-fatal: the card still shows how to start it */
123
- }
124
83
  // Connectors: which agents should be able to join. Only Claude needs an install
125
84
  // (its wake channel binds to an installed plugin); OpenCode auto-wires at spawn.
126
85
  const found = { claude: onPath("claude"), opencode: onPath("opencode") };
@@ -137,45 +96,51 @@ async function runFirstRun(yes, open) {
137
96
  log.line(`connector ${name}: ready (no install)`);
138
97
  }
139
98
  }
140
- // Two experts plus your own driving session, by default. These are setup-managed: refreshed when
141
- // DEMO_AGENTS changes (so persona edits actually land), but a file you've taken ownership of is
142
- // backed up first, never silently lost — see writeDemoAgent.
143
- mkdirSync(cotalPath("agents"), { recursive: true });
144
- for (const [name, body] of Object.entries(DEMO_AGENTS)) {
145
- writeDemoAgent(cotalPath("agents", `${name}.md`), body);
146
- }
147
- seedDefaultAgent(); // the generic persona `cotal spawn` (no name) launches
148
- p.log.success("Added david (the engineer), sven (the guide), and your session (me); they join when you spawn them or open the demo");
149
- log.line("demo-agents: wrote david + sven + me");
99
+ // Your agent: the generic `default` persona a bare `cotal spawn` launches one agent, yours to
100
+ // shape. This is the whole first-run default; the guided expert team is opt-in right below.
101
+ seedDefaultAgent();
102
+ p.log.success("Seeded your agent (.cotal/agents/default.md) spawn it with `cotal spawn` once your mesh is up");
103
+ log.line("default-agent: wrote default.md");
104
+ // The guided expert team (david the engineer + sven the guide + me, your session) is opt-in:
105
+ // `cotal setup --demo` (or `--full`). Keeps the default first run to one agent, not a crowd.
106
+ if (demo)
107
+ seedDemoTeam(log);
108
+ await ensureWebExtension();
150
109
  await offerGlobalInstall(yes);
151
110
  markOnboarded(ONBOARD_VERSION);
111
+ provenance.wrote("onboarded stamp", join(homeCotalDir(), "onboarded.json"));
152
112
  const cmd = displayCmd();
113
+ // The finale is the whole loop, minimal by default: start the mesh, talk to your one agent, stop.
114
+ // With --demo it names the team; without, it points at --demo (and the optional dashboard).
115
+ const driveLines = demo
116
+ ? [
117
+ `${ok("✓")} start the mesh ${dim(`${cmd} up --detach`)}`,
118
+ `${ok("✓")} drive a session ${dim(`${cmd} spawn me`)}`,
119
+ `${ok("✓")} ask the experts ${dim(`${cmd} spawn david · ${cmd} spawn sven`)}`,
120
+ `${ok("✓")} watch the mesh ${dim(`${cmd} console`)}`,
121
+ `${ok("✓")} stop everything ${dim(`${cmd} down`)}`,
122
+ ]
123
+ : [
124
+ `${ok("✓")} start the mesh ${dim(`${cmd} up --detach`)}`,
125
+ `${ok("✓")} talk to your agent ${dim(`${cmd} spawn`)}`,
126
+ `${ok("✓")} watch the mesh ${dim(`${cmd} console`)}`,
127
+ `${ok("✓")} stop everything ${dim(`${cmd} down`)}`,
128
+ ];
129
+ const tail = demo
130
+ ? [dim(`Visual dashboard: ${cmd} ext add cotal-web · then ${cmd} web`)]
131
+ : [
132
+ dim(`Want a visual dashboard? ${cmd} ext add cotal-web · then ${cmd} web`),
133
+ dim(`Want a guided team (david the engineer, sven the guide)? ${cmd} setup --demo`),
134
+ ];
153
135
  note([
154
- "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.",
136
+ "Everything is configured nothing has been started. Bring your mesh up when you're ready:",
155
137
  "",
156
- `${ok("✓")} drive a session ${dim(`${cmd} spawn me`)}`,
157
- `${ok("✓")} ask the engineer ${dim(`${cmd} spawn david`)}`,
158
- `${ok("✓")} ask the guide ${dim(`${cmd} spawn sven`)}`,
159
- `${ok("✓")} watch the mesh ${dim(`${cmd} console`)}`,
160
- `${ok("✓")} open the dashboard ${dim(WEB_URL)}`,
161
- `${ok("✓")} resume later ${dim(`${cmd} go`)}`,
162
- `${ok("✓")} stop everything ${dim(`${cmd} down`)}`,
138
+ ...driveLines,
163
139
  "",
164
- 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>".`),
140
+ ...tail,
141
+ dim(`Cotal not working? Tell your agent to send feedback (built-in cotal_feedback), or run ${cmd} feedback "<msg>".`),
165
142
  ].join("\n"), "You're set");
166
- if (!yes)
167
- await offerDemo(found.claude);
168
- else {
169
- // Agents/CI: bring up the control plane (delivery daemon, auth only → manager) so cotal_spawn /
170
- // despawn / purge work right away.
171
- try {
172
- await ensureControlPlane({ space: resolveSpace(process.cwd()), server: DEFAULT_SERVER });
173
- }
174
- catch {
175
- /* non-fatal */
176
- }
177
- }
178
- p.outro(brand(yes ? "Cotal is ready." : "Happy meshing."));
143
+ p.outro(brand(yes ? "Cotal is configured." : "Happy meshing."));
179
144
  function abort() {
180
145
  p.outro(brand(`Setup paused. Fix the step above and run \`${displayCmd()} setup\` again.`));
181
146
  process.exitCode = 1;
@@ -259,228 +224,79 @@ function claudePluginStep() {
259
224
  },
260
225
  };
261
226
  }
262
- /** Finale: a live demo a Claude the operator drives, with david and sven (manager-owned
263
- * teammates) helping. In cmux they get their own tabs; otherwise they run in the background and
264
- * the terminal is handed to the driving session. The demo spawns Claude sessions, so it needs
265
- * Claude Code. If declined / no Claude, fall back to the `cotal · ready` card. Skipped under --yes. */
266
- async function offerDemo(haveClaude) {
267
- const haveAgents = ["me", "david", "sven"].every((n) => existsSync(cotalPath("agents", `${n}.md`)));
268
- const isTTY = Boolean(process.stdin.isTTY);
269
- if (haveClaude && haveAgents && isTTY) {
270
- const cmux = inCmuxSurface();
271
- const tmux = inTmuxSurface();
272
- if (cmux) {
273
- const go = abortIfCancel(await p.confirm({
274
- message: "Open the cmux demo? A Claude you drive, with david and sven helping in cmux tabs.",
275
- initialValue: true,
276
- }));
277
- if (go) {
278
- ensureCmuxSession(cotalRoot());
279
- p.log.success("Session open: drive the 'cotal-main' pane; david and sven are on the mesh in the background.");
280
- return;
281
- }
282
- }
283
- if (tmux) {
284
- const go = abortIfCancel(await p.confirm({
285
- message: "Open the tmux demo? A Claude you drive, with david and sven helping in tmux windows.",
286
- initialValue: true,
287
- }));
288
- if (go) {
289
- ensureTmuxSession(cotalRoot());
290
- p.log.success("Session open: switch to the 'cotal-main' window; david and sven are warming up in the background.");
291
- return;
292
- }
293
- }
294
- if (!cmux && !tmux) {
295
- const go = abortIfCancel(await p.confirm({
296
- message: "Open the demo? A Claude you drive, with david and sven helping in the background.",
297
- initialValue: true,
298
- }));
299
- if (go) {
300
- // Background pty manager pre-spawns david/sven (managed, despawnable), then we hand this
301
- // terminal to the driving session. (auth → delivery daemon first, then the manager.)
302
- await ensureControlPlane({ space: resolveSpace(process.cwd()), server: DEFAULT_SERVER, spawn: [...DEMO_TEAM] });
303
- p.outro(brand("Launching your session... david and sven are warming up in the background."));
304
- await spawn(["me", "--prompt", ME_GREETING]);
305
- process.exit(0);
306
- }
307
- }
308
- }
309
- else if (isTTY && haveAgents && !haveClaude) {
310
- p.log.info(`The demo needs Claude Code. Install it (https://claude.com/claude-code), then run \`${displayCmd()} go\`.`);
311
- }
312
- // Declined, or no Claude: start the background control plane (delivery daemon, auth only → pty
313
- // manager) so cotal_spawn / despawn / purge still work, then leave them the quick-reference card.
314
- try {
315
- await ensureControlPlane({ space: resolveSpace(process.cwd()), server: DEFAULT_SERVER });
316
- }
317
- catch {
318
- /* non-fatal: the card still shows how to start it */
319
- }
227
+ /** The compact repeat-run: a one-glance status card, plus re-seeding the default persona if it's
228
+ * missing (announced). Nothing is launched the card tells you what's down and how to start it.
229
+ * `--demo` here adds the guided team to an already-configured machine (no need to re-narrate). */
230
+ async function runEnsure(demo) {
231
+ seedDefaultAgent(); // ensure `cotal spawn` (no name) always has a default to launch
232
+ if (demo)
233
+ seedDemoTeam(); // `cotal setup --demo` on a configured machine: add the team, then card
234
+ await ensureWebExtension();
320
235
  await readyCard(process.cwd());
321
236
  }
322
- /** Greeting the driving session auto-submits on start (no apostrophes it rides through
323
- * cmux's `bash -lc '…'` quoting). Teaches the capabilities by telling, not by calling tools,
324
- * so it does not depend on david/sven having joined yet when this first turn runs. */
325
- 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.";
326
- /** True when we're running inside a real cmux pane (cmux sets `CMUX_SURFACE_ID` per surface).
327
- * Opening/closing cmux tabs is only authorized from a live pane, so this — not the terminal
328
- * provider's `available()` (which only pings the app) — is the gate for opening it. */
329
- function inCmuxSurface() {
330
- return Boolean(process.env.CMUX_SURFACE_ID);
331
- }
332
- /** True when we're running inside a tmux session (tmux sets `$TMUX` to the socket path). */
333
- function inTmuxSurface() {
334
- return Boolean(process.env.TMUX);
335
- }
336
- /** (Re)open the cmux working session, idempotently. A background cmux-runtime manager pre-spawns
337
- * david/sven (so they're managed teammates you can `cotal_despawn`) into their own tabs; the
338
- * focused `cotal-main` workspace is the console + the driving session "me" (your foreground
339
- * driver). Re-running reuses whatever's already open — only missing tabs are created, so there's
340
- * never a second manager. The `me` pane presses Enter on its own cmux surface a few times to
341
- * auto-accept the one-time dev-channels prompt (the manager's cmux runtime does the same for
342
- * david/sven). */
343
- function ensureCmuxSession(cwd) {
344
- // Open/close cmux tabs by resolving the registered "cmux" terminal-layout provider, so the CLI
345
- // drives cmux without importing the extension (the composition root's import is what registers it).
346
- const term = registry.resolve("terminal", "cmux");
347
- // The cmux-tab manager becomes the control plane; drop any detached pty manager so they don't
348
- // both answer control requests.
349
- stopManager();
350
- // Describe each pane as plain argv (command + args + cwd) — the terminal provider owns all
351
- // shell quoting and the cmux layout. Invoke this CLI by its own argv (absolute node + entry),
352
- // not bare `cotal`, so the panes work whether installed via npx, `npm i -g`, or a dev clone (no
353
- // dependency on `cotal` being on PATH). The space follows the folder's auth so every pane matches
354
- // the running mesh.
355
- const cotal = selfArgv();
356
- const run = (...args) => ({ command: cotal[0], args: [...cotal.slice(1), ...args], cwd });
357
- const space = resolveSpace(cwd);
358
- // `space` reaches the panes as a discrete argv token, but keep it a bare token anyway so it can't
359
- // confuse downstream parsing.
360
- if (!/^[A-Za-z0-9_.-]+$/.test(space))
361
- throw new Error(`cotal setup: unsafe space ${JSON.stringify(space)} (allowed: letters, digits, _ . -)`);
362
- // Control plane: a cmux-runtime manager that pre-spawns david/sven into their own tabs and owns
363
- // them (so cotal_despawn / cotal_spawn work). A cmux tab persists after its process dies, so
364
- // "workspace exists" != "manager running" — gate on the live process. When none is up, drop the
365
- // dead manager + teammate tabs first, then open a fresh one; otherwise re-runs keep skipping a
366
- // never-restarted manager and david/sven never join.
367
- if (!cmuxManagerRunning(space)) {
368
- for (const label of ["cotal-manager", ...DEMO_TEAM.map((n) => `cotal-${n}`)])
369
- closeStaleTabs(term, label);
370
- term.open("cotal-manager", { panes: [run("supervise", "--runtime", "cmux", "--space", space, "--spawn", DEMO_TEAM.join(","))] }, { focus: false });
237
+ /** True when an installed extension contributes the `web` command (the dashboard moved out to
238
+ * `cotal-web` in stage 4) decides whether the ready-card says "start it" or "install it". */
239
+ function webInstalled() {
240
+ try {
241
+ return loadExtensionsManifest().extensions.some((e) => e.commands.some((cm) => cm.name === "web"));
371
242
  }
372
- // Your focused driver: console + the "me" session. Gate on the live driving session (not the
373
- // persistent tab) so a session you're driving is never disturbed; a dead/closed one gets its stale
374
- // tab dropped and reopened. The "me" pane sets `confirm` so the provider auto-clears Claude's
375
- // dev-channels prompt; the greeting rides as a plain argv token (the provider quotes it).
376
- if (!pgrepMatches(`spawn me --space ${space}`)) {
377
- closeStaleTabs(term, "cotal-main");
378
- term.open("cotal-main", {
379
- split: { direction: "vertical", ratio: 0.34 },
380
- panes: [
381
- run("console", "--space", space),
382
- { ...run("spawn", "me", "--space", space, "--prompt", ME_GREETING), confirm: true },
383
- ],
384
- }, { focus: true });
243
+ catch {
244
+ return false; // corrupt manifest the card stays honest ("not installed"); `ext` commands surface the error
385
245
  }
386
246
  }
387
- /** (Re)open the tmux working session, idempotently. Mirrors ensureCmuxSession using the "tmux"
388
- * terminal provider: a background window runs the tmux-runtime manager (pre-spawning david/sven);
389
- * the focused "cotal-main" window has the console + driving session "me". */
390
- function ensureTmuxSession(cwd) {
391
- const term = registry.resolve("terminal", "tmux");
392
- stopManager();
393
- const cotal = selfArgv();
394
- const run = (...args) => ({ command: cotal[0], args: [...cotal.slice(1), ...args], cwd });
395
- const space = resolveSpace(cwd);
396
- if (!/^[A-Za-z0-9_.-]+$/.test(space))
397
- throw new Error(`cotal setup: unsafe space ${JSON.stringify(space)} (allowed: letters, digits, _ . -)`);
398
- if (!tmuxManagerRunning(space)) {
399
- for (const label of ["cotal-manager", ...DEMO_TEAM.map((n) => `cotal-${n}`)])
400
- closeStaleTabs(term, label);
401
- term.open("cotal-manager", { panes: [run("supervise", "--runtime", "tmux", "--space", space, "--spawn", DEMO_TEAM.join(","))] }, { focus: false });
402
- }
403
- if (!pgrepMatches(`spawn me --space ${space}`)) {
404
- closeStaleTabs(term, "cotal-main");
405
- term.open("cotal-main", {
406
- split: { direction: "vertical", ratio: 0.34 },
407
- panes: [
408
- run("console", "--space", space),
409
- { ...run("spawn", "me", "--space", space, "--prompt", ME_GREETING), confirm: true },
410
- ],
411
- }, { focus: true });
247
+ /** Install the dashboard extension once, without turning setup into a launch command. This reuses the
248
+ * public `ext add` path in a child process so package install, peer linking, command verification,
249
+ * and manifest writes stay identical to an explicit `cotal ext add cotal-web`. Best-effort: setup is
250
+ * still useful on locked-down machines where npm/registry access is unavailable. */
251
+ async function ensureWebExtension() {
252
+ if (webInstalled())
253
+ return;
254
+ const spec = defaultWebExtensionSpec();
255
+ const s = p.spinner();
256
+ s.start("Installing the web dashboard extension");
257
+ const [bin, ...argv] = selfArgv();
258
+ const r = spawnSync(bin, [...argv, "ext", "add", spec], { encoding: "utf8" });
259
+ if (r.status === 0) {
260
+ s.stop("Installed the web dashboard extension");
261
+ if (r.stderr)
262
+ process.stderr.write(r.stderr);
263
+ if (r.stdout)
264
+ process.stdout.write(r.stdout);
265
+ return;
412
266
  }
267
+ s.stop("Couldn't install the web dashboard extension");
268
+ const tail = `${r.stdout ?? ""}${r.stderr ?? ""}`.trim().split("\n").slice(-6).join("\n");
269
+ p.log.warn(`${tail ? `${tail}\n\n` : ""}Install it later with ${dim(`${displayCmd()} ext add cotal-web`)}.`);
413
270
  }
414
- /** Close any lingering cmux tabs labelled `name` (dead tabs persist in the tab list after their
415
- * process exits) so a freshly opened one is the only instance. */
416
- function closeStaleTabs(term, name) {
417
- for (const ref of term.refs(name)) {
418
- try {
419
- term.close(ref);
420
- }
421
- catch {
422
- /* already gone */
423
- }
424
- }
271
+ function defaultWebExtensionSpec() {
272
+ const local = join(import.meta.dirname, "..", "..", "..", "web");
273
+ return existsSync(join(local, "package.json")) ? local : "cotal-web";
425
274
  }
426
- /** The compact repeat-run: quietly ensure the mesh + web are up here, then a one-glance card. */
427
- async function runEnsure() {
428
- seedDefaultAgent(); // ensure `cotal spawn` (no name) always has a default to launch
429
- let mesh = await meshStatus(process.cwd());
430
- if (!mesh.reachable) {
431
- const s = p.spinner();
432
- s.start("Starting the web for agents");
433
- try {
434
- // Match how the mesh last ran: open when this folder has no space auth (the frictionless
435
- // default), authed when it does — so restarting a downed open mesh doesn't come back JWT-authed.
436
- const authed = Boolean(loadSpaceAuth(authDir(cotalRoot())));
437
- await up(authed ? ["--detach"] : ["--detach", "--open"]);
438
- s.stop("Web for agents started");
439
- }
440
- catch (e) {
441
- s.stop(`Couldn't start it: ${e.message}`);
442
- process.exitCode = 1;
443
- return;
444
- }
445
- mesh = await meshStatus(process.cwd());
446
- }
447
- await ensureWeb({ space: mesh.space, server: mesh.server }).catch(() => { });
448
- // Inside cmux, re-running setup reopens your session (idempotent: reuse the live manager +
449
- // david/sven, open only missing tabs). Otherwise bring up the background pty control plane.
450
- try {
451
- if (inCmuxSurface())
452
- ensureCmuxSession(cotalRoot());
453
- else
454
- await ensureControlPlane({ space: mesh.space, server: mesh.server });
455
- }
456
- catch {
457
- /* non-fatal */
458
- }
459
- await readyCard(process.cwd());
460
- }
461
- /** The `cotal · ready` one-glance card: machine + mesh + web + manager status, plus the key
462
- * commands. Shared by the repeat-run ensure and the first-run no-demo finale. */
275
+ /** The `cotal · status` one-glance card: machine + mesh + web + manager status (read-only
276
+ * probes — displaying state is not depending on it), plus the key commands. */
463
277
  async function readyCard(cwd) {
464
278
  const mesh = await meshStatus(cwd);
465
279
  const m = await machineStatus();
466
280
  const web = await webUp();
467
- // The control plane is either the detached pty manager (pid file) or a live cmux-tab manager
468
- // (its tab lingers after it exits, so check the process, not the workspace list).
469
- const mgr = managerUp() || (inCmuxSurface() && cmuxManagerRunning(mesh.space));
281
+ const mgr = managerUp();
470
282
  const cmd = displayCmd();
283
+ const hasDemo = existsSync(cotalPath("agents", "david.md")); // the guided team is present ⇒ richer hint
471
284
  const line = (on, text) => `${on ? ok("✓") : dim("○")} ${text}`;
472
285
  note([
473
286
  line(m.nats !== "missing", `NATS ${dim(m.nats === "missing" ? "missing" : m.nats)}`),
474
287
  line(m.claudePlugin, `plugin ${dim(m.claudePlugin ? "installed" : "not installed")}`),
475
- line(mesh.reachable, `mesh ${dim(`${mesh.server} · space ${mesh.space}`)}`),
476
- line(web, `web ${dim(WEB_URL)}`),
477
- line(mgr, `manager ${dim(mgr ? "running" : "not running")}`),
288
+ line(mesh.reachable, `mesh ${dim(mesh.reachable ? `${mesh.server} · space ${mesh.space}` : `down — start: ${cmd} up --detach`)}`),
289
+ line(web, `web ${dim(web ? WEB_URL : webInstalled() ? `down — start: ${cmd} web` : `not installed — retry: ${cmd} setup`)}`),
290
+ line(mgr, `manager ${dim(mgr ? "running" : `not running — start: ${cmd} up, or: ${cmd} supervise`)}`),
478
291
  "",
479
- `resume: ${dim(`${cmd} go`)} ${dim("(reopen this session anytime)")}`,
480
- `watch it: ${dim(`${cmd} console`)} ${dim("(live TUI in this terminal)")}`,
481
- `drive it: ${dim(`${cmd} spawn me`)} ${dim("(or david / sven)")}`,
482
- `more: ${dim(`${cmd} web · ${cmd} down · ${cmd} feedback "<msg>" · ${cmd} --help`)}`,
483
- ].join("\n"), brandBold("cotal · ready"));
292
+ `start the mesh: ${dim(`${cmd} up --detach`)}`,
293
+ // Match the hint to what's actually on disk: the guided team (with --demo) vs the one default agent.
294
+ hasDemo
295
+ ? `drive it: ${dim(`${cmd} spawn me`)} ${dim("(or david / sven)")}`
296
+ : `drive it: ${dim(`${cmd} spawn`)} ${dim("(talk to your agent · guided team: " + cmd + " setup --demo)")}`,
297
+ `watch it: ${dim(`${cmd} console`)} ${dim("(live TUI in this terminal)")}`,
298
+ `more: ${dim(`${cmd} web · ${cmd} down · ${cmd} feedback "<msg>" · ${cmd} --help`)}`,
299
+ ].join("\n"), brandBold("cotal · status"));
484
300
  }
485
301
  /** Materialize a stable plugin marketplace under ~/.cotal/claude-plugin (surviving
486
302
  * npx cache eviction) and install the plugin from it. The marketplace name must stay
@@ -506,6 +322,7 @@ function installClaudePlugin() {
506
322
  owner: { name: "Cotal" },
507
323
  plugins: [{ name: "cotal", source: "./cotal" }],
508
324
  }, null, 2));
325
+ provenance.wrote("plugin marketplace", marketDir);
509
326
  // `add` fails when the marketplace is already registered; refresh it instead.
510
327
  const add = claude("plugin", "marketplace", "add", marketDir);
511
328
  if (add.status !== 0) {
@@ -530,20 +347,24 @@ function claude(...args) {
530
347
  const MANAGED_MARKER = "# managed by cotal-setup";
531
348
  /** Write a setup-managed demo persona, refreshing it when its DEMO_AGENTS body changes — but never
532
349
  * silently clobber a file the user has taken ownership of (one without the marker): back it up to
533
- * `<name>.md.bak` first. Missing or marker-carrying files are written in place. */
350
+ * `<name>.md.bak` first. Missing or marker-carrying files are written in place; every write is
351
+ * announced. */
534
352
  function writeDemoAgent(path, body) {
535
353
  if (existsSync(path)) {
536
354
  const cur = readFileSync(path, "utf8");
537
355
  if (cur === body)
538
356
  return; // already current
539
- if (!cur.includes(MANAGED_MARKER))
357
+ if (!cur.includes(MANAGED_MARKER)) {
540
358
  writeFileSync(`${path}.bak`, cur); // preserve a user/pre-marker edit
359
+ provenance.wrote("backup of your edited persona", `${path}.bak`);
360
+ }
541
361
  }
542
362
  writeFileSync(path, body);
363
+ provenance.wrote("persona", path);
543
364
  }
544
365
  /** The default persona `cotal spawn` (no name) launches: a generic mesh agent, seeded once and
545
366
  * then the user's to shape. Unlike the demo team it's never refreshed (seed-if-absent), so any
546
- * edits stand; deleting it just means the next `cotal setup`/`go` writes a fresh copy. */
367
+ * edits stand; deleting it just means the next `cotal setup` writes a fresh copy. */
547
368
  const DEFAULT_AGENT = `---
548
369
  name: default_agent
549
370
  role: default
@@ -566,6 +387,20 @@ function seedDefaultAgent() {
566
387
  return;
567
388
  mkdirSync(cotalPath("agents"), { recursive: true });
568
389
  writeFileSync(path, DEFAULT_AGENT);
390
+ provenance.wrote("default persona", path);
391
+ }
392
+ /** Seed the guided expert team — david (the engineer), sven (the guide), me (your session) — the
393
+ * opt-in richer first experience (`cotal setup --demo` / `--full`). These are setup-managed, unlike
394
+ * the seed-once default: refreshed when a DEMO_AGENTS body changes so persona edits actually land,
395
+ * but a file you've taken ownership of is backed up first, never silently lost (see writeDemoAgent).
396
+ * Every write is announced; `log` (present only in the narrated first run) also records it. */
397
+ function seedDemoTeam(log) {
398
+ mkdirSync(cotalPath("agents"), { recursive: true });
399
+ for (const [name, body] of Object.entries(DEMO_AGENTS)) {
400
+ writeDemoAgent(cotalPath("agents", `${name}.md`), body);
401
+ }
402
+ p.log.success("Added the guided team — david (the engineer), sven (the guide), and your session (me); spawn them when your mesh is up");
403
+ log?.line("demo-agents: wrote david + sven + me");
569
404
  }
570
405
  const DEMO_AGENTS = {
571
406
  david: `---