@seamnet/client 0.18.2 → 0.19.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/lib/init.js CHANGED
@@ -225,8 +225,9 @@ function registerSeamHome() {
225
225
  }
226
226
  }
227
227
 
228
- export function patchClaudeMd() {
229
- const claudeMdPath = join(process.cwd(), 'CLAUDE.md');
228
+ export function patchClaudeMd(projectDir = process.cwd()) {
229
+ // 默认值 process.cwd() 仅对当前 home 有效;跨 home 调用必须显式传 projectDir
230
+ const claudeMdPath = join(projectDir, 'CLAUDE.md');
230
231
  const rulesRef = '@.seam/CHANNEL_RULES.md';
231
232
  const identityRef = '@.seam/IDENTITY.md';
232
233
  const contactsRef = '@.seam/contacts.json';
@@ -260,10 +261,11 @@ export function patchClaudeMd() {
260
261
  * 从 npm package 的 templates/ 拷贝 CHANNEL_RULES.md 到 .seam/。
261
262
  * 每次 init 和 guardian 启动都覆盖——确保老 AI 升级后获得最新规则。
262
263
  */
263
- export function syncChannelRules() {
264
+ export function syncChannelRules(seamHome = SEAM_DIR) {
265
+ // 默认值 SEAM_DIR 仅对当前 home 有效;跨 home 调用必须显式传 seamHome
264
266
  const src = join(TEMPLATES_DIR, 'CHANNEL_RULES.md');
265
- const dest = join(SEAM_DIR, 'CHANNEL_RULES.md');
266
- if (!existsSync(SEAM_DIR)) mkdirSync(SEAM_DIR, { recursive: true });
267
+ const dest = join(seamHome, 'CHANNEL_RULES.md');
268
+ if (!existsSync(seamHome)) mkdirSync(seamHome, { recursive: true });
267
269
  const content = readFileSync(src, 'utf8');
268
270
  writeFileSync(dest, content);
269
271
  return dest;
@@ -1,31 +1,43 @@
1
1
  /**
2
- * Host-tool 模式:对所有已注册的 SEAM_HOME 滚动升级 + 重启 guardian
2
+ * Host-tool 模式:对所有已注册的 SEAM_HOME 跑完整 applyHomeUpgrade
3
3
  *
4
4
  * 流程:
5
5
  * 1. 全局升 npm 包(仅一次)
6
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。)
7
+ * 3. 逐个 entry 跑完整 applyHomeUpgrade(version.json / patch settings /
8
+ * syncChannelRules / patchClaudeMd / .mcp.json / 重启 guardian)。
9
+ * 顺序执行,每个 entry try/catch——一个 home 失败不中断其余。
10
+ * 4. 末尾报告 ok/fail 计数 + 每个 home 结果。
10
11
  *
11
12
  * 仅升级"当前在跑的 guardian"——目录还在但 guardian 没开的,跳过。
12
- * 想拉那些上来,下次自己 `seam-client guardian start` 即可,
13
- * 那时已是新版 npm 包。
14
- *
13
+ * 想拉那些上来,下次自己 `seam-client guardian start` 即可(已是新版包)。
15
14
  * 路人 CC(没装 seam-client / 没 register)天然不在 registry,不会被碰。
16
15
  */
17
16
 
17
+ import { existsSync } from 'node:fs';
18
18
  import { join, dirname } from 'node:path';
19
- import { execSync, spawnSync } from 'node:child_process';
20
- import { fileURLToPath } from 'node:url';
19
+ import { execSync } from 'node:child_process';
21
20
  import { readAll } from './registry.js';
21
+ import { applyHomeUpgrade } from './upgrade.js';
22
22
 
23
- const __filename = fileURLToPath(import.meta.url);
24
- const CLI_PATH = join(dirname(__filename), '..', 'bin', 'cli.js');
23
+ /**
24
+ * 检测某 home 是否还残留本地 node_modules/@seamnet/client。
25
+ * 只警告,不删——删本地包是单独的迁移决定,由人手动做。
26
+ */
27
+ function checkLocalInstallResidue(seamHome) {
28
+ const localPkg = join(dirname(seamHome), 'node_modules', '@seamnet', 'client');
29
+ if (existsSync(localPkg)) {
30
+ console.log(
31
+ ` ⚠️ ${dirname(seamHome)} 仍有本地包 node_modules/@seamnet/client——`
32
+ + '全局迁移未完成,建议手动删除本地 node_modules/@seamnet/client'
33
+ );
34
+ }
35
+ }
25
36
 
26
37
  export async function upgradeAll() {
27
38
  console.log('Seam — host-tool upgrade-all\n');
28
39
 
40
+ // 1. 全局升包(一次)
29
41
  console.log('1. npm install -g @seamnet/client@latest');
30
42
  try {
31
43
  execSync('npm install -g @seamnet/client@latest', { stdio: 'inherit' });
@@ -34,35 +46,37 @@ export async function upgradeAll() {
34
46
  process.exit(1);
35
47
  }
36
48
 
49
+ // 2. 读 registry
37
50
  const entries = readAll();
38
51
  if (entries.length === 0) {
39
52
  console.log('\n2. registry 为空——没有正在运行的 guardian,结束。');
40
53
  return;
41
54
  }
42
- console.log(`\n2. registry 里 ${entries.length} 个活 guardian,逐个重启`);
55
+ console.log(`\n2. registry 里 ${entries.length} 个活 guardian,逐个跑完整升级`);
43
56
 
44
- let okCount = 0;
57
+ // 3. 逐个 applyHomeUpgrade(一个失败不中断其余)
58
+ const results = [];
45
59
  for (const entry of entries) {
46
60
  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;
61
+ console.log(`\n → ${userId} (home=${seam_home})`);
62
+ checkLocalInstallResidue(seam_home);
63
+ try {
64
+ await applyHomeUpgrade({
65
+ seamHome: seam_home,
66
+ ccSession: tmux_session,
67
+ ccSocket: tmux_socket,
68
+ });
69
+ results.push({ userId, ok: true });
70
+ } catch (e) {
71
+ console.error(` FAILED: ${e.message}`);
72
+ results.push({ userId, ok: false, error: e.message });
64
73
  }
65
74
  }
66
75
 
67
- console.log(`\nDone. ${okCount}/${entries.length} 个 guardian 已重启。`);
76
+ // 4. 报告
77
+ const okCount = results.filter((r) => r.ok).length;
78
+ console.log(`\nDone. ${okCount}/${results.length} 个 home 升级成功。`);
79
+ for (const r of results) {
80
+ console.log(` ${r.ok ? 'ok ' : 'FAIL'} ${r.userId}${r.ok ? '' : ' — ' + r.error}`);
81
+ }
68
82
  }
package/lib/upgrade.js CHANGED
@@ -1,121 +1,182 @@
1
1
  /**
2
2
  * 升级已入网 AI 的 seam-client 配置。
3
3
  *
4
- * 做:
5
- * 1. npm install @seamnet/client@latest
6
- * 2. patch .claude/settings.json:SessionStart hook 命令(老包名 → bin 名)
7
- * + 预授权 seam CLI(permissions.allow Bash(seam *))
8
- * 3. 重新 patchClaudeMd(确保引用 @.seam/contacts.json 等新增的)
9
- * 3b. 清理 .mcp.json 里的 seam-im 条目(MCP server 已废弃)
10
- * 4. 重启 guardian(新 detached 后台进程生效)
4
+ * applyHomeUpgrade({ seamHome, ccSession, ccSocket })
5
+ * —— 对指定 home per-home 升级:写 version.json、patch settings.json、
6
+ * syncChannelRules、patchClaudeMd、剥 .mcp.json seam-im、
7
+ * pending_upgrade_restart、spawn 重启该 home guardian。
8
+ * 不含 npm install——caller 先装好包。所有路径基于 seamHome 参数,
9
+ * 不碰 process.cwd()、不碰 paths.js SEAM_DIR 常量。
11
10
  *
12
- * 不做:
13
- * - 不动 credentials.json / IDENTITY.md(那些是 AI 自己的)
14
- * - 不重新 register(用原有 userId)
11
+ * upgrade() —— per-project 命令:本地 npm install + applyHomeUpgrade(当前 home)。
12
+ *
13
+ * 不做:不动 credentials.json / IDENTITY.md(AI 自己的),不重新 register
15
14
  */
16
15
 
17
16
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
18
- import { join } from 'node:path';
19
- import { execSync } from 'node:child_process';
17
+ import { join, dirname } from 'node:path';
18
+ import { fileURLToPath } from 'node:url';
19
+ import { execSync, spawnSync } from 'node:child_process';
20
20
  import { SEAM_DIR } from './paths.js';
21
+ import { resolveTmuxSocketPath } from './guardian.js';
21
22
 
22
- export async function upgrade() {
23
- console.log('Seam — upgrading...\n');
23
+ const CLI_PATH = join(dirname(fileURLToPath(import.meta.url)), '..', 'bin', 'cli.js');
24
24
 
25
- // 1. 升包
26
- console.log('1. npm install @seamnet/client@latest');
27
- try {
28
- execSync('npm install @seamnet/client@latest', { stdio: 'inherit' });
29
- } catch (e) {
30
- console.error(` npm install failed: ${e.message}`);
31
- process.exit(1);
32
- }
25
+ function sleep(ms) {
26
+ return new Promise((r) => setTimeout(r, ms));
27
+ }
33
28
 
34
- // 1b. 更新 version.json
29
+ /**
30
+ * 读 upgrade.js 模块自身相邻的 package.json 版本号。
31
+ * caller 跑完 npm install(local 或 global)后该文件已被覆盖成新版,
32
+ * 此处运行时读取即拿到新版本号——不依赖 cwd/node_modules(host-tool
33
+ * 模式下非当前 home 没有本地 node_modules)。
34
+ */
35
+ function readOwnVersion() {
35
36
  try {
36
- const newPkg = JSON.parse(readFileSync(join(process.cwd(), 'node_modules', '@seamnet', 'client', 'package.json'), 'utf8'));
37
- const versionPath = join(SEAM_DIR, 'version.json');
38
- writeFileSync(versionPath, JSON.stringify({ version: newPkg.version, upgradedAt: new Date().toISOString() }, null, 2));
39
- console.log(` version.json updated to ${newPkg.version}`);
40
- } catch (e) {
41
- console.error(` version.json update failed (non-fatal): ${e.message}`);
37
+ const pkgPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
38
+ return JSON.parse(readFileSync(pkgPath, 'utf8')).version;
39
+ } catch {
40
+ return null;
42
41
  }
42
+ }
43
43
 
44
- // 2. patch settings.json hook
45
- console.log('2. patching .claude/settings.json SessionStart hook');
46
- const settingsPath = join(process.cwd(), '.claude', 'settings.json');
47
- if (existsSync(settingsPath)) {
44
+ /**
45
+ * 检测当前 tmux session/socket。SEAM_CC_SESSION env 优先,否则回退
46
+ * `tmux display-message`。回退只对"当前 home"成立——AI 在自己的终端里
47
+ * 跑 upgrade,display-message 解析到的就是它自己。跨 home 的 caller
48
+ * (upgrade-all)不用这个,改从 registry entry 取 session/socket。
49
+ */
50
+ function detectCurrentSession() {
51
+ const ccSocket = resolveTmuxSocketPath() || '';
52
+ const tmux = ccSocket ? `tmux -S "${ccSocket}"` : 'tmux';
53
+ let ccSession = process.env.SEAM_CC_SESSION || '';
54
+ if (!ccSession) {
48
55
  try {
49
- const s = JSON.parse(readFileSync(settingsPath, 'utf8'));
50
- let changed = false;
51
- const oldCmd = 'npx @seamnet/client autostart';
52
- const newCmd = 'npx seam-client autostart';
53
- for (const group of s.hooks?.SessionStart || []) {
54
- for (const h of group.hooks || []) {
55
- if (h.command === oldCmd) {
56
- h.command = newCmd;
57
- changed = true;
58
- }
56
+ ccSession = execSync(`${tmux} display-message -p '#S'`, { encoding: 'utf8' }).trim();
57
+ } catch {}
58
+ }
59
+ return { ccSession, ccSocket };
60
+ }
61
+
62
+ function patchSettings(settingsPath) {
63
+ if (!existsSync(settingsPath)) {
64
+ console.log(' no .claude/settings.json, skip');
65
+ return;
66
+ }
67
+ try {
68
+ const s = JSON.parse(readFileSync(settingsPath, 'utf8'));
69
+ let changed = false;
70
+ const oldCmd = 'npx @seamnet/client autostart';
71
+ const newCmd = 'npx seam-client autostart';
72
+ for (const group of s.hooks?.SessionStart || []) {
73
+ for (const h of group.hooks || []) {
74
+ if (h.command === oldCmd) {
75
+ h.command = newCmd;
76
+ changed = true;
59
77
  }
60
78
  }
61
- // 预授权 seam CLI:老 AI 升级后用 `seam` 发消息的路径立刻可用
62
- if (!s.permissions) s.permissions = {};
63
- if (!Array.isArray(s.permissions.allow)) s.permissions.allow = [];
64
- if (!s.permissions.allow.includes('Bash(seam *)')) {
65
- s.permissions.allow.push('Bash(seam *)');
66
- changed = true;
67
- }
68
- if (changed) {
69
- writeFileSync(settingsPath, JSON.stringify(s, null, 2));
70
- console.log(' settings.json patched (hook command + seam CLI 预授权 Bash(seam *))');
71
- } else {
72
- console.log(' settings.json already up-to-date, skip');
73
- }
74
- } catch (e) {
75
- console.error(` failed to patch settings.json: ${e.message}`);
76
79
  }
80
+ // 预授权 seam CLI:老 AI 升级后用 `seam` 发消息的路径立刻可用
81
+ if (!s.permissions) s.permissions = {};
82
+ if (!Array.isArray(s.permissions.allow)) s.permissions.allow = [];
83
+ if (!s.permissions.allow.includes('Bash(seam *)')) {
84
+ s.permissions.allow.push('Bash(seam *)');
85
+ changed = true;
86
+ }
87
+ if (changed) {
88
+ writeFileSync(settingsPath, JSON.stringify(s, null, 2));
89
+ console.log(' settings.json patched (hook command + seam CLI 预授权 Bash(seam *))');
90
+ } else {
91
+ console.log(' settings.json already up-to-date, skip');
92
+ }
93
+ } catch (e) {
94
+ console.error(` failed to patch settings.json: ${e.message}`);
77
95
  }
96
+ }
78
97
 
79
- // 3. 刷新 CLAUDE.md 引用(contacts.json 等新增的)
80
- console.log('3. refreshing CLAUDE.md references + CHANNEL_RULES.md');
98
+ function cleanMcpJson(mcpPath) {
81
99
  try {
82
- const { patchClaudeMd, syncChannelRules } = await import('./init.js');
83
- syncChannelRules();
84
- patchClaudeMd();
100
+ if (!existsSync(mcpPath)) {
101
+ console.log(' no .mcp.json, skip');
102
+ return;
103
+ }
104
+ const mcp = JSON.parse(readFileSync(mcpPath, 'utf8'));
105
+ if (mcp.mcpServers && Object.prototype.hasOwnProperty.call(mcp.mcpServers, 'seam-im')) {
106
+ delete mcp.mcpServers['seam-im'];
107
+ writeFileSync(mcpPath, JSON.stringify(mcp, null, 2));
108
+ console.log(' removed seam-im entry from .mcp.json');
109
+ } else {
110
+ console.log(' no seam-im entry, skip');
111
+ }
85
112
  } catch (e) {
86
- console.error(` refresh failed (non-fatal): ${e.message}`);
113
+ console.error(` .mcp.json cleanup failed (non-fatal): ${e.message}`);
87
114
  }
115
+ }
116
+
117
+ /**
118
+ * 对指定 home 做 per-home 升级。不含 npm install。
119
+ *
120
+ * guardian 重启一律 spawn 子进程(带 SEAM_HOME/SEAM_CC_SESSION/SEAM_CC_SOCKET
121
+ * env)——不在进程内调 guardianStop/Start,那俩认死的 SEAM_DIR/PID_PATH,
122
+ * 对非当前 home 会操作错对象。
123
+ *
124
+ * @param {object} o
125
+ * @param {string} o.seamHome 绝对路径,<project>/.seam
126
+ * @param {string} [o.ccSession] 该 home 的 tmux session(此处不回退 display-message)
127
+ * @param {string} [o.ccSocket] 该 home 的 tmux socket
128
+ */
129
+ export async function applyHomeUpgrade({ seamHome, ccSession, ccSocket }) {
130
+ if (!seamHome) throw new Error('applyHomeUpgrade: seamHome required');
131
+ const projectDir = dirname(seamHome);
88
132
 
89
- // 3b. 清理 .mcp.json 里的 seam-im 条目(MCP server 0.18.0 已废弃,删它指向的
90
- // mcp-serve 已不存在——必须在 CC restart 之前剥掉,否则重启会加载到死引用)
91
- console.log('3b. cleaning seam-im from .mcp.json');
133
+ // version.json
92
134
  try {
93
- const mcpPath = join(process.cwd(), '.mcp.json');
94
- if (existsSync(mcpPath)) {
95
- const mcp = JSON.parse(readFileSync(mcpPath, 'utf8'));
96
- if (mcp.mcpServers && Object.prototype.hasOwnProperty.call(mcp.mcpServers, 'seam-im')) {
97
- delete mcp.mcpServers['seam-im'];
98
- writeFileSync(mcpPath, JSON.stringify(mcp, null, 2));
99
- console.log(' removed seam-im entry from .mcp.json');
100
- } else {
101
- console.log(' no seam-im entry, skip');
102
- }
103
- } else {
104
- console.log(' no .mcp.json, skip');
135
+ const version = readOwnVersion();
136
+ if (version) {
137
+ writeFileSync(
138
+ join(seamHome, 'version.json'),
139
+ JSON.stringify({ version, upgradedAt: new Date().toISOString() }, null, 2)
140
+ );
141
+ console.log(` version.json ${version}`);
105
142
  }
106
143
  } catch (e) {
107
- console.error(` .mcp.json cleanup failed (non-fatal): ${e.message}`);
144
+ console.error(` version.json update failed (non-fatal): ${e.message}`);
108
145
  }
109
146
 
110
- // 4. 重启 guardian(让新代码生效)+ 标记需要自动重启 CC
111
- console.log('4. restarting guardian + scheduling CC auto-restart');
147
+ // patch .claude/settings.json
148
+ patchSettings(join(projectDir, '.claude', 'settings.json'));
149
+
150
+ // 刷新 CHANNEL_RULES.md + CLAUDE.md 引用(显式传 home/projectDir)
112
151
  try {
113
- const { guardianStop, guardianStart } = await import('./guardian.js');
114
- await guardianStop();
115
- await new Promise((r) => setTimeout(r, 1000));
152
+ const { syncChannelRules, patchClaudeMd } = await import('./init.js');
153
+ syncChannelRules(seamHome);
154
+ patchClaudeMd(projectDir);
155
+ console.log(' CHANNEL_RULES + CLAUDE.md refreshed');
156
+ } catch (e) {
157
+ console.error(` CHANNEL_RULES/CLAUDE.md refresh failed (non-fatal): ${e.message}`);
158
+ }
159
+
160
+ // 剥 .mcp.json 的 seam-im(CC restart 前剥掉指向已删 mcp-serve 的死引用)
161
+ cleanMcpJson(join(projectDir, '.mcp.json'));
116
162
 
117
- // pending_upgrade_restart 标记——新 guardian 启动时会读到并自动重启 CC
118
- const statePath = join(SEAM_DIR, 'state.json');
163
+ // 重启 guardian:spawn 子进程 stop 置 flag → start
164
+ const env = {
165
+ ...process.env,
166
+ SEAM_HOME: seamHome,
167
+ SEAM_CC_SESSION: ccSession || '',
168
+ SEAM_CC_SOCKET: ccSocket || '',
169
+ };
170
+ const stopRes = spawnSync(process.execPath, [CLI_PATH, 'stop'], { env, stdio: 'inherit' });
171
+ if (stopRes.status !== 0) {
172
+ // stop 非 0 不致命——guardian 可能本来就没在跑
173
+ console.log(` guardian stop exited ${stopRes.status}(可能本来没跑,继续)`);
174
+ }
175
+ await sleep(1000);
176
+
177
+ // 置 pending_upgrade_restart——新 guardian 启动时读到会自动重启 CC
178
+ try {
179
+ const statePath = join(seamHome, 'state.json');
119
180
  let stateJson = {};
120
181
  if (existsSync(statePath)) {
121
182
  try { stateJson = JSON.parse(readFileSync(statePath, 'utf8')); } catch {}
@@ -123,10 +184,36 @@ export async function upgrade() {
123
184
  if (!stateJson.guardian) stateJson.guardian = {};
124
185
  stateJson.guardian.pending_upgrade_restart = true;
125
186
  writeFileSync(statePath, JSON.stringify(stateJson, null, 2));
187
+ } catch (e) {
188
+ console.error(` state.json flag failed (non-fatal): ${e.message}`);
189
+ }
190
+
191
+ const startRes = spawnSync(process.execPath, [CLI_PATH, 'guardian', 'start'], { env, stdio: 'inherit' });
192
+ if (startRes.status !== 0) {
193
+ throw new Error(`guardian start failed (exit ${startRes.status})`);
194
+ }
195
+ console.log(' guardian restarted');
196
+ }
197
+
198
+ export async function upgrade() {
199
+ console.log('Seam — upgrading...\n');
200
+
201
+ // 1. 本地升包
202
+ console.log('1. npm install @seamnet/client@latest');
203
+ try {
204
+ execSync('npm install @seamnet/client@latest', { stdio: 'inherit' });
205
+ } catch (e) {
206
+ console.error(` npm install failed: ${e.message}`);
207
+ process.exit(1);
208
+ }
126
209
 
127
- await guardianStart();
210
+ // 2. 对当前 home 跑 applyHomeUpgrade(session 走 display-message 检测)
211
+ console.log('2. applying per-home upgrade');
212
+ const { ccSession, ccSocket } = detectCurrentSession();
213
+ try {
214
+ await applyHomeUpgrade({ seamHome: SEAM_DIR, ccSession, ccSocket });
128
215
  } catch (e) {
129
- console.error(` guardian restart failed: ${e.message}`);
216
+ console.error(` upgrade failed: ${e.message}`);
130
217
  console.log('\n 请手动 /exit 并重新启动 Claude Code。');
131
218
  return;
132
219
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seamnet/client",
3
- "version": "0.18.2",
3
+ "version": "0.19.0",
4
4
  "description": "One command to join Seam — the network where people and AI stay in sync.",
5
5
  "bin": {
6
6
  "seam-client": "bin/cli.js",