@myclaw163/clawclaw-cli 0.6.56 → 0.6.58

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.
Files changed (50) hide show
  1. package/bin/clawclaw-cli.mjs +3 -3
  2. package/package.json +1 -1
  3. package/scripts/sync-bundled-skill.mjs +1 -1
  4. package/skills/clawclaw/SKILL.md +7 -3
  5. package/skills/clawclaw/references/GAME-MECHANICS.md +5 -5
  6. package/skills/clawclaw/references/STREAM.md +4 -3
  7. package/src/cli.ts +0 -43
  8. package/src/commands/config.ts +30 -30
  9. package/src/commands/game-start-plan.test.ts +10 -68
  10. package/src/commands/game.ts +318 -173
  11. package/src/commands/history.ts +1 -1
  12. package/src/commands/peek.ts +16 -9
  13. package/src/commands/setup/codex.ts +248 -248
  14. package/src/commands/setup/hermes.test.ts +96 -96
  15. package/src/commands/setup/hermes.ts +76 -76
  16. package/src/commands/setup/index.ts +13 -13
  17. package/src/commands/setup/openclaw.test.ts +114 -114
  18. package/src/commands/setup/openclaw.ts +147 -147
  19. package/src/commands/strategy.ts +29 -38
  20. package/src/commands/watch.test.ts +7 -11
  21. package/src/commands/watch.ts +77 -66
  22. package/src/lib/game-client.ts +3 -3
  23. package/src/lib/host-config-patcher.test.ts +130 -130
  24. package/src/lib/host-config-patcher.ts +151 -151
  25. package/src/lib/hub-reminder.ts +19 -19
  26. package/src/lib/strategy-export.test.ts +1 -1
  27. package/src/runtime/event-daemon.test.ts +81 -2
  28. package/src/runtime/event-daemon.ts +325 -287
  29. package/src/runtime/owner-control.ts +150 -0
  30. package/src/runtime/runtime-logger.ts +11 -3
  31. package/src/runtime/ws-client.test.ts +57 -0
  32. package/src/runtime/ws-client.ts +2 -2
  33. package/src/sdk/index.ts +4 -4
  34. package/src/strategies/game-utils.test.ts +27 -1
  35. package/src/strategies/game-utils.ts +27 -4
  36. package/src/strategies/goals/crab-octopus-reflexes.ts +4 -4
  37. package/src/strategies/goals/crab-sabotage-top.ts +1 -1
  38. package/src/strategies/goals/keep-away-goal.ts +10 -7
  39. package/src/strategies/goals/kill-frenzy-top.ts +5 -5
  40. package/src/strategies/goals/kill-target-goal.ts +2 -2
  41. package/src/strategies/goals/kill-target-top.ts +2 -2
  42. package/src/strategies/goals/lone-kill-core.ts +3 -3
  43. package/src/strategies/goals/lone-kill-task-top.ts +1 -1
  44. package/src/strategies/goals/task-kill-report-top.ts +3 -3
  45. package/src/strategies/goals/warrior-shrimp-top.ts +2 -2
  46. package/src/strategies/pathfind/escape-planner.ts +10 -3
  47. package/src/strategies/spawn.ts +16 -5
  48. package/src/strategies/strategy-loop.ts +9 -3
  49. package/src/runtime/daemon.ts +0 -100
  50. package/src/runtime/opening-mover.ts +0 -303
@@ -309,14 +309,21 @@ export interface RouteInfo {
309
309
  nearThreat: boolean;
310
310
  }
311
311
 
312
- /** 沿路径每 4 格(16px)采样一次离威胁点的欧氏距离,含末端格;半径 ~200px 下足够密。 */
313
- function pathNearAny(grid: WalkableGrid, path: number[], threats: Position[], radiusPx: number): boolean {
312
+ /**
313
+ * 沿路径每 4 格(16px)采样一次离威胁点的欧氏距离,含末端格;半径 ~200px 下足够密。
314
+ * 豁免起点 from 的 radiusPx 邻域:逃跑那刻起点必然紧贴威胁,这一采样点不该把
315
+ * 「背向威胁、越走越远」的整条路线判危;真正夹在你与目的地之间、离起点超过
316
+ * radiusPx 的威胁仍会命中。对齐同文件 pathsCross 跳过 step 起点的语义。
317
+ */
318
+ function pathNearAny(grid: WalkableGrid, path: number[], threats: Position[], radiusPx: number, from: Position): boolean {
314
319
  for (let i = 0; i < path.length; i += 4) {
315
320
  const idx = Math.min(i, path.length - 1);
316
321
  const w = grid.cellToWorld(path[idx]);
322
+ if (Math.hypot(w.x - from.x, w.y - from.y) <= radiusPx) continue;
317
323
  if (threats.some(t => Math.hypot(w.x - t.x, w.y - t.y) <= radiusPx)) return true;
318
324
  }
319
325
  const end = grid.cellToWorld(path[path.length - 1]);
326
+ if (Math.hypot(end.x - from.x, end.y - from.y) <= radiusPx) return false;
320
327
  return threats.some(t => Math.hypot(end.x - t.x, end.y - t.y) <= radiusPx);
321
328
  }
322
329
 
@@ -342,7 +349,7 @@ export function assessRoutes(
342
349
  if (units === Infinity) return { distancePx: Infinity, nearThreat: false };
343
350
  return {
344
351
  distancePx: units * PX_PER_UNIT,
345
- nearThreat: threats.length > 0 && pathNearAny(grid, calc.pathToSource(cell), threats, threatRadiusPx),
352
+ nearThreat: threats.length > 0 && pathNearAny(grid, calc.pathToSource(cell), threats, threatRadiusPx, from),
346
353
  };
347
354
  });
348
355
  }
@@ -11,16 +11,25 @@ const __dirname = dirname(__filename);
11
11
  const fwd = (p: string) => p.replace(/\\/g, '/');
12
12
  const BIN_MJS = fwd(resolve(__dirname, '..', '..', 'bin', 'clawclaw-cli.mjs'));
13
13
 
14
+ function isTrue(value: string | undefined): boolean {
15
+ return typeof value === 'string' && value.trim().toLowerCase() === 'true';
16
+ }
17
+
18
+ function isDeveloperDiagnosticsEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
19
+ return isTrue(env.CCL_DEBUG_RUNTIME) || isTrue(env.CCL_TEST) || isTrue(env['ccl-test']);
20
+ }
21
+
14
22
  export function spawnStrategyLoop(
15
23
  strategyId: string,
16
24
  strategyArgs?: string[],
17
- opts?: { source?: string; gameId?: string; role?: string },
25
+ opts?: { source?: string; gameId?: string; role?: string; detached?: boolean; writeRuntimeFiles?: boolean },
18
26
  ): ReturnType<typeof spawn> {
19
27
  if (!existsSync(BIN_MJS)) throw new Error('clawclaw-cli bin entry not found');
20
28
  const isWin = process.platform === 'win32';
29
+ const writeRuntimeFiles = opts?.writeRuntimeFiles ?? false;
21
30
 
22
31
  let logFile: string | undefined;
23
- if (isWin) {
32
+ if (isWin && isDeveloperDiagnosticsEnabled()) {
24
33
  try {
25
34
  const store = new AuthStore();
26
35
  const active = store.getActive();
@@ -37,17 +46,19 @@ export function spawnStrategyLoop(
37
46
  const args = ['_strategy', strategyId];
38
47
  if (strategyArgs && strategyArgs.length > 0) args.push('--', ...strategyArgs);
39
48
 
49
+ const detached = opts?.detached ?? false;
40
50
  const child = spawn(process.execPath, [BIN_MJS, ...args], {
41
- detached: true,
51
+ detached,
42
52
  stdio: ['ignore', 'ignore', 'ignore'],
43
53
  env: {
44
54
  ...process.env,
45
55
  ...(isWin && logFile ? { CLAWCLAW_LOG_FILE: logFile } : {}),
56
+ ...(writeRuntimeFiles ? { CLAWCLAW_STRATEGY_RUNTIME_FILES: '1' } : {}),
46
57
  },
47
58
  windowsHide: true,
48
59
  });
49
60
 
50
- if (child.pid) {
61
+ if (child.pid && writeRuntimeFiles) {
51
62
  try {
52
63
  const store = new AuthStore();
53
64
  const active = store.getActive();
@@ -66,6 +77,6 @@ export function spawnStrategyLoop(
66
77
  } catch {}
67
78
  }
68
79
 
69
- child.unref();
80
+ if (detached) child.unref();
70
81
  return child;
71
82
  }
@@ -49,6 +49,10 @@ function getStatusPath(): string {
49
49
  return join(getProfileStateDir(profile), 'auto.json');
50
50
  }
51
51
 
52
+ function shouldWriteStrategyRuntimeFiles(): boolean {
53
+ return process.env.CLAWCLAW_STRATEGY_RUNTIME_FILES === '1';
54
+ }
55
+
52
56
  export function isStrategyRunning(): boolean {
53
57
  try {
54
58
  const pidPath = getPidPath();
@@ -353,7 +357,7 @@ export async function runStrategyLoop(strategyId: string, args?: string[]): Prom
353
357
  const store = EventStore.forActiveAccount();
354
358
  const pidPath = getPidPath();
355
359
 
356
- writeFileSync(pidPath, String(process.pid));
360
+ if (shouldWriteStrategyRuntimeFiles()) writeFileSync(pidPath, String(process.pid));
357
361
  store.append({ type: 'auto', message: 'strategy started', strategy: strategyId, pid: process.pid });
358
362
 
359
363
  const client = GameClient.fromAuth();
@@ -442,7 +446,7 @@ export async function runStrategyLoop(strategyId: string, args?: string[]): Prom
442
446
  const newEvents = extractNewEvents(result);
443
447
  store.appendNewEvents(newEvents);
444
448
  // action 结果里回来的 corpse_spotted 也入账尸体记忆——主循环只 observe state.new_events,
445
- // 那条通道会被 daemon 抢游标,这里补上避免尸体坐标漏记(ctx.knownCorpses 与 list() 同引用,立即可见)。
449
+ // 那条通道会被 runtime WS listener 抢游标,这里补上避免尸体坐标漏记(ctx.knownCorpses 与 list() 同引用,立即可见)。
446
450
  corpseMemory.ingestEvents(newEvents);
447
451
  learnTeammatesFromEvents(newEvents, teammates);
448
452
  if (currentRole === 'shrimp_pistol' && ownKillEvent(newEvents, currentPlayerName)) {
@@ -753,5 +757,7 @@ export async function runStrategyLoop(strategyId: string, args?: string[]): Prom
753
757
 
754
758
  process.off('SIGINT', onSignal);
755
759
  process.off('SIGTERM', onSignal);
756
- try { unlinkSync(pidPath); } catch {}
760
+ if (shouldWriteStrategyRuntimeFiles()) {
761
+ try { unlinkSync(pidPath); } catch {}
762
+ }
757
763
  }
@@ -1,100 +0,0 @@
1
- import { existsSync, mkdirSync, openSync, readFileSync } from 'fs';
2
- import { resolve, dirname, delimiter } from 'path';
3
- import { spawn, execSync } from 'child_process';
4
- import { join } from 'path';
5
- import { fileURLToPath } from 'url';
6
- import { getProfileStateDir, getProfileLogsDir } from '../lib/init-command.js';
7
- import { AuthStore } from '../lib/auth.js';
8
- import { RuntimeLogger } from './runtime-logger.js';
9
-
10
- const log = new RuntimeLogger();
11
-
12
- const __filename = fileURLToPath(import.meta.url);
13
- const __dirname = dirname(__filename);
14
-
15
- function isPidAlive(pid: number): boolean {
16
- try { process.kill(pid, 0); return true; } catch { return false; }
17
- }
18
-
19
- export function getRunningDaemonPid(stateDir: string): number | null {
20
- const runtimePath = join(stateDir, 'runtime.json');
21
- if (!existsSync(runtimePath)) return null;
22
- try {
23
- const info = JSON.parse(readFileSync(runtimePath, 'utf8'));
24
- if (typeof info?.pid === 'number' && isPidAlive(info.pid)) return info.pid;
25
- } catch {}
26
- return null;
27
- }
28
-
29
- export async function waitDaemonExit(stateDir: string, timeoutMs = 15000): Promise<void> {
30
- const runtimePath = join(stateDir, 'runtime.json');
31
- let pid: number | undefined;
32
- try {
33
- if (existsSync(runtimePath)) {
34
- const info = JSON.parse(readFileSync(runtimePath, 'utf8'));
35
- if (typeof info?.pid === 'number') pid = info.pid;
36
- }
37
- } catch {}
38
-
39
- const deadline = Date.now() + timeoutMs;
40
- while (Date.now() < deadline) {
41
- const fileGone = !existsSync(runtimePath);
42
- const procGone = pid === undefined ? true : !isPidAlive(pid);
43
- if (fileGone && procGone) return;
44
- await new Promise(r => setTimeout(r, 200));
45
- }
46
- throw new Error(
47
- `Daemon did not exit within ${timeoutMs}ms (pid=${pid ?? 'unknown'}).`,
48
- );
49
- }
50
-
51
- export function spawnDaemon(): { pid: number; logFile: string } {
52
- const store = new AuthStore();
53
- const active = store.getActive();
54
- if (!active) throw new Error('Not logged in.');
55
-
56
- const stateDir = getProfileStateDir(active);
57
- mkdirSync(stateDir, { recursive: true });
58
-
59
- const existingPid = getRunningDaemonPid(stateDir);
60
- if (existingPid) {
61
- throw new Error(`Daemon already running (pid=${existingPid}). Stop it first.`);
62
- }
63
-
64
- const logsDir = getProfileLogsDir(active);
65
- mkdirSync(logsDir, { recursive: true });
66
-
67
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
68
- const logFile = join(logsDir, `daemon-${timestamp}.log`);
69
- const logFd = openSync(logFile, 'a');
70
-
71
- const fwd = (p: string) => p.replace(/\\/g, '/');
72
- const binEntry = fwd(resolve(__dirname, '..', '..', 'bin', 'clawclaw-cli.mjs'));
73
- if (!existsSync(binEntry)) throw new Error('clawclaw-cli bin entry not found');
74
-
75
- let npmGlobalModules = '';
76
- try { npmGlobalModules = execSync('npm root -g', { encoding: 'utf8', windowsHide: true }).trim(); } catch {}
77
-
78
- const existingNodePath = process.env.NODE_PATH ?? '';
79
- const nodePath = [npmGlobalModules, existingNodePath].filter(Boolean).join(delimiter);
80
- const isWin = process.platform === 'win32';
81
-
82
- const child = spawn(process.execPath, [binEntry, '_daemon'], {
83
- detached: true,
84
- stdio: isWin ? ['ignore', 'ignore', 'ignore'] : ['ignore', logFd, logFd],
85
- cwd: process.cwd(),
86
- env: {
87
- ...process.env,
88
- NODE_PATH: nodePath,
89
- ...(isWin ? { CLAWCLAW_LOG_FILE: logFile } : {}),
90
- },
91
- windowsHide: true,
92
- });
93
-
94
- child.on('error', (err) => {
95
- log.error('DAEMON_SPAWN', `failed to start daemon`, err);
96
- });
97
-
98
- child.unref();
99
- return { pid: child.pid!, logFile };
100
- }
@@ -1,303 +0,0 @@
1
- import { spawn } from 'child_process';
2
- import { appendFileSync, existsSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync } from 'fs';
3
- import { delimiter, dirname, join, resolve } from 'path';
4
- import { execSync } from 'child_process';
5
- import { fileURLToPath } from 'url';
6
- import { Action } from '../sdk/action.js';
7
- import { AuthStore } from '../lib/auth.js';
8
- import { GameClient } from '../lib/game-client.js';
9
- import { getProfileLogsDir, getProfileStateDir } from '../lib/init-command.js';
10
- import { isStrategyRunning as isAutoRunning } from '../strategies/strategy-loop.js';
11
-
12
- const __filename = fileURLToPath(import.meta.url);
13
- const __dirname = dirname(__filename);
14
-
15
- const WANDERING_WINDOW_MS = 30_000;
16
- const STARTUP_WAIT_MS = 10 * 60_000;
17
- const POLL_MS = 1000;
18
- const CANCEL_TTL_MS = 45_000;
19
-
20
- interface Point {
21
- x: number;
22
- y: number;
23
- }
24
-
25
- function sleep(ms: number): Promise<void> {
26
- return new Promise(resolve => setTimeout(resolve, ms));
27
- }
28
-
29
- function debug(message: string): void {
30
- if (process.env.CLAWCLAW_LOG_FILE) {
31
- try {
32
- appendFileSync(process.env.CLAWCLAW_LOG_FILE, `[${new Date().toISOString()}] ${message}\n`);
33
- } catch {}
34
- }
35
- }
36
-
37
- function isPidAlive(pid: number): boolean {
38
- try { process.kill(pid, 0); return true; } catch { return false; }
39
- }
40
-
41
- function activeProfile() {
42
- const profile = new AuthStore().getActive();
43
- if (!profile) throw new Error('Not logged in.');
44
- return profile;
45
- }
46
-
47
- function pidPath(): string {
48
- return join(getProfileStateDir(activeProfile()), 'opening-mover.pid');
49
- }
50
-
51
- function cancelPath(): string {
52
- return join(getProfileStateDir(activeProfile()), 'opening-mover.cancel');
53
- }
54
-
55
- function readCancelToken(path: string): number {
56
- try {
57
- const token = Number(readFileSync(path, 'utf8').trim());
58
- return Number.isFinite(token) ? token : 0;
59
- } catch {
60
- return 0;
61
- }
62
- }
63
-
64
- function cleanupPid(path: string): void {
65
- try {
66
- const current = Number(readFileSync(path, 'utf8').trim());
67
- if (current === process.pid) unlinkSync(path);
68
- } catch {}
69
- }
70
-
71
- export function stopOpeningMoverIfRunning(): void {
72
- try {
73
- const path = pidPath();
74
- if (!existsSync(path)) return;
75
- const pid = Number(readFileSync(path, 'utf8').trim());
76
- if (pid > 0 && isPidAlive(pid)) {
77
- try { process.kill(pid, 'SIGTERM'); } catch {}
78
- }
79
- try { unlinkSync(path); } catch {}
80
- } catch {}
81
- }
82
-
83
- export function cancelOpeningMoverWindow(): void {
84
- try {
85
- const path = cancelPath();
86
- mkdirSync(dirname(path), { recursive: true });
87
- writeFileSync(path, String(Date.now()));
88
- } catch {}
89
- }
90
-
91
- function roomAnchor(r: any): Point | null {
92
- const taskLocations = Array.isArray(r?.task_locations) ? r.task_locations : [];
93
- const candidates = taskLocations
94
- .map((tl: any) => ({ x: Number(tl?.x ?? tl?.[0]), y: Number(tl?.y ?? tl?.[1]) }))
95
- .filter((p: Point) => Number.isFinite(p.x) && Number.isFinite(p.y));
96
- if (candidates.length > 0) return candidates[Math.floor(Math.random() * candidates.length)];
97
-
98
- const poly: number[][] = Array.isArray(r?.polygon) ? r.polygon : [];
99
- if (poly.length === 0) return null;
100
-
101
- let area = 0;
102
- let cx = 0;
103
- let cy = 0;
104
- for (let i = 0; i < poly.length; i += 1) {
105
- const [x1, y1] = poly[i];
106
- const [x2, y2] = poly[(i + 1) % poly.length];
107
- const cross = x1 * y2 - x2 * y1;
108
- area += cross;
109
- cx += (x1 + x2) * cross;
110
- cy += (y1 + y2) * cross;
111
- }
112
- area *= 0.5;
113
- if (Math.abs(area) < 1e-6) {
114
- return {
115
- x: poly.reduce((sum, p) => sum + p[0], 0) / poly.length,
116
- y: poly.reduce((sum, p) => sum + p[1], 0) / poly.length,
117
- };
118
- }
119
- return { x: cx / (6 * area), y: cy / (6 * area) };
120
- }
121
-
122
- function randomTarget(mapData: any): Point | null {
123
- const rooms = Array.isArray(mapData?.rooms) ? mapData.rooms : [];
124
- const anchors = rooms.map(roomAnchor).filter((p: Point | null): p is Point => p !== null);
125
- if (anchors.length === 0) return null;
126
- return anchors[Math.floor(Math.random() * anchors.length)];
127
- }
128
-
129
- export async function runOpeningMover(): Promise<void> {
130
- const path = pidPath();
131
- const cancelFile = cancelPath();
132
- mkdirSync(dirname(path), { recursive: true });
133
- writeFileSync(path, String(process.pid));
134
-
135
- let running = true;
136
- process.on('SIGINT', () => { running = false; });
137
- process.on('SIGTERM', () => { running = false; });
138
-
139
- const client = GameClient.fromAuth();
140
- const startupDeadline = Date.now() + STARTUP_WAIT_MS;
141
- let wanderingDeadline = 0;
142
- let helperMovingUntil = 0;
143
- let lastPhase: string | null = null;
144
- let lastCancelToken = 0;
145
- let windowActive = false;
146
-
147
- try {
148
- debug('opening mover started');
149
- try { await client.discoverGameServer(); } catch {}
150
- while (running) {
151
- let state: any = null;
152
- try {
153
- state = await client.getGameState();
154
- } catch {
155
- await sleep(1500);
156
- continue;
157
- }
158
-
159
- if (!state) {
160
- if (Date.now() >= startupDeadline) break;
161
- await sleep(1500);
162
- continue;
163
- }
164
- if (state.phase === 'game_over') break;
165
-
166
- const cancelToken = readCancelToken(cancelFile);
167
- if (cancelToken > lastCancelToken) {
168
- lastCancelToken = cancelToken;
169
- if (Date.now() - cancelToken <= CANCEL_TTL_MS) {
170
- windowActive = false;
171
- wanderingDeadline = 0;
172
- helperMovingUntil = 0;
173
- if (state.phase === 'wandering') lastPhase = 'wandering';
174
- debug('movement helper window cancelled by manual action');
175
- }
176
- }
177
-
178
- if (state.phase !== 'wandering') {
179
- lastPhase = state.phase;
180
- windowActive = false;
181
- wanderingDeadline = 0;
182
- helperMovingUntil = 0;
183
- await sleep(POLL_MS);
184
- continue;
185
- }
186
-
187
- if (lastPhase !== 'wandering') {
188
- lastPhase = 'wandering';
189
- helperMovingUntil = 0;
190
- if (isAutoRunning()) {
191
- windowActive = false;
192
- wanderingDeadline = 0;
193
- debug('wandering detected; auto running, movement helper skipped');
194
- } else if (state.you?.currently_moving || state.you?.doing_task) {
195
- windowActive = false;
196
- wanderingDeadline = 0;
197
- debug('wandering detected; player already active, movement helper skipped');
198
- } else {
199
- windowActive = true;
200
- wanderingDeadline = Date.now() + WANDERING_WINDOW_MS;
201
- debug('wandering detected; movement helper window started');
202
- }
203
- }
204
-
205
- if (!windowActive) {
206
- await sleep(POLL_MS);
207
- continue;
208
- }
209
- if (Date.now() >= wanderingDeadline) {
210
- windowActive = false;
211
- wanderingDeadline = 0;
212
- debug('movement helper window expired');
213
- await sleep(POLL_MS);
214
- continue;
215
- }
216
-
217
- if (isAutoRunning()) {
218
- windowActive = false;
219
- wanderingDeadline = 0;
220
- debug('auto started; movement helper window stopped');
221
- await sleep(POLL_MS);
222
- continue;
223
- }
224
-
225
- if (state.you?.currently_moving || state.you?.doing_task || Date.now() < helperMovingUntil) {
226
- await sleep(POLL_MS);
227
- continue;
228
- }
229
-
230
- let mapData: any = null;
231
- try {
232
- mapData = await client.getMap();
233
- } catch {
234
- await sleep(POLL_MS);
235
- continue;
236
- }
237
- const target = randomTarget(mapData);
238
- if (!target) {
239
- await sleep(POLL_MS);
240
- continue;
241
- }
242
-
243
- let result: any;
244
- try {
245
- result = await client.submitAction(Action.move(target).toJSON() as any);
246
- } catch {
247
- await sleep(POLL_MS);
248
- continue;
249
- }
250
- const actionResult = result?.data ?? result;
251
- const durationSecs = Number(actionResult?.duration_secs ?? result?.duration_secs ?? 2);
252
- debug(`opening move submitted target=(${target.x},${target.y}) duration=${durationSecs}`);
253
- helperMovingUntil = Date.now() + Math.max(0.5, durationSecs) * 1000 + 500;
254
- }
255
- } finally {
256
- debug('opening mover stopped');
257
- cleanupPid(path);
258
- }
259
- }
260
-
261
- export function spawnOpeningMover(): { pid: number; logFile: string } {
262
- const profile = activeProfile();
263
- const stateDir = getProfileStateDir(profile);
264
- mkdirSync(stateDir, { recursive: true });
265
-
266
- const path = join(stateDir, 'opening-mover.pid');
267
- if (existsSync(path)) {
268
- try {
269
- const pid = Number(readFileSync(path, 'utf8').trim());
270
- if (pid > 0 && isPidAlive(pid)) return { pid, logFile: '' };
271
- } catch {}
272
- try { unlinkSync(path); } catch {}
273
- }
274
-
275
- const logsDir = getProfileLogsDir(profile);
276
- mkdirSync(logsDir, { recursive: true });
277
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
278
- const logFile = join(logsDir, `opening-mover-${timestamp}.log`);
279
- const logFd = openSync(logFile, 'a');
280
-
281
- const binEntry = resolve(__dirname, '..', '..', 'bin', 'clawclaw-cli.mjs').replace(/\\/g, '/');
282
- if (!existsSync(binEntry)) throw new Error('clawclaw-cli bin entry not found');
283
-
284
- let npmGlobalModules = '';
285
- try { npmGlobalModules = execSync('npm root -g', { encoding: 'utf8', windowsHide: true }).trim(); } catch {}
286
- const nodePath = [npmGlobalModules, process.env.NODE_PATH ?? ''].filter(Boolean).join(delimiter);
287
- const isWin = process.platform === 'win32';
288
-
289
- const child = spawn(process.execPath, [binEntry, '_opener'], {
290
- detached: true,
291
- stdio: isWin ? ['ignore', 'ignore', 'ignore'] : ['ignore', logFd, logFd],
292
- cwd: process.cwd(),
293
- env: {
294
- ...process.env,
295
- NODE_PATH: nodePath,
296
- ...(isWin ? { CLAWCLAW_LOG_FILE: logFile } : {}),
297
- },
298
- windowsHide: true,
299
- });
300
- child.unref();
301
- if (child.pid) writeFileSync(path, String(child.pid));
302
- return { pid: child.pid!, logFile };
303
- }