ac-framework 1.9.8 → 1.9.9
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/README.md +13 -10
- package/package.json +1 -1
- package/src/agents/config-store.js +3 -0
- package/src/agents/orchestrator.js +28 -2
- package/src/agents/runtime.js +75 -3
- package/src/agents/state-store.js +4 -0
- package/src/commands/agents.js +298 -56
- package/src/commands/init.js +9 -5
- package/src/mcp/collab-server.js +74 -25
- package/src/services/dependency-installer.js +72 -4
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ It combines three layers in one CLI:
|
|
|
6
6
|
- template-based assistant configurations for multiple IDEs and AI CLIs
|
|
7
7
|
- a built-in spec-driven workflow inspired by OpenSpec / spec-driven development
|
|
8
8
|
- a persistent local memory system with MCP integration for supported assistants
|
|
9
|
-
- an optional collaborative multi-agent runtime powered by OpenCode + tmux
|
|
9
|
+
- an optional collaborative multi-agent runtime powered by OpenCode + zellij (tmux fallback)
|
|
10
10
|
|
|
11
11
|
## Why AC Framework
|
|
12
12
|
|
|
@@ -26,7 +26,7 @@ The goal is simple: help AI write better code, with more context, more disciplin
|
|
|
26
26
|
- `Spec-driven workflow` - use `acfm spec` to initialize, create, validate, continue, and archive structured changes.
|
|
27
27
|
- `Persistent memory` - store architectural decisions, bugfixes, refactors, conventions, and context in a local SQLite memory database.
|
|
28
28
|
- `MCP integration` - connect the memory system to supported assistants through MCP so they can recall and save context directly.
|
|
29
|
-
- `Collaborative agents (optional)` - enable SynapseGrid to run planner/critic/coder/reviewer in coordinated
|
|
29
|
+
- `Collaborative agents (optional)` - enable SynapseGrid to run planner/critic/coder/reviewer in coordinated zellij panes (tmux fallback) with shared context.
|
|
30
30
|
- `GitHub sync` - use `acfm init --latest` or `acfm update` to pull the latest framework content from GitHub.
|
|
31
31
|
- `Legacy compatibility` - `.acfm/` is the new default, but existing `openspec/` directories still work.
|
|
32
32
|
|
|
@@ -57,7 +57,7 @@ The CLI now guides you through:
|
|
|
57
57
|
2. choose one or more assistants from that template
|
|
58
58
|
3. install the matching root instruction files like `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, or `copilot-instructions.md`
|
|
59
59
|
4. optionally initialize NexusVault persistent memory and MCP connections
|
|
60
|
-
5. optionally enable SynapseGrid collaborative agents (auto-installs OpenCode + tmux)
|
|
60
|
+
5. optionally enable SynapseGrid collaborative agents (auto-installs OpenCode + zellij/tmux)
|
|
61
61
|
|
|
62
62
|
If enabled, `acfm init` also auto-installs the optional SynapseGrid MCP server into detected assistants.
|
|
63
63
|
|
|
@@ -130,7 +130,7 @@ Some assistants include bundled companions automatically:
|
|
|
130
130
|
|
|
131
131
|
### Collaborative Agents (Optional)
|
|
132
132
|
|
|
133
|
-
SynapseGrid is an optional collaborative runtime that coordinates 4 OpenCode-backed roles in tmux
|
|
133
|
+
SynapseGrid is an optional collaborative runtime that coordinates 4 OpenCode-backed roles in multiplexer panes (zellij preferred, tmux fallback):
|
|
134
134
|
- planner
|
|
135
135
|
- critic
|
|
136
136
|
- coder
|
|
@@ -140,15 +140,18 @@ Each role runs in turn against a shared, accumulating context so outputs from on
|
|
|
140
140
|
|
|
141
141
|
| Command | Description |
|
|
142
142
|
|---|---|
|
|
143
|
-
| `acfm agents setup` | Install optional dependencies (`opencode` and `tmux`) |
|
|
144
|
-
| `acfm agents doctor` | Validate OpenCode/
|
|
143
|
+
| `acfm agents setup` | Install optional dependencies (`opencode` and `zellij`/`tmux`) |
|
|
144
|
+
| `acfm agents doctor` | Validate OpenCode/multiplexer/model preflight before start |
|
|
145
145
|
| `acfm agents install-mcps` | Install SynapseGrid MCP server for detected assistants |
|
|
146
146
|
| `acfm agents uninstall-mcps` | Remove SynapseGrid MCP server from assistants |
|
|
147
147
|
| `acfm agents start --task "..." --model-coder provider/model` | Start session with optional per-role models |
|
|
148
|
+
| `acfm agents start --task "..." --mux zellij` | Start session forcing zellij backend (`auto`/`tmux` also supported) |
|
|
149
|
+
| `acfm agents runtime get` | Show configured multiplexer backend (`auto`, `zellij`, `tmux`) |
|
|
150
|
+
| `acfm agents runtime set zellij` | Persist preferred multiplexer backend |
|
|
148
151
|
| `acfm agents resume` | Resume a previous session and recreate workers if needed |
|
|
149
152
|
| `acfm agents list` | List recent SynapseGrid sessions |
|
|
150
|
-
| `acfm agents attach` | Attach directly to the SynapseGrid
|
|
151
|
-
| `acfm agents live` | Attach to full live
|
|
153
|
+
| `acfm agents attach` | Attach directly to the active SynapseGrid multiplexer session |
|
|
154
|
+
| `acfm agents live` | Attach to full live multiplexer view (all agents) |
|
|
152
155
|
| `acfm agents logs` | Show recent worker logs (all roles or one role) |
|
|
153
156
|
| `acfm agents transcript --role all --limit 40` | Show captured cross-agent transcript |
|
|
154
157
|
| `acfm agents summary` | Show generated collaboration meeting summary |
|
|
@@ -166,7 +169,7 @@ Each role runs in turn against a shared, accumulating context so outputs from on
|
|
|
166
169
|
|
|
167
170
|
When driving SynapseGrid from another agent via MCP, prefer asynchronous run tools over role-by-role stepping:
|
|
168
171
|
|
|
169
|
-
- `collab_start_session` to initialize session and optional tmux workers
|
|
172
|
+
- `collab_start_session` to initialize session and optional zellij/tmux workers
|
|
170
173
|
- `collab_invoke_team` to launch full 4-role collaboration run
|
|
171
174
|
- `collab_wait_run` to wait for completion/failure with bounded timeout
|
|
172
175
|
- `collab_get_result` to fetch final consolidated output and run diagnostics
|
|
@@ -180,7 +183,7 @@ When driving SynapseGrid from another agent via MCP, prefer asynchronous run too
|
|
|
180
183
|
- Attach to worker panes with `acfm agents live` (or `acfm agents attach`) to see real-time role discussion.
|
|
181
184
|
- Inspect worker errors quickly with `acfm agents logs --role all --lines 120`.
|
|
182
185
|
- Inspect collaborative discussion with `acfm agents transcript` and `acfm agents summary`.
|
|
183
|
-
- MCP starts can now create tmux workers directly; if your assistant used headless steps before, start a new session and ensure worker spawning is enabled.
|
|
186
|
+
- MCP starts can now create zellij/tmux workers directly; if your assistant used headless steps before, start a new session and ensure worker spawning is enabled.
|
|
184
187
|
- Configure role models directly at start (for example `--model-planner`, `--model-coder`) or persist defaults via `acfm agents model choose` / `acfm agents model set`.
|
|
185
188
|
- Default SynapseGrid model fallback is `opencode/mimo-v2-pro-free`.
|
|
186
189
|
- Run `acfm agents doctor` when panes look idle to confirm model/provider preflight health.
|
package/package.json
CHANGED
|
@@ -10,10 +10,13 @@ const CONFIG_PATH = join(ACFM_DIR, 'config.json');
|
|
|
10
10
|
|
|
11
11
|
function normalizeConfig(raw) {
|
|
12
12
|
const agents = raw?.agents && typeof raw.agents === 'object' ? raw.agents : {};
|
|
13
|
+
const configuredMultiplexer = typeof agents.multiplexer === 'string' ? agents.multiplexer.trim().toLowerCase() : '';
|
|
14
|
+
const multiplexer = ['auto', 'zellij', 'tmux'].includes(configuredMultiplexer) ? configuredMultiplexer : 'auto';
|
|
13
15
|
return {
|
|
14
16
|
agents: {
|
|
15
17
|
defaultModel: normalizeModelId(agents.defaultModel) || DEFAULT_SYNAPSE_MODEL,
|
|
16
18
|
defaultRoleModels: sanitizeRoleModels(agents.defaultRoleModels),
|
|
19
|
+
multiplexer,
|
|
17
20
|
},
|
|
18
21
|
};
|
|
19
22
|
}
|
|
@@ -19,6 +19,20 @@ import {
|
|
|
19
19
|
withSessionLock,
|
|
20
20
|
} from './state-store.js';
|
|
21
21
|
|
|
22
|
+
async function finalizeSessionArtifacts(state) {
|
|
23
|
+
const runState = ensureRunState(state);
|
|
24
|
+
const summaryMd = buildMeetingSummary(state.messages, runState, runState.sharedContext);
|
|
25
|
+
await writeMeetingSummary(state.sessionId, summaryMd);
|
|
26
|
+
const completedRun = {
|
|
27
|
+
...runState,
|
|
28
|
+
finalSummary: extractFinalSummary(state.messages, runState),
|
|
29
|
+
};
|
|
30
|
+
return saveSessionState({
|
|
31
|
+
...state,
|
|
32
|
+
run: completedRun,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
22
36
|
function buildRuntimePrompt({ state, role }) {
|
|
23
37
|
const roleContext = ROLE_SYSTEM_PROMPTS[role] || '';
|
|
24
38
|
const collaborativePrompt = buildAgentPrompt({
|
|
@@ -63,6 +77,13 @@ function ensureRunState(state) {
|
|
|
63
77
|
round: state.round || 1,
|
|
64
78
|
events: [],
|
|
65
79
|
finalSummary: null,
|
|
80
|
+
sharedContext: {
|
|
81
|
+
decisions: [],
|
|
82
|
+
openIssues: [],
|
|
83
|
+
risks: [],
|
|
84
|
+
actionItems: [],
|
|
85
|
+
notes: [],
|
|
86
|
+
},
|
|
66
87
|
lastError: null,
|
|
67
88
|
policy: {
|
|
68
89
|
timeoutPerRoleMs: 180000,
|
|
@@ -141,6 +162,7 @@ export async function runTurn(sessionId, options = {}) {
|
|
|
141
162
|
if (shouldStop(state)) {
|
|
142
163
|
if (state.status === 'running') {
|
|
143
164
|
state = await stopSession(state, 'completed');
|
|
165
|
+
state = await finalizeSessionArtifacts(state);
|
|
144
166
|
}
|
|
145
167
|
return state;
|
|
146
168
|
}
|
|
@@ -178,6 +200,7 @@ export async function runTurn(sessionId, options = {}) {
|
|
|
178
200
|
|
|
179
201
|
if (shouldStop(state)) {
|
|
180
202
|
state = await stopSession(state, 'completed');
|
|
203
|
+
state = await finalizeSessionArtifacts(state);
|
|
181
204
|
}
|
|
182
205
|
|
|
183
206
|
return state;
|
|
@@ -244,6 +267,7 @@ export async function executeActiveTurn(sessionId, role, options = {}) {
|
|
|
244
267
|
|
|
245
268
|
if (shouldStop(state)) {
|
|
246
269
|
state = await stopSession(state, 'completed');
|
|
270
|
+
state = await finalizeSessionArtifacts(state);
|
|
247
271
|
}
|
|
248
272
|
|
|
249
273
|
return state;
|
|
@@ -332,6 +356,9 @@ export async function runWorkerIteration(sessionId, role, options = {}) {
|
|
|
332
356
|
events: outputEvents,
|
|
333
357
|
}));
|
|
334
358
|
state = await saveSessionState(applyRoleFailurePolicy(state, role, errorMessage));
|
|
359
|
+
if (state.status === 'failed') {
|
|
360
|
+
state = await finalizeSessionArtifacts(state);
|
|
361
|
+
}
|
|
335
362
|
return state;
|
|
336
363
|
}
|
|
337
364
|
|
|
@@ -360,8 +387,6 @@ export async function runWorkerIteration(sessionId, role, options = {}) {
|
|
|
360
387
|
|
|
361
388
|
if (shouldStop(state)) {
|
|
362
389
|
state = await stopSession(state, 'completed');
|
|
363
|
-
const summaryMd = buildMeetingSummary(state.messages, ensureRunState(state), ensureRunState(state).sharedContext);
|
|
364
|
-
await writeMeetingSummary(sessionId, summaryMd);
|
|
365
390
|
const finalRun = appendRunEvent({
|
|
366
391
|
...ensureRunState(state),
|
|
367
392
|
status: 'completed',
|
|
@@ -372,6 +397,7 @@ export async function runWorkerIteration(sessionId, role, options = {}) {
|
|
|
372
397
|
...state,
|
|
373
398
|
run: finalRun,
|
|
374
399
|
});
|
|
400
|
+
state = await finalizeSessionArtifacts(state);
|
|
375
401
|
}
|
|
376
402
|
|
|
377
403
|
return state;
|
package/src/agents/runtime.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { dirname, resolve } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { writeFile } from 'node:fs/promises';
|
|
4
5
|
import { COLLAB_ROLES } from './constants.js';
|
|
5
6
|
|
|
6
7
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -10,7 +11,7 @@ export function roleLogPath(sessionDir, role) {
|
|
|
10
11
|
return resolve(sessionDir, `${role}.log`);
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
function runCommand(command, args, options = {}) {
|
|
14
15
|
return new Promise((resolvePromise, rejectPromise) => {
|
|
15
16
|
const child = spawn(command, args, {
|
|
16
17
|
cwd: options.cwd || process.cwd(),
|
|
@@ -42,6 +43,14 @@ export function runTmux(command, args, options = {}) {
|
|
|
42
43
|
});
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
function workerCommand(sessionId, role, roleLog) {
|
|
47
|
+
return `bash -lc 'node "${runnerPath}" agents worker --session ${sessionId} --role ${role} 2>&1 | tee -a "${roleLog}"'`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function runTmux(command, args, options = {}) {
|
|
51
|
+
return runCommand(command, args, options);
|
|
52
|
+
}
|
|
53
|
+
|
|
45
54
|
export async function spawnTmuxSession({ sessionName, sessionDir, sessionId }) {
|
|
46
55
|
const role0 = COLLAB_ROLES[0];
|
|
47
56
|
const role0Log = roleLogPath(sessionDir, role0);
|
|
@@ -52,7 +61,7 @@ export async function spawnTmuxSession({ sessionName, sessionDir, sessionId }) {
|
|
|
52
61
|
sessionName,
|
|
53
62
|
'-n',
|
|
54
63
|
role0,
|
|
55
|
-
|
|
64
|
+
workerCommand(sessionId, role0, role0Log),
|
|
56
65
|
]);
|
|
57
66
|
|
|
58
67
|
for (let idx = 1; idx < COLLAB_ROLES.length; idx += 1) {
|
|
@@ -63,7 +72,7 @@ export async function spawnTmuxSession({ sessionName, sessionDir, sessionId }) {
|
|
|
63
72
|
'-t',
|
|
64
73
|
sessionName,
|
|
65
74
|
'-v',
|
|
66
|
-
|
|
75
|
+
workerCommand(sessionId, role, roleLog),
|
|
67
76
|
]);
|
|
68
77
|
}
|
|
69
78
|
|
|
@@ -80,3 +89,66 @@ export async function tmuxSessionExists(sessionName) {
|
|
|
80
89
|
return false;
|
|
81
90
|
}
|
|
82
91
|
}
|
|
92
|
+
|
|
93
|
+
async function writeZellijLayout({ layoutPath, sessionId, sessionDir }) {
|
|
94
|
+
const panes = COLLAB_ROLES.map((role) => {
|
|
95
|
+
const roleLog = roleLogPath(sessionDir, role);
|
|
96
|
+
const cmd = workerCommand(sessionId, role, roleLog).replace(/"/g, '\\"');
|
|
97
|
+
return ` pane name="${role}" command="bash" args { "-lc" "${cmd}" }`;
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const content = [
|
|
101
|
+
'layout {',
|
|
102
|
+
' default_tab_template {',
|
|
103
|
+
' tab name="SynapseGrid" {',
|
|
104
|
+
' pane split_direction="vertical" {',
|
|
105
|
+
' pane split_direction="horizontal" {',
|
|
106
|
+
panes[0],
|
|
107
|
+
panes[1],
|
|
108
|
+
' }',
|
|
109
|
+
' pane split_direction="horizontal" {',
|
|
110
|
+
panes[2],
|
|
111
|
+
panes[3],
|
|
112
|
+
' }',
|
|
113
|
+
' }',
|
|
114
|
+
' }',
|
|
115
|
+
' }',
|
|
116
|
+
'}',
|
|
117
|
+
'',
|
|
118
|
+
].join('\n');
|
|
119
|
+
|
|
120
|
+
await writeFile(layoutPath, content, 'utf8');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function spawnZellijSession({ sessionName, sessionDir, sessionId }) {
|
|
124
|
+
const layoutPath = resolve(sessionDir, 'synapsegrid-layout.kdl');
|
|
125
|
+
await writeZellijLayout({ layoutPath, sessionId, sessionDir });
|
|
126
|
+
await runCommand('zellij', ['--session', sessionName, '--layout', layoutPath, '--detach']);
|
|
127
|
+
return { layoutPath };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function zellijSessionExists(sessionName) {
|
|
131
|
+
try {
|
|
132
|
+
const result = await runCommand('zellij', ['list-sessions']);
|
|
133
|
+
const lines = result.stdout.split('\n').map((line) => line.trim()).filter(Boolean);
|
|
134
|
+
return lines.some((line) => line === sessionName || line.startsWith(`${sessionName} `));
|
|
135
|
+
} catch {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function runZellij(args, options = {}) {
|
|
141
|
+
return runCommand('zellij', args, options);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function resolveMultiplexer(preferred = 'auto', hasTmuxCommand = false, hasZellijCommand = false) {
|
|
145
|
+
if (preferred === 'tmux') {
|
|
146
|
+
return hasTmuxCommand ? 'tmux' : null;
|
|
147
|
+
}
|
|
148
|
+
if (preferred === 'zellij') {
|
|
149
|
+
return hasZellijCommand ? 'zellij' : null;
|
|
150
|
+
}
|
|
151
|
+
if (hasZellijCommand) return 'zellij';
|
|
152
|
+
if (hasTmuxCommand) return 'tmux';
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
@@ -73,6 +73,8 @@ function initialState(task, options = {}) {
|
|
|
73
73
|
model: options.model || null,
|
|
74
74
|
roleModels: sanitizeRoleModels(options.roleModels),
|
|
75
75
|
opencodeBin: options.opencodeBin || null,
|
|
76
|
+
multiplexer: options.multiplexer || 'auto',
|
|
77
|
+
multiplexerSessionName: options.multiplexerSessionName || null,
|
|
76
78
|
tmuxSessionName: options.tmuxSessionName || null,
|
|
77
79
|
run: createRunState(options.runPolicy, Number.isInteger(options.maxRounds) ? options.maxRounds : DEFAULT_MAX_ROUNDS),
|
|
78
80
|
messages: [
|
|
@@ -208,6 +210,8 @@ export async function listSessions(limit = 20) {
|
|
|
208
210
|
round: state.round,
|
|
209
211
|
maxRounds: state.maxRounds,
|
|
210
212
|
tmuxSessionName: state.tmuxSessionName || null,
|
|
213
|
+
multiplexer: state.multiplexer || 'auto',
|
|
214
|
+
multiplexerSessionName: state.multiplexerSessionName || null,
|
|
211
215
|
createdAt: state.createdAt,
|
|
212
216
|
updatedAt: state.updatedAt,
|
|
213
217
|
mtime: stateStats.mtimeMs,
|
package/src/commands/agents.js
CHANGED
|
@@ -26,8 +26,18 @@ import {
|
|
|
26
26
|
saveSessionState,
|
|
27
27
|
setCurrentSession,
|
|
28
28
|
stopSession,
|
|
29
|
+
writeMeetingSummary,
|
|
29
30
|
} from '../agents/state-store.js';
|
|
30
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
roleLogPath,
|
|
33
|
+
runTmux,
|
|
34
|
+
runZellij,
|
|
35
|
+
spawnTmuxSession,
|
|
36
|
+
spawnZellijSession,
|
|
37
|
+
tmuxSessionExists,
|
|
38
|
+
zellijSessionExists,
|
|
39
|
+
resolveMultiplexer,
|
|
40
|
+
} from '../agents/runtime.js';
|
|
31
41
|
import { getAgentsConfigPath, loadAgentsConfig, updateAgentsConfig } from '../agents/config-store.js';
|
|
32
42
|
import {
|
|
33
43
|
buildEffectiveRoleModels,
|
|
@@ -60,18 +70,53 @@ async function ensureSessionId(required = true) {
|
|
|
60
70
|
function printStartSummary(state) {
|
|
61
71
|
console.log(chalk.green(`✓ ${COLLAB_SYSTEM_NAME} session started`));
|
|
62
72
|
console.log(chalk.dim(` Session: ${state.sessionId}`));
|
|
63
|
-
|
|
73
|
+
const multiplexer = state.multiplexer || 'auto';
|
|
74
|
+
const muxSessionName = state.multiplexerSessionName || state.tmuxSessionName || '-';
|
|
75
|
+
console.log(chalk.dim(` Multiplexer: ${multiplexer}`));
|
|
76
|
+
console.log(chalk.dim(` Session name: ${muxSessionName}`));
|
|
64
77
|
console.log(chalk.dim(` Task: ${state.task}`));
|
|
65
78
|
console.log(chalk.dim(` Roles: ${state.roles.join(', ')}`));
|
|
66
79
|
console.log();
|
|
67
80
|
console.log(chalk.cyan('Attach with:'));
|
|
68
|
-
|
|
81
|
+
if (multiplexer === 'zellij') {
|
|
82
|
+
console.log(chalk.white(` zellij attach ${muxSessionName}`));
|
|
83
|
+
} else {
|
|
84
|
+
console.log(chalk.white(` tmux attach -t ${muxSessionName}`));
|
|
85
|
+
}
|
|
69
86
|
console.log(chalk.white(' acfm agents live'));
|
|
70
87
|
console.log();
|
|
71
88
|
console.log(chalk.cyan('Interact with:'));
|
|
72
89
|
console.log(chalk.white(' acfm agents send "your message"'));
|
|
73
90
|
}
|
|
74
91
|
|
|
92
|
+
function validateMultiplexer(value) {
|
|
93
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
94
|
+
if (!['auto', 'zellij', 'tmux'].includes(normalized)) {
|
|
95
|
+
throw new Error('--mux must be one of: auto|zellij|tmux');
|
|
96
|
+
}
|
|
97
|
+
return normalized;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function sessionMuxName(state) {
|
|
101
|
+
return state.multiplexerSessionName || state.tmuxSessionName || `acfm-synapse-${state.sessionId.slice(0, 8)}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function sessionExistsForMux(multiplexer, sessionName) {
|
|
105
|
+
if (multiplexer === 'zellij') return zellijSessionExists(sessionName);
|
|
106
|
+
return tmuxSessionExists(sessionName);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function attachToMux(multiplexer, sessionName, readonly = false) {
|
|
110
|
+
if (multiplexer === 'zellij') {
|
|
111
|
+
await runZellij(['attach', sessionName], { stdio: 'inherit' });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const args = ['attach'];
|
|
115
|
+
if (readonly) args.push('-r');
|
|
116
|
+
args.push('-t', sessionName);
|
|
117
|
+
await runTmux('tmux', args, { stdio: 'inherit' });
|
|
118
|
+
}
|
|
119
|
+
|
|
75
120
|
function toMarkdownTranscript(state, transcript) {
|
|
76
121
|
const displayedRound = Math.min(state.round, state.maxRounds);
|
|
77
122
|
const lines = [
|
|
@@ -181,12 +226,51 @@ export function agentsCommand() {
|
|
|
181
226
|
const agents = new Command('agents')
|
|
182
227
|
.description(`${COLLAB_SYSTEM_NAME} — collaborative multi-agent system powered by OpenCode`);
|
|
183
228
|
|
|
229
|
+
agents.addHelpText('after', `
|
|
230
|
+
Examples:
|
|
231
|
+
acfm agents start --task "Implement auth flow" --mux auto
|
|
232
|
+
acfm agents setup
|
|
233
|
+
acfm agents runtime get
|
|
234
|
+
acfm agents runtime set zellij
|
|
235
|
+
acfm agents model choose
|
|
236
|
+
acfm agents model list
|
|
237
|
+
acfm agents transcript --role all --limit 40
|
|
238
|
+
acfm agents summary
|
|
239
|
+
acfm agents export --format md --out ./session.md
|
|
240
|
+
`);
|
|
241
|
+
|
|
184
242
|
agents
|
|
185
243
|
.command('setup')
|
|
186
|
-
.description('Install optional collaboration dependencies (OpenCode + tmux)')
|
|
244
|
+
.description('Install optional collaboration dependencies (OpenCode + zellij/tmux)')
|
|
245
|
+
.option('--yes', 'Install dependencies without interactive confirmation', false)
|
|
187
246
|
.option('--json', 'Output as JSON')
|
|
188
247
|
.action(async (opts) => {
|
|
189
|
-
|
|
248
|
+
let installZellij = true;
|
|
249
|
+
let installTmux = true;
|
|
250
|
+
|
|
251
|
+
if (!opts.yes && !opts.json) {
|
|
252
|
+
const answers = await inquirer.prompt([
|
|
253
|
+
{
|
|
254
|
+
type: 'confirm',
|
|
255
|
+
name: 'installZellij',
|
|
256
|
+
message: 'Install zellij (recommended, multiplatform backend)?',
|
|
257
|
+
default: true,
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
type: 'confirm',
|
|
261
|
+
name: 'installTmux',
|
|
262
|
+
message: 'Install tmux as fallback backend?',
|
|
263
|
+
default: true,
|
|
264
|
+
},
|
|
265
|
+
]);
|
|
266
|
+
installZellij = Boolean(answers.installZellij);
|
|
267
|
+
installTmux = Boolean(answers.installTmux);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const result = ensureCollabDependencies({
|
|
271
|
+
installZellij,
|
|
272
|
+
installTmux,
|
|
273
|
+
});
|
|
190
274
|
let collabMcp = null;
|
|
191
275
|
|
|
192
276
|
if (result.success) {
|
|
@@ -199,7 +283,9 @@ export function agentsCommand() {
|
|
|
199
283
|
if (!opts.json) {
|
|
200
284
|
const oLabel = result.opencode.success ? chalk.green('ok') : chalk.red('failed');
|
|
201
285
|
const tLabel = result.tmux.success ? chalk.green('ok') : chalk.red('failed');
|
|
286
|
+
const zLabel = result.zellij.success ? chalk.green('ok') : chalk.red('failed');
|
|
202
287
|
console.log(`OpenCode: ${oLabel} - ${result.opencode.message}`);
|
|
288
|
+
console.log(`zellij: ${zLabel} - ${result.zellij.message}`);
|
|
203
289
|
console.log(`tmux: ${tLabel} - ${result.tmux.message}`);
|
|
204
290
|
if (collabMcp) {
|
|
205
291
|
console.log(`Collab MCP: ${chalk.green('ok')} - installed ${collabMcp.success}/${collabMcp.installed} on detected assistants`);
|
|
@@ -286,10 +372,10 @@ export function agentsCommand() {
|
|
|
286
372
|
}
|
|
287
373
|
console.log(chalk.bold('SynapseGrid Sessions'));
|
|
288
374
|
for (const item of sessions) {
|
|
289
|
-
|
|
375
|
+
console.log(
|
|
290
376
|
`${chalk.cyan(item.sessionId.slice(0, 8))} ${item.status.padEnd(10)} ` +
|
|
291
377
|
`round ${String(item.round).padStart(2)}/${String(item.maxRounds).padEnd(2)} ` +
|
|
292
|
-
`${item.tmuxSessionName || '-'} ${item.task}`
|
|
378
|
+
`${item.multiplexer || 'auto'}:${item.multiplexerSessionName || item.tmuxSessionName || '-'} ${item.task}`
|
|
293
379
|
);
|
|
294
380
|
}
|
|
295
381
|
}
|
|
@@ -302,15 +388,17 @@ export function agentsCommand() {
|
|
|
302
388
|
|
|
303
389
|
agents
|
|
304
390
|
.command('attach')
|
|
305
|
-
.description('Attach terminal to active SynapseGrid
|
|
391
|
+
.description('Attach terminal to active SynapseGrid multiplexer session')
|
|
306
392
|
.action(async () => {
|
|
307
393
|
try {
|
|
308
394
|
const sessionId = await ensureSessionId(true);
|
|
309
395
|
const state = await loadSessionState(sessionId);
|
|
310
|
-
|
|
311
|
-
|
|
396
|
+
const multiplexer = state.multiplexer || 'tmux';
|
|
397
|
+
const muxSessionName = sessionMuxName(state);
|
|
398
|
+
if (!muxSessionName) {
|
|
399
|
+
throw new Error('No multiplexer session registered for active collaborative session');
|
|
312
400
|
}
|
|
313
|
-
await
|
|
401
|
+
await attachToMux(multiplexer, muxSessionName, false);
|
|
314
402
|
} catch (error) {
|
|
315
403
|
console.error(chalk.red(`Error: ${error.message}`));
|
|
316
404
|
process.exit(1);
|
|
@@ -319,23 +407,22 @@ export function agentsCommand() {
|
|
|
319
407
|
|
|
320
408
|
agents
|
|
321
409
|
.command('live')
|
|
322
|
-
.description('Attach to live
|
|
410
|
+
.description('Attach to live collaboration view (all agent panes)')
|
|
323
411
|
.option('--readonly', 'Attach in read-only mode', false)
|
|
324
412
|
.action(async (opts) => {
|
|
325
413
|
try {
|
|
326
414
|
const sessionId = await ensureSessionId(true);
|
|
327
415
|
const state = await loadSessionState(sessionId);
|
|
328
|
-
|
|
329
|
-
|
|
416
|
+
const multiplexer = state.multiplexer || 'tmux';
|
|
417
|
+
const muxSessionName = sessionMuxName(state);
|
|
418
|
+
if (!muxSessionName) {
|
|
419
|
+
throw new Error('No multiplexer session registered for active collaborative session');
|
|
330
420
|
}
|
|
331
|
-
const
|
|
332
|
-
if (!
|
|
333
|
-
throw new Error(
|
|
421
|
+
const sessionExists = await sessionExistsForMux(multiplexer, muxSessionName);
|
|
422
|
+
if (!sessionExists) {
|
|
423
|
+
throw new Error(`${multiplexer} session ${muxSessionName} no longer exists. Run: acfm agents resume`);
|
|
334
424
|
}
|
|
335
|
-
|
|
336
|
-
if (opts.readonly) args.push('-r');
|
|
337
|
-
args.push('-t', state.tmuxSessionName);
|
|
338
|
-
await runTmux('tmux', args, { stdio: 'inherit' });
|
|
425
|
+
await attachToMux(multiplexer, muxSessionName, Boolean(opts.readonly));
|
|
339
426
|
} catch (error) {
|
|
340
427
|
console.error(chalk.red(`Error: ${error.message}`));
|
|
341
428
|
process.exit(1);
|
|
@@ -344,48 +431,60 @@ export function agentsCommand() {
|
|
|
344
431
|
|
|
345
432
|
agents
|
|
346
433
|
.command('resume')
|
|
347
|
-
.description('Resume a previous session and optionally recreate
|
|
434
|
+
.description('Resume a previous session and optionally recreate multiplexer workers')
|
|
348
435
|
.option('--session <id>', 'Session ID to resume (defaults to current)')
|
|
349
|
-
.option('--no-recreate', 'Do not recreate
|
|
350
|
-
.option('--no-attach', 'Do not attach
|
|
436
|
+
.option('--no-recreate', 'Do not recreate multiplexer session/workers when missing')
|
|
437
|
+
.option('--no-attach', 'Do not attach multiplexer after resume')
|
|
351
438
|
.option('--json', 'Output as JSON')
|
|
352
439
|
.action(async (opts) => {
|
|
353
440
|
try {
|
|
354
441
|
const sessionId = opts.session || await ensureSessionId(true);
|
|
355
442
|
let state = await loadSessionState(sessionId);
|
|
443
|
+
const multiplexer = state.multiplexer || 'tmux';
|
|
356
444
|
|
|
357
|
-
|
|
358
|
-
|
|
445
|
+
if (multiplexer === 'zellij' && !hasCommand('zellij')) {
|
|
446
|
+
throw new Error('zellij is not installed. Run: acfm agents setup');
|
|
447
|
+
}
|
|
448
|
+
if (multiplexer === 'tmux' && !hasCommand('tmux')) {
|
|
449
|
+
throw new Error('tmux is not installed. Run: acfm agents setup');
|
|
450
|
+
}
|
|
359
451
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
452
|
+
const muxSessionName = sessionMuxName(state);
|
|
453
|
+
const muxExists = await sessionExistsForMux(multiplexer, muxSessionName);
|
|
454
|
+
|
|
455
|
+
if (!muxExists && opts.recreate) {
|
|
364
456
|
const sessionDir = getSessionDir(state.sessionId);
|
|
365
|
-
|
|
457
|
+
if (multiplexer === 'zellij') {
|
|
458
|
+
await spawnZellijSession({ sessionName: muxSessionName, sessionDir, sessionId: state.sessionId });
|
|
459
|
+
} else {
|
|
460
|
+
await spawnTmuxSession({ sessionName: muxSessionName, sessionDir, sessionId: state.sessionId });
|
|
461
|
+
}
|
|
366
462
|
}
|
|
367
463
|
|
|
368
464
|
state = await saveSessionState({
|
|
369
465
|
...state,
|
|
370
466
|
status: 'running',
|
|
371
|
-
|
|
467
|
+
multiplexer,
|
|
468
|
+
multiplexerSessionName: muxSessionName,
|
|
469
|
+
tmuxSessionName: multiplexer === 'tmux' ? muxSessionName : (state.tmuxSessionName || null),
|
|
372
470
|
});
|
|
373
471
|
await setCurrentSession(state.sessionId);
|
|
374
472
|
|
|
375
473
|
output({
|
|
376
474
|
sessionId: state.sessionId,
|
|
377
475
|
status: state.status,
|
|
378
|
-
|
|
379
|
-
|
|
476
|
+
multiplexer,
|
|
477
|
+
multiplexerSessionName: muxSessionName,
|
|
478
|
+
recreatedSession: !muxExists && Boolean(opts.recreate),
|
|
380
479
|
}, opts.json);
|
|
381
480
|
|
|
382
481
|
if (!opts.json) {
|
|
383
482
|
console.log(chalk.green(`✓ Resumed session ${state.sessionId}`));
|
|
384
|
-
console.log(chalk.dim(`
|
|
483
|
+
console.log(chalk.dim(` ${multiplexer}: ${muxSessionName}`));
|
|
385
484
|
}
|
|
386
485
|
|
|
387
486
|
if (opts.attach) {
|
|
388
|
-
await
|
|
487
|
+
await attachToMux(multiplexer, muxSessionName, false);
|
|
389
488
|
}
|
|
390
489
|
} catch (error) {
|
|
391
490
|
output({ error: error.message }, opts.json);
|
|
@@ -453,6 +552,77 @@ export function agentsCommand() {
|
|
|
453
552
|
.command('model')
|
|
454
553
|
.description('Manage default SynapseGrid model configuration');
|
|
455
554
|
|
|
555
|
+
const runtime = agents
|
|
556
|
+
.command('runtime')
|
|
557
|
+
.description('Manage SynapseGrid runtime backend settings');
|
|
558
|
+
|
|
559
|
+
runtime
|
|
560
|
+
.command('get')
|
|
561
|
+
.description('Show configured multiplexer backend')
|
|
562
|
+
.option('--json', 'Output as JSON')
|
|
563
|
+
.action(async (opts) => {
|
|
564
|
+
try {
|
|
565
|
+
const cfg = await loadAgentsConfig();
|
|
566
|
+
const configured = validateMultiplexer(cfg.agents.multiplexer || 'auto');
|
|
567
|
+
const resolved = resolveMultiplexer(configured, hasCommand('tmux'), hasCommand('zellij'));
|
|
568
|
+
const payload = {
|
|
569
|
+
configPath: getAgentsConfigPath(),
|
|
570
|
+
multiplexer: configured,
|
|
571
|
+
resolved,
|
|
572
|
+
available: {
|
|
573
|
+
zellij: hasCommand('zellij'),
|
|
574
|
+
tmux: hasCommand('tmux'),
|
|
575
|
+
},
|
|
576
|
+
};
|
|
577
|
+
output(payload, opts.json);
|
|
578
|
+
if (!opts.json) {
|
|
579
|
+
console.log(chalk.bold('SynapseGrid runtime backend'));
|
|
580
|
+
console.log(chalk.dim(`Config: ${payload.configPath}`));
|
|
581
|
+
console.log(chalk.dim(`Configured: ${configured}`));
|
|
582
|
+
console.log(chalk.dim(`Resolved: ${resolved || 'none'}`));
|
|
583
|
+
console.log(chalk.dim(`zellij=${payload.available.zellij} tmux=${payload.available.tmux}`));
|
|
584
|
+
}
|
|
585
|
+
} catch (error) {
|
|
586
|
+
output({ error: error.message }, opts.json);
|
|
587
|
+
if (!opts.json) console.error(chalk.red(`Error: ${error.message}`));
|
|
588
|
+
process.exit(1);
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
runtime
|
|
593
|
+
.command('set <mux>')
|
|
594
|
+
.description('Set multiplexer backend: auto|zellij|tmux')
|
|
595
|
+
.option('--json', 'Output as JSON')
|
|
596
|
+
.action(async (mux, opts) => {
|
|
597
|
+
try {
|
|
598
|
+
const selected = validateMultiplexer(mux);
|
|
599
|
+
const updated = await updateAgentsConfig((current) => ({
|
|
600
|
+
agents: {
|
|
601
|
+
defaultModel: current.agents.defaultModel,
|
|
602
|
+
defaultRoleModels: { ...current.agents.defaultRoleModels },
|
|
603
|
+
multiplexer: selected,
|
|
604
|
+
},
|
|
605
|
+
}));
|
|
606
|
+
const resolved = resolveMultiplexer(updated.agents.multiplexer, hasCommand('tmux'), hasCommand('zellij'));
|
|
607
|
+
const payload = {
|
|
608
|
+
success: true,
|
|
609
|
+
configPath: getAgentsConfigPath(),
|
|
610
|
+
multiplexer: updated.agents.multiplexer,
|
|
611
|
+
resolved,
|
|
612
|
+
};
|
|
613
|
+
output(payload, opts.json);
|
|
614
|
+
if (!opts.json) {
|
|
615
|
+
console.log(chalk.green('✓ SynapseGrid runtime backend updated'));
|
|
616
|
+
console.log(chalk.dim(` Configured: ${payload.multiplexer}`));
|
|
617
|
+
console.log(chalk.dim(` Resolved: ${payload.resolved || 'none'}`));
|
|
618
|
+
}
|
|
619
|
+
} catch (error) {
|
|
620
|
+
output({ error: error.message }, opts.json);
|
|
621
|
+
if (!opts.json) console.error(chalk.red(`Error: ${error.message}`));
|
|
622
|
+
process.exit(1);
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
|
|
456
626
|
model
|
|
457
627
|
.command('list')
|
|
458
628
|
.description('List available OpenCode models grouped by provider')
|
|
@@ -510,6 +680,7 @@ export function agentsCommand() {
|
|
|
510
680
|
configPath: getAgentsConfigPath(),
|
|
511
681
|
defaultModel: config.agents.defaultModel,
|
|
512
682
|
defaultRoleModels: config.agents.defaultRoleModels,
|
|
683
|
+
multiplexer: config.agents.multiplexer,
|
|
513
684
|
};
|
|
514
685
|
output(payload, opts.json);
|
|
515
686
|
if (!opts.json) {
|
|
@@ -519,6 +690,7 @@ export function agentsCommand() {
|
|
|
519
690
|
for (const role of COLLAB_ROLES) {
|
|
520
691
|
console.log(chalk.dim(`- ${role}: ${payload.defaultRoleModels[role] || '(none)'}`));
|
|
521
692
|
}
|
|
693
|
+
console.log(chalk.dim(`Multiplexer: ${payload.multiplexer || 'auto'}`));
|
|
522
694
|
}
|
|
523
695
|
} catch (error) {
|
|
524
696
|
output({ error: error.message }, opts.json);
|
|
@@ -594,6 +766,7 @@ export function agentsCommand() {
|
|
|
594
766
|
agents: {
|
|
595
767
|
defaultModel: current.agents.defaultModel,
|
|
596
768
|
defaultRoleModels: { ...current.agents.defaultRoleModels },
|
|
769
|
+
multiplexer: current.agents.multiplexer || 'auto',
|
|
597
770
|
},
|
|
598
771
|
};
|
|
599
772
|
|
|
@@ -654,6 +827,7 @@ export function agentsCommand() {
|
|
|
654
827
|
agents: {
|
|
655
828
|
defaultModel: current.agents.defaultModel,
|
|
656
829
|
defaultRoleModels: { ...current.agents.defaultRoleModels },
|
|
830
|
+
multiplexer: current.agents.multiplexer || 'auto',
|
|
657
831
|
},
|
|
658
832
|
};
|
|
659
833
|
if (role === 'all') {
|
|
@@ -702,6 +876,7 @@ export function agentsCommand() {
|
|
|
702
876
|
agents: {
|
|
703
877
|
defaultModel: current.agents.defaultModel,
|
|
704
878
|
defaultRoleModels: { ...current.agents.defaultRoleModels },
|
|
879
|
+
multiplexer: current.agents.multiplexer || 'auto',
|
|
705
880
|
},
|
|
706
881
|
};
|
|
707
882
|
if (role === 'all') {
|
|
@@ -865,17 +1040,15 @@ export function agentsCommand() {
|
|
|
865
1040
|
.option('--model-critic <id>', 'Model for critic role (provider/model)')
|
|
866
1041
|
.option('--model-coder <id>', 'Model for coder role (provider/model)')
|
|
867
1042
|
.option('--model-reviewer <id>', 'Model for reviewer role (provider/model)')
|
|
1043
|
+
.option('--mux <name>', 'Multiplexer backend: auto|zellij|tmux')
|
|
868
1044
|
.option('--cwd <path>', 'Working directory for agents', process.cwd())
|
|
869
|
-
.option('--attach', 'Attach
|
|
1045
|
+
.option('--attach', 'Attach multiplexer immediately after start', false)
|
|
870
1046
|
.option('--json', 'Output as JSON')
|
|
871
1047
|
.action(async (opts) => {
|
|
872
1048
|
try {
|
|
873
1049
|
if (!hasCommand('opencode')) {
|
|
874
1050
|
throw new Error('OpenCode is not installed. Run: acfm agents setup');
|
|
875
1051
|
}
|
|
876
|
-
if (!hasCommand('tmux')) {
|
|
877
|
-
throw new Error('tmux is not installed. Run: acfm agents setup');
|
|
878
|
-
}
|
|
879
1052
|
const opencodeBin = resolveCommandPath('opencode');
|
|
880
1053
|
if (!opencodeBin) {
|
|
881
1054
|
throw new Error('OpenCode binary not found. Run: acfm agents setup');
|
|
@@ -888,6 +1061,11 @@ export function agentsCommand() {
|
|
|
888
1061
|
}
|
|
889
1062
|
|
|
890
1063
|
const config = await loadAgentsConfig();
|
|
1064
|
+
const configuredMux = validateMultiplexer(opts.mux || config.agents.multiplexer || 'auto');
|
|
1065
|
+
const selectedMux = resolveMultiplexer(configuredMux, hasCommand('tmux'), hasCommand('zellij'));
|
|
1066
|
+
if (!selectedMux) {
|
|
1067
|
+
throw new Error('No multiplexer found. Install zellij or tmux with: acfm agents setup');
|
|
1068
|
+
}
|
|
891
1069
|
const cliModel = assertValidModelIdOrNull('--model', opts.model || null);
|
|
892
1070
|
const cliRoleModels = parseRoleModelOptions(opts);
|
|
893
1071
|
for (const [role, model] of Object.entries(cliRoleModels)) {
|
|
@@ -922,30 +1100,47 @@ export function agentsCommand() {
|
|
|
922
1100
|
maxRounds,
|
|
923
1101
|
model: globalModel,
|
|
924
1102
|
roleModels,
|
|
1103
|
+
multiplexer: selectedMux,
|
|
925
1104
|
workingDirectory: resolve(opts.cwd),
|
|
926
1105
|
opencodeBin,
|
|
927
1106
|
});
|
|
928
|
-
const
|
|
1107
|
+
const muxSessionName = `acfm-synapse-${state.sessionId.slice(0, 8)}`;
|
|
929
1108
|
const sessionDir = getSessionDir(state.sessionId);
|
|
1109
|
+
|
|
1110
|
+
if (selectedMux === 'zellij') {
|
|
1111
|
+
await spawnZellijSession({
|
|
1112
|
+
sessionName: muxSessionName,
|
|
1113
|
+
sessionDir,
|
|
1114
|
+
sessionId: state.sessionId,
|
|
1115
|
+
});
|
|
1116
|
+
} else {
|
|
1117
|
+
await spawnTmuxSession({
|
|
1118
|
+
sessionName: muxSessionName,
|
|
1119
|
+
sessionDir,
|
|
1120
|
+
sessionId: state.sessionId,
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
|
|
930
1124
|
const updated = await saveSessionState({
|
|
931
1125
|
...state,
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
await spawnTmuxSession({
|
|
936
|
-
sessionName: tmuxSessionName,
|
|
937
|
-
sessionDir,
|
|
938
|
-
sessionId: state.sessionId,
|
|
1126
|
+
multiplexer: selectedMux,
|
|
1127
|
+
multiplexerSessionName: muxSessionName,
|
|
1128
|
+
tmuxSessionName: selectedMux === 'tmux' ? muxSessionName : null,
|
|
939
1129
|
});
|
|
940
1130
|
|
|
941
|
-
output({
|
|
1131
|
+
output({
|
|
1132
|
+
sessionId: updated.sessionId,
|
|
1133
|
+
multiplexer: selectedMux,
|
|
1134
|
+
multiplexerSessionName: muxSessionName,
|
|
1135
|
+
status: updated.status,
|
|
1136
|
+
}, opts.json);
|
|
942
1137
|
if (!opts.json) {
|
|
943
1138
|
printStartSummary(updated);
|
|
944
1139
|
printModelConfig(updated);
|
|
945
1140
|
}
|
|
946
1141
|
|
|
947
1142
|
if (opts.attach) {
|
|
948
|
-
await
|
|
1143
|
+
await attachToMux(selectedMux, muxSessionName, false);
|
|
949
1144
|
}
|
|
950
1145
|
} catch (error) {
|
|
951
1146
|
output({ error: error.message }, opts.json);
|
|
@@ -996,14 +1191,16 @@ export function agentsCommand() {
|
|
|
996
1191
|
console.log(chalk.dim(`Run error: ${summary.lastError.message}`));
|
|
997
1192
|
}
|
|
998
1193
|
console.log(chalk.dim(`Global model: ${state.model || '(opencode default)'}`));
|
|
1194
|
+
console.log(chalk.dim(`Multiplexer: ${state.multiplexer || 'auto'} (${sessionMuxName(state)})`));
|
|
999
1195
|
for (const role of COLLAB_ROLES) {
|
|
1000
1196
|
const configured = state.roleModels?.[role] || '-';
|
|
1001
1197
|
const effective = effectiveRoleModels[role] || '(opencode default)';
|
|
1002
1198
|
console.log(chalk.dim(` ${role.padEnd(8)} configured=${configured} effective=${effective}`));
|
|
1003
1199
|
}
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
}
|
|
1200
|
+
const meetingLogPath = resolve(getSessionDir(state.sessionId), 'meeting-log.md');
|
|
1201
|
+
const meetingSummaryPath = resolve(getSessionDir(state.sessionId), 'meeting-summary.md');
|
|
1202
|
+
console.log(chalk.dim(`meeting-log: ${existsSync(meetingLogPath) ? meetingLogPath : 'not generated yet'}`));
|
|
1203
|
+
console.log(chalk.dim(`meeting-summary: ${existsSync(meetingSummaryPath) ? meetingSummaryPath : 'not generated yet'}`));
|
|
1007
1204
|
}
|
|
1008
1205
|
} catch (error) {
|
|
1009
1206
|
output({ error: error.message }, opts.json);
|
|
@@ -1020,6 +1217,7 @@ export function agentsCommand() {
|
|
|
1020
1217
|
try {
|
|
1021
1218
|
const sessionId = await ensureSessionId(true);
|
|
1022
1219
|
let state = await loadSessionState(sessionId);
|
|
1220
|
+
const meetingSummaryPath = resolve(getSessionDir(state.sessionId), 'meeting-summary.md');
|
|
1023
1221
|
state = await stopSession(state, 'stopped');
|
|
1024
1222
|
if (state.run && state.run.status === 'running') {
|
|
1025
1223
|
state = await saveSessionState({
|
|
@@ -1036,15 +1234,51 @@ export function agentsCommand() {
|
|
|
1036
1234
|
},
|
|
1037
1235
|
});
|
|
1038
1236
|
}
|
|
1039
|
-
|
|
1237
|
+
|
|
1238
|
+
if (!existsSync(meetingSummaryPath)) {
|
|
1239
|
+
const fallbackSummary = [
|
|
1240
|
+
'# SynapseGrid Meeting Summary',
|
|
1241
|
+
'',
|
|
1242
|
+
`Session: ${state.sessionId}`,
|
|
1243
|
+
`Status: ${state.status}`,
|
|
1244
|
+
'',
|
|
1245
|
+
'This summary was auto-generated at stop time because the run did not complete normally.',
|
|
1246
|
+
'',
|
|
1247
|
+
'## Last message',
|
|
1248
|
+
state.messages?.[state.messages.length - 1]?.content || '(none)',
|
|
1249
|
+
'',
|
|
1250
|
+
].join('\n');
|
|
1251
|
+
await writeMeetingSummary(state.sessionId, fallbackSummary);
|
|
1252
|
+
if (state.run && !state.run.finalSummary) {
|
|
1253
|
+
state = await saveSessionState({
|
|
1254
|
+
...state,
|
|
1255
|
+
run: {
|
|
1256
|
+
...state.run,
|
|
1257
|
+
finalSummary: fallbackSummary,
|
|
1258
|
+
},
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
const multiplexer = state.multiplexer || 'tmux';
|
|
1264
|
+
const muxSessionName = sessionMuxName(state);
|
|
1265
|
+
if (multiplexer === 'zellij' && muxSessionName && hasCommand('zellij')) {
|
|
1040
1266
|
try {
|
|
1041
|
-
await
|
|
1267
|
+
await runZellij(['delete-session', muxSessionName]);
|
|
1268
|
+
} catch {
|
|
1269
|
+
// ignore if already closed
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
if (multiplexer === 'tmux' && muxSessionName && hasCommand('tmux')) {
|
|
1273
|
+
try {
|
|
1274
|
+
await runTmux('tmux', ['kill-session', '-t', muxSessionName]);
|
|
1042
1275
|
} catch {
|
|
1043
1276
|
// ignore if already closed
|
|
1044
1277
|
}
|
|
1045
1278
|
}
|
|
1046
1279
|
output({ sessionId: state.sessionId, status: state.status }, opts.json);
|
|
1047
1280
|
if (!opts.json) console.log(chalk.green('✓ Collaborative session stopped'));
|
|
1281
|
+
|
|
1048
1282
|
} catch (error) {
|
|
1049
1283
|
output({ error: error.message }, opts.json);
|
|
1050
1284
|
if (!opts.json) console.error(chalk.red(`Error: ${error.message}`));
|
|
@@ -1142,11 +1376,17 @@ export function agentsCommand() {
|
|
|
1142
1376
|
try {
|
|
1143
1377
|
const opencodeBin = resolveCommandPath('opencode');
|
|
1144
1378
|
const tmuxInstalled = hasCommand('tmux');
|
|
1379
|
+
const zellijInstalled = hasCommand('zellij');
|
|
1145
1380
|
const cfg = await loadAgentsConfig();
|
|
1146
1381
|
const defaultModel = cfg.agents.defaultModel || DEFAULT_SYNAPSE_MODEL;
|
|
1382
|
+
const configuredMux = validateMultiplexer(cfg.agents.multiplexer || 'auto');
|
|
1383
|
+
const resolvedMux = resolveMultiplexer(configuredMux, tmuxInstalled, zellijInstalled);
|
|
1147
1384
|
const result = {
|
|
1148
1385
|
opencodeBin,
|
|
1149
1386
|
tmuxInstalled,
|
|
1387
|
+
zellijInstalled,
|
|
1388
|
+
configuredMultiplexer: configuredMux,
|
|
1389
|
+
resolvedMultiplexer: resolvedMux,
|
|
1150
1390
|
defaultModel,
|
|
1151
1391
|
defaultRoleModels: cfg.agents.defaultRoleModels,
|
|
1152
1392
|
preflight: null,
|
|
@@ -1166,12 +1406,14 @@ export function agentsCommand() {
|
|
|
1166
1406
|
if (!opts.json) {
|
|
1167
1407
|
console.log(chalk.bold('SynapseGrid doctor'));
|
|
1168
1408
|
console.log(chalk.dim(`opencode: ${opencodeBin || 'not found'}`));
|
|
1409
|
+
console.log(chalk.dim(`zellij: ${zellijInstalled ? 'installed' : 'not installed'}`));
|
|
1169
1410
|
console.log(chalk.dim(`tmux: ${tmuxInstalled ? 'installed' : 'not installed'}`));
|
|
1411
|
+
console.log(chalk.dim(`multiplexer: configured=${configuredMux} resolved=${resolvedMux || 'none'}`));
|
|
1170
1412
|
console.log(chalk.dim(`default model: ${defaultModel}`));
|
|
1171
1413
|
console.log(chalk.dim(`preflight: ${result.preflight?.ok ? 'ok' : `failed - ${result.preflight?.error || 'unknown error'}`}`));
|
|
1172
1414
|
}
|
|
1173
1415
|
|
|
1174
|
-
if (!result.preflight?.ok) process.exit(1);
|
|
1416
|
+
if (!result.preflight?.ok || !result.resolvedMultiplexer) process.exit(1);
|
|
1175
1417
|
} catch (error) {
|
|
1176
1418
|
output({ error: error.message }, opts.json);
|
|
1177
1419
|
if (!opts.json) console.error(chalk.red(`Error: ${error.message}`));
|
package/src/commands/init.js
CHANGED
|
@@ -156,7 +156,8 @@ async function setupPersistentMemory() {
|
|
|
156
156
|
async function setupCollaborativeSystem() {
|
|
157
157
|
const hasOpenCode = hasCommand('opencode');
|
|
158
158
|
const hasTmux = hasCommand('tmux');
|
|
159
|
-
const
|
|
159
|
+
const hasZellij = hasCommand('zellij');
|
|
160
|
+
const alreadyReady = hasOpenCode && (hasZellij || hasTmux);
|
|
160
161
|
|
|
161
162
|
console.log();
|
|
162
163
|
await animatedSeparator(60);
|
|
@@ -168,10 +169,10 @@ async function setupCollaborativeSystem() {
|
|
|
168
169
|
console.log(
|
|
169
170
|
chalk.hex('#636E72')(
|
|
170
171
|
` ${COLLAB_SYSTEM_NAME} launches a real-time collaborative agent war-room with\n` +
|
|
171
|
-
' 4 coordinated roles (planner, critic, coder, reviewer) in
|
|
172
|
+
' 4 coordinated roles (planner, critic, coder, reviewer) in multiplexer panes.\n\n' +
|
|
172
173
|
' Each round is turn-based with shared incremental context, so every\n' +
|
|
173
174
|
' contribution from one agent is fed to the next, not isolated fan-out.\n\n' +
|
|
174
|
-
` Dependencies: ${chalk.hex('#DFE6E9')('OpenCode')} + ${chalk.hex('#DFE6E9')('tmux')}`
|
|
175
|
+
` Dependencies: ${chalk.hex('#DFE6E9')('OpenCode')} + ${chalk.hex('#DFE6E9')('zellij')} (${chalk.hex('#DFE6E9')('tmux')} fallback)`
|
|
175
176
|
)
|
|
176
177
|
);
|
|
177
178
|
console.log();
|
|
@@ -223,7 +224,8 @@ async function setupCollaborativeSystem() {
|
|
|
223
224
|
|
|
224
225
|
if (alreadyReady) {
|
|
225
226
|
console.log();
|
|
226
|
-
|
|
227
|
+
const mux = hasZellij ? 'zellij' : 'tmux';
|
|
228
|
+
console.log(chalk.hex('#00B894')(` ◆ OpenCode and ${mux} are already available.`));
|
|
227
229
|
await installCollabMcpConnections();
|
|
228
230
|
console.log(chalk.hex('#636E72')(' Run `acfm agents start --task "..."` to launch collaboration.'));
|
|
229
231
|
console.log();
|
|
@@ -234,11 +236,13 @@ async function setupCollaborativeSystem() {
|
|
|
234
236
|
console.log(chalk.hex('#B2BEC3')(` Installing ${COLLAB_SYSTEM_NAME} dependencies...`));
|
|
235
237
|
console.log();
|
|
236
238
|
|
|
237
|
-
const result = ensureCollabDependencies();
|
|
239
|
+
const result = ensureCollabDependencies({ installZellij: true, installTmux: true });
|
|
238
240
|
|
|
239
241
|
const oColor = result.opencode.success ? chalk.hex('#00B894') : chalk.hex('#D63031');
|
|
242
|
+
const zColor = result.zellij.success ? chalk.hex('#00B894') : chalk.hex('#D63031');
|
|
240
243
|
const tColor = result.tmux.success ? chalk.hex('#00B894') : chalk.hex('#D63031');
|
|
241
244
|
console.log(oColor(` ◆ OpenCode: ${result.opencode.message}`));
|
|
245
|
+
console.log(zColor(` ◆ zellij: ${result.zellij.message}`));
|
|
242
246
|
console.log(tColor(` ◆ tmux: ${result.tmux.message}`));
|
|
243
247
|
console.log();
|
|
244
248
|
|
package/src/mcp/collab-server.js
CHANGED
|
@@ -17,7 +17,13 @@ import { COLLAB_ROLES } from '../agents/constants.js';
|
|
|
17
17
|
import { buildEffectiveRoleModels, sanitizeRoleModels } from '../agents/model-selection.js';
|
|
18
18
|
import { runWorkerIteration } from '../agents/orchestrator.js';
|
|
19
19
|
import { getSessionDir } from '../agents/state-store.js';
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
spawnTmuxSession,
|
|
22
|
+
spawnZellijSession,
|
|
23
|
+
tmuxSessionExists,
|
|
24
|
+
zellijSessionExists,
|
|
25
|
+
resolveMultiplexer,
|
|
26
|
+
} from '../agents/runtime.js';
|
|
21
27
|
import {
|
|
22
28
|
addUserMessage,
|
|
23
29
|
createSession,
|
|
@@ -29,6 +35,7 @@ import {
|
|
|
29
35
|
stopSession,
|
|
30
36
|
} from '../agents/state-store.js';
|
|
31
37
|
import { hasCommand, resolveCommandPath } from '../services/dependency-installer.js';
|
|
38
|
+
import { loadAgentsConfig } from '../agents/config-store.js';
|
|
32
39
|
|
|
33
40
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
34
41
|
const runnerPath = resolve(__dirname, '../../bin/acfm.js');
|
|
@@ -67,6 +74,11 @@ function launchAutopilot(sessionId) {
|
|
|
67
74
|
child.unref();
|
|
68
75
|
}
|
|
69
76
|
|
|
77
|
+
async function muxExists(multiplexer, sessionName) {
|
|
78
|
+
if (multiplexer === 'zellij') return zellijSessionExists(sessionName);
|
|
79
|
+
return tmuxSessionExists(sessionName);
|
|
80
|
+
}
|
|
81
|
+
|
|
70
82
|
class MCPCollabServer {
|
|
71
83
|
constructor() {
|
|
72
84
|
this.server = new McpServer({
|
|
@@ -92,7 +104,7 @@ class MCPCollabServer {
|
|
|
92
104
|
reviewer: z.string().optional(),
|
|
93
105
|
}).partial().optional().describe('Optional per-role models (provider/model)'),
|
|
94
106
|
cwd: z.string().optional().describe('Working directory for agents'),
|
|
95
|
-
spawnWorkers: z.boolean().default(true).describe('Create
|
|
107
|
+
spawnWorkers: z.boolean().default(true).describe('Create multiplexer workers and panes'),
|
|
96
108
|
runPolicy: z.object({
|
|
97
109
|
timeoutPerRoleMs: z.number().int().positive().optional(),
|
|
98
110
|
retryOnTimeout: z.number().int().min(0).optional(),
|
|
@@ -107,8 +119,11 @@ class MCPCollabServer {
|
|
|
107
119
|
throw new Error('OpenCode binary not found in PATH. Run: acfm agents setup');
|
|
108
120
|
}
|
|
109
121
|
|
|
110
|
-
|
|
111
|
-
|
|
122
|
+
const config = await loadAgentsConfig();
|
|
123
|
+
const configuredMux = config.agents.multiplexer || 'auto';
|
|
124
|
+
const multiplexer = resolveMultiplexer(configuredMux, hasCommand('tmux'), hasCommand('zellij'));
|
|
125
|
+
if (spawnWorkers && !multiplexer) {
|
|
126
|
+
throw new Error('No multiplexer found (zellij/tmux). Run: acfm agents setup');
|
|
112
127
|
}
|
|
113
128
|
|
|
114
129
|
const state = await createSession(task, {
|
|
@@ -119,18 +134,31 @@ class MCPCollabServer {
|
|
|
119
134
|
workingDirectory,
|
|
120
135
|
opencodeBin,
|
|
121
136
|
runPolicy,
|
|
137
|
+
multiplexer: multiplexer || configuredMux,
|
|
122
138
|
});
|
|
123
139
|
let updated = state;
|
|
124
140
|
if (spawnWorkers) {
|
|
125
|
-
const
|
|
141
|
+
const sessionName = `acfm-synapse-${state.sessionId.slice(0, 8)}`;
|
|
126
142
|
const sessionDir = getSessionDir(state.sessionId);
|
|
127
|
-
|
|
128
|
-
|
|
143
|
+
if (multiplexer === 'zellij') {
|
|
144
|
+
await spawnZellijSession({ sessionName, sessionDir, sessionId: state.sessionId });
|
|
145
|
+
} else {
|
|
146
|
+
await spawnTmuxSession({ sessionName, sessionDir, sessionId: state.sessionId });
|
|
147
|
+
}
|
|
148
|
+
updated = await saveSessionState({
|
|
149
|
+
...state,
|
|
150
|
+
multiplexer,
|
|
151
|
+
multiplexerSessionName: sessionName,
|
|
152
|
+
tmuxSessionName: multiplexer === 'tmux' ? sessionName : null,
|
|
153
|
+
});
|
|
129
154
|
}
|
|
130
155
|
await setCurrentSession(state.sessionId);
|
|
131
156
|
|
|
132
|
-
const
|
|
133
|
-
const
|
|
157
|
+
const mux = updated.multiplexer || null;
|
|
158
|
+
const muxSessionName = updated.multiplexerSessionName || updated.tmuxSessionName || null;
|
|
159
|
+
const attachCommand = muxSessionName
|
|
160
|
+
? (mux === 'zellij' ? `zellij attach ${muxSessionName}` : `tmux attach -t ${muxSessionName}`)
|
|
161
|
+
: null;
|
|
134
162
|
return {
|
|
135
163
|
content: [{
|
|
136
164
|
type: 'text',
|
|
@@ -142,7 +170,8 @@ class MCPCollabServer {
|
|
|
142
170
|
roleModels: updated.roleModels || {},
|
|
143
171
|
effectiveRoleModels: buildEffectiveRoleModels(updated, updated.model || null),
|
|
144
172
|
run: summarizeRun(updated),
|
|
145
|
-
|
|
173
|
+
multiplexer: mux,
|
|
174
|
+
multiplexerSessionName: muxSessionName,
|
|
146
175
|
attachCommand,
|
|
147
176
|
}, null, 2),
|
|
148
177
|
}],
|
|
@@ -169,7 +198,7 @@ class MCPCollabServer {
|
|
|
169
198
|
throw new Error(`Session is ${state.status}. Resume/start before invoking.`);
|
|
170
199
|
}
|
|
171
200
|
|
|
172
|
-
if (!state.tmuxSessionName) {
|
|
201
|
+
if (!state.multiplexerSessionName && !state.tmuxSessionName) {
|
|
173
202
|
launchAutopilot(state.sessionId);
|
|
174
203
|
}
|
|
175
204
|
|
|
@@ -193,8 +222,13 @@ class MCPCollabServer {
|
|
|
193
222
|
status: state.status,
|
|
194
223
|
run: summarizeRun(state),
|
|
195
224
|
latestEvent: latestRunEvent(state),
|
|
196
|
-
|
|
197
|
-
|
|
225
|
+
multiplexer: state.multiplexer || null,
|
|
226
|
+
multiplexerSessionName: state.multiplexerSessionName || state.tmuxSessionName || null,
|
|
227
|
+
attachCommand: state.multiplexerSessionName
|
|
228
|
+
? (state.multiplexer === 'zellij'
|
|
229
|
+
? `zellij attach ${state.multiplexerSessionName}`
|
|
230
|
+
: `tmux attach -t ${state.multiplexerSessionName}`)
|
|
231
|
+
: (state.tmuxSessionName ? `tmux attach -t ${state.tmuxSessionName}` : null),
|
|
198
232
|
}, null, 2),
|
|
199
233
|
}],
|
|
200
234
|
};
|
|
@@ -408,10 +442,10 @@ class MCPCollabServer {
|
|
|
408
442
|
|
|
409
443
|
this.server.tool(
|
|
410
444
|
'collab_resume_session',
|
|
411
|
-
'Resume session and recreate
|
|
445
|
+
'Resume session and recreate workers if needed',
|
|
412
446
|
{
|
|
413
447
|
sessionId: z.string().optional().describe('Session ID (defaults to current session)'),
|
|
414
|
-
recreateWorkers: z.boolean().default(true).describe('Recreate
|
|
448
|
+
recreateWorkers: z.boolean().default(true).describe('Recreate multiplexer session when missing'),
|
|
415
449
|
},
|
|
416
450
|
async ({ sessionId, recreateWorkers }) => {
|
|
417
451
|
try {
|
|
@@ -419,24 +453,37 @@ class MCPCollabServer {
|
|
|
419
453
|
if (!id) throw new Error('No active session found');
|
|
420
454
|
let state = await loadSessionState(id);
|
|
421
455
|
|
|
422
|
-
const
|
|
423
|
-
|
|
456
|
+
const multiplexer = state.multiplexer || resolveMultiplexer('auto', hasCommand('tmux'), hasCommand('zellij'));
|
|
457
|
+
if (!multiplexer) {
|
|
458
|
+
throw new Error('No multiplexer found (zellij/tmux). Run: acfm agents setup');
|
|
459
|
+
}
|
|
460
|
+
const sessionName = state.multiplexerSessionName || state.tmuxSessionName || `acfm-synapse-${state.sessionId.slice(0, 8)}`;
|
|
461
|
+
const sessionExists = await muxExists(multiplexer, sessionName);
|
|
424
462
|
|
|
425
|
-
if (!
|
|
426
|
-
if (!hasCommand('tmux')) {
|
|
427
|
-
throw new Error('tmux is not installed. Run: acfm agents setup');
|
|
428
|
-
}
|
|
463
|
+
if (!sessionExists && recreateWorkers) {
|
|
429
464
|
const sessionDir = getSessionDir(state.sessionId);
|
|
430
|
-
|
|
465
|
+
if (multiplexer === 'zellij') {
|
|
466
|
+
if (!hasCommand('zellij')) throw new Error('zellij is not installed. Run: acfm agents setup');
|
|
467
|
+
await spawnZellijSession({ sessionName, sessionDir, sessionId: state.sessionId });
|
|
468
|
+
} else {
|
|
469
|
+
if (!hasCommand('tmux')) throw new Error('tmux is not installed. Run: acfm agents setup');
|
|
470
|
+
await spawnTmuxSession({ sessionName, sessionDir, sessionId: state.sessionId });
|
|
471
|
+
}
|
|
431
472
|
}
|
|
432
473
|
|
|
433
474
|
state = await saveSessionState({
|
|
434
475
|
...state,
|
|
435
476
|
status: 'running',
|
|
436
|
-
|
|
477
|
+
multiplexer,
|
|
478
|
+
multiplexerSessionName: sessionName,
|
|
479
|
+
tmuxSessionName: multiplexer === 'tmux' ? sessionName : state.tmuxSessionName || null,
|
|
437
480
|
});
|
|
438
481
|
await setCurrentSession(state.sessionId);
|
|
439
482
|
|
|
483
|
+
const attachCommand = multiplexer === 'zellij'
|
|
484
|
+
? `zellij attach ${sessionName}`
|
|
485
|
+
: `tmux attach -t ${sessionName}`;
|
|
486
|
+
|
|
440
487
|
return {
|
|
441
488
|
content: [{
|
|
442
489
|
type: 'text',
|
|
@@ -444,8 +491,10 @@ class MCPCollabServer {
|
|
|
444
491
|
success: true,
|
|
445
492
|
sessionId: state.sessionId,
|
|
446
493
|
status: state.status,
|
|
447
|
-
|
|
448
|
-
|
|
494
|
+
multiplexer,
|
|
495
|
+
multiplexerSessionName: sessionName,
|
|
496
|
+
recreatedWorkers: !sessionExists && recreateWorkers,
|
|
497
|
+
attachCommand,
|
|
449
498
|
}, null, 2),
|
|
450
499
|
}],
|
|
451
500
|
};
|
|
@@ -17,6 +17,13 @@ function run(command, args, options = {}) {
|
|
|
17
17
|
});
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
function runInstallCommand(command) {
|
|
21
|
+
if (platform() === 'win32') {
|
|
22
|
+
return run('cmd.exe', ['/c', command], { stdio: 'inherit' });
|
|
23
|
+
}
|
|
24
|
+
return run('bash', ['-lc', command], { stdio: 'inherit' });
|
|
25
|
+
}
|
|
26
|
+
|
|
20
27
|
export function hasCommand(command) {
|
|
21
28
|
return Boolean(resolveCommandPath(command));
|
|
22
29
|
}
|
|
@@ -78,6 +85,29 @@ function resolveTmuxInstallCommand() {
|
|
|
78
85
|
return null;
|
|
79
86
|
}
|
|
80
87
|
|
|
88
|
+
function resolveZellijInstallCommand() {
|
|
89
|
+
if (platform() === 'darwin') {
|
|
90
|
+
if (hasCommand('brew')) return 'brew install zellij';
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (platform() === 'linux') {
|
|
95
|
+
if (hasCommand('apt-get')) return 'sudo apt-get update && sudo apt-get install -y zellij';
|
|
96
|
+
if (hasCommand('dnf')) return 'sudo dnf install -y zellij';
|
|
97
|
+
if (hasCommand('yum')) return 'sudo yum install -y zellij';
|
|
98
|
+
if (hasCommand('pacman')) return 'sudo pacman -S --noconfirm zellij';
|
|
99
|
+
if (hasCommand('zypper')) return 'sudo zypper --non-interactive install zellij';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (platform() === 'win32') {
|
|
103
|
+
if (hasCommand('winget')) return 'winget install --id zellij-org.zellij -e';
|
|
104
|
+
if (hasCommand('choco')) return 'choco install zellij -y';
|
|
105
|
+
if (hasCommand('scoop')) return 'scoop install zellij';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
81
111
|
export function installTmux() {
|
|
82
112
|
if (hasCommand('tmux')) {
|
|
83
113
|
return { success: true, installed: false, message: 'tmux already installed' };
|
|
@@ -92,7 +122,7 @@ export function installTmux() {
|
|
|
92
122
|
};
|
|
93
123
|
}
|
|
94
124
|
|
|
95
|
-
const result =
|
|
125
|
+
const result = runInstallCommand(installCommand);
|
|
96
126
|
if (result.status !== 0) {
|
|
97
127
|
return { success: false, installed: false, message: 'tmux installation command failed' };
|
|
98
128
|
}
|
|
@@ -106,12 +136,50 @@ export function installTmux() {
|
|
|
106
136
|
};
|
|
107
137
|
}
|
|
108
138
|
|
|
109
|
-
export function
|
|
139
|
+
export function installZellij() {
|
|
140
|
+
if (hasCommand('zellij')) {
|
|
141
|
+
return { success: true, installed: false, message: 'zellij already installed' };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const installCommand = resolveZellijInstallCommand();
|
|
145
|
+
if (!installCommand) {
|
|
146
|
+
return {
|
|
147
|
+
success: false,
|
|
148
|
+
installed: false,
|
|
149
|
+
message: 'No supported package manager detected for automatic zellij installation',
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const result = runInstallCommand(installCommand);
|
|
154
|
+
if (result.status !== 0) {
|
|
155
|
+
return { success: false, installed: false, message: 'zellij installation command failed' };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
success: hasCommand('zellij'),
|
|
160
|
+
installed: true,
|
|
161
|
+
message: hasCommand('zellij')
|
|
162
|
+
? 'zellij installed successfully'
|
|
163
|
+
: 'zellij installer finished but binary is not available in PATH yet',
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function ensureCollabDependencies(options = {}) {
|
|
168
|
+
const installTmuxEnabled = options.installTmux ?? true;
|
|
169
|
+
const installZellijEnabled = options.installZellij ?? true;
|
|
110
170
|
const opencode = installOpenCode();
|
|
111
|
-
const tmux =
|
|
171
|
+
const tmux = installTmuxEnabled
|
|
172
|
+
? installTmux()
|
|
173
|
+
: { success: hasCommand('tmux'), installed: false, message: hasCommand('tmux') ? 'tmux already installed' : 'tmux installation skipped' };
|
|
174
|
+
const zellij = installZellijEnabled
|
|
175
|
+
? installZellij()
|
|
176
|
+
: { success: hasCommand('zellij'), installed: false, message: hasCommand('zellij') ? 'zellij already installed' : 'zellij installation skipped' };
|
|
177
|
+
|
|
178
|
+
const hasMultiplexer = tmux.success || zellij.success;
|
|
112
179
|
return {
|
|
113
180
|
opencode,
|
|
114
181
|
tmux,
|
|
115
|
-
|
|
182
|
+
zellij,
|
|
183
|
+
success: opencode.success && hasMultiplexer,
|
|
116
184
|
};
|
|
117
185
|
}
|