ac-framework 1.9.9 → 2.1.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/README.md +5 -0
- package/package.json +1 -1
- package/src/agents/config-store.js +13 -0
- package/src/agents/orchestrator.js +46 -2
- package/src/agents/runtime.js +48 -6
- package/src/agents/state-store.js +169 -4
- package/src/commands/agents.js +272 -31
- package/src/commands/init.js +8 -1
- package/src/mcp/collab-server.js +22 -9
- package/src/services/dependency-installer.js +177 -3
package/README.md
CHANGED
|
@@ -147,6 +147,7 @@ Each role runs in turn against a shared, accumulating context so outputs from on
|
|
|
147
147
|
| `acfm agents start --task "..." --model-coder provider/model` | Start session with optional per-role models |
|
|
148
148
|
| `acfm agents start --task "..." --mux zellij` | Start session forcing zellij backend (`auto`/`tmux` also supported) |
|
|
149
149
|
| `acfm agents runtime get` | Show configured multiplexer backend (`auto`, `zellij`, `tmux`) |
|
|
150
|
+
| `acfm agents runtime install-zellij` | Download latest zellij release into `~/.acfm/tools/zellij` |
|
|
150
151
|
| `acfm agents runtime set zellij` | Persist preferred multiplexer backend |
|
|
151
152
|
| `acfm agents resume` | Resume a previous session and recreate workers if needed |
|
|
152
153
|
| `acfm agents list` | List recent SynapseGrid sessions |
|
|
@@ -155,6 +156,7 @@ Each role runs in turn against a shared, accumulating context so outputs from on
|
|
|
155
156
|
| `acfm agents logs` | Show recent worker logs (all roles or one role) |
|
|
156
157
|
| `acfm agents transcript --role all --limit 40` | Show captured cross-agent transcript |
|
|
157
158
|
| `acfm agents summary` | Show generated collaboration meeting summary |
|
|
159
|
+
| `acfm agents artifacts` | Show artifact paths/existence for current session |
|
|
158
160
|
| `acfm agents export --format md --out file.md` | Export transcript in Markdown or JSON |
|
|
159
161
|
| `acfm agents send "..."` | Send a new user message into the active session |
|
|
160
162
|
| `acfm agents status` | Show current collaborative session state |
|
|
@@ -187,12 +189,15 @@ When driving SynapseGrid from another agent via MCP, prefer asynchronous run too
|
|
|
187
189
|
- Configure role models directly at start (for example `--model-planner`, `--model-coder`) or persist defaults via `acfm agents model choose` / `acfm agents model set`.
|
|
188
190
|
- Default SynapseGrid model fallback is `opencode/mimo-v2-pro-free`.
|
|
189
191
|
- Run `acfm agents doctor` when panes look idle to confirm model/provider preflight health.
|
|
192
|
+
- When zellij is managed by AC Framework, its binary path is saved in `~/.acfm/config.json` and executed directly by SynapseGrid.
|
|
190
193
|
|
|
191
194
|
Each collaborative session now keeps human-readable artifacts under `~/.acfm/synapsegrid/<sessionId>/`:
|
|
192
195
|
- `transcript.jsonl`: full chronological message stream
|
|
193
196
|
- `turns/*.json`: one file per round/role turn with captured output metadata
|
|
194
197
|
- `meeting-log.md`: incremental meeting notes generated per turn
|
|
195
198
|
- `meeting-summary.md`: final consolidated summary (roles, decisions, open issues, risks, action items)
|
|
199
|
+
- `turns/raw/*.ndjson`: raw OpenCode event stream captured per role/round
|
|
200
|
+
- `turns/raw/*.stderr.log`: stderr capture per role/round
|
|
196
201
|
|
|
197
202
|
### Spec Workflow
|
|
198
203
|
|
package/package.json
CHANGED
|
@@ -12,11 +12,24 @@ function normalizeConfig(raw) {
|
|
|
12
12
|
const agents = raw?.agents && typeof raw.agents === 'object' ? raw.agents : {};
|
|
13
13
|
const configuredMultiplexer = typeof agents.multiplexer === 'string' ? agents.multiplexer.trim().toLowerCase() : '';
|
|
14
14
|
const multiplexer = ['auto', 'zellij', 'tmux'].includes(configuredMultiplexer) ? configuredMultiplexer : 'auto';
|
|
15
|
+
const zellij = agents?.zellij && typeof agents.zellij === 'object' ? agents.zellij : {};
|
|
16
|
+
const zellijStrategy = typeof zellij.strategy === 'string' ? zellij.strategy.trim().toLowerCase() : 'auto';
|
|
17
|
+
const strategy = ['auto', 'managed', 'system'].includes(zellijStrategy) ? zellijStrategy : 'auto';
|
|
18
|
+
const binaryPath = typeof zellij.binaryPath === 'string' && zellij.binaryPath.trim() ? zellij.binaryPath.trim() : null;
|
|
19
|
+
const version = typeof zellij.version === 'string' && zellij.version.trim() ? zellij.version.trim() : null;
|
|
20
|
+
const source = typeof zellij.source === 'string' && zellij.source.trim() ? zellij.source.trim() : null;
|
|
21
|
+
|
|
15
22
|
return {
|
|
16
23
|
agents: {
|
|
17
24
|
defaultModel: normalizeModelId(agents.defaultModel) || DEFAULT_SYNAPSE_MODEL,
|
|
18
25
|
defaultRoleModels: sanitizeRoleModels(agents.defaultRoleModels),
|
|
19
26
|
multiplexer,
|
|
27
|
+
zellij: {
|
|
28
|
+
strategy,
|
|
29
|
+
binaryPath,
|
|
30
|
+
version,
|
|
31
|
+
source,
|
|
32
|
+
},
|
|
20
33
|
},
|
|
21
34
|
};
|
|
22
35
|
}
|
|
@@ -11,10 +11,12 @@ import {
|
|
|
11
11
|
} from './run-state.js';
|
|
12
12
|
import {
|
|
13
13
|
addAgentMessage,
|
|
14
|
+
appendTurnRawCapture,
|
|
14
15
|
appendMeetingTurn,
|
|
15
16
|
loadSessionState,
|
|
16
17
|
saveSessionState,
|
|
17
18
|
stopSession,
|
|
19
|
+
updateSessionDiagnostics,
|
|
18
20
|
writeMeetingSummary,
|
|
19
21
|
withSessionLock,
|
|
20
22
|
} from './state-store.js';
|
|
@@ -33,6 +35,21 @@ async function finalizeSessionArtifacts(state) {
|
|
|
33
35
|
});
|
|
34
36
|
}
|
|
35
37
|
|
|
38
|
+
async function maybeRecordNoTurnDiagnostic(state) {
|
|
39
|
+
const runState = ensureRunState(state);
|
|
40
|
+
const hasTurnEvents = Array.isArray(runState.events)
|
|
41
|
+
? runState.events.some((event) => event?.type === 'role_succeeded' || event?.type === 'role_failed')
|
|
42
|
+
: false;
|
|
43
|
+
if (!hasTurnEvents) {
|
|
44
|
+
await updateSessionDiagnostics(state.sessionId, {
|
|
45
|
+
warning: 'Session ended before any role turn was captured',
|
|
46
|
+
runStatus: runState.status,
|
|
47
|
+
stateStatus: state.status,
|
|
48
|
+
runEventCount: Array.isArray(runState.events) ? runState.events.length : 0,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
36
53
|
function buildRuntimePrompt({ state, role }) {
|
|
37
54
|
const roleContext = ROLE_SYSTEM_PROMPTS[role] || '';
|
|
38
55
|
const collaborativePrompt = buildAgentPrompt({
|
|
@@ -162,6 +179,7 @@ export async function runTurn(sessionId, options = {}) {
|
|
|
162
179
|
if (shouldStop(state)) {
|
|
163
180
|
if (state.status === 'running') {
|
|
164
181
|
state = await stopSession(state, 'completed');
|
|
182
|
+
await maybeRecordNoTurnDiagnostic(state);
|
|
165
183
|
state = await finalizeSessionArtifacts(state);
|
|
166
184
|
}
|
|
167
185
|
return state;
|
|
@@ -200,6 +218,7 @@ export async function runTurn(sessionId, options = {}) {
|
|
|
200
218
|
|
|
201
219
|
if (shouldStop(state)) {
|
|
202
220
|
state = await stopSession(state, 'completed');
|
|
221
|
+
await maybeRecordNoTurnDiagnostic(state);
|
|
203
222
|
state = await finalizeSessionArtifacts(state);
|
|
204
223
|
}
|
|
205
224
|
|
|
@@ -267,6 +286,7 @@ export async function executeActiveTurn(sessionId, role, options = {}) {
|
|
|
267
286
|
|
|
268
287
|
if (shouldStop(state)) {
|
|
269
288
|
state = await stopSession(state, 'completed');
|
|
289
|
+
await maybeRecordNoTurnDiagnostic(state);
|
|
270
290
|
state = await finalizeSessionArtifacts(state);
|
|
271
291
|
}
|
|
272
292
|
|
|
@@ -317,6 +337,8 @@ export async function runWorkerIteration(sessionId, role, options = {}) {
|
|
|
317
337
|
const prompt = buildRuntimePrompt({ state, role });
|
|
318
338
|
let content;
|
|
319
339
|
let outputEvents = [];
|
|
340
|
+
let outputStdout = '';
|
|
341
|
+
let outputStderr = '';
|
|
320
342
|
let effectiveModel = null;
|
|
321
343
|
let failed = false;
|
|
322
344
|
let errorMessage = '';
|
|
@@ -340,6 +362,8 @@ export async function runWorkerIteration(sessionId, role, options = {}) {
|
|
|
340
362
|
});
|
|
341
363
|
content = output.text;
|
|
342
364
|
outputEvents = output.events || [];
|
|
365
|
+
outputStdout = output.stdout || '';
|
|
366
|
+
outputStderr = output.stderr || '';
|
|
343
367
|
} catch (error) {
|
|
344
368
|
failed = true;
|
|
345
369
|
errorMessage = error.message;
|
|
@@ -348,15 +372,26 @@ export async function runWorkerIteration(sessionId, role, options = {}) {
|
|
|
348
372
|
|
|
349
373
|
state = await addAgentMessage(state, role, content);
|
|
350
374
|
if (failed) {
|
|
351
|
-
|
|
375
|
+
const failedTurn = createTurnRecord({
|
|
352
376
|
round: state.round,
|
|
353
377
|
role,
|
|
354
378
|
model: effectiveModel,
|
|
355
379
|
content,
|
|
356
380
|
events: outputEvents,
|
|
357
|
-
})
|
|
381
|
+
});
|
|
382
|
+
await appendMeetingTurn(sessionId, failedTurn);
|
|
383
|
+
await appendTurnRawCapture(sessionId, failedTurn, {
|
|
384
|
+
stdout: outputStdout,
|
|
385
|
+
stderr: outputStderr,
|
|
386
|
+
events: outputEvents,
|
|
387
|
+
});
|
|
388
|
+
await updateSessionDiagnostics(sessionId, {
|
|
389
|
+
lastError: errorMessage,
|
|
390
|
+
lastFailedRole: role,
|
|
391
|
+
});
|
|
358
392
|
state = await saveSessionState(applyRoleFailurePolicy(state, role, errorMessage));
|
|
359
393
|
if (state.status === 'failed') {
|
|
394
|
+
await maybeRecordNoTurnDiagnostic(state);
|
|
360
395
|
state = await finalizeSessionArtifacts(state);
|
|
361
396
|
}
|
|
362
397
|
return state;
|
|
@@ -370,6 +405,14 @@ export async function runWorkerIteration(sessionId, role, options = {}) {
|
|
|
370
405
|
events: outputEvents,
|
|
371
406
|
});
|
|
372
407
|
await appendMeetingTurn(sessionId, turnRecord);
|
|
408
|
+
await appendTurnRawCapture(sessionId, turnRecord, {
|
|
409
|
+
stdout: outputStdout,
|
|
410
|
+
stderr: outputStderr,
|
|
411
|
+
events: outputEvents,
|
|
412
|
+
});
|
|
413
|
+
await updateSessionDiagnostics(sessionId, {
|
|
414
|
+
lastError: null,
|
|
415
|
+
});
|
|
373
416
|
|
|
374
417
|
const updatedShared = updateSharedContext(ensureRunState(state).sharedContext, turnRecord);
|
|
375
418
|
const succeededRun = appendRunEvent({
|
|
@@ -397,6 +440,7 @@ export async function runWorkerIteration(sessionId, role, options = {}) {
|
|
|
397
440
|
...state,
|
|
398
441
|
run: finalRun,
|
|
399
442
|
});
|
|
443
|
+
await maybeRecordNoTurnDiagnostic(state);
|
|
400
444
|
state = await finalizeSessionArtifacts(state);
|
|
401
445
|
}
|
|
402
446
|
|
package/src/agents/runtime.js
CHANGED
|
@@ -43,6 +43,10 @@ function runCommand(command, args, options = {}) {
|
|
|
43
43
|
});
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
function sleep(ms) {
|
|
47
|
+
return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
|
|
48
|
+
}
|
|
49
|
+
|
|
46
50
|
function workerCommand(sessionId, role, roleLog) {
|
|
47
51
|
return `bash -lc 'node "${runnerPath}" agents worker --session ${sessionId} --role ${role} 2>&1 | tee -a "${roleLog}"'`;
|
|
48
52
|
}
|
|
@@ -120,16 +124,53 @@ async function writeZellijLayout({ layoutPath, sessionId, sessionDir }) {
|
|
|
120
124
|
await writeFile(layoutPath, content, 'utf8');
|
|
121
125
|
}
|
|
122
126
|
|
|
123
|
-
export async function spawnZellijSession({
|
|
127
|
+
export async function spawnZellijSession({
|
|
128
|
+
sessionName,
|
|
129
|
+
sessionDir,
|
|
130
|
+
sessionId,
|
|
131
|
+
binaryPath,
|
|
132
|
+
waitForSessionMs = 10000,
|
|
133
|
+
pollIntervalMs = 250,
|
|
134
|
+
runCommandImpl,
|
|
135
|
+
spawnImpl,
|
|
136
|
+
}) {
|
|
124
137
|
const layoutPath = resolve(sessionDir, 'synapsegrid-layout.kdl');
|
|
125
138
|
await writeZellijLayout({ layoutPath, sessionId, sessionDir });
|
|
126
|
-
|
|
127
|
-
|
|
139
|
+
const command = binaryPath || process.env.ACFM_ZELLIJ_BIN || 'zellij';
|
|
140
|
+
|
|
141
|
+
const spawnFn = spawnImpl || spawn;
|
|
142
|
+
const child = spawnFn(command, ['--session', sessionName, '--layout', layoutPath], {
|
|
143
|
+
cwd: sessionDir,
|
|
144
|
+
env: process.env,
|
|
145
|
+
detached: true,
|
|
146
|
+
stdio: 'ignore',
|
|
147
|
+
});
|
|
148
|
+
if (typeof child.unref === 'function') {
|
|
149
|
+
child.unref();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const startedAt = Date.now();
|
|
153
|
+
while ((Date.now() - startedAt) < waitForSessionMs) {
|
|
154
|
+
// eslint-disable-next-line no-await-in-loop
|
|
155
|
+
const exists = await zellijSessionExists(sessionName, binaryPath, { runCommandImpl });
|
|
156
|
+
if (exists) {
|
|
157
|
+
return { layoutPath };
|
|
158
|
+
}
|
|
159
|
+
// eslint-disable-next-line no-await-in-loop
|
|
160
|
+
await sleep(pollIntervalMs);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
throw new Error(
|
|
164
|
+
`Timed out waiting for zellij session '${sessionName}' to start (binary: ${command}). ` +
|
|
165
|
+
'Try `acfm agents doctor` or fallback with `acfm agents start --mux tmux ...`'
|
|
166
|
+
);
|
|
128
167
|
}
|
|
129
168
|
|
|
130
|
-
export async function zellijSessionExists(sessionName) {
|
|
169
|
+
export async function zellijSessionExists(sessionName, binaryPath, options = {}) {
|
|
131
170
|
try {
|
|
132
|
-
const
|
|
171
|
+
const runner = options.runCommandImpl || runCommand;
|
|
172
|
+
const command = binaryPath || process.env.ACFM_ZELLIJ_BIN || 'zellij';
|
|
173
|
+
const result = await runner(command, ['list-sessions']);
|
|
133
174
|
const lines = result.stdout.split('\n').map((line) => line.trim()).filter(Boolean);
|
|
134
175
|
return lines.some((line) => line === sessionName || line.startsWith(`${sessionName} `));
|
|
135
176
|
} catch {
|
|
@@ -138,7 +179,8 @@ export async function zellijSessionExists(sessionName) {
|
|
|
138
179
|
}
|
|
139
180
|
|
|
140
181
|
export async function runZellij(args, options = {}) {
|
|
141
|
-
|
|
182
|
+
const command = options.binaryPath || process.env.ACFM_ZELLIJ_BIN || 'zellij';
|
|
183
|
+
return runCommand(command, args, options);
|
|
142
184
|
}
|
|
143
185
|
|
|
144
186
|
export function resolveMultiplexer(preferred = 'auto', hasTmuxCommand = false, hasZellijCommand = false) {
|
|
@@ -55,6 +55,28 @@ function getMeetingSummaryPath(sessionId) {
|
|
|
55
55
|
return join(getSessionDir(sessionId), 'meeting-summary.md');
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
function getTurnsRawDir(sessionId) {
|
|
59
|
+
return join(getTurnsDir(sessionId), 'raw');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getDiagnosticsPath(sessionId) {
|
|
63
|
+
return join(getSessionDir(sessionId), 'diagnostics.json');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function sessionArtifactPaths(sessionId) {
|
|
67
|
+
return {
|
|
68
|
+
sessionDir: getSessionDir(sessionId),
|
|
69
|
+
statePath: getSessionStatePath(sessionId),
|
|
70
|
+
transcriptPath: getTranscriptPath(sessionId),
|
|
71
|
+
turnsDir: getTurnsDir(sessionId),
|
|
72
|
+
turnsRawDir: getTurnsRawDir(sessionId),
|
|
73
|
+
meetingLogPath: getMeetingLogPath(sessionId),
|
|
74
|
+
meetingLogJsonlPath: getMeetingLogJsonlPath(sessionId),
|
|
75
|
+
meetingSummaryPath: getMeetingSummaryPath(sessionId),
|
|
76
|
+
diagnosticsPath: getDiagnosticsPath(sessionId),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
58
80
|
function initialState(task, options = {}) {
|
|
59
81
|
const sessionId = randomUUID();
|
|
60
82
|
const createdAt = new Date().toISOString();
|
|
@@ -94,13 +116,98 @@ export async function createSession(task, options = {}) {
|
|
|
94
116
|
await ensureSessionRoot();
|
|
95
117
|
const state = initialState(task, options);
|
|
96
118
|
const sessionDir = getSessionDir(state.sessionId);
|
|
119
|
+
const turnsDir = getTurnsDir(state.sessionId);
|
|
120
|
+
const turnsRawDir = getTurnsRawDir(state.sessionId);
|
|
121
|
+
const meetingLogPath = getMeetingLogPath(state.sessionId);
|
|
122
|
+
const meetingLogJsonlPath = getMeetingLogJsonlPath(state.sessionId);
|
|
123
|
+
const meetingSummaryPath = getMeetingSummaryPath(state.sessionId);
|
|
124
|
+
const diagnosticsPath = getDiagnosticsPath(state.sessionId);
|
|
97
125
|
await mkdir(sessionDir, { recursive: true });
|
|
126
|
+
await mkdir(turnsDir, { recursive: true });
|
|
127
|
+
await mkdir(turnsRawDir, { recursive: true });
|
|
98
128
|
await writeFile(getSessionStatePath(state.sessionId), JSON.stringify(state, null, 2) + '\n', 'utf8');
|
|
129
|
+
await writeFile(meetingLogPath, [
|
|
130
|
+
'# SynapseGrid Meeting Log',
|
|
131
|
+
'',
|
|
132
|
+
`Session: ${state.sessionId}`,
|
|
133
|
+
`Created: ${state.createdAt}`,
|
|
134
|
+
'',
|
|
135
|
+
'_No turns captured yet._',
|
|
136
|
+
'',
|
|
137
|
+
].join('\n'), 'utf8');
|
|
138
|
+
await writeFile(meetingLogJsonlPath, '', 'utf8');
|
|
139
|
+
await writeFile(meetingSummaryPath, [
|
|
140
|
+
'# SynapseGrid Meeting Summary',
|
|
141
|
+
'',
|
|
142
|
+
`Session: ${state.sessionId}`,
|
|
143
|
+
'Status: running',
|
|
144
|
+
'',
|
|
145
|
+
'Summary is pending until the first turn is captured.',
|
|
146
|
+
'',
|
|
147
|
+
].join('\n'), 'utf8');
|
|
148
|
+
await writeFile(diagnosticsPath, JSON.stringify({
|
|
149
|
+
sessionId: state.sessionId,
|
|
150
|
+
createdAt: state.createdAt,
|
|
151
|
+
turnCount: 0,
|
|
152
|
+
lastTurnAt: null,
|
|
153
|
+
lastError: null,
|
|
154
|
+
}, null, 2) + '\n', 'utf8');
|
|
99
155
|
await writeCurrentSession(state.sessionId, state.updatedAt);
|
|
100
156
|
await appendTranscript(state.sessionId, state.messages[0]);
|
|
101
157
|
return state;
|
|
102
158
|
}
|
|
103
159
|
|
|
160
|
+
export async function ensureSessionArtifacts(sessionId, state = null) {
|
|
161
|
+
const paths = sessionArtifactPaths(sessionId);
|
|
162
|
+
await mkdir(paths.sessionDir, { recursive: true });
|
|
163
|
+
await mkdir(paths.turnsDir, { recursive: true });
|
|
164
|
+
await mkdir(paths.turnsRawDir, { recursive: true });
|
|
165
|
+
|
|
166
|
+
const currentState = state || (existsSync(paths.statePath)
|
|
167
|
+
? JSON.parse(await readFile(paths.statePath, 'utf8'))
|
|
168
|
+
: null);
|
|
169
|
+
|
|
170
|
+
if (!existsSync(paths.meetingLogPath)) {
|
|
171
|
+
await writeFile(paths.meetingLogPath, [
|
|
172
|
+
'# SynapseGrid Meeting Log',
|
|
173
|
+
'',
|
|
174
|
+
`Session: ${sessionId}`,
|
|
175
|
+
`Created: ${currentState?.createdAt || new Date().toISOString()}`,
|
|
176
|
+
'',
|
|
177
|
+
'_No turns captured yet._',
|
|
178
|
+
'',
|
|
179
|
+
].join('\n'), 'utf8');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!existsSync(paths.meetingLogJsonlPath)) {
|
|
183
|
+
await writeFile(paths.meetingLogJsonlPath, '', 'utf8');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!existsSync(paths.meetingSummaryPath)) {
|
|
187
|
+
await writeFile(paths.meetingSummaryPath, [
|
|
188
|
+
'# SynapseGrid Meeting Summary',
|
|
189
|
+
'',
|
|
190
|
+
`Session: ${sessionId}`,
|
|
191
|
+
`Status: ${currentState?.status || 'running'}`,
|
|
192
|
+
'',
|
|
193
|
+
'Summary is pending until the first turn is captured.',
|
|
194
|
+
'',
|
|
195
|
+
].join('\n'), 'utf8');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (!existsSync(paths.diagnosticsPath)) {
|
|
199
|
+
await writeFile(paths.diagnosticsPath, JSON.stringify({
|
|
200
|
+
sessionId,
|
|
201
|
+
createdAt: currentState?.createdAt || new Date().toISOString(),
|
|
202
|
+
turnCount: 0,
|
|
203
|
+
lastTurnAt: null,
|
|
204
|
+
lastError: null,
|
|
205
|
+
}, null, 2) + '\n', 'utf8');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return paths;
|
|
209
|
+
}
|
|
210
|
+
|
|
104
211
|
export async function appendTranscript(sessionId, message) {
|
|
105
212
|
const transcriptPath = getTranscriptPath(sessionId);
|
|
106
213
|
const line = JSON.stringify(message) + '\n';
|
|
@@ -112,18 +219,25 @@ export async function appendTranscript(sessionId, message) {
|
|
|
112
219
|
}
|
|
113
220
|
|
|
114
221
|
export async function appendMeetingTurn(sessionId, turnRecord) {
|
|
115
|
-
const
|
|
116
|
-
|
|
222
|
+
const {
|
|
223
|
+
sessionDir,
|
|
224
|
+
turnsDir,
|
|
225
|
+
turnsRawDir,
|
|
226
|
+
meetingLogPath,
|
|
227
|
+
meetingLogJsonlPath,
|
|
228
|
+
diagnosticsPath,
|
|
229
|
+
} = sessionArtifactPaths(sessionId);
|
|
117
230
|
await mkdir(sessionDir, { recursive: true });
|
|
118
231
|
await mkdir(turnsDir, { recursive: true });
|
|
232
|
+
await mkdir(turnsRawDir, { recursive: true });
|
|
119
233
|
|
|
120
234
|
const safeRole = String(turnRecord?.role || 'unknown').replace(/[^a-z0-9_-]/gi, '_');
|
|
121
235
|
const safeRound = Number.isInteger(turnRecord?.round) ? turnRecord.round : 0;
|
|
122
236
|
const turnFilePath = join(turnsDir, `${String(safeRound).padStart(3, '0')}-${safeRole}.json`);
|
|
123
237
|
await writeFile(turnFilePath, JSON.stringify(turnRecord, null, 2) + '\n', 'utf8');
|
|
124
238
|
|
|
125
|
-
const mdPath =
|
|
126
|
-
const jsonlPath =
|
|
239
|
+
const mdPath = meetingLogPath;
|
|
240
|
+
const jsonlPath = meetingLogJsonlPath;
|
|
127
241
|
const snippet = (turnRecord?.snippet || '').trim() || '(empty output)';
|
|
128
242
|
const keyPoints = Array.isArray(turnRecord?.keyPoints) ? turnRecord.keyPoints : [];
|
|
129
243
|
|
|
@@ -149,6 +263,16 @@ export async function appendMeetingTurn(sessionId, turnRecord) {
|
|
|
149
263
|
}
|
|
150
264
|
|
|
151
265
|
await appendFile(jsonlPath, JSON.stringify(turnRecord) + '\n', 'utf8');
|
|
266
|
+
|
|
267
|
+
const diagnostics = existsSync(diagnosticsPath)
|
|
268
|
+
? JSON.parse(await readFile(diagnosticsPath, 'utf8'))
|
|
269
|
+
: { sessionId, turnCount: 0 };
|
|
270
|
+
diagnostics.turnCount = Number.isInteger(diagnostics.turnCount) ? diagnostics.turnCount + 1 : 1;
|
|
271
|
+
diagnostics.lastTurnAt = turnRecord?.timestamp || new Date().toISOString();
|
|
272
|
+
diagnostics.lastRole = safeRole;
|
|
273
|
+
diagnostics.lastRound = safeRound;
|
|
274
|
+
await writeFile(diagnosticsPath, JSON.stringify(diagnostics, null, 2) + '\n', 'utf8');
|
|
275
|
+
|
|
152
276
|
return {
|
|
153
277
|
turnFilePath,
|
|
154
278
|
meetingLogPath: mdPath,
|
|
@@ -156,6 +280,47 @@ export async function appendMeetingTurn(sessionId, turnRecord) {
|
|
|
156
280
|
};
|
|
157
281
|
}
|
|
158
282
|
|
|
283
|
+
export async function appendTurnRawCapture(sessionId, turnRecord, capture = {}) {
|
|
284
|
+
const { turnsRawDir } = sessionArtifactPaths(sessionId);
|
|
285
|
+
await mkdir(turnsRawDir, { recursive: true });
|
|
286
|
+
|
|
287
|
+
const safeRole = String(turnRecord?.role || 'unknown').replace(/[^a-z0-9_-]/gi, '_');
|
|
288
|
+
const safeRound = Number.isInteger(turnRecord?.round) ? turnRecord.round : 0;
|
|
289
|
+
const baseName = `${String(safeRound).padStart(3, '0')}-${safeRole}`;
|
|
290
|
+
const ndjsonPath = join(turnsRawDir, `${baseName}.ndjson`);
|
|
291
|
+
const stderrPath = join(turnsRawDir, `${baseName}.stderr.log`);
|
|
292
|
+
const metaPath = join(turnsRawDir, `${baseName}.meta.json`);
|
|
293
|
+
|
|
294
|
+
await writeFile(ndjsonPath, String(capture.stdout || ''), 'utf8');
|
|
295
|
+
await writeFile(stderrPath, String(capture.stderr || ''), 'utf8');
|
|
296
|
+
await writeFile(metaPath, JSON.stringify({
|
|
297
|
+
round: safeRound,
|
|
298
|
+
role: safeRole,
|
|
299
|
+
model: turnRecord?.model || null,
|
|
300
|
+
capturedAt: new Date().toISOString(),
|
|
301
|
+
stdoutBytes: Buffer.byteLength(String(capture.stdout || ''), 'utf8'),
|
|
302
|
+
stderrBytes: Buffer.byteLength(String(capture.stderr || ''), 'utf8'),
|
|
303
|
+
eventCount: Array.isArray(capture.events) ? capture.events.length : 0,
|
|
304
|
+
}, null, 2) + '\n', 'utf8');
|
|
305
|
+
|
|
306
|
+
return { ndjsonPath, stderrPath, metaPath };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export async function updateSessionDiagnostics(sessionId, patch = {}) {
|
|
310
|
+
const { diagnosticsPath, sessionDir } = sessionArtifactPaths(sessionId);
|
|
311
|
+
await mkdir(sessionDir, { recursive: true });
|
|
312
|
+
const current = existsSync(diagnosticsPath)
|
|
313
|
+
? JSON.parse(await readFile(diagnosticsPath, 'utf8'))
|
|
314
|
+
: { sessionId, turnCount: 0, lastTurnAt: null };
|
|
315
|
+
const next = {
|
|
316
|
+
...current,
|
|
317
|
+
...patch,
|
|
318
|
+
updatedAt: new Date().toISOString(),
|
|
319
|
+
};
|
|
320
|
+
await writeFile(diagnosticsPath, JSON.stringify(next, null, 2) + '\n', 'utf8');
|
|
321
|
+
return next;
|
|
322
|
+
}
|
|
323
|
+
|
|
159
324
|
export async function writeMeetingSummary(sessionId, summaryMarkdown) {
|
|
160
325
|
const outputPath = getMeetingSummaryPath(sessionId);
|
|
161
326
|
await writeFile(outputPath, String(summaryMarkdown || '').trimEnd() + '\n', 'utf8');
|
package/src/commands/agents.js
CHANGED
|
@@ -18,6 +18,7 @@ import { runWorkerIteration } from '../agents/orchestrator.js';
|
|
|
18
18
|
import {
|
|
19
19
|
addUserMessage,
|
|
20
20
|
createSession,
|
|
21
|
+
ensureSessionArtifacts,
|
|
21
22
|
getSessionDir,
|
|
22
23
|
loadCurrentSessionId,
|
|
23
24
|
loadSessionState,
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
saveSessionState,
|
|
27
28
|
setCurrentSession,
|
|
28
29
|
stopSession,
|
|
30
|
+
sessionArtifactPaths,
|
|
29
31
|
writeMeetingSummary,
|
|
30
32
|
} from '../agents/state-store.js';
|
|
31
33
|
import {
|
|
@@ -45,7 +47,13 @@ import {
|
|
|
45
47
|
normalizeModelId,
|
|
46
48
|
sanitizeRoleModels,
|
|
47
49
|
} from '../agents/model-selection.js';
|
|
48
|
-
import {
|
|
50
|
+
import {
|
|
51
|
+
ensureCollabDependencies,
|
|
52
|
+
hasCommand,
|
|
53
|
+
resolveCommandPath,
|
|
54
|
+
resolveManagedZellijPath,
|
|
55
|
+
installManagedZellijLatest,
|
|
56
|
+
} from '../services/dependency-installer.js';
|
|
49
57
|
|
|
50
58
|
function output(data, json) {
|
|
51
59
|
if (json) {
|
|
@@ -101,14 +109,42 @@ function sessionMuxName(state) {
|
|
|
101
109
|
return state.multiplexerSessionName || state.tmuxSessionName || `acfm-synapse-${state.sessionId.slice(0, 8)}`;
|
|
102
110
|
}
|
|
103
111
|
|
|
104
|
-
async function sessionExistsForMux(multiplexer, sessionName) {
|
|
105
|
-
if (multiplexer === 'zellij') return zellijSessionExists(sessionName);
|
|
112
|
+
async function sessionExistsForMux(multiplexer, sessionName, zellijPath = null) {
|
|
113
|
+
if (multiplexer === 'zellij') return zellijSessionExists(sessionName, zellijPath);
|
|
106
114
|
return tmuxSessionExists(sessionName);
|
|
107
115
|
}
|
|
108
116
|
|
|
109
|
-
|
|
117
|
+
function resolveConfiguredZellijPath(config) {
|
|
118
|
+
const strategy = config?.agents?.zellij?.strategy || 'auto';
|
|
119
|
+
if (strategy === 'system') {
|
|
120
|
+
return resolveCommandPath('zellij');
|
|
121
|
+
}
|
|
122
|
+
const managed = resolveManagedZellijPath(config);
|
|
123
|
+
if (managed) return managed;
|
|
124
|
+
return resolveCommandPath('zellij');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function shouldUseManagedZellij(config) {
|
|
128
|
+
const strategy = config?.agents?.zellij?.strategy || 'auto';
|
|
129
|
+
if (strategy === 'managed') return true;
|
|
130
|
+
if (strategy === 'system') return false;
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function resolveMultiplexerWithPaths(config, requestedMux = 'auto') {
|
|
135
|
+
const zellijPath = resolveConfiguredZellijPath(config);
|
|
136
|
+
const tmuxPath = resolveCommandPath('tmux');
|
|
137
|
+
const selected = resolveMultiplexer(requestedMux, Boolean(tmuxPath), Boolean(zellijPath));
|
|
138
|
+
return {
|
|
139
|
+
selected,
|
|
140
|
+
zellijPath,
|
|
141
|
+
tmuxPath,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function attachToMux(multiplexer, sessionName, readonly = false, zellijPath = null) {
|
|
110
146
|
if (multiplexer === 'zellij') {
|
|
111
|
-
await runZellij(['attach', sessionName], { stdio: 'inherit' });
|
|
147
|
+
await runZellij(['attach', sessionName], { stdio: 'inherit', binaryPath: zellijPath });
|
|
112
148
|
return;
|
|
113
149
|
}
|
|
114
150
|
const args = ['attach'];
|
|
@@ -206,6 +242,18 @@ async function readSessionArtifact(sessionId, filename) {
|
|
|
206
242
|
return readFile(path, 'utf8');
|
|
207
243
|
}
|
|
208
244
|
|
|
245
|
+
async function collectArtifactStatus(sessionId) {
|
|
246
|
+
await ensureSessionArtifacts(sessionId);
|
|
247
|
+
const paths = sessionArtifactPaths(sessionId);
|
|
248
|
+
return {
|
|
249
|
+
sessionId,
|
|
250
|
+
checkedAt: new Date().toISOString(),
|
|
251
|
+
artifacts: Object.fromEntries(
|
|
252
|
+
Object.entries(paths).map(([key, value]) => [key, { path: value, exists: existsSync(value) }])
|
|
253
|
+
),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
209
257
|
async function preflightModel({ opencodeBin, model, cwd }) {
|
|
210
258
|
const selected = normalizeModelId(model) || DEFAULT_SYNAPSE_MODEL;
|
|
211
259
|
try {
|
|
@@ -230,7 +278,9 @@ export function agentsCommand() {
|
|
|
230
278
|
Examples:
|
|
231
279
|
acfm agents start --task "Implement auth flow" --mux auto
|
|
232
280
|
acfm agents setup
|
|
281
|
+
acfm agents artifacts
|
|
233
282
|
acfm agents runtime get
|
|
283
|
+
acfm agents runtime install-zellij
|
|
234
284
|
acfm agents runtime set zellij
|
|
235
285
|
acfm agents model choose
|
|
236
286
|
acfm agents model list
|
|
@@ -270,28 +320,50 @@ Examples:
|
|
|
270
320
|
const result = ensureCollabDependencies({
|
|
271
321
|
installZellij,
|
|
272
322
|
installTmux,
|
|
323
|
+
preferManagedZellij: installZellij,
|
|
273
324
|
});
|
|
325
|
+
const awaited = await result;
|
|
274
326
|
let collabMcp = null;
|
|
275
327
|
|
|
276
|
-
if (
|
|
328
|
+
if (awaited.success) {
|
|
277
329
|
const { detectAndInstallMCPs } = await import('../services/mcp-installer.js');
|
|
278
330
|
collabMcp = detectAndInstallMCPs({ target: 'collab' });
|
|
279
331
|
}
|
|
280
332
|
|
|
281
|
-
const payload = { ...
|
|
333
|
+
const payload = { ...awaited, collabMcp };
|
|
282
334
|
output(payload, opts.json);
|
|
283
335
|
if (!opts.json) {
|
|
284
|
-
const oLabel =
|
|
285
|
-
const tLabel =
|
|
286
|
-
const zLabel =
|
|
287
|
-
console.log(`OpenCode: ${oLabel} - ${
|
|
288
|
-
console.log(`zellij: ${zLabel} - ${
|
|
289
|
-
|
|
336
|
+
const oLabel = awaited.opencode.success ? chalk.green('ok') : chalk.red('failed');
|
|
337
|
+
const tLabel = awaited.tmux.success ? chalk.green('ok') : chalk.red('failed');
|
|
338
|
+
const zLabel = awaited.zellij.success ? chalk.green('ok') : chalk.red('failed');
|
|
339
|
+
console.log(`OpenCode: ${oLabel} - ${awaited.opencode.message}`);
|
|
340
|
+
console.log(`zellij: ${zLabel} - ${awaited.zellij.message}`);
|
|
341
|
+
if (awaited.zellij.binaryPath) {
|
|
342
|
+
console.log(chalk.dim(` ${awaited.zellij.binaryPath}`));
|
|
343
|
+
}
|
|
344
|
+
console.log(`tmux: ${tLabel} - ${awaited.tmux.message}`);
|
|
290
345
|
if (collabMcp) {
|
|
291
346
|
console.log(`Collab MCP: ${chalk.green('ok')} - installed ${collabMcp.success}/${collabMcp.installed} on detected assistants`);
|
|
292
347
|
}
|
|
293
348
|
}
|
|
294
|
-
|
|
349
|
+
|
|
350
|
+
if (awaited.zellij.success && awaited.zellij.source === 'managed' && awaited.zellij.binaryPath) {
|
|
351
|
+
await updateAgentsConfig((current) => ({
|
|
352
|
+
agents: {
|
|
353
|
+
defaultModel: current.agents.defaultModel,
|
|
354
|
+
defaultRoleModels: { ...current.agents.defaultRoleModels },
|
|
355
|
+
multiplexer: current.agents.multiplexer || 'auto',
|
|
356
|
+
zellij: {
|
|
357
|
+
strategy: 'managed',
|
|
358
|
+
binaryPath: awaited.zellij.binaryPath,
|
|
359
|
+
version: awaited.zellij.version || null,
|
|
360
|
+
source: 'managed',
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
}));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (!awaited.success) process.exit(1);
|
|
295
367
|
});
|
|
296
368
|
|
|
297
369
|
agents
|
|
@@ -393,12 +465,14 @@ Examples:
|
|
|
393
465
|
try {
|
|
394
466
|
const sessionId = await ensureSessionId(true);
|
|
395
467
|
const state = await loadSessionState(sessionId);
|
|
468
|
+
const cfg = await loadAgentsConfig();
|
|
469
|
+
const zellijPath = resolveConfiguredZellijPath(cfg);
|
|
396
470
|
const multiplexer = state.multiplexer || 'tmux';
|
|
397
471
|
const muxSessionName = sessionMuxName(state);
|
|
398
472
|
if (!muxSessionName) {
|
|
399
473
|
throw new Error('No multiplexer session registered for active collaborative session');
|
|
400
474
|
}
|
|
401
|
-
await attachToMux(multiplexer, muxSessionName, false);
|
|
475
|
+
await attachToMux(multiplexer, muxSessionName, false, zellijPath);
|
|
402
476
|
} catch (error) {
|
|
403
477
|
console.error(chalk.red(`Error: ${error.message}`));
|
|
404
478
|
process.exit(1);
|
|
@@ -413,16 +487,18 @@ Examples:
|
|
|
413
487
|
try {
|
|
414
488
|
const sessionId = await ensureSessionId(true);
|
|
415
489
|
const state = await loadSessionState(sessionId);
|
|
490
|
+
const cfg = await loadAgentsConfig();
|
|
491
|
+
const zellijPath = resolveConfiguredZellijPath(cfg);
|
|
416
492
|
const multiplexer = state.multiplexer || 'tmux';
|
|
417
493
|
const muxSessionName = sessionMuxName(state);
|
|
418
494
|
if (!muxSessionName) {
|
|
419
495
|
throw new Error('No multiplexer session registered for active collaborative session');
|
|
420
496
|
}
|
|
421
|
-
const sessionExists = await sessionExistsForMux(multiplexer, muxSessionName);
|
|
497
|
+
const sessionExists = await sessionExistsForMux(multiplexer, muxSessionName, zellijPath);
|
|
422
498
|
if (!sessionExists) {
|
|
423
499
|
throw new Error(`${multiplexer} session ${muxSessionName} no longer exists. Run: acfm agents resume`);
|
|
424
500
|
}
|
|
425
|
-
await attachToMux(multiplexer, muxSessionName, Boolean(opts.readonly));
|
|
501
|
+
await attachToMux(multiplexer, muxSessionName, Boolean(opts.readonly), zellijPath);
|
|
426
502
|
} catch (error) {
|
|
427
503
|
console.error(chalk.red(`Error: ${error.message}`));
|
|
428
504
|
process.exit(1);
|
|
@@ -441,21 +517,29 @@ Examples:
|
|
|
441
517
|
const sessionId = opts.session || await ensureSessionId(true);
|
|
442
518
|
let state = await loadSessionState(sessionId);
|
|
443
519
|
const multiplexer = state.multiplexer || 'tmux';
|
|
520
|
+
const cfg = await loadAgentsConfig();
|
|
521
|
+
const zellijPath = resolveConfiguredZellijPath(cfg);
|
|
522
|
+
const tmuxPath = resolveCommandPath('tmux');
|
|
444
523
|
|
|
445
|
-
if (multiplexer === 'zellij' && !
|
|
524
|
+
if (multiplexer === 'zellij' && !zellijPath) {
|
|
446
525
|
throw new Error('zellij is not installed. Run: acfm agents setup');
|
|
447
526
|
}
|
|
448
|
-
if (multiplexer === 'tmux' && !
|
|
527
|
+
if (multiplexer === 'tmux' && !tmuxPath) {
|
|
449
528
|
throw new Error('tmux is not installed. Run: acfm agents setup');
|
|
450
529
|
}
|
|
451
530
|
|
|
452
531
|
const muxSessionName = sessionMuxName(state);
|
|
453
|
-
const muxExists = await sessionExistsForMux(multiplexer, muxSessionName);
|
|
532
|
+
const muxExists = await sessionExistsForMux(multiplexer, muxSessionName, zellijPath);
|
|
454
533
|
|
|
455
534
|
if (!muxExists && opts.recreate) {
|
|
456
535
|
const sessionDir = getSessionDir(state.sessionId);
|
|
457
536
|
if (multiplexer === 'zellij') {
|
|
458
|
-
await spawnZellijSession({
|
|
537
|
+
await spawnZellijSession({
|
|
538
|
+
sessionName: muxSessionName,
|
|
539
|
+
sessionDir,
|
|
540
|
+
sessionId: state.sessionId,
|
|
541
|
+
binaryPath: zellijPath,
|
|
542
|
+
});
|
|
459
543
|
} else {
|
|
460
544
|
await spawnTmuxSession({ sessionName: muxSessionName, sessionDir, sessionId: state.sessionId });
|
|
461
545
|
}
|
|
@@ -484,7 +568,7 @@ Examples:
|
|
|
484
568
|
}
|
|
485
569
|
|
|
486
570
|
if (opts.attach) {
|
|
487
|
-
await attachToMux(multiplexer, muxSessionName, false);
|
|
571
|
+
await attachToMux(multiplexer, muxSessionName, false, zellijPath);
|
|
488
572
|
}
|
|
489
573
|
} catch (error) {
|
|
490
574
|
output({ error: error.message }, opts.json);
|
|
@@ -564,15 +648,19 @@ Examples:
|
|
|
564
648
|
try {
|
|
565
649
|
const cfg = await loadAgentsConfig();
|
|
566
650
|
const configured = validateMultiplexer(cfg.agents.multiplexer || 'auto');
|
|
567
|
-
const
|
|
651
|
+
const zellijPath = resolveConfiguredZellijPath(cfg);
|
|
652
|
+
const tmuxPath = resolveCommandPath('tmux');
|
|
653
|
+
const resolved = resolveMultiplexer(configured, Boolean(tmuxPath), Boolean(zellijPath));
|
|
568
654
|
const payload = {
|
|
569
655
|
configPath: getAgentsConfigPath(),
|
|
570
656
|
multiplexer: configured,
|
|
571
657
|
resolved,
|
|
572
658
|
available: {
|
|
573
|
-
zellij:
|
|
574
|
-
tmux:
|
|
659
|
+
zellij: Boolean(zellijPath),
|
|
660
|
+
tmux: Boolean(tmuxPath),
|
|
575
661
|
},
|
|
662
|
+
zellij: cfg.agents.zellij,
|
|
663
|
+
zellijPath,
|
|
576
664
|
};
|
|
577
665
|
output(payload, opts.json);
|
|
578
666
|
if (!opts.json) {
|
|
@@ -581,6 +669,9 @@ Examples:
|
|
|
581
669
|
console.log(chalk.dim(`Configured: ${configured}`));
|
|
582
670
|
console.log(chalk.dim(`Resolved: ${resolved || 'none'}`));
|
|
583
671
|
console.log(chalk.dim(`zellij=${payload.available.zellij} tmux=${payload.available.tmux}`));
|
|
672
|
+
if (payload.zellijPath) {
|
|
673
|
+
console.log(chalk.dim(`zellij path: ${payload.zellijPath}`));
|
|
674
|
+
}
|
|
584
675
|
}
|
|
585
676
|
} catch (error) {
|
|
586
677
|
output({ error: error.message }, opts.json);
|
|
@@ -601,9 +692,14 @@ Examples:
|
|
|
601
692
|
defaultModel: current.agents.defaultModel,
|
|
602
693
|
defaultRoleModels: { ...current.agents.defaultRoleModels },
|
|
603
694
|
multiplexer: selected,
|
|
695
|
+
zellij: {
|
|
696
|
+
...(current.agents.zellij || { strategy: 'auto', binaryPath: null, version: null, source: null }),
|
|
697
|
+
},
|
|
604
698
|
},
|
|
605
699
|
}));
|
|
606
|
-
const
|
|
700
|
+
const zellijPath = resolveConfiguredZellijPath(updated);
|
|
701
|
+
const tmuxPath = resolveCommandPath('tmux');
|
|
702
|
+
const resolved = resolveMultiplexer(updated.agents.multiplexer, Boolean(tmuxPath), Boolean(zellijPath));
|
|
607
703
|
const payload = {
|
|
608
704
|
success: true,
|
|
609
705
|
configPath: getAgentsConfigPath(),
|
|
@@ -623,6 +719,46 @@ Examples:
|
|
|
623
719
|
}
|
|
624
720
|
});
|
|
625
721
|
|
|
722
|
+
runtime
|
|
723
|
+
.command('install-zellij')
|
|
724
|
+
.description('Install latest zellij release managed by AC Framework')
|
|
725
|
+
.option('--json', 'Output as JSON')
|
|
726
|
+
.action(async (opts) => {
|
|
727
|
+
try {
|
|
728
|
+
const result = await installManagedZellijLatest();
|
|
729
|
+
if (!result.success) {
|
|
730
|
+
output(result, opts.json);
|
|
731
|
+
if (!opts.json) console.error(chalk.red(`Error: ${result.message}`));
|
|
732
|
+
process.exit(1);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
await updateAgentsConfig((current) => ({
|
|
736
|
+
agents: {
|
|
737
|
+
defaultModel: current.agents.defaultModel,
|
|
738
|
+
defaultRoleModels: { ...current.agents.defaultRoleModels },
|
|
739
|
+
multiplexer: current.agents.multiplexer || 'auto',
|
|
740
|
+
zellij: {
|
|
741
|
+
strategy: result.source === 'system' ? 'system' : 'managed',
|
|
742
|
+
binaryPath: result.binaryPath,
|
|
743
|
+
version: result.version || null,
|
|
744
|
+
source: result.source || 'managed',
|
|
745
|
+
},
|
|
746
|
+
},
|
|
747
|
+
}));
|
|
748
|
+
|
|
749
|
+
output(result, opts.json);
|
|
750
|
+
if (!opts.json) {
|
|
751
|
+
console.log(chalk.green('✓ Managed zellij ready'));
|
|
752
|
+
console.log(chalk.dim(` Version: ${result.version || 'unknown'}`));
|
|
753
|
+
console.log(chalk.dim(` Binary: ${result.binaryPath}`));
|
|
754
|
+
}
|
|
755
|
+
} catch (error) {
|
|
756
|
+
output({ error: error.message }, opts.json);
|
|
757
|
+
if (!opts.json) console.error(chalk.red(`Error: ${error.message}`));
|
|
758
|
+
process.exit(1);
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
|
|
626
762
|
model
|
|
627
763
|
.command('list')
|
|
628
764
|
.description('List available OpenCode models grouped by provider')
|
|
@@ -767,6 +903,9 @@ Examples:
|
|
|
767
903
|
defaultModel: current.agents.defaultModel,
|
|
768
904
|
defaultRoleModels: { ...current.agents.defaultRoleModels },
|
|
769
905
|
multiplexer: current.agents.multiplexer || 'auto',
|
|
906
|
+
zellij: {
|
|
907
|
+
...(current.agents.zellij || { strategy: 'auto', binaryPath: null, version: null, source: null }),
|
|
908
|
+
},
|
|
770
909
|
},
|
|
771
910
|
};
|
|
772
911
|
|
|
@@ -828,6 +967,9 @@ Examples:
|
|
|
828
967
|
defaultModel: current.agents.defaultModel,
|
|
829
968
|
defaultRoleModels: { ...current.agents.defaultRoleModels },
|
|
830
969
|
multiplexer: current.agents.multiplexer || 'auto',
|
|
970
|
+
zellij: {
|
|
971
|
+
...(current.agents.zellij || { strategy: 'auto', binaryPath: null, version: null, source: null }),
|
|
972
|
+
},
|
|
831
973
|
},
|
|
832
974
|
};
|
|
833
975
|
if (role === 'all') {
|
|
@@ -877,6 +1019,9 @@ Examples:
|
|
|
877
1019
|
defaultModel: current.agents.defaultModel,
|
|
878
1020
|
defaultRoleModels: { ...current.agents.defaultRoleModels },
|
|
879
1021
|
multiplexer: current.agents.multiplexer || 'auto',
|
|
1022
|
+
zellij: {
|
|
1023
|
+
...(current.agents.zellij || { strategy: 'auto', binaryPath: null, version: null, source: null }),
|
|
1024
|
+
},
|
|
880
1025
|
},
|
|
881
1026
|
};
|
|
882
1027
|
if (role === 'all') {
|
|
@@ -985,6 +1130,66 @@ Examples:
|
|
|
985
1130
|
}
|
|
986
1131
|
});
|
|
987
1132
|
|
|
1133
|
+
agents
|
|
1134
|
+
.command('artifacts')
|
|
1135
|
+
.description('Show SynapseGrid artifact paths and existence status')
|
|
1136
|
+
.option('--session <id>', 'Session ID (defaults to current)')
|
|
1137
|
+
.option('--watch', 'Continuously watch artifact status', false)
|
|
1138
|
+
.option('--interval <ms>', 'Polling interval in milliseconds for --watch', '1500')
|
|
1139
|
+
.option('--json', 'Output as JSON')
|
|
1140
|
+
.action(async (opts) => {
|
|
1141
|
+
try {
|
|
1142
|
+
const sessionId = opts.session || await ensureSessionId(true);
|
|
1143
|
+
const intervalMs = Number.parseInt(opts.interval, 10);
|
|
1144
|
+
if (!Number.isInteger(intervalMs) || intervalMs <= 0) {
|
|
1145
|
+
throw new Error('--interval must be a positive integer');
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
const printSnapshot = async () => {
|
|
1149
|
+
const snapshot = await collectArtifactStatus(sessionId);
|
|
1150
|
+
if (opts.json) {
|
|
1151
|
+
process.stdout.write(JSON.stringify(snapshot) + '\n');
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
console.log(chalk.bold('SynapseGrid artifacts'));
|
|
1156
|
+
console.log(chalk.dim(`Session: ${snapshot.sessionId}`));
|
|
1157
|
+
console.log(chalk.dim(`Checked: ${snapshot.checkedAt}`));
|
|
1158
|
+
for (const [key, meta] of Object.entries(snapshot.artifacts)) {
|
|
1159
|
+
console.log(chalk.dim(`${key}: ${meta.exists ? 'ok' : 'missing'} -> ${meta.path}`));
|
|
1160
|
+
}
|
|
1161
|
+
};
|
|
1162
|
+
|
|
1163
|
+
if (!opts.watch) {
|
|
1164
|
+
const snapshot = await collectArtifactStatus(sessionId);
|
|
1165
|
+
output(snapshot, opts.json);
|
|
1166
|
+
if (!opts.json) {
|
|
1167
|
+
console.log(chalk.bold('SynapseGrid artifacts'));
|
|
1168
|
+
for (const [key, meta] of Object.entries(snapshot.artifacts)) {
|
|
1169
|
+
console.log(chalk.dim(`${key}: ${meta.exists ? 'ok' : 'missing'} -> ${meta.path}`));
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
if (!opts.json) {
|
|
1176
|
+
console.log(chalk.cyan('Watching artifacts (Ctrl+C to stop)\n'));
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
while (true) {
|
|
1180
|
+
if (!opts.json) {
|
|
1181
|
+
process.stdout.write('\x1Bc');
|
|
1182
|
+
}
|
|
1183
|
+
await printSnapshot();
|
|
1184
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, intervalMs));
|
|
1185
|
+
}
|
|
1186
|
+
} catch (error) {
|
|
1187
|
+
output({ error: error.message }, opts.json);
|
|
1188
|
+
if (!opts.json) console.error(chalk.red(`Error: ${error.message}`));
|
|
1189
|
+
process.exit(1);
|
|
1190
|
+
}
|
|
1191
|
+
});
|
|
1192
|
+
|
|
988
1193
|
agents
|
|
989
1194
|
.command('export')
|
|
990
1195
|
.description('Export collaborative transcript')
|
|
@@ -1062,7 +1267,31 @@ Examples:
|
|
|
1062
1267
|
|
|
1063
1268
|
const config = await loadAgentsConfig();
|
|
1064
1269
|
const configuredMux = validateMultiplexer(opts.mux || config.agents.multiplexer || 'auto');
|
|
1065
|
-
const
|
|
1270
|
+
const muxResolution = resolveMultiplexerWithPaths(config, configuredMux);
|
|
1271
|
+
let selectedMux = muxResolution.selected;
|
|
1272
|
+
let zellijPath = muxResolution.zellijPath;
|
|
1273
|
+
if (!selectedMux) {
|
|
1274
|
+
if (configuredMux !== 'tmux' && shouldUseManagedZellij(config)) {
|
|
1275
|
+
const installResult = await installManagedZellijLatest();
|
|
1276
|
+
if (installResult.success && installResult.binaryPath) {
|
|
1277
|
+
await updateAgentsConfig((current) => ({
|
|
1278
|
+
agents: {
|
|
1279
|
+
defaultModel: current.agents.defaultModel,
|
|
1280
|
+
defaultRoleModels: { ...current.agents.defaultRoleModels },
|
|
1281
|
+
multiplexer: current.agents.multiplexer || 'auto',
|
|
1282
|
+
zellij: {
|
|
1283
|
+
strategy: 'managed',
|
|
1284
|
+
binaryPath: installResult.binaryPath,
|
|
1285
|
+
version: installResult.version || null,
|
|
1286
|
+
source: 'managed',
|
|
1287
|
+
},
|
|
1288
|
+
},
|
|
1289
|
+
}));
|
|
1290
|
+
zellijPath = installResult.binaryPath;
|
|
1291
|
+
selectedMux = resolveMultiplexer(configuredMux, Boolean(resolveCommandPath('tmux')), Boolean(zellijPath));
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1066
1295
|
if (!selectedMux) {
|
|
1067
1296
|
throw new Error('No multiplexer found. Install zellij or tmux with: acfm agents setup');
|
|
1068
1297
|
}
|
|
@@ -1112,6 +1341,7 @@ Examples:
|
|
|
1112
1341
|
sessionName: muxSessionName,
|
|
1113
1342
|
sessionDir,
|
|
1114
1343
|
sessionId: state.sessionId,
|
|
1344
|
+
binaryPath: zellijPath,
|
|
1115
1345
|
});
|
|
1116
1346
|
} else {
|
|
1117
1347
|
await spawnTmuxSession({
|
|
@@ -1140,7 +1370,7 @@ Examples:
|
|
|
1140
1370
|
}
|
|
1141
1371
|
|
|
1142
1372
|
if (opts.attach) {
|
|
1143
|
-
await attachToMux(selectedMux, muxSessionName, false);
|
|
1373
|
+
await attachToMux(selectedMux, muxSessionName, false, zellijPath);
|
|
1144
1374
|
}
|
|
1145
1375
|
} catch (error) {
|
|
1146
1376
|
output({ error: error.message }, opts.json);
|
|
@@ -1176,6 +1406,7 @@ Examples:
|
|
|
1176
1406
|
try {
|
|
1177
1407
|
const sessionId = await ensureSessionId(true);
|
|
1178
1408
|
const state = await loadSessionState(sessionId);
|
|
1409
|
+
await ensureSessionArtifacts(sessionId, state);
|
|
1179
1410
|
const effectiveRoleModels = buildEffectiveRoleModels(state, state.model || null);
|
|
1180
1411
|
output({ ...state, effectiveRoleModels }, opts.json);
|
|
1181
1412
|
if (!opts.json) {
|
|
@@ -1199,8 +1430,12 @@ Examples:
|
|
|
1199
1430
|
}
|
|
1200
1431
|
const meetingLogPath = resolve(getSessionDir(state.sessionId), 'meeting-log.md');
|
|
1201
1432
|
const meetingSummaryPath = resolve(getSessionDir(state.sessionId), 'meeting-summary.md');
|
|
1433
|
+
const turnsDirPath = resolve(getSessionDir(state.sessionId), 'turns');
|
|
1434
|
+
const rawDirPath = resolve(getSessionDir(state.sessionId), 'turns', 'raw');
|
|
1202
1435
|
console.log(chalk.dim(`meeting-log: ${existsSync(meetingLogPath) ? meetingLogPath : 'not generated yet'}`));
|
|
1203
1436
|
console.log(chalk.dim(`meeting-summary: ${existsSync(meetingSummaryPath) ? meetingSummaryPath : 'not generated yet'}`));
|
|
1437
|
+
console.log(chalk.dim(`turns: ${existsSync(turnsDirPath) ? turnsDirPath : 'not generated yet'}`));
|
|
1438
|
+
console.log(chalk.dim(`turns/raw: ${existsSync(rawDirPath) ? rawDirPath : 'not generated yet'}`));
|
|
1204
1439
|
}
|
|
1205
1440
|
} catch (error) {
|
|
1206
1441
|
output({ error: error.message }, opts.json);
|
|
@@ -1262,9 +1497,11 @@ Examples:
|
|
|
1262
1497
|
|
|
1263
1498
|
const multiplexer = state.multiplexer || 'tmux';
|
|
1264
1499
|
const muxSessionName = sessionMuxName(state);
|
|
1265
|
-
|
|
1500
|
+
const cfg = await loadAgentsConfig();
|
|
1501
|
+
const zellijPath = resolveConfiguredZellijPath(cfg);
|
|
1502
|
+
if (multiplexer === 'zellij' && muxSessionName && zellijPath) {
|
|
1266
1503
|
try {
|
|
1267
|
-
await runZellij(['delete-session', muxSessionName]);
|
|
1504
|
+
await runZellij(['delete-session', muxSessionName], { binaryPath: zellijPath });
|
|
1268
1505
|
} catch {
|
|
1269
1506
|
// ignore if already closed
|
|
1270
1507
|
}
|
|
@@ -1376,8 +1613,9 @@ Examples:
|
|
|
1376
1613
|
try {
|
|
1377
1614
|
const opencodeBin = resolveCommandPath('opencode');
|
|
1378
1615
|
const tmuxInstalled = hasCommand('tmux');
|
|
1379
|
-
const zellijInstalled = hasCommand('zellij');
|
|
1380
1616
|
const cfg = await loadAgentsConfig();
|
|
1617
|
+
const zellijPath = resolveConfiguredZellijPath(cfg);
|
|
1618
|
+
const zellijInstalled = Boolean(zellijPath);
|
|
1381
1619
|
const defaultModel = cfg.agents.defaultModel || DEFAULT_SYNAPSE_MODEL;
|
|
1382
1620
|
const configuredMux = validateMultiplexer(cfg.agents.multiplexer || 'auto');
|
|
1383
1621
|
const resolvedMux = resolveMultiplexer(configuredMux, tmuxInstalled, zellijInstalled);
|
|
@@ -1385,6 +1623,8 @@ Examples:
|
|
|
1385
1623
|
opencodeBin,
|
|
1386
1624
|
tmuxInstalled,
|
|
1387
1625
|
zellijInstalled,
|
|
1626
|
+
zellijPath,
|
|
1627
|
+
zellijConfig: cfg.agents.zellij,
|
|
1388
1628
|
configuredMultiplexer: configuredMux,
|
|
1389
1629
|
resolvedMultiplexer: resolvedMux,
|
|
1390
1630
|
defaultModel,
|
|
@@ -1407,6 +1647,7 @@ Examples:
|
|
|
1407
1647
|
console.log(chalk.bold('SynapseGrid doctor'));
|
|
1408
1648
|
console.log(chalk.dim(`opencode: ${opencodeBin || 'not found'}`));
|
|
1409
1649
|
console.log(chalk.dim(`zellij: ${zellijInstalled ? 'installed' : 'not installed'}`));
|
|
1650
|
+
if (zellijPath) console.log(chalk.dim(`zellij path: ${zellijPath}`));
|
|
1410
1651
|
console.log(chalk.dim(`tmux: ${tmuxInstalled ? 'installed' : 'not installed'}`));
|
|
1411
1652
|
console.log(chalk.dim(`multiplexer: configured=${configuredMux} resolved=${resolvedMux || 'none'}`));
|
|
1412
1653
|
console.log(chalk.dim(`default model: ${defaultModel}`));
|
package/src/commands/init.js
CHANGED
|
@@ -236,13 +236,20 @@ async function setupCollaborativeSystem() {
|
|
|
236
236
|
console.log(chalk.hex('#B2BEC3')(` Installing ${COLLAB_SYSTEM_NAME} dependencies...`));
|
|
237
237
|
console.log();
|
|
238
238
|
|
|
239
|
-
const result = ensureCollabDependencies({
|
|
239
|
+
const result = await ensureCollabDependencies({
|
|
240
|
+
installZellij: true,
|
|
241
|
+
installTmux: true,
|
|
242
|
+
preferManagedZellij: true,
|
|
243
|
+
});
|
|
240
244
|
|
|
241
245
|
const oColor = result.opencode.success ? chalk.hex('#00B894') : chalk.hex('#D63031');
|
|
242
246
|
const zColor = result.zellij.success ? chalk.hex('#00B894') : chalk.hex('#D63031');
|
|
243
247
|
const tColor = result.tmux.success ? chalk.hex('#00B894') : chalk.hex('#D63031');
|
|
244
248
|
console.log(oColor(` ◆ OpenCode: ${result.opencode.message}`));
|
|
245
249
|
console.log(zColor(` ◆ zellij: ${result.zellij.message}`));
|
|
250
|
+
if (result.zellij.binaryPath) {
|
|
251
|
+
console.log(chalk.hex('#636E72')(` ${result.zellij.binaryPath}`));
|
|
252
|
+
}
|
|
246
253
|
console.log(tColor(` ◆ tmux: ${result.tmux.message}`));
|
|
247
254
|
console.log();
|
|
248
255
|
|
package/src/mcp/collab-server.js
CHANGED
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
setCurrentSession,
|
|
35
35
|
stopSession,
|
|
36
36
|
} from '../agents/state-store.js';
|
|
37
|
-
import { hasCommand, resolveCommandPath } from '../services/dependency-installer.js';
|
|
37
|
+
import { hasCommand, resolveCommandPath, resolveManagedZellijPath } from '../services/dependency-installer.js';
|
|
38
38
|
import { loadAgentsConfig } from '../agents/config-store.js';
|
|
39
39
|
|
|
40
40
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -74,8 +74,18 @@ function launchAutopilot(sessionId) {
|
|
|
74
74
|
child.unref();
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
function resolveConfiguredZellijPath(config) {
|
|
78
|
+
const strategy = config?.agents?.zellij?.strategy || 'auto';
|
|
79
|
+
if (strategy === 'system') {
|
|
80
|
+
return resolveCommandPath('zellij');
|
|
81
|
+
}
|
|
82
|
+
const managed = resolveManagedZellijPath(config);
|
|
83
|
+
if (managed) return managed;
|
|
84
|
+
return resolveCommandPath('zellij');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function muxExists(multiplexer, sessionName, zellijPath = null) {
|
|
88
|
+
if (multiplexer === 'zellij') return zellijSessionExists(sessionName, zellijPath);
|
|
79
89
|
return tmuxSessionExists(sessionName);
|
|
80
90
|
}
|
|
81
91
|
|
|
@@ -121,7 +131,8 @@ class MCPCollabServer {
|
|
|
121
131
|
|
|
122
132
|
const config = await loadAgentsConfig();
|
|
123
133
|
const configuredMux = config.agents.multiplexer || 'auto';
|
|
124
|
-
const
|
|
134
|
+
const zellijPath = resolveConfiguredZellijPath(config);
|
|
135
|
+
const multiplexer = resolveMultiplexer(configuredMux, hasCommand('tmux'), Boolean(zellijPath));
|
|
125
136
|
if (spawnWorkers && !multiplexer) {
|
|
126
137
|
throw new Error('No multiplexer found (zellij/tmux). Run: acfm agents setup');
|
|
127
138
|
}
|
|
@@ -141,7 +152,7 @@ class MCPCollabServer {
|
|
|
141
152
|
const sessionName = `acfm-synapse-${state.sessionId.slice(0, 8)}`;
|
|
142
153
|
const sessionDir = getSessionDir(state.sessionId);
|
|
143
154
|
if (multiplexer === 'zellij') {
|
|
144
|
-
await spawnZellijSession({ sessionName, sessionDir, sessionId: state.sessionId });
|
|
155
|
+
await spawnZellijSession({ sessionName, sessionDir, sessionId: state.sessionId, binaryPath: zellijPath });
|
|
145
156
|
} else {
|
|
146
157
|
await spawnTmuxSession({ sessionName, sessionDir, sessionId: state.sessionId });
|
|
147
158
|
}
|
|
@@ -452,19 +463,21 @@ class MCPCollabServer {
|
|
|
452
463
|
const id = sessionId || await loadCurrentSessionId();
|
|
453
464
|
if (!id) throw new Error('No active session found');
|
|
454
465
|
let state = await loadSessionState(id);
|
|
466
|
+
const config = await loadAgentsConfig();
|
|
467
|
+
const zellijPath = resolveConfiguredZellijPath(config);
|
|
455
468
|
|
|
456
|
-
const multiplexer = state.multiplexer || resolveMultiplexer('auto', hasCommand('tmux'),
|
|
469
|
+
const multiplexer = state.multiplexer || resolveMultiplexer('auto', hasCommand('tmux'), Boolean(zellijPath));
|
|
457
470
|
if (!multiplexer) {
|
|
458
471
|
throw new Error('No multiplexer found (zellij/tmux). Run: acfm agents setup');
|
|
459
472
|
}
|
|
460
473
|
const sessionName = state.multiplexerSessionName || state.tmuxSessionName || `acfm-synapse-${state.sessionId.slice(0, 8)}`;
|
|
461
|
-
const sessionExists = await muxExists(multiplexer, sessionName);
|
|
474
|
+
const sessionExists = await muxExists(multiplexer, sessionName, zellijPath);
|
|
462
475
|
|
|
463
476
|
if (!sessionExists && recreateWorkers) {
|
|
464
477
|
const sessionDir = getSessionDir(state.sessionId);
|
|
465
478
|
if (multiplexer === 'zellij') {
|
|
466
|
-
if (!
|
|
467
|
-
await spawnZellijSession({ sessionName, sessionDir, sessionId: state.sessionId });
|
|
479
|
+
if (!zellijPath) throw new Error('zellij is not installed. Run: acfm agents setup');
|
|
480
|
+
await spawnZellijSession({ sessionName, sessionDir, sessionId: state.sessionId, binaryPath: zellijPath });
|
|
468
481
|
} else {
|
|
469
482
|
if (!hasCommand('tmux')) throw new Error('tmux is not installed. Run: acfm agents setup');
|
|
470
483
|
await spawnTmuxSession({ sessionName, sessionDir, sessionId: state.sessionId });
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
|
+
import { chmod, mkdir, rm, writeFile } from 'node:fs/promises';
|
|
3
4
|
import { join } from 'node:path';
|
|
4
|
-
import { platform } from 'node:os';
|
|
5
|
+
import { arch, homedir, platform } from 'node:os';
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
5
7
|
|
|
6
8
|
function preferredOpenCodePath() {
|
|
7
9
|
const home = process.env.HOME;
|
|
@@ -24,6 +26,61 @@ function runInstallCommand(command) {
|
|
|
24
26
|
return run('bash', ['-lc', command], { stdio: 'inherit' });
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
async function fetchJson(url) {
|
|
30
|
+
const response = await fetch(url, {
|
|
31
|
+
headers: {
|
|
32
|
+
Accept: 'application/vnd.github+json',
|
|
33
|
+
'User-Agent': 'ac-framework',
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
throw new Error(`Request failed (${response.status}) while fetching ${url}`);
|
|
38
|
+
}
|
|
39
|
+
return response.json();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function sha256HexFromBuffer(buffer) {
|
|
43
|
+
const hash = createHash('sha256');
|
|
44
|
+
hash.update(buffer);
|
|
45
|
+
return hash.digest('hex');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function managedToolsRoot() {
|
|
49
|
+
return join(homedir(), '.acfm', 'tools', 'zellij');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function platformAssetPrefix() {
|
|
53
|
+
const p = platform();
|
|
54
|
+
const a = arch();
|
|
55
|
+
if (p === 'linux' && a === 'x64') return 'zellij-x86_64-unknown-linux-musl';
|
|
56
|
+
if (p === 'linux' && a === 'arm64') return 'zellij-aarch64-unknown-linux-musl';
|
|
57
|
+
if (p === 'darwin' && a === 'x64') return 'zellij-x86_64-apple-darwin';
|
|
58
|
+
if (p === 'darwin' && a === 'arm64') return 'zellij-aarch64-apple-darwin';
|
|
59
|
+
if (p === 'win32' && a === 'x64') return 'zellij-x86_64-pc-windows-msvc';
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function managedZellijBinaryPath(version) {
|
|
64
|
+
const fileName = platform() === 'win32' ? 'zellij.exe' : 'zellij';
|
|
65
|
+
return join(managedToolsRoot(), version, fileName);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function extractTarball(tarPath, outputDir) {
|
|
69
|
+
return run('tar', ['-xzf', tarPath, '-C', outputDir]);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function findReleaseAsset(release, suffix) {
|
|
73
|
+
return (release.assets || []).find((asset) => asset.name === suffix) || null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function resolveManagedZellijPath(config = null) {
|
|
77
|
+
const fromEnv = process.env.ACFM_ZELLIJ_BIN;
|
|
78
|
+
if (fromEnv && existsSync(fromEnv)) return fromEnv;
|
|
79
|
+
const configured = config?.agents?.zellij?.binaryPath;
|
|
80
|
+
if (configured && existsSync(configured)) return configured;
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
27
84
|
export function hasCommand(command) {
|
|
28
85
|
return Boolean(resolveCommandPath(command));
|
|
29
86
|
}
|
|
@@ -164,15 +221,132 @@ export function installZellij() {
|
|
|
164
221
|
};
|
|
165
222
|
}
|
|
166
223
|
|
|
167
|
-
export function
|
|
224
|
+
export async function installManagedZellijLatest() {
|
|
225
|
+
const existingSystem = resolveCommandPath('zellij');
|
|
226
|
+
if (existingSystem) {
|
|
227
|
+
return {
|
|
228
|
+
success: true,
|
|
229
|
+
installed: false,
|
|
230
|
+
version: null,
|
|
231
|
+
binaryPath: existingSystem,
|
|
232
|
+
message: 'zellij already installed in system PATH',
|
|
233
|
+
source: 'system',
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const prefix = platformAssetPrefix();
|
|
238
|
+
if (!prefix) {
|
|
239
|
+
return {
|
|
240
|
+
success: false,
|
|
241
|
+
installed: false,
|
|
242
|
+
version: null,
|
|
243
|
+
binaryPath: null,
|
|
244
|
+
message: `Unsupported OS/arch for managed zellij install: ${platform()}/${arch()}`,
|
|
245
|
+
source: 'managed',
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
const release = await fetchJson('https://api.github.com/repos/zellij-org/zellij/releases/latest');
|
|
251
|
+
const version = String(release.tag_name || '').trim() || 'latest';
|
|
252
|
+
|
|
253
|
+
if (platform() === 'win32') {
|
|
254
|
+
const zipAsset = findReleaseAsset(release, `${prefix}.zip`);
|
|
255
|
+
if (!zipAsset) {
|
|
256
|
+
throw new Error(`No matching Windows asset found for ${prefix}`);
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
success: false,
|
|
260
|
+
installed: false,
|
|
261
|
+
version,
|
|
262
|
+
binaryPath: null,
|
|
263
|
+
message: 'Managed Windows zellij install is not implemented yet; use winget/choco/scoop.',
|
|
264
|
+
source: 'managed',
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const tarAsset = findReleaseAsset(release, `${prefix}.tar.gz`);
|
|
269
|
+
if (!tarAsset?.browser_download_url) {
|
|
270
|
+
throw new Error(`No matching zellij asset found for ${prefix}`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const targetDir = join(managedToolsRoot(), version);
|
|
274
|
+
const binaryPath = managedZellijBinaryPath(version);
|
|
275
|
+
if (existsSync(binaryPath)) {
|
|
276
|
+
return {
|
|
277
|
+
success: true,
|
|
278
|
+
installed: false,
|
|
279
|
+
version,
|
|
280
|
+
binaryPath,
|
|
281
|
+
message: `Managed zellij already installed (${version})`,
|
|
282
|
+
source: 'managed',
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
await mkdir(targetDir, { recursive: true });
|
|
287
|
+
const tmpTarPath = join(targetDir, `${prefix}.tar.gz.download`);
|
|
288
|
+
const response = await fetch(tarAsset.browser_download_url, {
|
|
289
|
+
headers: { 'User-Agent': 'ac-framework' },
|
|
290
|
+
});
|
|
291
|
+
if (!response.ok) {
|
|
292
|
+
throw new Error(`Failed downloading ${tarAsset.name} (${response.status})`);
|
|
293
|
+
}
|
|
294
|
+
const raw = Buffer.from(await response.arrayBuffer());
|
|
295
|
+
|
|
296
|
+
const expectedDigest = String(tarAsset.digest || '').replace(/^sha256:/, '');
|
|
297
|
+
if (expectedDigest) {
|
|
298
|
+
const actualDigest = sha256HexFromBuffer(raw);
|
|
299
|
+
if (actualDigest !== expectedDigest) {
|
|
300
|
+
throw new Error(`Digest mismatch for ${tarAsset.name}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
await writeFile(tmpTarPath, raw);
|
|
305
|
+
const extracted = extractTarball(tmpTarPath, targetDir);
|
|
306
|
+
await rm(tmpTarPath, { force: true });
|
|
307
|
+
if (extracted.status !== 0) {
|
|
308
|
+
throw new Error('Failed extracting zellij tarball');
|
|
309
|
+
}
|
|
310
|
+
if (!existsSync(binaryPath)) {
|
|
311
|
+
throw new Error(`zellij binary not found after extraction at ${binaryPath}`);
|
|
312
|
+
}
|
|
313
|
+
await chmod(binaryPath, 0o755);
|
|
314
|
+
|
|
315
|
+
const versionProbe = run(binaryPath, ['--version']);
|
|
316
|
+
if (versionProbe.status !== 0) {
|
|
317
|
+
throw new Error('Installed zellij binary failed version check');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
success: true,
|
|
322
|
+
installed: true,
|
|
323
|
+
version,
|
|
324
|
+
binaryPath,
|
|
325
|
+
message: `Managed zellij installed (${version})`,
|
|
326
|
+
source: 'managed',
|
|
327
|
+
};
|
|
328
|
+
} catch (error) {
|
|
329
|
+
return {
|
|
330
|
+
success: false,
|
|
331
|
+
installed: false,
|
|
332
|
+
version: null,
|
|
333
|
+
binaryPath: null,
|
|
334
|
+
message: `Managed zellij install failed: ${error.message}`,
|
|
335
|
+
source: 'managed',
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export async function ensureCollabDependencies(options = {}) {
|
|
168
341
|
const installTmuxEnabled = options.installTmux ?? true;
|
|
169
342
|
const installZellijEnabled = options.installZellij ?? true;
|
|
343
|
+
const preferManagedZellij = options.preferManagedZellij ?? false;
|
|
170
344
|
const opencode = installOpenCode();
|
|
171
345
|
const tmux = installTmuxEnabled
|
|
172
346
|
? installTmux()
|
|
173
347
|
: { success: hasCommand('tmux'), installed: false, message: hasCommand('tmux') ? 'tmux already installed' : 'tmux installation skipped' };
|
|
174
348
|
const zellij = installZellijEnabled
|
|
175
|
-
? installZellij()
|
|
349
|
+
? (preferManagedZellij ? await installManagedZellijLatest() : installZellij())
|
|
176
350
|
: { success: hasCommand('zellij'), installed: false, message: hasCommand('zellij') ? 'zellij already installed' : 'zellij installation skipped' };
|
|
177
351
|
|
|
178
352
|
const hasMultiplexer = tmux.success || zellij.success;
|