@cotal-ai/cli 0.9.0 → 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.
- package/dist/command.d.ts +12 -3
- package/dist/command.d.ts.map +1 -1
- package/dist/command.js +58 -11
- package/dist/command.js.map +1 -1
- package/dist/commands/agents.d.ts +64 -0
- package/dist/commands/agents.d.ts.map +1 -0
- package/dist/commands/agents.js +83 -0
- package/dist/commands/agents.js.map +1 -0
- package/dist/commands/channels.d.ts +2 -1
- package/dist/commands/channels.d.ts.map +1 -1
- package/dist/commands/channels.js +3 -16
- package/dist/commands/channels.js.map +1 -1
- package/dist/commands/completion.d.ts +6 -5
- package/dist/commands/completion.d.ts.map +1 -1
- package/dist/commands/completion.js +37 -14
- package/dist/commands/completion.js.map +1 -1
- package/dist/commands/console.d.ts +2 -1
- package/dist/commands/console.d.ts.map +1 -1
- package/dist/commands/console.js +2 -12
- package/dist/commands/console.js.map +1 -1
- package/dist/commands/down.d.ts +3 -2
- package/dist/commands/down.d.ts.map +1 -1
- package/dist/commands/down.js +4 -12
- package/dist/commands/down.js.map +1 -1
- package/dist/commands/ext.d.ts +18 -0
- package/dist/commands/ext.d.ts.map +1 -0
- package/dist/commands/ext.js +220 -0
- package/dist/commands/ext.js.map +1 -0
- package/dist/commands/feedback.d.ts +2 -2
- package/dist/commands/feedback.d.ts.map +1 -1
- package/dist/commands/feedback.js +4 -283
- package/dist/commands/feedback.js.map +1 -1
- package/dist/commands/history.d.ts +2 -1
- package/dist/commands/history.d.ts.map +1 -1
- package/dist/commands/history.js +3 -13
- package/dist/commands/history.js.map +1 -1
- package/dist/commands/join.d.ts +2 -1
- package/dist/commands/join.d.ts.map +1 -1
- package/dist/commands/join.js +2 -18
- package/dist/commands/join.js.map +1 -1
- package/dist/commands/meshes.d.ts +2 -1
- package/dist/commands/meshes.d.ts.map +1 -1
- package/dist/commands/meshes.js +1 -1
- package/dist/commands/meshes.js.map +1 -1
- package/dist/commands/mint.d.ts +2 -1
- package/dist/commands/mint.d.ts.map +1 -1
- package/dist/commands/mint.js +3 -14
- package/dist/commands/mint.js.map +1 -1
- package/dist/commands/personas.d.ts +2 -2
- package/dist/commands/personas.d.ts.map +1 -1
- package/dist/commands/personas.js +23 -20
- package/dist/commands/personas.js.map +1 -1
- package/dist/commands/send.d.ts +2 -2
- package/dist/commands/send.d.ts.map +1 -1
- package/dist/commands/send.js +21 -24
- package/dist/commands/send.js.map +1 -1
- package/dist/commands/setup.d.ts +30 -8
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +151 -316
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/spawn.d.ts +117 -2
- package/dist/commands/spawn.d.ts.map +1 -1
- package/dist/commands/spawn.js +137 -48
- package/dist/commands/spawn.js.map +1 -1
- package/dist/commands/topology.d.ts +2 -1
- package/dist/commands/topology.d.ts.map +1 -1
- package/dist/commands/topology.js +4 -8
- package/dist/commands/topology.js.map +1 -1
- package/dist/commands/up.d.ts +9 -4
- package/dist/commands/up.d.ts.map +1 -1
- package/dist/commands/up.js +96 -56
- package/dist/commands/up.js.map +1 -1
- package/dist/commands/use.d.ts +2 -2
- package/dist/commands/use.d.ts.map +1 -1
- package/dist/commands/use.js +2 -2
- package/dist/commands/use.js.map +1 -1
- package/dist/ext-loader.d.ts +13 -0
- package/dist/ext-loader.d.ts.map +1 -0
- package/dist/ext-loader.js +121 -0
- package/dist/ext-loader.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +203 -70
- package/dist/index.js.map +1 -1
- package/dist/lib/attach-client.d.ts +20 -0
- package/dist/lib/attach-client.d.ts.map +1 -0
- package/dist/lib/attach-client.js +218 -0
- package/dist/lib/attach-client.js.map +1 -0
- package/dist/lib/completion.d.ts +8 -0
- package/dist/lib/completion.d.ts.map +1 -0
- package/dist/lib/completion.js +66 -0
- package/dist/lib/completion.js.map +1 -0
- package/dist/lib/connect.d.ts +1 -73
- package/dist/lib/connect.d.ts.map +1 -1
- package/dist/lib/connect.js +4 -90
- package/dist/lib/connect.js.map +1 -1
- package/dist/lib/control.d.ts +34 -0
- package/dist/lib/control.d.ts.map +1 -0
- package/dist/lib/control.js +65 -0
- package/dist/lib/control.js.map +1 -0
- package/dist/lib/delivery-proc.d.ts +2 -0
- package/dist/lib/delivery-proc.d.ts.map +1 -1
- package/dist/lib/manager-proc.d.ts +5 -13
- package/dist/lib/manager-proc.d.ts.map +1 -1
- package/dist/lib/manager-proc.js +3 -42
- package/dist/lib/manager-proc.js.map +1 -1
- package/dist/lib/manifest/live.d.ts.map +1 -1
- package/dist/lib/manifest/live.js +4 -1
- package/dist/lib/manifest/live.js.map +1 -1
- package/dist/lib/status.d.ts +8 -4
- package/dist/lib/status.d.ts.map +1 -1
- package/dist/lib/status.js +21 -5
- package/dist/lib/status.js.map +1 -1
- package/dist/ui.d.ts +1 -12
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +4 -12
- package/dist/ui.js.map +1 -1
- package/package.json +8 -6
- package/dist/commands/demo.d.ts +0 -2
- package/dist/commands/demo.d.ts.map +0 -1
- package/dist/commands/demo.js +0 -163
- package/dist/commands/demo.js.map +0 -1
- package/dist/commands/web.d.ts +0 -32
- package/dist/commands/web.d.ts.map +0 -1
- package/dist/commands/web.js +0 -307
- package/dist/commands/web.js.map +0 -1
- package/dist/web/app.js +0 -823
- package/dist/web/graph.html +0 -207
- package/dist/web/graph.js +0 -512
- package/dist/web/index.html +0 -350
package/dist/commands/setup.js
CHANGED
|
@@ -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 {
|
|
7
|
-
import {
|
|
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,
|
|
16
|
-
import {
|
|
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
|
|
22
|
-
|
|
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
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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),
|
|
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; `
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
async function runFirstRun(yes,
|
|
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
|
|
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
|
-
//
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
"
|
|
136
|
+
"Everything is configured — nothing has been started. Bring your mesh up when you're ready:",
|
|
155
137
|
"",
|
|
156
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
/**
|
|
323
|
-
*
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
373
|
-
|
|
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
|
-
/**
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
const
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
|
427
|
-
|
|
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
|
-
|
|
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" :
|
|
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
|
-
`
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
|
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: `---
|