@seamnet/client 0.15.0 → 0.16.1
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/cli.js +26 -12
- package/bin/seam.js +26 -0
- package/lib/guardian.js +40 -12
- package/lib/init.js +25 -2
- package/lib/paths.js +19 -2
- package/lib/registry.js +101 -0
- package/lib/tell.js +67 -0
- package/lib/upgrade-all.js +68 -0
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { parseArgs } from 'node:util';
|
|
4
|
-
import {
|
|
5
|
-
import { status } from '../lib/status.js';
|
|
6
|
-
import { stop } from '../lib/stop.js';
|
|
4
|
+
import { join } from 'node:path';
|
|
7
5
|
|
|
8
6
|
const command = process.argv[2];
|
|
9
7
|
|
|
@@ -12,13 +10,19 @@ if (!command || command === '--help' || command === '-h') {
|
|
|
12
10
|
seam-client — join the Seam network
|
|
13
11
|
|
|
14
12
|
Commands:
|
|
15
|
-
init
|
|
16
|
-
|
|
17
|
-
guardian
|
|
18
|
-
autostart
|
|
19
|
-
status
|
|
20
|
-
stop
|
|
21
|
-
|
|
13
|
+
init --invite-code <code> --name <name> Register and join Seam
|
|
14
|
+
--api <url> API base (default: $SEAM_API_BASE || https://seam.chat)
|
|
15
|
+
guardian start|stop|run Manage guardian background process
|
|
16
|
+
autostart Start guardian if credentials exist and not running (CC SessionStart hook)
|
|
17
|
+
status Check Seam status
|
|
18
|
+
stop Stop guardian
|
|
19
|
+
upgrade Upgrade this AI's seam-client (per-AI mode)
|
|
20
|
+
upgrade-all Host-tool: bump global package + restart all registered guardians
|
|
21
|
+
mcp-serve Start MCP server (used by Claude Code)
|
|
22
|
+
|
|
23
|
+
Environment:
|
|
24
|
+
SEAM_HOME Absolute path to a .seam data directory. Resolution priority:
|
|
25
|
+
1) $SEAM_HOME, 2) cwd-upwalk for .seam/, 3) $HOME/.seam fallback.
|
|
22
26
|
|
|
23
27
|
Example:
|
|
24
28
|
npx seam-client init --invite-code ABC123 --name my-ai
|
|
@@ -46,8 +50,11 @@ try {
|
|
|
46
50
|
console.error('Error: --name is required');
|
|
47
51
|
process.exit(1);
|
|
48
52
|
}
|
|
49
|
-
|
|
53
|
+
if (!process.env.SEAM_HOME) {
|
|
54
|
+
process.env.SEAM_HOME = join(process.cwd(), '.seam');
|
|
55
|
+
}
|
|
50
56
|
const apiBase = values.api || process.env.SEAM_API_BASE || 'https://seam.chat';
|
|
57
|
+
const { init } = await import('../lib/init.js');
|
|
51
58
|
await init({
|
|
52
59
|
inviteCode: values['invite-code'],
|
|
53
60
|
name: values.name,
|
|
@@ -55,14 +62,21 @@ try {
|
|
|
55
62
|
});
|
|
56
63
|
break;
|
|
57
64
|
}
|
|
58
|
-
case 'status':
|
|
65
|
+
case 'status': {
|
|
66
|
+
const { status } = await import('../lib/status.js');
|
|
59
67
|
await status();
|
|
60
68
|
break;
|
|
69
|
+
}
|
|
61
70
|
case 'upgrade': {
|
|
62
71
|
const { upgrade } = await import('../lib/upgrade.js');
|
|
63
72
|
await upgrade();
|
|
64
73
|
break;
|
|
65
74
|
}
|
|
75
|
+
case 'upgrade-all': {
|
|
76
|
+
const { upgradeAll } = await import('../lib/upgrade-all.js');
|
|
77
|
+
await upgradeAll();
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
66
80
|
case 'stop': {
|
|
67
81
|
const { guardianStop } = await import('../lib/guardian.js');
|
|
68
82
|
await guardianStop();
|
package/bin/seam.js
CHANGED
|
@@ -47,6 +47,7 @@ function printHelp() {
|
|
|
47
47
|
' seam msg group --group <id> (--text <text> | --text-file <path> | --image <path>)',
|
|
48
48
|
' seam contacts list',
|
|
49
49
|
' seam contacts get --user <userId>',
|
|
50
|
+
' seam tell --to <id|alias> [--from <name>] --text <text>',
|
|
50
51
|
' seam wechat bind',
|
|
51
52
|
' seam wechat send (--text <text> | --text-file <path> | --image <path> | --file <path>)',
|
|
52
53
|
' seam wechat status',
|
|
@@ -169,6 +170,30 @@ async function cmdMsgSend(restArgs) {
|
|
|
169
170
|
}
|
|
170
171
|
}
|
|
171
172
|
|
|
173
|
+
async function cmdTell(restArgs) {
|
|
174
|
+
const { values } = parseArgs({
|
|
175
|
+
args: restArgs,
|
|
176
|
+
options: {
|
|
177
|
+
to: { type: 'string' },
|
|
178
|
+
from: { type: 'string' },
|
|
179
|
+
text: { type: 'string' },
|
|
180
|
+
'text-file': { type: 'string' },
|
|
181
|
+
},
|
|
182
|
+
strict: false,
|
|
183
|
+
});
|
|
184
|
+
if (!values.to) output(false, '--to required');
|
|
185
|
+
const text = readText({ text: values.text, textFile: values['text-file'] });
|
|
186
|
+
if (text == null) output(false, '--text or --text-file required');
|
|
187
|
+
const { tell } = await import('../lib/tell.js');
|
|
188
|
+
const res = await tell({
|
|
189
|
+
to: values.to,
|
|
190
|
+
from: values.from,
|
|
191
|
+
text,
|
|
192
|
+
guardianRequest,
|
|
193
|
+
});
|
|
194
|
+
output(res.ok !== false, res);
|
|
195
|
+
}
|
|
196
|
+
|
|
172
197
|
async function cmdMsgGroup(restArgs) {
|
|
173
198
|
const { values } = parseArgs({
|
|
174
199
|
args: restArgs,
|
|
@@ -471,6 +496,7 @@ function parseDuration(str) {
|
|
|
471
496
|
if (action === 'group') return await cmdMsgGroup(rest);
|
|
472
497
|
output(false, `Unknown msg action: ${action}`);
|
|
473
498
|
}
|
|
499
|
+
if (domain === 'tell') return await cmdTell(argv.slice(1));
|
|
474
500
|
if (domain === 'contacts') return await cmdContacts(action, rest);
|
|
475
501
|
if (domain === 'guardian') return await cmdGuardian(action);
|
|
476
502
|
if (domain === 'wechat') return await cmdWechat(action, rest);
|
package/lib/guardian.js
CHANGED
|
@@ -15,6 +15,7 @@ import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync, openSyn
|
|
|
15
15
|
import { join, isAbsolute } from 'node:path';
|
|
16
16
|
import { execSync, spawn } from 'node:child_process';
|
|
17
17
|
import { SEAM_DIR, CREDENTIALS_PATH, SOCKET_PATH, LOGS_DIR, PID_PATH } from './paths.js';
|
|
18
|
+
import { writeEntry as registryWrite, removeEntry as registryRemove } from './registry.js';
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* 解析 $TMUX 里的 socket path。返回可用于 `tmux -S <path>` 的绝对路径或 null。
|
|
@@ -88,10 +89,16 @@ export async function guardianStart() {
|
|
|
88
89
|
const socketPath = resolveTmuxSocketPath();
|
|
89
90
|
const tmux = tmuxCmd(socketPath);
|
|
90
91
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
// SEAM_CC_SESSION env 优先:upgrade-all / 跨 shell 启动时,调用方知道目标 AI
|
|
93
|
+
// 的 tmux session,直接传过来;不传才回退到 `tmux display-message`(取 attached
|
|
94
|
+
// client 的 session)。回退在跨 shell 场景下会拿错 session——比如缝在自己的 tmux
|
|
95
|
+
// 里跑 `cd /home/claude_code/du && seam-client upgrade`,会把"缝"写进渡的 registry。
|
|
96
|
+
let ccSession = process.env.SEAM_CC_SESSION || '';
|
|
97
|
+
if (!ccSession) {
|
|
98
|
+
try {
|
|
99
|
+
ccSession = execSync(`${tmux} display-message -p '#S'`, { encoding: 'utf8' }).trim();
|
|
100
|
+
} catch {}
|
|
101
|
+
}
|
|
95
102
|
|
|
96
103
|
const cwd = process.cwd();
|
|
97
104
|
const localCli = join(cwd, 'node_modules', '@seamnet', 'client', 'bin', 'cli.js');
|
|
@@ -136,16 +143,36 @@ export async function guardianRun() {
|
|
|
136
143
|
console.error(`Guardian already running (pid: ${existingPid}). Exiting.`);
|
|
137
144
|
process.exit(0);
|
|
138
145
|
}
|
|
139
|
-
// 写 PID(覆盖 guardianStart 写的 child.pid),退出时清理
|
|
140
|
-
writeFileSync(PID_PATH, String(process.pid));
|
|
141
|
-
const cleanPid = () => { try { if (existsSync(PID_PATH)) unlinkSync(PID_PATH); } catch {} };
|
|
142
|
-
process.on('exit', cleanPid);
|
|
143
|
-
process.on('SIGTERM', () => { cleanPid(); process.exit(0); });
|
|
144
|
-
process.on('SIGINT', () => { cleanPid(); process.exit(0); });
|
|
145
|
-
|
|
146
146
|
const credentials = JSON.parse(readFileSync(CREDENTIALS_PATH, 'utf8'));
|
|
147
147
|
const ccSession = process.env.SEAM_CC_SESSION || '';
|
|
148
148
|
const ccSocket = process.env.SEAM_CC_SOCKET || resolveTmuxSocketPath() || '';
|
|
149
|
+
|
|
150
|
+
// 写 PID(覆盖 guardianStart 写的 child.pid),退出时清理
|
|
151
|
+
writeFileSync(PID_PATH, String(process.pid));
|
|
152
|
+
|
|
153
|
+
// 写 cross-AI registry entry(~/.shared/seam-registry/<userId>.json)
|
|
154
|
+
try {
|
|
155
|
+
registryWrite({
|
|
156
|
+
userId: credentials.userId,
|
|
157
|
+
aliases: [credentials.name].filter(Boolean),
|
|
158
|
+
seam_home: SEAM_DIR,
|
|
159
|
+
tmux_session: ccSession,
|
|
160
|
+
tmux_socket: ccSocket,
|
|
161
|
+
socket: SOCKET_PATH,
|
|
162
|
+
pid: process.pid,
|
|
163
|
+
started_at: new Date().toISOString(),
|
|
164
|
+
});
|
|
165
|
+
} catch (e) {
|
|
166
|
+
console.error(`[guardian] registry write failed (non-fatal): ${e.message}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const cleanup = () => {
|
|
170
|
+
try { if (existsSync(PID_PATH)) unlinkSync(PID_PATH); } catch {}
|
|
171
|
+
try { registryRemove(credentials.userId); } catch {}
|
|
172
|
+
};
|
|
173
|
+
process.on('exit', cleanup);
|
|
174
|
+
process.on('SIGTERM', () => { cleanup(); process.exit(0); });
|
|
175
|
+
process.on('SIGINT', () => { cleanup(); process.exit(0); });
|
|
149
176
|
const jsonlPath = join(LOGS_DIR, 'guardian.jsonl');
|
|
150
177
|
|
|
151
178
|
if (!existsSync(LOGS_DIR)) mkdirSync(LOGS_DIR, { recursive: true });
|
|
@@ -236,10 +263,11 @@ export async function guardianRun() {
|
|
|
236
263
|
}, 10000);
|
|
237
264
|
}
|
|
238
265
|
|
|
239
|
-
// Keep alive + graceful shutdown (pid 文件清理)
|
|
266
|
+
// Keep alive + graceful shutdown (pid 文件清理 + registry 清理)
|
|
240
267
|
const shutdown = async (signal) => {
|
|
241
268
|
hub.logger('guardian').info('shutdown_signal', { signal });
|
|
242
269
|
try { if (existsSync(PID_PATH)) unlinkSync(PID_PATH); } catch {}
|
|
270
|
+
try { registryRemove(credentials.userId); } catch {}
|
|
243
271
|
await stop();
|
|
244
272
|
process.exit(0);
|
|
245
273
|
};
|
package/lib/init.js
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, writeFileSync, readFileSync, chmodSync } from 'n
|
|
|
2
2
|
import { execSync } from 'node:child_process';
|
|
3
3
|
import { join, dirname } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
5
6
|
import { register } from './api.js';
|
|
6
7
|
import {
|
|
7
8
|
SEAM_DIR, CREDENTIALS_PATH, VERSION_PATH,
|
|
@@ -85,9 +86,12 @@ export async function init({ inviteCode, name, apiBase }) {
|
|
|
85
86
|
// Step 9: Write .mcp.json in current directory
|
|
86
87
|
writeMcpConfig(result);
|
|
87
88
|
|
|
88
|
-
// Step 10: Write .claude/settings.json (pre-authorize MCP)
|
|
89
|
+
// Step 10: Write .claude/settings.json (pre-authorize MCP + inject SEAM_HOME)
|
|
89
90
|
writeSettings();
|
|
90
91
|
|
|
92
|
+
// Step 10b: Register this SEAM_HOME so `upgrade-all` can find it
|
|
93
|
+
registerSeamHome();
|
|
94
|
+
|
|
91
95
|
// Step 11: Add @IDENTITY.md to CLAUDE.md
|
|
92
96
|
patchClaudeMd();
|
|
93
97
|
|
|
@@ -196,6 +200,10 @@ function writeSettings() {
|
|
|
196
200
|
|
|
197
201
|
settings.enableAllProjectMcpServers = true;
|
|
198
202
|
|
|
203
|
+
// host-tool: CC 子进程注入 SEAM_HOME,paths.js 第一级 fallback 命中
|
|
204
|
+
if (!settings.env) settings.env = {};
|
|
205
|
+
settings.env.SEAM_HOME = SEAM_DIR;
|
|
206
|
+
|
|
199
207
|
// SessionStart hook: CC 启动时自动拉起 guardian(如果还没跑)
|
|
200
208
|
if (!settings.hooks) settings.hooks = {};
|
|
201
209
|
if (!Array.isArray(settings.hooks.SessionStart)) settings.hooks.SessionStart = [];
|
|
@@ -218,7 +226,22 @@ function writeSettings() {
|
|
|
218
226
|
}
|
|
219
227
|
|
|
220
228
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
221
|
-
console.log(
|
|
229
|
+
console.log(` MCP pre-authorized + env.SEAM_HOME=${SEAM_DIR} + SessionStart hook wired.`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function registerSeamHome() {
|
|
233
|
+
const homesPath = join(homedir(), '.seam-homes');
|
|
234
|
+
let lines = [];
|
|
235
|
+
if (existsSync(homesPath)) {
|
|
236
|
+
lines = readFileSync(homesPath, 'utf8').split('\n').filter(Boolean);
|
|
237
|
+
}
|
|
238
|
+
if (!lines.includes(SEAM_DIR)) {
|
|
239
|
+
lines.push(SEAM_DIR);
|
|
240
|
+
writeFileSync(homesPath, lines.join('\n') + '\n');
|
|
241
|
+
console.log(` Registered to ${homesPath}`);
|
|
242
|
+
} else {
|
|
243
|
+
console.log(` Already registered in ${homesPath}`);
|
|
244
|
+
}
|
|
222
245
|
}
|
|
223
246
|
|
|
224
247
|
export function patchClaudeMd() {
|
package/lib/paths.js
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
|
-
import { join } from 'node:path';
|
|
1
|
+
import { join, resolve, dirname } from 'node:path';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
2
4
|
|
|
3
|
-
export
|
|
5
|
+
export function resolveSeamHome() {
|
|
6
|
+
if (process.env.SEAM_HOME) return resolve(process.env.SEAM_HOME);
|
|
7
|
+
|
|
8
|
+
let dir = process.cwd();
|
|
9
|
+
while (true) {
|
|
10
|
+
const candidate = join(dir, '.seam');
|
|
11
|
+
if (existsSync(candidate)) return candidate;
|
|
12
|
+
const parent = dirname(dir);
|
|
13
|
+
if (parent === dir) break;
|
|
14
|
+
dir = parent;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return join(homedir(), '.seam');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const SEAM_DIR = resolveSeamHome();
|
|
4
21
|
export const CREDENTIALS_PATH = join(SEAM_DIR, 'credentials.json');
|
|
5
22
|
export const CONFIG_PATH = join(SEAM_DIR, 'config.json');
|
|
6
23
|
export const VERSION_PATH = join(SEAM_DIR, 'version.json');
|
package/lib/registry.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-AI registry:~/.shared/seam-registry/<userId>.json
|
|
3
|
+
*
|
|
4
|
+
* Guardian 启动时写 entry,shutdown 时删 entry。`seam tell` 读 entries 查目标
|
|
5
|
+
* AI。死信检测在读时做:pid 已死的 entry 直接删除。
|
|
6
|
+
*
|
|
7
|
+
* 并发写:每条 entry 单独文件 + atomic rename(写 .tmp 再 rename)。
|
|
8
|
+
* 同 userId 双 guardian 不应出现(guardian 单例锁保证),出现则后写覆盖先写。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, readFileSync, writeFileSync, readdirSync, mkdirSync, renameSync, unlinkSync } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { homedir } from 'node:os';
|
|
14
|
+
|
|
15
|
+
const REGISTRY_DIR = join(homedir(), '.shared', 'seam-registry');
|
|
16
|
+
|
|
17
|
+
function ensureDir() {
|
|
18
|
+
if (!existsSync(REGISTRY_DIR)) {
|
|
19
|
+
mkdirSync(REGISTRY_DIR, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isAlive(pid) {
|
|
24
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
25
|
+
try {
|
|
26
|
+
process.kill(pid, 0);
|
|
27
|
+
return true;
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function entryPath(userId) {
|
|
34
|
+
return join(REGISTRY_DIR, `${userId}.json`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function writeEntry(entry) {
|
|
38
|
+
if (!entry?.userId) throw new Error('writeEntry: userId required');
|
|
39
|
+
ensureDir();
|
|
40
|
+
const target = entryPath(entry.userId);
|
|
41
|
+
const tmp = `${target}.tmp.${process.pid}.${Date.now()}`;
|
|
42
|
+
writeFileSync(tmp, JSON.stringify(entry, null, 2));
|
|
43
|
+
renameSync(tmp, target);
|
|
44
|
+
return target;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function removeEntry(userId) {
|
|
48
|
+
if (!userId) return;
|
|
49
|
+
const target = entryPath(userId);
|
|
50
|
+
try {
|
|
51
|
+
if (existsSync(target)) unlinkSync(target);
|
|
52
|
+
} catch {}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 列出所有活 entry。死信(pid 已死)会被顺手删除。
|
|
57
|
+
* 顺便清掉残留 .tmp.* 文件(上次崩溃留下的)。
|
|
58
|
+
*/
|
|
59
|
+
export function readAll() {
|
|
60
|
+
if (!existsSync(REGISTRY_DIR)) return [];
|
|
61
|
+
let files;
|
|
62
|
+
try {
|
|
63
|
+
files = readdirSync(REGISTRY_DIR);
|
|
64
|
+
} catch {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
const alive = [];
|
|
68
|
+
for (const f of files) {
|
|
69
|
+
const full = join(REGISTRY_DIR, f);
|
|
70
|
+
if (f.includes('.tmp.')) {
|
|
71
|
+
try { unlinkSync(full); } catch {}
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (!f.endsWith('.json')) continue;
|
|
75
|
+
let entry;
|
|
76
|
+
try {
|
|
77
|
+
entry = JSON.parse(readFileSync(full, 'utf8'));
|
|
78
|
+
} catch {
|
|
79
|
+
try { unlinkSync(full); } catch {}
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (!isAlive(entry.pid)) {
|
|
83
|
+
try { unlinkSync(full); } catch {}
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
alive.push(entry);
|
|
87
|
+
}
|
|
88
|
+
return alive;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function findByAlias(needle) {
|
|
92
|
+
if (!needle) return null;
|
|
93
|
+
const entries = readAll();
|
|
94
|
+
for (const e of entries) {
|
|
95
|
+
if (e.userId === needle) return e;
|
|
96
|
+
if (Array.isArray(e.aliases) && e.aliases.includes(needle)) return e;
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export { REGISTRY_DIR };
|
package/lib/tell.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* seam tell:跨 AI 单条投递。
|
|
3
|
+
*
|
|
4
|
+
* 优先走 cross-AI registry —— 目标 AI 在本机 → 直接调 tmux sendToCC 注入;
|
|
5
|
+
* 不在或 socket 死 → fallback 走 IM 发到对方 userId。
|
|
6
|
+
*
|
|
7
|
+
* 入站 inject 加 from header:`💬 [本机 ← <from>] <text>`
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
11
|
+
import { createRequire } from 'node:module';
|
|
12
|
+
import { findByAlias } from './registry.js';
|
|
13
|
+
import { CREDENTIALS_PATH } from './paths.js';
|
|
14
|
+
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
|
|
17
|
+
function resolveSelfName() {
|
|
18
|
+
if (!existsSync(CREDENTIALS_PATH)) return null;
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(readFileSync(CREDENTIALS_PATH, 'utf8'))?.name || null;
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function formatLocalInject(from, text) {
|
|
27
|
+
const tag = from ? `[本机 ← ${from}]` : '[本机]';
|
|
28
|
+
return `💬 ${tag} ${text}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @returns {Promise<{ok:true, channel:'local'|'im', target:string} | {ok:false, error:string}>}
|
|
33
|
+
*/
|
|
34
|
+
export async function tell({ to, from, text, guardianRequest }) {
|
|
35
|
+
if (!to) return { ok: false, error: '--to required' };
|
|
36
|
+
if (!text) return { ok: false, error: '--text required' };
|
|
37
|
+
|
|
38
|
+
const fromName = from || resolveSelfName() || '?';
|
|
39
|
+
|
|
40
|
+
const entry = findByAlias(to);
|
|
41
|
+
if (entry?.tmux_session) {
|
|
42
|
+
try {
|
|
43
|
+
const { sendToCC } = require('./tmux-utils.cjs');
|
|
44
|
+
const injectText = formatLocalInject(fromName, text);
|
|
45
|
+
sendToCC(entry.tmux_session, injectText, { socketPath: entry.tmux_socket || undefined });
|
|
46
|
+
return { ok: true, channel: 'local', target: entry.userId };
|
|
47
|
+
} catch (e) {
|
|
48
|
+
// 本机命中但 CC 不在线 / tmux session 死了 → 落 IM
|
|
49
|
+
if (!guardianRequest) {
|
|
50
|
+
return { ok: false, error: `local inject failed: ${e.message}` };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!guardianRequest) {
|
|
56
|
+
return { ok: false, error: `target "${to}" not in registry and no IM fallback wired` };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const targetUserId = entry?.userId || to;
|
|
60
|
+
try {
|
|
61
|
+
const res = await guardianRequest({ action: 'send_im', to: targetUserId, text });
|
|
62
|
+
if (res?.ok === false) return { ok: false, error: res.error || 'IM fallback failed' };
|
|
63
|
+
return { ok: true, channel: 'im', target: targetUserId };
|
|
64
|
+
} catch (e) {
|
|
65
|
+
return { ok: false, error: `IM fallback error: ${e.message}` };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-tool 模式:对所有已注册的 SEAM_HOME 滚动升级 + 重启 guardian。
|
|
3
|
+
*
|
|
4
|
+
* 流程:
|
|
5
|
+
* 1. 全局升 npm 包(仅一次)
|
|
6
|
+
* 2. 读 ~/.shared/seam-registry/*.json(当前在跑的 guardian 列表)
|
|
7
|
+
* 3. 每个 entry spawn 子进程 stop → start guardian
|
|
8
|
+
* (SEAM_HOME / SEAM_CC_SESSION / SEAM_CC_SOCKET 从 entry 取,
|
|
9
|
+
* 避免 guardianStart 回退到 `tmux display-message` 抓错 attached client。)
|
|
10
|
+
*
|
|
11
|
+
* 仅升级"当前在跑的 guardian"——目录还在但 guardian 没开的,跳过。
|
|
12
|
+
* 想拉那些上来,下次自己 `seam-client guardian start` 即可,
|
|
13
|
+
* 那时已是新版 npm 包。
|
|
14
|
+
*
|
|
15
|
+
* 路人 CC(没装 seam-client / 没 register)天然不在 registry,不会被碰。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { join, dirname } from 'node:path';
|
|
19
|
+
import { execSync, spawnSync } from 'node:child_process';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
import { readAll } from './registry.js';
|
|
22
|
+
|
|
23
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
24
|
+
const CLI_PATH = join(dirname(__filename), '..', 'bin', 'cli.js');
|
|
25
|
+
|
|
26
|
+
export async function upgradeAll() {
|
|
27
|
+
console.log('Seam — host-tool upgrade-all\n');
|
|
28
|
+
|
|
29
|
+
console.log('1. npm install -g @seamnet/client@latest');
|
|
30
|
+
try {
|
|
31
|
+
execSync('npm install -g @seamnet/client@latest', { stdio: 'inherit' });
|
|
32
|
+
} catch (e) {
|
|
33
|
+
console.error(` npm install -g failed: ${e.message}`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const entries = readAll();
|
|
38
|
+
if (entries.length === 0) {
|
|
39
|
+
console.log('\n2. registry 为空——没有正在运行的 guardian,结束。');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
console.log(`\n2. registry 里 ${entries.length} 个活 guardian,逐个重启`);
|
|
43
|
+
|
|
44
|
+
let okCount = 0;
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
const { userId, seam_home, tmux_session, tmux_socket } = entry;
|
|
47
|
+
console.log(`\n → ${userId} (home=${seam_home}, session=${tmux_session || '?'})`);
|
|
48
|
+
|
|
49
|
+
const env = {
|
|
50
|
+
...process.env,
|
|
51
|
+
SEAM_HOME: seam_home,
|
|
52
|
+
SEAM_CC_SESSION: tmux_session || '',
|
|
53
|
+
SEAM_CC_SOCKET: tmux_socket || '',
|
|
54
|
+
};
|
|
55
|
+
const stopRes = spawnSync('node', [CLI_PATH, 'stop'], { env, stdio: 'inherit' });
|
|
56
|
+
if (stopRes.status !== 0) {
|
|
57
|
+
console.log(' stop 非零退出(继续)');
|
|
58
|
+
}
|
|
59
|
+
const startRes = spawnSync('node', [CLI_PATH, 'guardian', 'start'], { env, stdio: 'inherit' });
|
|
60
|
+
if (startRes.status !== 0) {
|
|
61
|
+
console.error(` start 失败 (exit ${startRes.status})`);
|
|
62
|
+
} else {
|
|
63
|
+
okCount += 1;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log(`\nDone. ${okCount}/${entries.length} 个 guardian 已重启。`);
|
|
68
|
+
}
|