@seamnet/client 0.14.5 → 0.15.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/bin/seam.js +8 -65
- package/lib/guardian.js +12 -20
- package/lib/tmux-utils.cjs +122 -0
- package/package.json +1 -1
package/bin/seam.js
CHANGED
|
@@ -377,43 +377,11 @@ async function cmdInvite(subAction, restArgs) {
|
|
|
377
377
|
|
|
378
378
|
// === cc (Claude Code 管理) ===
|
|
379
379
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
function ccGetSessions() {
|
|
383
|
-
let lines;
|
|
384
|
-
try {
|
|
385
|
-
lines = execSync('tmux list-sessions -F "#{session_name}"', {
|
|
386
|
-
encoding: 'utf8', timeout: 5000,
|
|
387
|
-
}).trim().split('\n').filter(Boolean);
|
|
388
|
-
} catch {
|
|
389
|
-
return [];
|
|
390
|
-
}
|
|
391
|
-
return lines.map(name => {
|
|
392
|
-
let ccRunning = false;
|
|
393
|
-
try {
|
|
394
|
-
const panePid = execSync(`tmux display-message -t "${name}" -p '#{pane_pid}'`, {
|
|
395
|
-
encoding: 'utf8', timeout: 3000,
|
|
396
|
-
}).trim();
|
|
397
|
-
// pane 进程本身可能就是 claude(tmux 直接启动时),也可能是 bash 的子进程
|
|
398
|
-
const self = execSync(`ps -o comm= -p ${panePid} 2>/dev/null`, {
|
|
399
|
-
encoding: 'utf8', timeout: 3000,
|
|
400
|
-
}).trim();
|
|
401
|
-
let child = '';
|
|
402
|
-
try {
|
|
403
|
-
child = execSync(`ps -o comm= --ppid ${panePid} 2>/dev/null`, {
|
|
404
|
-
encoding: 'utf8', timeout: 3000,
|
|
405
|
-
}).trim().split('\n').pop() || '';
|
|
406
|
-
} catch {}
|
|
407
|
-
ccRunning = ['claude', 'claude-code'].includes(self) ||
|
|
408
|
-
['claude', 'claude-code'].includes(child);
|
|
409
|
-
} catch {}
|
|
410
|
-
return { session: name, cc: ccRunning };
|
|
411
|
-
});
|
|
412
|
-
}
|
|
380
|
+
const tmuxUtils = require('../lib/tmux-utils.cjs');
|
|
413
381
|
|
|
414
382
|
async function cmdCc(subAction, restArgs) {
|
|
415
383
|
if (!subAction || subAction === 'list') {
|
|
416
|
-
const sessions =
|
|
384
|
+
const sessions = tmuxUtils.listSessions();
|
|
417
385
|
if (sessions.length === 0) output(true, { sessions: [], message: 'no tmux sessions' });
|
|
418
386
|
output(true, { sessions });
|
|
419
387
|
return;
|
|
@@ -430,10 +398,7 @@ async function cmdCc(subAction, restArgs) {
|
|
|
430
398
|
});
|
|
431
399
|
if (!values.session) output(false, '--session required');
|
|
432
400
|
try {
|
|
433
|
-
const text =
|
|
434
|
-
`tmux capture-pane -t "${values.session}" -p | tail -${parseInt(values.lines) || 20}`,
|
|
435
|
-
{ encoding: 'utf8', timeout: 5000 }
|
|
436
|
-
);
|
|
401
|
+
const text = tmuxUtils.readOutput(values.session, parseInt(values.lines) || 20);
|
|
437
402
|
output(true, { session: values.session, lines: text.trimEnd().split('\n') });
|
|
438
403
|
} catch (e) {
|
|
439
404
|
output(false, `read failed: ${e.message}`);
|
|
@@ -452,24 +417,11 @@ async function cmdCc(subAction, restArgs) {
|
|
|
452
417
|
});
|
|
453
418
|
if (!values.session) output(false, '--session required');
|
|
454
419
|
if (!values.text) output(false, '--text required');
|
|
455
|
-
const sessions = ccGetSessions();
|
|
456
|
-
const target = sessions.find(s => s.session === values.session);
|
|
457
|
-
if (!target) output(false, `session "${values.session}" not found`);
|
|
458
|
-
if (!target.cc) output(false, `session "${values.session}": CC not running`);
|
|
459
420
|
try {
|
|
460
|
-
|
|
461
|
-
// CC 用 raw terminal mode,合在一起发会丢回车
|
|
462
|
-
execSync(
|
|
463
|
-
`tmux send-keys -t "${values.session}" -l ${JSON.stringify(values.text)}`,
|
|
464
|
-
{ timeout: 5000 }
|
|
465
|
-
);
|
|
466
|
-
execSync(
|
|
467
|
-
`tmux send-keys -t "${values.session}" Enter`,
|
|
468
|
-
{ timeout: 5000 }
|
|
469
|
-
);
|
|
421
|
+
tmuxUtils.sendToCC(values.session, values.text);
|
|
470
422
|
output(true, { session: values.session, sent: values.text });
|
|
471
423
|
} catch (e) {
|
|
472
|
-
output(false,
|
|
424
|
+
output(false, e.message);
|
|
473
425
|
}
|
|
474
426
|
return;
|
|
475
427
|
}
|
|
@@ -484,21 +436,12 @@ async function cmdCc(subAction, restArgs) {
|
|
|
484
436
|
strict: false,
|
|
485
437
|
});
|
|
486
438
|
if (!values.dir) output(false, '--dir required');
|
|
487
|
-
|
|
488
|
-
const sessionName = values.session || join(values.dir).split('/').pop();
|
|
489
|
-
// 检查 session 是否已存在
|
|
490
|
-
const existing = ccGetSessions();
|
|
491
|
-
if (existing.find(s => s.session === sessionName)) {
|
|
492
|
-
output(false, `session "${sessionName}" already exists`);
|
|
493
|
-
}
|
|
439
|
+
const sessionName = values.session || values.dir.split('/').pop();
|
|
494
440
|
try {
|
|
495
|
-
|
|
496
|
-
`tmux new-session -d -s "${sessionName}" -c "${values.dir}" 'claude --dangerously-skip-permissions'`,
|
|
497
|
-
{ timeout: 10000 }
|
|
498
|
-
);
|
|
441
|
+
tmuxUtils.startCC(values.dir, sessionName);
|
|
499
442
|
output(true, { session: sessionName, dir: values.dir, started: true });
|
|
500
443
|
} catch (e) {
|
|
501
|
-
output(false,
|
|
444
|
+
output(false, e.message);
|
|
502
445
|
}
|
|
503
446
|
return;
|
|
504
447
|
}
|
package/lib/guardian.js
CHANGED
|
@@ -219,29 +219,21 @@ export async function guardianRun() {
|
|
|
219
219
|
? '🔄 [Seam] 升级完成。Guardian 将在 10 秒后重启 CC 以加载新的 MCP 工具。你会在新对话里看到单个 seam 工具取代原来的多个工具。'
|
|
220
220
|
: '🔄 [Seam] 入网完成。Guardian 将在 10 秒后重启 CC 以加载 MCP 工具。重启后你会在新的对话里读到 IDENTITY.md。';
|
|
221
221
|
hub.inject(notice);
|
|
222
|
-
setTimeout(() => {
|
|
222
|
+
setTimeout(async () => {
|
|
223
223
|
try {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
);
|
|
234
|
-
guardianState.set('cc_restarted', new Date().toISOString());
|
|
235
|
-
if (isUpgradeRestart) guardianState.delete('pending_upgrade_restart');
|
|
236
|
-
hub.logger('guardian').info('cc_restart_injected');
|
|
237
|
-
} catch (e) {
|
|
238
|
-
hub.logger('guardian').error('cc_restart_failed', e);
|
|
239
|
-
}
|
|
240
|
-
}, 3000);
|
|
224
|
+
const { restartCC } = require('./tmux-utils.cjs');
|
|
225
|
+
const ok = await restartCC(safeSession, { socketPath: ccSocket, continueSession: true });
|
|
226
|
+
if (ok) {
|
|
227
|
+
guardianState.set('cc_restarted', new Date().toISOString());
|
|
228
|
+
if (isUpgradeRestart) guardianState.delete('pending_upgrade_restart');
|
|
229
|
+
hub.logger('guardian').info('cc_restart_injected');
|
|
230
|
+
} else {
|
|
231
|
+
hub.logger('guardian').error('cc_restart_failed', new Error('restartCC returned false'));
|
|
232
|
+
}
|
|
241
233
|
} catch (e) {
|
|
242
|
-
hub.logger('guardian').error('
|
|
234
|
+
hub.logger('guardian').error('cc_restart_failed', e);
|
|
243
235
|
}
|
|
244
|
-
}, 10000);
|
|
236
|
+
}, 10000);
|
|
245
237
|
}
|
|
246
238
|
|
|
247
239
|
// Keep alive + graceful shutdown (pid 文件清理)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tmux-utils — 可靠的 tmux 交互。
|
|
3
|
+
*
|
|
4
|
+
* CC 用 raw terminal mode,tmux send-keys 需要特殊处理:
|
|
5
|
+
* 1. 文本用 -l (literal) 发,不解释特殊键
|
|
6
|
+
* 2. Enter 单独发
|
|
7
|
+
* 3. 发完验证是否被接收
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { execSync } = require('node:child_process');
|
|
11
|
+
const { existsSync, readFileSync } = require('node:fs');
|
|
12
|
+
const { join } = require('node:path');
|
|
13
|
+
|
|
14
|
+
const EXEC_OPTS = { encoding: 'utf8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] };
|
|
15
|
+
|
|
16
|
+
function tmuxCmd(socketPath) {
|
|
17
|
+
return socketPath ? `tmux -S "${socketPath}"` : 'tmux';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function listSessions(socketPath) {
|
|
21
|
+
const tmux = tmuxCmd(socketPath);
|
|
22
|
+
let lines;
|
|
23
|
+
try {
|
|
24
|
+
lines = execSync(`${tmux} list-sessions -F "#{session_name}"`, EXEC_OPTS)
|
|
25
|
+
.trim().split('\n').filter(Boolean);
|
|
26
|
+
} catch {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
return lines.map(name => {
|
|
30
|
+
const cc = isCCRunning(name, socketPath);
|
|
31
|
+
return { session: name, cc };
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isCCRunning(session, socketPath) {
|
|
36
|
+
const tmux = tmuxCmd(socketPath);
|
|
37
|
+
try {
|
|
38
|
+
const panePid = execSync(
|
|
39
|
+
`${tmux} display-message -t "${session}" -p '#{pane_pid}'`, EXEC_OPTS
|
|
40
|
+
).trim();
|
|
41
|
+
const self = execSync(`ps -o comm= -p ${panePid} 2>/dev/null`, EXEC_OPTS).trim();
|
|
42
|
+
if (['claude', 'claude-code'].includes(self)) return true;
|
|
43
|
+
try {
|
|
44
|
+
const child = execSync(`ps -o comm= --ppid ${panePid} 2>/dev/null`, EXEC_OPTS)
|
|
45
|
+
.trim().split('\n').pop() || '';
|
|
46
|
+
if (['claude', 'claude-code'].includes(child)) return true;
|
|
47
|
+
} catch {}
|
|
48
|
+
} catch {}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 给 tmux session 里的 CC 发用户消息。
|
|
54
|
+
* 用 -l (literal) 发文本 + 单独 Enter,保证 raw terminal mode 下不丢回车。
|
|
55
|
+
*/
|
|
56
|
+
function sendToCC(session, text, { socketPath, skipCheck = false } = {}) {
|
|
57
|
+
const tmux = tmuxCmd(socketPath);
|
|
58
|
+
if (!skipCheck && !isCCRunning(session, socketPath)) {
|
|
59
|
+
throw new Error(`session "${session}": CC not running`);
|
|
60
|
+
}
|
|
61
|
+
// 用 -l 发纯文本(不解释特殊键)
|
|
62
|
+
execSync(`${tmux} send-keys -t "${session}" -l ${JSON.stringify(text)}`, EXEC_OPTS);
|
|
63
|
+
// 单独发 Enter 提交
|
|
64
|
+
execSync(`${tmux} send-keys -t "${session}" Enter`, EXEC_OPTS);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 读 tmux session 的最后 N 行输出。
|
|
69
|
+
*/
|
|
70
|
+
function readOutput(session, lines = 20, socketPath) {
|
|
71
|
+
const tmux = tmuxCmd(socketPath);
|
|
72
|
+
return execSync(
|
|
73
|
+
`${tmux} capture-pane -t "${session}" -p | tail -${parseInt(lines) || 20}`,
|
|
74
|
+
EXEC_OPTS
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 在新 tmux session 里启动 CC。
|
|
80
|
+
*/
|
|
81
|
+
function startCC(dir, session, socketPath) {
|
|
82
|
+
const tmux = tmuxCmd(socketPath);
|
|
83
|
+
if (!existsSync(dir)) throw new Error(`dir not found: ${dir}`);
|
|
84
|
+
try {
|
|
85
|
+
execSync(`${tmux} has-session -t "${session}" 2>/dev/null`, EXEC_OPTS);
|
|
86
|
+
throw new Error(`session "${session}" already exists`);
|
|
87
|
+
} catch (e) {
|
|
88
|
+
if (e.message.includes('already exists')) throw e;
|
|
89
|
+
}
|
|
90
|
+
execSync(
|
|
91
|
+
`${tmux} new-session -d -s "${session}" -c "${dir}" 'claude --dangerously-skip-permissions'`,
|
|
92
|
+
{ ...EXEC_OPTS, timeout: 10000 }
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 给 CC 发 /exit 然后重启(带 --continue)。
|
|
98
|
+
* guardian 升级后自动重启 CC 用这个。
|
|
99
|
+
*/
|
|
100
|
+
function restartCC(session, { socketPath, continueSession = true } = {}) {
|
|
101
|
+
const tmux = tmuxCmd(socketPath);
|
|
102
|
+
// 发 /exit
|
|
103
|
+
execSync(`${tmux} send-keys -t "${session}" -l '/exit'`, EXEC_OPTS);
|
|
104
|
+
execSync(`${tmux} send-keys -t "${session}" Enter`, EXEC_OPTS);
|
|
105
|
+
// 等 CC 退出
|
|
106
|
+
return new Promise((resolve) => {
|
|
107
|
+
setTimeout(() => {
|
|
108
|
+
try {
|
|
109
|
+
const cmd = continueSession
|
|
110
|
+
? 'claude --dangerously-skip-permissions --continue'
|
|
111
|
+
: 'claude --dangerously-skip-permissions';
|
|
112
|
+
execSync(`${tmux} send-keys -t "${session}" -l ${JSON.stringify(cmd)}`, EXEC_OPTS);
|
|
113
|
+
execSync(`${tmux} send-keys -t "${session}" Enter`, EXEC_OPTS);
|
|
114
|
+
resolve(true);
|
|
115
|
+
} catch (e) {
|
|
116
|
+
resolve(false);
|
|
117
|
+
}
|
|
118
|
+
}, 3000);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = { listSessions, isCCRunning, sendToCC, readOutput, startCC, restartCC, tmuxCmd };
|