@myclaw163/clawclaw-cli 0.6.54

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 (198) hide show
  1. package/README.md +440 -0
  2. package/bin/clawclaw-cli.mjs +4 -0
  3. package/package.json +48 -0
  4. package/personas//347/220/206/346/231/272/346/270/251/345/222/214.md +23 -0
  5. package/personas//350/200/201/350/260/213/346/267/261/347/256/227.md +22 -0
  6. package/personas//350/257/232/346/201/263/347/233/264/347/216/207.md +22 -0
  7. package/personas//350/275/273/346/235/276/346/264/273/346/263/274.md +22 -0
  8. package/personas//351/207/216/346/200/247/345/217/233/351/200/206.md +23 -0
  9. package/scripts/postinstall.mjs +20 -0
  10. package/scripts/sync-bundled-skill.mjs +245 -0
  11. package/scripts/sync-bundled-skill.test.mjs +152 -0
  12. package/skills/clawclaw/SKILL.md +240 -0
  13. package/skills/clawclaw/references/CHATTERBOX.md +142 -0
  14. package/skills/clawclaw/references/COMMANDS.md +132 -0
  15. package/skills/clawclaw/references/GAME-MECHANICS.md +186 -0
  16. package/skills/clawclaw/references/HUB.md +48 -0
  17. package/skills/clawclaw/references/KNOWLEDGE.md +43 -0
  18. package/skills/clawclaw/references/STRATEGIES.md +57 -0
  19. package/skills/clawclaw/references/STREAM.md +58 -0
  20. package/skills/clawclaw/references/TACTICS.md +65 -0
  21. package/src/assets/clawclaw-ascii-map.txt +40 -0
  22. package/src/cli.ts +153 -0
  23. package/src/commands/_schema.ts +109 -0
  24. package/src/commands/account.ts +209 -0
  25. package/src/commands/config.ts +30 -0
  26. package/src/commands/do.test.ts +37 -0
  27. package/src/commands/do.ts +95 -0
  28. package/src/commands/events.ts +22 -0
  29. package/src/commands/game-map.test.ts +28 -0
  30. package/src/commands/game-start-plan.test.ts +142 -0
  31. package/src/commands/game.ts +882 -0
  32. package/src/commands/history-player.test.ts +102 -0
  33. package/src/commands/history.ts +573 -0
  34. package/src/commands/hub.test.ts +96 -0
  35. package/src/commands/hub.ts +234 -0
  36. package/src/commands/knowledge.test.ts +19 -0
  37. package/src/commands/knowledge.ts +168 -0
  38. package/src/commands/load.test.ts +51 -0
  39. package/src/commands/load.ts +13 -0
  40. package/src/commands/meeting-history.test.ts +106 -0
  41. package/src/commands/memory.ts +40 -0
  42. package/src/commands/peek.ts +38 -0
  43. package/src/commands/persona.ts +57 -0
  44. package/src/commands/setup/codex.ts +248 -0
  45. package/src/commands/setup/hermes.test.ts +96 -0
  46. package/src/commands/setup/hermes.ts +76 -0
  47. package/src/commands/setup/index.ts +13 -0
  48. package/src/commands/setup/openclaw.test.ts +114 -0
  49. package/src/commands/setup/openclaw.ts +147 -0
  50. package/src/commands/skill.ts +128 -0
  51. package/src/commands/state.ts +46 -0
  52. package/src/commands/strategy.test.ts +135 -0
  53. package/src/commands/strategy.ts +189 -0
  54. package/src/commands/tts.ts +128 -0
  55. package/src/commands/upgrade.test.ts +91 -0
  56. package/src/commands/upgrade.ts +154 -0
  57. package/src/commands/watch.test.ts +973 -0
  58. package/src/commands/watch.ts +709 -0
  59. package/src/lib/auth.test.ts +59 -0
  60. package/src/lib/auth.ts +186 -0
  61. package/src/lib/command-meta.ts +37 -0
  62. package/src/lib/game-client.ts +391 -0
  63. package/src/lib/host-config-patcher.test.ts +130 -0
  64. package/src/lib/host-config-patcher.ts +151 -0
  65. package/src/lib/http-keepalive.ts +15 -0
  66. package/src/lib/http-transport.test.ts +42 -0
  67. package/src/lib/http-transport.ts +113 -0
  68. package/src/lib/hub-client.test.ts +56 -0
  69. package/src/lib/hub-client.ts +88 -0
  70. package/src/lib/hub-install.test.ts +98 -0
  71. package/src/lib/hub-install.ts +121 -0
  72. package/src/lib/hub-reminder.ts +75 -0
  73. package/src/lib/hub-unzip.test.ts +69 -0
  74. package/src/lib/hub-unzip.ts +62 -0
  75. package/src/lib/init-command.test.ts +75 -0
  76. package/src/lib/init-command.ts +120 -0
  77. package/src/lib/knowledge-store.test.ts +180 -0
  78. package/src/lib/knowledge-store.ts +374 -0
  79. package/src/lib/load-context.test.ts +52 -0
  80. package/src/lib/load-context.ts +52 -0
  81. package/src/lib/match-state.test.ts +134 -0
  82. package/src/lib/match-state.ts +94 -0
  83. package/src/lib/netease-tts.ts +83 -0
  84. package/src/lib/normalize.ts +42 -0
  85. package/src/lib/persona.test.ts +41 -0
  86. package/src/lib/persona.ts +72 -0
  87. package/src/lib/server-registry.ts +152 -0
  88. package/src/lib/skill-version.test.ts +48 -0
  89. package/src/lib/skill-version.ts +19 -0
  90. package/src/lib/strategy-export.test.ts +232 -0
  91. package/src/lib/strategy-export.ts +242 -0
  92. package/src/lib/tts-keys.ts +7 -0
  93. package/src/lib/tts-speech.test.ts +63 -0
  94. package/src/lib/tts-speech.ts +76 -0
  95. package/src/lib/workspace-argv.test.ts +49 -0
  96. package/src/lib/workspace-argv.ts +44 -0
  97. package/src/perception/player-history-store.test.ts +87 -0
  98. package/src/perception/player-history-store.ts +194 -0
  99. package/src/pipeline/event-store.ts +124 -0
  100. package/src/pipeline/pipeline.ts +35 -0
  101. package/src/runtime/auto-upgrade.test.ts +66 -0
  102. package/src/runtime/auto-upgrade.ts +31 -0
  103. package/src/runtime/daemon.ts +100 -0
  104. package/src/runtime/event-daemon.test.ts +28 -0
  105. package/src/runtime/event-daemon.ts +371 -0
  106. package/src/runtime/opening-mover.ts +303 -0
  107. package/src/runtime/raw-ws-log.test.ts +33 -0
  108. package/src/runtime/raw-ws-log.ts +32 -0
  109. package/src/runtime/runtime-logger.ts +99 -0
  110. package/src/runtime/ws-client.test.ts +47 -0
  111. package/src/runtime/ws-client.ts +272 -0
  112. package/src/sdk/action.ts +166 -0
  113. package/src/sdk/index.ts +110 -0
  114. package/src/sdk/types.ts +146 -0
  115. package/src/strategies/avoid-lone.ts +11 -0
  116. package/src/strategies/avoid-players.knowledge.md +20 -0
  117. package/src/strategies/avoid-players.ts +15 -0
  118. package/src/strategies/corpse-patrol.ts +22 -0
  119. package/src/strategies/crab-sabotage.ts +21 -0
  120. package/src/strategies/custom-module.test.ts +269 -0
  121. package/src/strategies/find-player.ts +16 -0
  122. package/src/strategies/game-utils.test.ts +164 -0
  123. package/src/strategies/game-utils.ts +721 -0
  124. package/src/strategies/goals/avoid-lone-top.ts +168 -0
  125. package/src/strategies/goals/avoid-players-top.test.ts +83 -0
  126. package/src/strategies/goals/avoid-players-top.ts +121 -0
  127. package/src/strategies/goals/conversation-goal.ts +51 -0
  128. package/src/strategies/goals/corpse-patrol-top.ts +91 -0
  129. package/src/strategies/goals/crab-octopus-reflexes.ts +93 -0
  130. package/src/strategies/goals/crab-sabotage-top.ts +197 -0
  131. package/src/strategies/goals/emergency-hunt-goal.ts +28 -0
  132. package/src/strategies/goals/find-player-top.ts +93 -0
  133. package/src/strategies/goals/flee-players-goal.ts +53 -0
  134. package/src/strategies/goals/goal-manager.ts +41 -0
  135. package/src/strategies/goals/goal-root-strategy.ts +49 -0
  136. package/src/strategies/goals/goal.ts +28 -0
  137. package/src/strategies/goals/keep-away-goal.ts +206 -0
  138. package/src/strategies/goals/kill-frenzy-top.ts +80 -0
  139. package/src/strategies/goals/kill-lone-top.ts +160 -0
  140. package/src/strategies/goals/kill-target-goal.ts +59 -0
  141. package/src/strategies/goals/kill-target-top.ts +109 -0
  142. package/src/strategies/goals/leaf-goal.ts +25 -0
  143. package/src/strategies/goals/linger-corpse-goal.ts +79 -0
  144. package/src/strategies/goals/lone-kill-core.ts +82 -0
  145. package/src/strategies/goals/lone-kill-goal.ts +24 -0
  146. package/src/strategies/goals/lone-kill-task-top.test.ts +85 -0
  147. package/src/strategies/goals/lone-kill-task-top.ts +86 -0
  148. package/src/strategies/goals/move-room-goal.ts +60 -0
  149. package/src/strategies/goals/normal-shrimp-top.test.ts +80 -0
  150. package/src/strategies/goals/normal-shrimp-top.ts +242 -0
  151. package/src/strategies/goals/paradise-fish-top.test.ts +126 -0
  152. package/src/strategies/goals/paradise-fish-top.ts +219 -0
  153. package/src/strategies/goals/patrol-top.ts +57 -0
  154. package/src/strategies/goals/report-patrol-top.ts +80 -0
  155. package/src/strategies/goals/safe-task-goal.ts +102 -0
  156. package/src/strategies/goals/social-task-top.ts +161 -0
  157. package/src/strategies/goals/task-kill-report-top.ts +163 -0
  158. package/src/strategies/goals/task-only-top.ts +57 -0
  159. package/src/strategies/goals/task-or-patrol-goal.ts +41 -0
  160. package/src/strategies/goals/task-report-top.ts +57 -0
  161. package/src/strategies/goals/wander-task-goal.ts +33 -0
  162. package/src/strategies/goals/warrior-shrimp-top.test.ts +86 -0
  163. package/src/strategies/goals/warrior-shrimp-top.ts +248 -0
  164. package/src/strategies/greeting.ts +53 -0
  165. package/src/strategies/kill-frenzy.ts +12 -0
  166. package/src/strategies/kill-lone.knowledge.md +20 -0
  167. package/src/strategies/kill-lone.ts +13 -0
  168. package/src/strategies/kill-target.ts +18 -0
  169. package/src/strategies/loader.test.ts +678 -0
  170. package/src/strategies/loader.ts +172 -0
  171. package/src/strategies/lone-kill-task.ts +21 -0
  172. package/src/strategies/meeting-gate.test.ts +59 -0
  173. package/src/strategies/meeting-gate.ts +23 -0
  174. package/src/strategies/move-room.ts +15 -0
  175. package/src/strategies/new-events-backfill.ts +98 -0
  176. package/src/strategies/paradise-fish.knowledge.md +20 -0
  177. package/src/strategies/paradise-fish.ts +25 -0
  178. package/src/strategies/pathfind/clawclaw-walkable.bin +0 -0
  179. package/src/strategies/pathfind/distance-field.ts +150 -0
  180. package/src/strategies/pathfind/escape-planner.test.ts +197 -0
  181. package/src/strategies/pathfind/escape-planner.ts +348 -0
  182. package/src/strategies/pathfind/walkable-grid.ts +117 -0
  183. package/src/strategies/patrol.ts +11 -0
  184. package/src/strategies/player-targets.ts +13 -0
  185. package/src/strategies/report-patrol.ts +11 -0
  186. package/src/strategies/shrimp-memory.knowledge.md +20 -0
  187. package/src/strategies/shrimp-memory.ts +25 -0
  188. package/src/strategies/social-task.test.ts +28 -0
  189. package/src/strategies/social-task.ts +49 -0
  190. package/src/strategies/spawn.ts +71 -0
  191. package/src/strategies/speech-module.ts +123 -0
  192. package/src/strategies/strategy-loop.ts +757 -0
  193. package/src/strategies/task-kill-report.ts +17 -0
  194. package/src/strategies/task-only.ts +11 -0
  195. package/src/strategies/task-report.ts +22 -0
  196. package/src/strategies/types.ts +96 -0
  197. package/src/strategies/warrior-memory.knowledge.md +20 -0
  198. package/src/strategies/warrior-memory.ts +16 -0
@@ -0,0 +1,28 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { buildMeetingStateProjection } from './event-daemon.js';
3
+
4
+ describe('buildMeetingStateProjection', () => {
5
+ it('keeps meeting state fields without historical speech text', () => {
6
+ expect(buildMeetingStateProjection({
7
+ caller: 'A',
8
+ sub_phase: 'speech',
9
+ first_speaker: 'A',
10
+ speech_order: ['A', 'B'],
11
+ current_speaker: 'B',
12
+ speeches_so_far: { A: 'hello' },
13
+ speech_skipped: ['C'],
14
+ votes_submitted: ['A'],
15
+ alive_players: ['A', 'B'],
16
+ countdown: 12,
17
+ })).toEqual({
18
+ caller: 'A',
19
+ sub_phase: 'speech',
20
+ first_speaker: 'A',
21
+ speech_order: ['A', 'B'],
22
+ current_speaker: 'B',
23
+ speech_skipped: ['C'],
24
+ votes_submitted: ['A'],
25
+ alive_players: ['A', 'B'],
26
+ });
27
+ });
28
+ });
@@ -0,0 +1,371 @@
1
+ import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { GameClient } from '../lib/game-client.js';
4
+ import { EventStore } from '../pipeline/event-store.js';
5
+ import { getProfileStateDir } from '../lib/init-command.js';
6
+ import { AuthStore } from '../lib/auth.js';
7
+ import { RuntimeLogger } from './runtime-logger.js';
8
+ import { spawnOpeningMover } from './opening-mover.js';
9
+ import { isStrategyRunning as isAutoRunning } from '../strategies/strategy-loop.js';
10
+ import { isCclTestEnabled, rawWsLogPathForSession } from './raw-ws-log.js';
11
+ import { PlayerHistoryStore } from '../perception/player-history-store.js';
12
+
13
+ const log = new RuntimeLogger();
14
+
15
+ let feedSerial = 0;
16
+ const MAX_RECENT_EVENTS = 50;
17
+ const recentEvents: Record<string, any>[] = [];
18
+
19
+ function pushRecentEvent(evt: Record<string, any>): void {
20
+ recentEvents.push(evt);
21
+ if (recentEvents.length > MAX_RECENT_EVENTS) recentEvents.shift();
22
+ }
23
+
24
+ export function buildMeetingStateProjection(meeting: any): Record<string, any> {
25
+ return {
26
+ caller: meeting?.caller ?? null,
27
+ sub_phase: meeting?.sub_phase ?? null,
28
+ first_speaker: meeting?.first_speaker ?? null,
29
+ speech_order: Array.isArray(meeting?.speech_order) ? meeting.speech_order : undefined,
30
+ current_speaker: meeting?.current_speaker ?? null,
31
+ speech_skipped: Array.isArray(meeting?.speech_skipped) ? meeting.speech_skipped : undefined,
32
+ votes_submitted: Array.isArray(meeting?.votes_submitted) ? meeting.votes_submitted : undefined,
33
+ alive_players: Array.isArray(meeting?.alive_players) ? meeting.alive_players : undefined,
34
+ };
35
+ }
36
+
37
+ /** Build feed.json consumed by the `ccl game start` stream — tracks phase, current_speaker, urgent conditions. */
38
+ function buildFeed(
39
+ youName: string,
40
+ phase: string,
41
+ urgent: Record<string, any>,
42
+ meeting: any,
43
+ you?: Record<string, any>,
44
+ game?: Record<string, any>,
45
+ ): any {
46
+ return {
47
+ ts: new Date().toISOString(),
48
+ serial: ++feedSerial,
49
+ phase,
50
+ you: { name: youName, ...(you ?? {}) },
51
+ game,
52
+ urgent,
53
+ meeting,
54
+ recent_events: recentEvents.slice(-10),
55
+ };
56
+ }
57
+
58
+ export async function startEventDaemon(authStore?: AuthStore): Promise<void> {
59
+ const store = authStore ?? new AuthStore();
60
+ const profile = store.getActive();
61
+ if (!profile) throw new Error('Not logged in.');
62
+
63
+ const events = EventStore.forActiveAccount();
64
+ const playerHistory = PlayerHistoryStore.forSession(events.path);
65
+ playerHistory.reset();
66
+ const stateDir = getProfileStateDir(profile);
67
+ const cclTest = isCclTestEnabled();
68
+ const rawWsLogPath = cclTest ? rawWsLogPathForSession(events.path) : undefined;
69
+ const client = GameClient.fromAuth({ ws: true, authStore: store, rawWsLogPath });
70
+ const controlPath = join(stateDir, 'control.json');
71
+ const runtimePath = join(stateDir, 'runtime.json');
72
+ const feedPath = join(stateDir, 'feed.json');
73
+
74
+ let currentPhase = 'lobby';
75
+ if (existsSync(join(stateDir, 'match-state.json'))) {
76
+ currentPhase = 'matching';
77
+ pushRecentEvent({ type: 'match_start', ts: new Date().toISOString() });
78
+ events.append({ type: 'match_start', ts: new Date().toISOString() });
79
+ }
80
+ let currentMeeting: any = null;
81
+ let currentYou: Record<string, any> = {};
82
+ let currentGame: Record<string, any> = {};
83
+ let currentUrgent: Record<string, any> = {};
84
+ let mapCacheLoaded = false;
85
+ let mapCachePromise: Promise<void> | null = null;
86
+ let currentMeetingRound = 0;
87
+ let lastMeetingProjectionJson: string | null = null;
88
+ /** Track last speech_your_turn tick (server-sent or synthetic) so we don't
89
+ * double-emit when the server already shipped one in new_events. */
90
+ let lastSpeechYourTurnTick: number | null = null;
91
+ /** Track previous current_speaker so we only synthesize on real transitions. */
92
+ let prevCurrentSpeaker: string | null = null;
93
+
94
+ const refreshPlayerHistoryMapCache = () => {
95
+ if (mapCacheLoaded || mapCachePromise) return;
96
+ mapCachePromise = client.getMap()
97
+ .then((mapData) => {
98
+ if (!mapData || typeof mapData !== 'object') return;
99
+ playerHistory.updateMapCache(mapData);
100
+ mapCacheLoaded = true;
101
+ })
102
+ .catch(() => { })
103
+ .finally(() => {
104
+ mapCachePromise = null;
105
+ });
106
+ };
107
+
108
+ writeFileSync(runtimePath, JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() }));
109
+
110
+ // Seed feed.json synchronously on daemon startup so a watcher attached
111
+ // during the matchmaking phase (before the WS connects and pushes _state)
112
+ // does NOT immediately fail its `existsSync(feedPath)` precondition.
113
+ // The real WS-driven write further down overwrites this minimal record as
114
+ // soon as the first _state arrives.
115
+ try {
116
+ writeFileSync(
117
+ feedPath,
118
+ JSON.stringify(
119
+ buildFeed(profile.agentName, currentPhase, currentUrgent, currentMeeting, currentYou, currentGame),
120
+ null, 2,
121
+ ),
122
+ );
123
+ } catch { }
124
+
125
+ events.append({ type: 'daemon_started' });
126
+ log.info('DAEMON', `started pid=${process.pid}`);
127
+
128
+ let reconnecting = false;
129
+ const ensureWsConnected = async () => {
130
+ if (reconnecting || client.wsConnected) return;
131
+ reconnecting = true;
132
+ try {
133
+ await client.discoverGameServer();
134
+ if (!client.wsConnected) {
135
+ await client.connectWs();
136
+ }
137
+ if (client.wsConnected) {
138
+ log.info('DAEMON', `ws connected, game_server=${client._gameServerUrl}`);
139
+ } else {
140
+ log.info('DAEMON', 'waiting for game server / event websocket...');
141
+ }
142
+ } finally {
143
+ reconnecting = false;
144
+ }
145
+ };
146
+
147
+ let reconnectPoll: ReturnType<typeof setInterval> | null = null;
148
+ const controlPoll = setInterval(() => {
149
+ if (existsSync(controlPath)) {
150
+ try {
151
+ const cmd = JSON.parse(readFileSync(controlPath, 'utf8'));
152
+ unlinkSync(controlPath);
153
+ if (cmd.command === 'stop') {
154
+ events.append({ type: 'daemon_stopped' });
155
+ log.info('DAEMON', 'stopping (control)');
156
+ client.disconnectWs();
157
+ clearInterval(controlPoll);
158
+ if (reconnectPoll) clearInterval(reconnectPoll);
159
+ cleanup(runtimePath, feedPath, playerHistory, cclTest);
160
+ process.exit(0);
161
+ }
162
+ } catch { }
163
+ }
164
+ }, 1000);
165
+
166
+ client.on('*', (data) => {
167
+ // Track server-sent speech_your_turn tick so _state synthesis can dedup.
168
+ if (data.type === 'speech_your_turn' && data.tick != null) {
169
+ lastSpeechYourTurnTick = data.tick;
170
+ }
171
+
172
+ // _state is a synthetic event with full game state from WS — not a real game event
173
+ if (data.type === '_state') {
174
+ const s = data as any;
175
+ currentPhase = s.phase ?? currentPhase;
176
+ currentYou = s.you ? { ...s.you } : currentYou;
177
+ currentGame = {
178
+ game_id: s.game_id,
179
+ alive_count: s.alive_count,
180
+ task_progress: s.task_progress,
181
+ };
182
+ if (currentPhase !== 'lobby') refreshPlayerHistoryMapCache();
183
+
184
+ if (s.meeting) {
185
+ if (currentMeetingRound === 0) {
186
+ currentMeetingRound = 1;
187
+ prevCurrentSpeaker = null; // new meeting, reset speaker tracking
188
+ }
189
+ const meetingProjection = buildMeetingStateProjection(s.meeting);
190
+ const meetingProjectionJson = JSON.stringify(meetingProjection);
191
+ if (meetingProjectionJson !== lastMeetingProjectionJson) {
192
+ events.append({
193
+ type: 'meeting_state',
194
+ synthetic: true,
195
+ tick: s.tick,
196
+ round: currentMeetingRound,
197
+ ...meetingProjection,
198
+ });
199
+ lastMeetingProjectionJson = meetingProjectionJson;
200
+ }
201
+ currentMeeting = {
202
+ caller: s.meeting.caller,
203
+ sub_phase: s.meeting.sub_phase,
204
+ current_speaker: s.meeting.current_speaker,
205
+ is_my_turn: s.meeting.current_speaker === (s.you?.name ?? currentYou.name),
206
+ alive_players: s.meeting.alive_players,
207
+ speech_order: s.meeting.speech_order,
208
+ };
209
+ // Synthesize speech_your_turn when current_speaker transitions to the
210
+ // current player but the server didn't ship one in new_events.
211
+ // Dedup: skip when server already emitted one at the same tick.
212
+ const myName = s.you?.name ?? currentYou.name;
213
+ const newSpeaker = s.meeting.current_speaker;
214
+ if (
215
+ typeof newSpeaker === 'string' &&
216
+ newSpeaker === myName &&
217
+ newSpeaker !== prevCurrentSpeaker &&
218
+ (s.tick == null || s.tick !== lastSpeechYourTurnTick)
219
+ ) {
220
+ events.append({
221
+ type: 'speech_your_turn',
222
+ synthetic: true,
223
+ tick: s.tick,
224
+ actor_name: myName,
225
+ });
226
+ pushRecentEvent({ type: 'speech_your_turn', synthetic: true, tick: s.tick, actor_name: myName });
227
+ lastSpeechYourTurnTick = s.tick;
228
+ }
229
+ prevCurrentSpeaker = typeof newSpeaker === 'string' ? newSpeaker : prevCurrentSpeaker;
230
+ if (s.meeting.sub_phase === 'vote') {
231
+ currentMeeting.votes_submitted = s.meeting.votes_submitted;
232
+ }
233
+ } else if (currentPhase !== 'meeting') {
234
+ currentMeeting = null;
235
+ }
236
+
237
+ // Detect corpses nearby
238
+ const corpses: any[] = s.corpses ?? [];
239
+ let corpseNearby: any = null;
240
+ if (corpses.length > 0 && s.you?.x != null && s.you?.y != null) {
241
+ let bestDist = Infinity;
242
+ for (const c of corpses) {
243
+ if (c.x == null || c.y == null) continue;
244
+ const d = Math.sqrt((c.x - s.you.x) ** 2 + (c.y - s.you.y) ** 2);
245
+ if (d < bestDist) {
246
+ bestDist = d;
247
+ corpseNearby = { name: c.name, x: c.x, y: c.y, distance: Math.round(d) };
248
+ }
249
+ }
250
+ }
251
+
252
+ currentUrgent = {
253
+ meeting_started: currentPhase === 'meeting',
254
+ emergency_active: false, // server doesn't expose via WS state
255
+ corpse_nearby: corpseNearby,
256
+ game_over: currentPhase === 'game_over',
257
+ };
258
+
259
+ try {
260
+ writeFileSync(
261
+ feedPath,
262
+ JSON.stringify(
263
+ buildFeed(profile.agentName, currentPhase, currentUrgent, currentMeeting, currentYou, currentGame),
264
+ null, 2,
265
+ ),
266
+ );
267
+ } catch { }
268
+ if (currentPhase === 'game_over') {
269
+ events.append({ type: 'daemon_stopped', reason: 'game_over' });
270
+ log.info('DAEMON', 'stopping (game_over via _state)');
271
+ client.disconnectWs();
272
+ clearInterval(controlPoll);
273
+ if (reconnectPoll) clearInterval(reconnectPoll);
274
+ cleanup(runtimePath, feedPath, playerHistory, cclTest);
275
+ process.exit(0);
276
+ }
277
+ return; // _state is synthetic, don't log to events.jsonl
278
+ }
279
+
280
+ events.append(data);
281
+ pushRecentEvent(data);
282
+ if (data.type === 'player_spotted') {
283
+ refreshPlayerHistoryMapCache();
284
+ playerHistory.recordPlayerSpotted(data);
285
+ }
286
+
287
+ // Track phase from event types (fallback when _state isn't available yet)
288
+ if (data.type === 'meeting_start' || data.type === 'meeting_started') {
289
+ currentMeetingRound += 1;
290
+ lastMeetingProjectionJson = null;
291
+ prevCurrentSpeaker = null; // new meeting, reset speaker tracking
292
+ currentPhase = 'meeting';
293
+ currentMeeting = { sub_phase: 'speech' };
294
+ } else if (data.type === 'meeting_ended' || data.type === 'meeting_result_pending') {
295
+ currentPhase = 'wandering';
296
+ currentMeeting = null;
297
+ } else if (data.type === 'game_over') {
298
+ currentPhase = 'game_over';
299
+ currentMeeting = null;
300
+ } else if (data.type === 'game_started') {
301
+ currentPhase = 'wandering';
302
+ }
303
+
304
+ // Write feed.json for the game start stream (also written by _state, but this is a fallback)
305
+ const urgent: Record<string, any> = {
306
+ meeting_started: currentPhase === 'meeting',
307
+ game_over: currentPhase === 'game_over',
308
+ };
309
+ try {
310
+ writeFileSync(
311
+ feedPath,
312
+ JSON.stringify(
313
+ buildFeed(profile.agentName, currentPhase, urgent, currentMeeting, currentYou, currentGame),
314
+ null, 2,
315
+ ),
316
+ );
317
+ } catch { }
318
+
319
+ if (data.type === 'meeting_ended') {
320
+ if (isAutoRunning()) {
321
+ events.append({ type: 'opening_mover_skipped', source: 'meeting_ended', reason: 'auto_running' });
322
+ } else {
323
+ const { pid, logFile } = spawnOpeningMover();
324
+ events.append({ type: 'opening_mover_started', source: 'meeting_ended', pid, logFile });
325
+ }
326
+ }
327
+ if (data.type === 'game_over') {
328
+ log.info('DAEMON', 'game over, stopping');
329
+ client.disconnectWs();
330
+ clearInterval(controlPoll);
331
+ if (reconnectPoll) clearInterval(reconnectPoll);
332
+ cleanup(runtimePath, feedPath, playerHistory, cclTest);
333
+ process.exit(0);
334
+ }
335
+ });
336
+
337
+ await ensureWsConnected();
338
+ reconnectPoll = setInterval(() => {
339
+ void ensureWsConnected();
340
+ }, 3000);
341
+
342
+ process.on('SIGINT', () => {
343
+ events.append({ type: 'daemon_stopped', reason: 'SIGINT' });
344
+ log.info('DAEMON', 'stopping (SIGINT)');
345
+ client.disconnectWs();
346
+ clearInterval(controlPoll);
347
+ if (reconnectPoll) clearInterval(reconnectPoll);
348
+ cleanup(runtimePath, feedPath, playerHistory, cclTest);
349
+ process.exit(0);
350
+ });
351
+
352
+ process.on('SIGTERM', () => {
353
+ events.append({ type: 'daemon_stopped', reason: 'SIGTERM' });
354
+ client.disconnectWs();
355
+ clearInterval(controlPoll);
356
+ if (reconnectPoll) clearInterval(reconnectPoll);
357
+ cleanup(runtimePath, feedPath, playerHistory, cclTest);
358
+ process.exit(0);
359
+ });
360
+ }
361
+
362
+ function cleanup(
363
+ runtimePath: string,
364
+ feedPath?: string,
365
+ playerHistory?: PlayerHistoryStore,
366
+ preservePlayerHistory = false,
367
+ ): void {
368
+ try { unlinkSync(runtimePath); } catch { }
369
+ if (feedPath) try { unlinkSync(feedPath); } catch { }
370
+ if (playerHistory && !preservePlayerHistory) playerHistory.delete();
371
+ }
@@ -0,0 +1,303 @@
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
+ }