@seamnet/client 0.15.0 → 0.16.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/cli.js +26 -12
- package/bin/seam.js +26 -0
- package/lib/guardian.js +30 -8
- 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 +67 -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。
|
|
@@ -136,16 +137,36 @@ export async function guardianRun() {
|
|
|
136
137
|
console.error(`Guardian already running (pid: ${existingPid}). Exiting.`);
|
|
137
138
|
process.exit(0);
|
|
138
139
|
}
|
|
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
140
|
const credentials = JSON.parse(readFileSync(CREDENTIALS_PATH, 'utf8'));
|
|
147
141
|
const ccSession = process.env.SEAM_CC_SESSION || '';
|
|
148
142
|
const ccSocket = process.env.SEAM_CC_SOCKET || resolveTmuxSocketPath() || '';
|
|
143
|
+
|
|
144
|
+
// 写 PID(覆盖 guardianStart 写的 child.pid),退出时清理
|
|
145
|
+
writeFileSync(PID_PATH, String(process.pid));
|
|
146
|
+
|
|
147
|
+
// 写 cross-AI registry entry(~/.shared/seam-registry/<userId>.json)
|
|
148
|
+
try {
|
|
149
|
+
registryWrite({
|
|
150
|
+
userId: credentials.userId,
|
|
151
|
+
aliases: [credentials.name].filter(Boolean),
|
|
152
|
+
seam_home: SEAM_DIR,
|
|
153
|
+
tmux_session: ccSession,
|
|
154
|
+
tmux_socket: ccSocket,
|
|
155
|
+
socket: SOCKET_PATH,
|
|
156
|
+
pid: process.pid,
|
|
157
|
+
started_at: new Date().toISOString(),
|
|
158
|
+
});
|
|
159
|
+
} catch (e) {
|
|
160
|
+
console.error(`[guardian] registry write failed (non-fatal): ${e.message}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const cleanup = () => {
|
|
164
|
+
try { if (existsSync(PID_PATH)) unlinkSync(PID_PATH); } catch {}
|
|
165
|
+
try { registryRemove(credentials.userId); } catch {}
|
|
166
|
+
};
|
|
167
|
+
process.on('exit', cleanup);
|
|
168
|
+
process.on('SIGTERM', () => { cleanup(); process.exit(0); });
|
|
169
|
+
process.on('SIGINT', () => { cleanup(); process.exit(0); });
|
|
149
170
|
const jsonlPath = join(LOGS_DIR, 'guardian.jsonl');
|
|
150
171
|
|
|
151
172
|
if (!existsSync(LOGS_DIR)) mkdirSync(LOGS_DIR, { recursive: true });
|
|
@@ -236,10 +257,11 @@ export async function guardianRun() {
|
|
|
236
257
|
}, 10000);
|
|
237
258
|
}
|
|
238
259
|
|
|
239
|
-
// Keep alive + graceful shutdown (pid 文件清理)
|
|
260
|
+
// Keep alive + graceful shutdown (pid 文件清理 + registry 清理)
|
|
240
261
|
const shutdown = async (signal) => {
|
|
241
262
|
hub.logger('guardian').info('shutdown_signal', { signal });
|
|
242
263
|
try { if (existsSync(PID_PATH)) unlinkSync(PID_PATH); } catch {}
|
|
264
|
+
try { registryRemove(credentials.userId); } catch {}
|
|
243
265
|
await stop();
|
|
244
266
|
process.exit(0);
|
|
245
267
|
};
|
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,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-tool 模式:对所有已注册的 SEAM_HOME 滚动升级 + 重启 guardian。
|
|
3
|
+
*
|
|
4
|
+
* 流程:
|
|
5
|
+
* 1. 全局升 npm 包(仅一次)
|
|
6
|
+
* 2. 读 ~/.seam-homes,遍历每个 SEAM_HOME
|
|
7
|
+
* 3. 每个 SEAM_HOME spawn 子进程 stop → start guardian(隔离 paths.js 缓存)
|
|
8
|
+
* 4. 清掉目录已不存在的 entry
|
|
9
|
+
*
|
|
10
|
+
* 注:本文件是 stage 1 的 registry 实现;stage 2 会替换为 ~/.shared/seam-registry/<userId>.json。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import { join, dirname } from 'node:path';
|
|
15
|
+
import { homedir } from 'node:os';
|
|
16
|
+
import { execSync, spawnSync } from 'node:child_process';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
|
|
19
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
20
|
+
const CLI_PATH = join(dirname(__filename), '..', 'bin', 'cli.js');
|
|
21
|
+
|
|
22
|
+
export async function upgradeAll() {
|
|
23
|
+
console.log('Seam — host-tool upgrade-all\n');
|
|
24
|
+
|
|
25
|
+
console.log('1. npm install -g @seamnet/client@latest');
|
|
26
|
+
try {
|
|
27
|
+
execSync('npm install -g @seamnet/client@latest', { stdio: 'inherit' });
|
|
28
|
+
} catch (e) {
|
|
29
|
+
console.error(` npm install -g failed: ${e.message}`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const homesPath = join(homedir(), '.seam-homes');
|
|
34
|
+
if (!existsSync(homesPath)) {
|
|
35
|
+
console.log('\n2. ~/.seam-homes 不存在——无已注册 AI,结束。');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const lines = readFileSync(homesPath, 'utf8').split('\n').filter(Boolean);
|
|
39
|
+
console.log(`\n2. ${lines.length} 个已注册 SEAM_HOME,逐个重启 guardian`);
|
|
40
|
+
|
|
41
|
+
const alive = [];
|
|
42
|
+
for (const home of lines) {
|
|
43
|
+
console.log(`\n → ${home}`);
|
|
44
|
+
if (!existsSync(home)) {
|
|
45
|
+
console.log(' 跳过(目录已不存在,从 registry 移除)');
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const env = { ...process.env, SEAM_HOME: home };
|
|
49
|
+
const stopRes = spawnSync('node', [CLI_PATH, 'stop'], { env, stdio: 'inherit' });
|
|
50
|
+
if (stopRes.status !== 0) {
|
|
51
|
+
console.log(' stop 非零退出(guardian 可能未运行,继续)');
|
|
52
|
+
}
|
|
53
|
+
const startRes = spawnSync('node', [CLI_PATH, 'guardian', 'start'], { env, stdio: 'inherit' });
|
|
54
|
+
if (startRes.status !== 0) {
|
|
55
|
+
console.error(` start 失败 (exit ${startRes.status})`);
|
|
56
|
+
} else {
|
|
57
|
+
alive.push(home);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (alive.length !== lines.length) {
|
|
62
|
+
writeFileSync(homesPath, alive.length ? alive.join('\n') + '\n' : '');
|
|
63
|
+
console.log(`\n registry 清理:${lines.length} → ${alive.length}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log(`\nDone. ${alive.length} 个 AI guardian 已重启。`);
|
|
67
|
+
}
|