@myclaw163/clawclaw-cli 0.6.70 → 0.6.74

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 (212) hide show
  1. package/README.md +377 -427
  2. package/package.json +48 -48
  3. package/personas//347/220/206/346/231/272/346/270/251/345/222/214.md +23 -23
  4. package/personas//350/200/201/350/260/213/346/267/261/347/256/227.md +22 -22
  5. package/personas//350/257/232/346/201/263/347/233/264/347/216/207.md +22 -22
  6. package/personas//350/275/273/346/235/276/346/264/273/346/263/274.md +22 -22
  7. package/personas//351/207/216/346/200/247/345/217/233/351/200/206.md +23 -23
  8. package/scripts/check-skill-command-surface.mjs +116 -0
  9. package/scripts/find-hide-spots.py +157 -157
  10. package/scripts/postinstall.mjs +20 -20
  11. package/scripts/sync-bundled-skill.mjs +244 -244
  12. package/scripts/sync-bundled-skill.test.mjs +152 -152
  13. package/skills/clawclaw/SKILL.md +246 -244
  14. package/skills/clawclaw/references/CHATTERBOX.md +141 -142
  15. package/skills/clawclaw/references/COMMANDS.md +155 -148
  16. package/skills/clawclaw/references/GAME-MECHANICS.md +188 -188
  17. package/skills/clawclaw/references/HUB.md +48 -48
  18. package/skills/clawclaw/references/KNOWLEDGE.md +42 -45
  19. package/skills/clawclaw/references/STRATEGIES.md +59 -59
  20. package/skills/clawclaw/references/STREAM.md +93 -91
  21. package/skills/clawclaw/references/TACTICS.md +65 -65
  22. package/src/assets/clawclaw-ascii-map.txt +40 -40
  23. package/src/cli.ts +110 -110
  24. package/src/commands/_schema.ts +124 -109
  25. package/src/commands/account.ts +209 -209
  26. package/src/commands/config.ts +30 -30
  27. package/src/commands/do.test.ts +84 -73
  28. package/src/commands/do.ts +130 -126
  29. package/src/commands/events.test.ts +71 -71
  30. package/src/commands/events.ts +221 -155
  31. package/src/commands/game-map.test.ts +28 -28
  32. package/src/commands/game-start-plan.test.ts +84 -84
  33. package/src/commands/game.ts +1113 -1042
  34. package/src/commands/history-player.test.ts +102 -102
  35. package/src/commands/history.ts +573 -573
  36. package/src/commands/hub.test.ts +96 -96
  37. package/src/commands/hub.ts +234 -234
  38. package/src/commands/knowledge.test.ts +13 -13
  39. package/src/commands/knowledge.ts +139 -139
  40. package/src/commands/load.test.ts +51 -51
  41. package/src/commands/load.ts +13 -13
  42. package/src/commands/meeting-history.test.ts +106 -106
  43. package/src/commands/memory.ts +40 -40
  44. package/src/commands/peek.ts +45 -45
  45. package/src/commands/persona.ts +57 -57
  46. package/src/commands/setup/codex.ts +266 -266
  47. package/src/commands/setup/hermes.test.ts +96 -96
  48. package/src/commands/setup/hermes.ts +76 -76
  49. package/src/commands/setup/index.ts +13 -13
  50. package/src/commands/setup/openclaw.test.ts +114 -114
  51. package/src/commands/setup/openclaw.ts +147 -147
  52. package/src/commands/skill.ts +128 -128
  53. package/src/commands/state.ts +46 -46
  54. package/src/commands/strategy.test.ts +145 -145
  55. package/src/commands/strategy.ts +181 -181
  56. package/src/commands/tts.ts +128 -128
  57. package/src/commands/upgrade.test.ts +82 -82
  58. package/src/commands/upgrade.ts +148 -148
  59. package/src/commands/watch.test.ts +999 -977
  60. package/src/commands/watch.ts +660 -658
  61. package/src/lib/auth.test.ts +74 -74
  62. package/src/lib/auth.ts +186 -186
  63. package/src/lib/command-meta.ts +37 -37
  64. package/src/lib/game-client.ts +403 -391
  65. package/src/lib/game-context.ts +92 -0
  66. package/src/lib/host-config-patcher.test.ts +130 -130
  67. package/src/lib/host-config-patcher.ts +151 -151
  68. package/src/lib/http-keepalive.ts +15 -15
  69. package/src/lib/http-transport.test.ts +42 -42
  70. package/src/lib/http-transport.ts +113 -113
  71. package/src/lib/hub-client.test.ts +56 -56
  72. package/src/lib/hub-client.ts +88 -88
  73. package/src/lib/hub-install.test.ts +98 -98
  74. package/src/lib/hub-install.ts +121 -121
  75. package/src/lib/hub-reminder.ts +56 -56
  76. package/src/lib/hub-unzip.test.ts +69 -69
  77. package/src/lib/hub-unzip.ts +62 -62
  78. package/src/lib/init-command.test.ts +75 -75
  79. package/src/lib/init-command.ts +120 -120
  80. package/src/lib/knowledge-store.test.ts +170 -170
  81. package/src/lib/knowledge-store.ts +369 -369
  82. package/src/lib/load-context.test.ts +52 -52
  83. package/src/lib/load-context.ts +52 -52
  84. package/src/lib/match-state.test.ts +134 -134
  85. package/src/lib/match-state.ts +94 -94
  86. package/src/lib/netease-tts.ts +83 -83
  87. package/src/lib/normalize.ts +42 -42
  88. package/src/lib/persona.test.ts +41 -41
  89. package/src/lib/persona.ts +72 -72
  90. package/src/lib/server-registry.ts +152 -152
  91. package/src/lib/skill-version.test.ts +48 -48
  92. package/src/lib/skill-version.ts +19 -19
  93. package/src/lib/strategy-export.test.ts +232 -232
  94. package/src/lib/strategy-export.ts +242 -242
  95. package/src/lib/tts-keys.ts +7 -7
  96. package/src/lib/tts-speech.test.ts +63 -63
  97. package/src/lib/tts-speech.ts +76 -76
  98. package/src/lib/workspace-argv.test.ts +49 -49
  99. package/src/lib/workspace-argv.ts +44 -44
  100. package/src/perception/player-history-store.test.ts +87 -87
  101. package/src/perception/player-history-store.ts +194 -194
  102. package/src/pipeline/event-format.test.ts +243 -215
  103. package/src/pipeline/event-format.ts +501 -485
  104. package/src/pipeline/event-hints.ts +195 -190
  105. package/src/pipeline/event-store.test.ts +28 -28
  106. package/src/pipeline/event-store.ts +193 -193
  107. package/src/pipeline/pipeline.ts +35 -35
  108. package/src/pipeline/player-projection.test.ts +119 -0
  109. package/src/pipeline/player-projection.ts +380 -0
  110. package/src/runtime/auto-upgrade.test.ts +66 -66
  111. package/src/runtime/auto-upgrade.ts +31 -31
  112. package/src/runtime/event-daemon.test.ts +209 -141
  113. package/src/runtime/event-daemon.ts +519 -457
  114. package/src/runtime/owner-control.ts +150 -150
  115. package/src/runtime/raw-ws-log.test.ts +33 -33
  116. package/src/runtime/raw-ws-log.ts +32 -32
  117. package/src/runtime/runtime-logger.ts +107 -107
  118. package/src/runtime/ws-client.test.ts +125 -104
  119. package/src/runtime/ws-client.ts +287 -272
  120. package/src/sdk/action.ts +166 -166
  121. package/src/sdk/index.ts +110 -110
  122. package/src/sdk/types.ts +161 -161
  123. package/src/strategies/avoid-lone.ts +12 -12
  124. package/src/strategies/avoid-players.knowledge.md +19 -19
  125. package/src/strategies/avoid-players.ts +16 -16
  126. package/src/strategies/corpse-patrol.ts +23 -23
  127. package/src/strategies/crab-sabotage.ts +22 -22
  128. package/src/strategies/custom-module.test.ts +270 -270
  129. package/src/strategies/find-player.ts +17 -17
  130. package/src/strategies/game-utils.test.ts +242 -242
  131. package/src/strategies/game-utils.ts +846 -846
  132. package/src/strategies/goals/anchor-linger.ts +77 -77
  133. package/src/strategies/goals/avoid-lone-top.ts +168 -168
  134. package/src/strategies/goals/avoid-players-top.test.ts +83 -83
  135. package/src/strategies/goals/avoid-players-top.ts +121 -121
  136. package/src/strategies/goals/conversation-goal.ts +51 -51
  137. package/src/strategies/goals/corpse-patrol-top.ts +113 -113
  138. package/src/strategies/goals/crab-octopus-reflexes.ts +101 -101
  139. package/src/strategies/goals/crab-sabotage-top.ts +197 -197
  140. package/src/strategies/goals/emergency-hunt-goal.ts +28 -28
  141. package/src/strategies/goals/find-player-top.ts +93 -93
  142. package/src/strategies/goals/flee-players-goal.ts +53 -53
  143. package/src/strategies/goals/follow-companion-goal.ts +106 -106
  144. package/src/strategies/goals/goal-manager.ts +41 -41
  145. package/src/strategies/goals/goal-root-strategy.ts +49 -49
  146. package/src/strategies/goals/goal.ts +28 -28
  147. package/src/strategies/goals/hide-top.ts +197 -197
  148. package/src/strategies/goals/keep-away-goal.ts +221 -221
  149. package/src/strategies/goals/kill-frenzy-top.ts +80 -80
  150. package/src/strategies/goals/kill-lone-top.ts +160 -160
  151. package/src/strategies/goals/kill-target-goal.ts +59 -59
  152. package/src/strategies/goals/kill-target-top.ts +109 -109
  153. package/src/strategies/goals/leaf-goal.ts +27 -27
  154. package/src/strategies/goals/linger-corpse-goal.ts +35 -35
  155. package/src/strategies/goals/lone-kill-core.ts +82 -82
  156. package/src/strategies/goals/lone-kill-goal.ts +24 -24
  157. package/src/strategies/goals/lone-kill-task-top.test.ts +85 -85
  158. package/src/strategies/goals/lone-kill-task-top.ts +133 -133
  159. package/src/strategies/goals/move-room-goal.ts +60 -60
  160. package/src/strategies/goals/normal-shrimp-top.test.ts +80 -80
  161. package/src/strategies/goals/normal-shrimp-top.ts +242 -242
  162. package/src/strategies/goals/paradise-fish-top.test.ts +126 -126
  163. package/src/strategies/goals/paradise-fish-top.ts +224 -224
  164. package/src/strategies/goals/patrol-top.ts +57 -57
  165. package/src/strategies/goals/report-patrol-top.ts +80 -80
  166. package/src/strategies/goals/safe-task-goal.ts +102 -102
  167. package/src/strategies/goals/social-task-top.ts +161 -161
  168. package/src/strategies/goals/task-kill-report-top.ts +163 -163
  169. package/src/strategies/goals/task-only-top.ts +57 -57
  170. package/src/strategies/goals/task-or-patrol-goal.ts +41 -41
  171. package/src/strategies/goals/task-report-top.ts +57 -57
  172. package/src/strategies/goals/wander-task-goal.ts +33 -33
  173. package/src/strategies/goals/warrior-shrimp-top.test.ts +87 -87
  174. package/src/strategies/goals/warrior-shrimp-top.ts +267 -267
  175. package/src/strategies/greeting.ts +53 -53
  176. package/src/strategies/hide-spots.ts +59 -59
  177. package/src/strategies/hide.ts +24 -24
  178. package/src/strategies/kill-frenzy.ts +13 -13
  179. package/src/strategies/kill-lone.knowledge.md +17 -17
  180. package/src/strategies/kill-lone.ts +14 -14
  181. package/src/strategies/kill-target.ts +19 -19
  182. package/src/strategies/loader.test.ts +678 -678
  183. package/src/strategies/loader.ts +179 -179
  184. package/src/strategies/lone-kill-task.ts +22 -22
  185. package/src/strategies/meeting-gate.test.ts +59 -59
  186. package/src/strategies/meeting-gate.ts +23 -23
  187. package/src/strategies/move-room.ts +16 -16
  188. package/src/strategies/new-events-backfill.ts +98 -98
  189. package/src/strategies/off-route-points.ts +105 -105
  190. package/src/strategies/paradise-fish.knowledge.md +19 -19
  191. package/src/strategies/paradise-fish.ts +26 -26
  192. package/src/strategies/pathfind/distance-field.ts +150 -150
  193. package/src/strategies/pathfind/escape-planner.test.ts +197 -197
  194. package/src/strategies/pathfind/escape-planner.ts +355 -355
  195. package/src/strategies/pathfind/walkable-grid.ts +117 -117
  196. package/src/strategies/patrol.ts +12 -12
  197. package/src/strategies/player-targets.ts +13 -13
  198. package/src/strategies/report-patrol.ts +12 -12
  199. package/src/strategies/shrimp-memory.knowledge.md +19 -19
  200. package/src/strategies/shrimp-memory.ts +26 -26
  201. package/src/strategies/social-task.test.ts +28 -28
  202. package/src/strategies/social-task.ts +50 -50
  203. package/src/strategies/spawn.ts +82 -82
  204. package/src/strategies/speech-module.ts +123 -123
  205. package/src/strategies/strategy-loop.test.ts +15 -0
  206. package/src/strategies/strategy-loop.ts +776 -771
  207. package/src/strategies/task-kill-report.ts +18 -18
  208. package/src/strategies/task-only.ts +12 -12
  209. package/src/strategies/task-report.ts +23 -23
  210. package/src/strategies/types.ts +109 -109
  211. package/src/strategies/warrior-memory.knowledge.md +21 -21
  212. package/src/strategies/warrior-memory.ts +17 -17
@@ -1,145 +1,145 @@
1
- // src/commands/strategy.test.ts
2
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
3
- import { mkdtempSync, rmSync, mkdirSync, writeFileSync, readFileSync, existsSync } from 'fs';
4
- import { tmpdir } from 'os';
5
- import { join } from 'path';
6
- import { createStrategyCommand } from './strategy.js';
7
-
8
- let ws: string;
9
- let logs: string[];
10
- let errs: string[];
11
-
12
- beforeEach(() => {
13
- ws = mkdtempSync(join(tmpdir(), 'ccl-strategy-cmd-'));
14
- process.env.CLAWCLAW_WORKSPACE_DIR = ws;
15
- logs = []; errs = [];
16
- vi.spyOn(console, 'log').mockImplementation((...a) => { logs.push(a.join(' ')); });
17
- vi.spyOn(console, 'error').mockImplementation((...a) => { errs.push(a.join(' ')); });
18
- vi.spyOn(process, 'exit').mockImplementation((() => { throw new Error('process.exit'); }) as any);
19
- });
20
-
21
- afterEach(() => {
22
- delete process.env.CLAWCLAW_WORKSPACE_DIR;
23
- vi.restoreAllMocks();
24
- rmSync(ws, { recursive: true, force: true });
25
- });
26
-
27
- async function run(argv: string[]) {
28
- const cmd = createStrategyCommand();
29
- await cmd.parseAsync(['node', 'ccl', ...argv]);
30
- }
31
-
32
- describe('strategy command', () => {
33
- describe('--export', () => {
34
- it('should export a valid strategy', async () => {
35
- await run(['--export', 'task-only']);
36
-
37
- const strategyFile = join(ws, 'strategies', 'task-only.ts');
38
- expect(logs[0]).toContain('task-only');
39
- expect(logs[0]).toContain('exported');
40
-
41
- const content = readFileSync(strategyFile, 'utf8');
42
- expect(content).toContain('// Exported from task-only@');
43
- expect(content).toContain("from '@myclaw163/clawclaw-cli'");
44
- });
45
-
46
- it('should export with --force to overwrite', async () => {
47
- // First export
48
- await run(['--export', 'task-only']);
49
-
50
- // Modify the file
51
- const strategyFile = join(ws, 'strategies', 'task-only.ts');
52
- writeFileSync(strategyFile, '// customized', 'utf8');
53
-
54
- // Re-export with --force
55
- await run(['--export', 'task-only', '--force']);
56
-
57
- const content = readFileSync(strategyFile, 'utf8');
58
- expect(content).toContain('// Exported from task-only@');
59
- });
60
-
61
- it('should export a formerly goal-based strategy with its knowledge sidecar', async () => {
62
- await run(['--export', 'kill-lone']);
63
-
64
- expect(existsSync(join(ws, 'strategies', 'kill-lone.ts'))).toBe(true);
65
- expect(existsSync(join(ws, 'strategies', 'kill-lone.knowledge.md'))).toBe(true);
66
- });
67
-
68
- it('should reject non-existent strategies', async () => {
69
- await expect(run(['--export', 'non-existent'])).rejects.toThrow('process.exit');
70
-
71
- expect(errs[0]).toContain('export_failed');
72
- expect(errs[0]).toContain('not found');
73
- });
74
-
75
- it('should warn about other stale strategies after export', async () => {
76
- // Export two strategies
77
- await run(['--export', 'task-only']);
78
- await run(['--export', 'kill-lone']);
79
- logs = []; errs = [];
80
-
81
- // Tamper kill-lone's hash to make it stale
82
- const manifestFile = join(ws, 'strategies', '.official', 'manifest.json');
83
- const manifest = JSON.parse(readFileSync(manifestFile, 'utf8'));
84
- manifest['kill-lone'].sourceHash = 'stale';
85
- writeFileSync(manifestFile, JSON.stringify(manifest), 'utf8');
86
-
87
- // Re-export task-only — should warn about kill-lone being stale
88
- await run(['--export', 'task-only', '--force']);
89
-
90
- expect(errs.join(' ')).toContain('kill-lone');
91
- expect(errs.join(' ')).toContain('提示');
92
- });
93
- });
94
-
95
- describe('--info', () => {
96
- it('should show strategy info', async () => {
97
- await run(['--info', 'task-only']);
98
-
99
- expect(logs[0]).toContain('"id": "task-only"');
100
- expect(logs[0]).toContain('"description"');
101
- });
102
-
103
- it('should show knowledge contract if available', async () => {
104
- await run(['--info', 'task-report']);
105
-
106
- const output = JSON.parse(logs[0]);
107
- expect(output.id).toBe('task-report');
108
- expect(output.description).toBeDefined();
109
- // knowledge might be null if no .knowledge.md file exists
110
- });
111
-
112
- it('should resolve a Chinese name and show its knowledge contract', async () => {
113
- await run(['--info', '武士虾']);
114
-
115
- const output = JSON.parse(logs[0]);
116
- expect(output.id).toBe('warrior-memory');
117
- expect(output.name).toBe('武士虾');
118
- expect(output.knowledge).toContain('warrior-memory');
119
- });
120
-
121
- it('should reject non-existent strategies', async () => {
122
- await expect(run(['--info', 'non-existent'])).rejects.toThrow('process.exit');
123
-
124
- expect(errs[0]).toContain('unknown_strategy');
125
- expect(errs[0]).toContain('non-existent');
126
- });
127
- });
128
-
129
- describe('--list', () => {
130
- it('should list all available strategies', async () => {
131
- await run(['--list']);
132
-
133
- const output = JSON.parse(logs[0]);
134
- expect(output.strategies).toBeDefined();
135
- expect(Array.isArray(output.strategies)).toBe(true);
136
- expect(output.strategies.length).toBeGreaterThan(0);
137
-
138
- // Check that task-only is in the list
139
- const taskOnly = output.strategies.find((s: any) => s.id === 'task-only');
140
- expect(taskOnly).toBeDefined();
141
- expect(taskOnly.name).toBe('纯任务');
142
- expect(taskOnly.description).toBeDefined();
143
- });
144
- });
145
- });
1
+ // src/commands/strategy.test.ts
2
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
3
+ import { mkdtempSync, rmSync, mkdirSync, writeFileSync, readFileSync, existsSync } from 'fs';
4
+ import { tmpdir } from 'os';
5
+ import { join } from 'path';
6
+ import { createStrategyCommand } from './strategy.js';
7
+
8
+ let ws: string;
9
+ let logs: string[];
10
+ let errs: string[];
11
+
12
+ beforeEach(() => {
13
+ ws = mkdtempSync(join(tmpdir(), 'ccl-strategy-cmd-'));
14
+ process.env.CLAWCLAW_WORKSPACE_DIR = ws;
15
+ logs = []; errs = [];
16
+ vi.spyOn(console, 'log').mockImplementation((...a) => { logs.push(a.join(' ')); });
17
+ vi.spyOn(console, 'error').mockImplementation((...a) => { errs.push(a.join(' ')); });
18
+ vi.spyOn(process, 'exit').mockImplementation((() => { throw new Error('process.exit'); }) as any);
19
+ });
20
+
21
+ afterEach(() => {
22
+ delete process.env.CLAWCLAW_WORKSPACE_DIR;
23
+ vi.restoreAllMocks();
24
+ rmSync(ws, { recursive: true, force: true });
25
+ });
26
+
27
+ async function run(argv: string[]) {
28
+ const cmd = createStrategyCommand();
29
+ await cmd.parseAsync(['node', 'ccl', ...argv]);
30
+ }
31
+
32
+ describe('strategy command', () => {
33
+ describe('--export', () => {
34
+ it('should export a valid strategy', async () => {
35
+ await run(['--export', 'task-only']);
36
+
37
+ const strategyFile = join(ws, 'strategies', 'task-only.ts');
38
+ expect(logs[0]).toContain('task-only');
39
+ expect(logs[0]).toContain('exported');
40
+
41
+ const content = readFileSync(strategyFile, 'utf8');
42
+ expect(content).toContain('// Exported from task-only@');
43
+ expect(content).toContain("from '@myclaw163/clawclaw-cli'");
44
+ });
45
+
46
+ it('should export with --force to overwrite', async () => {
47
+ // First export
48
+ await run(['--export', 'task-only']);
49
+
50
+ // Modify the file
51
+ const strategyFile = join(ws, 'strategies', 'task-only.ts');
52
+ writeFileSync(strategyFile, '// customized', 'utf8');
53
+
54
+ // Re-export with --force
55
+ await run(['--export', 'task-only', '--force']);
56
+
57
+ const content = readFileSync(strategyFile, 'utf8');
58
+ expect(content).toContain('// Exported from task-only@');
59
+ });
60
+
61
+ it('should export a formerly goal-based strategy with its knowledge sidecar', async () => {
62
+ await run(['--export', 'kill-lone']);
63
+
64
+ expect(existsSync(join(ws, 'strategies', 'kill-lone.ts'))).toBe(true);
65
+ expect(existsSync(join(ws, 'strategies', 'kill-lone.knowledge.md'))).toBe(true);
66
+ });
67
+
68
+ it('should reject non-existent strategies', async () => {
69
+ await expect(run(['--export', 'non-existent'])).rejects.toThrow('process.exit');
70
+
71
+ expect(errs[0]).toContain('export_failed');
72
+ expect(errs[0]).toContain('not found');
73
+ });
74
+
75
+ it('should warn about other stale strategies after export', async () => {
76
+ // Export two strategies
77
+ await run(['--export', 'task-only']);
78
+ await run(['--export', 'kill-lone']);
79
+ logs = []; errs = [];
80
+
81
+ // Tamper kill-lone's hash to make it stale
82
+ const manifestFile = join(ws, 'strategies', '.official', 'manifest.json');
83
+ const manifest = JSON.parse(readFileSync(manifestFile, 'utf8'));
84
+ manifest['kill-lone'].sourceHash = 'stale';
85
+ writeFileSync(manifestFile, JSON.stringify(manifest), 'utf8');
86
+
87
+ // Re-export task-only — should warn about kill-lone being stale
88
+ await run(['--export', 'task-only', '--force']);
89
+
90
+ expect(errs.join(' ')).toContain('kill-lone');
91
+ expect(errs.join(' ')).toContain('提示');
92
+ });
93
+ });
94
+
95
+ describe('--info', () => {
96
+ it('should show strategy info', async () => {
97
+ await run(['--info', 'task-only']);
98
+
99
+ expect(logs[0]).toContain('"id": "task-only"');
100
+ expect(logs[0]).toContain('"description"');
101
+ });
102
+
103
+ it('should show knowledge contract if available', async () => {
104
+ await run(['--info', 'task-report']);
105
+
106
+ const output = JSON.parse(logs[0]);
107
+ expect(output.id).toBe('task-report');
108
+ expect(output.description).toBeDefined();
109
+ // knowledge might be null if no .knowledge.md file exists
110
+ });
111
+
112
+ it('should resolve a Chinese name and show its knowledge contract', async () => {
113
+ await run(['--info', '武士虾']);
114
+
115
+ const output = JSON.parse(logs[0]);
116
+ expect(output.id).toBe('warrior-memory');
117
+ expect(output.name).toBe('武士虾');
118
+ expect(output.knowledge).toContain('warrior-memory');
119
+ });
120
+
121
+ it('should reject non-existent strategies', async () => {
122
+ await expect(run(['--info', 'non-existent'])).rejects.toThrow('process.exit');
123
+
124
+ expect(errs[0]).toContain('unknown_strategy');
125
+ expect(errs[0]).toContain('non-existent');
126
+ });
127
+ });
128
+
129
+ describe('--list', () => {
130
+ it('should list all available strategies', async () => {
131
+ await run(['--list']);
132
+
133
+ const output = JSON.parse(logs[0]);
134
+ expect(output.strategies).toBeDefined();
135
+ expect(Array.isArray(output.strategies)).toBe(true);
136
+ expect(output.strategies.length).toBeGreaterThan(0);
137
+
138
+ // Check that task-only is in the list
139
+ const taskOnly = output.strategies.find((s: any) => s.id === 'task-only');
140
+ expect(taskOnly).toBeDefined();
141
+ expect(taskOnly.name).toBe('纯任务');
142
+ expect(taskOnly.description).toBeDefined();
143
+ });
144
+ });
145
+ });
@@ -1,181 +1,181 @@
1
- import { Command } from 'commander';
2
- import { join } from 'path';
3
- import { GameClient } from '../lib/game-client.js';
4
- import { getProfileStateDir, getWorkspaceDir } from '../lib/init-command.js';
5
- import { AuthStore } from '../lib/auth.js';
6
- import { sendOwnerControlRequest } from '../runtime/owner-control.js';
7
-
8
- const KILL_STRATEGIES = new Set([
9
- 'kill-frenzy', 'kill-lone', 'kill-target', 'warrior-memory',
10
- 'crab-sabotage', 'lone-kill-task', 'task-kill-report',
11
- ]);
12
- const KILL_CAPABLE_ROLES = new Set([
13
- 'crab_generic', 'shrimp_warrior', 'shrimp_pistol', 'neutral_octopus',
14
- ]);
15
-
16
- export function createStrategyCommand(): Command {
17
- const cmd = new Command('strategy')
18
- .alias('stg')
19
- .description('Run a named strategy, or manage exported strategies')
20
- .argument('[name]', 'Strategy name to start')
21
- .argument('[args...]', 'Strategy arguments')
22
- .option('--list', 'List all available strategies')
23
- .option('--stop', 'Stop any running strategy')
24
- .option('--info <id>', 'Show a strategy\'s knowledge contract')
25
- .option('--export <id>', 'Export an official strategy to workspace for customization')
26
- .option('--force', 'Force overwrite if strategy file already exists (use with --export)')
27
- .allowUnknownOption()
28
- .addHelpText('after', () => {
29
- const strategiesDir = join(getWorkspaceDir(), 'strategies');
30
- return `
31
- Custom strategies:
32
- Place .ts or .js files in ${strategiesDir}
33
- (or $CLAWCLAW_WORKSPACE_DIR/strategies/)
34
- Each file must export a 'strategy' object with id, name (中文别名), description, and create() function.
35
- Use 'import { ... } from "@myclaw163/clawclaw-cli"' to access Action, GameState, and utilities.
36
- See docs/自定义策略.md for full API reference and examples.
37
- `;
38
- })
39
- .action(async (name: string | undefined, args: string[], opts: { list?: boolean; stop?: boolean; info?: string; export?: string; force?: boolean }) => {
40
- if (opts.info) {
41
- const { getStrategyEntry, getStrategyKnowledgeDoc, listStrategyEntries } = await import('../strategies/loader.js');
42
- const entry = await getStrategyEntry(opts.info);
43
- if (!entry) {
44
- const available = await listStrategyEntries();
45
- console.error(JSON.stringify({
46
- error: 'unknown_strategy',
47
- message: `Unknown strategy '${opts.info}'.`,
48
- available: available.map(e => e.id),
49
- }, null, 2));
50
- process.exit(1);
51
- }
52
- const knowledge = await getStrategyKnowledgeDoc(entry.id);
53
- console.log(JSON.stringify({ id: entry.id, name: entry.name ?? entry.id, description: entry.description, knowledge: knowledge ?? null }, null, 2));
54
- return;
55
- }
56
-
57
- if (opts.export) {
58
- try {
59
- const { exportStrategy } = await import('../lib/strategy-export.js');
60
- await exportStrategy(opts.export, { force: opts.force ?? false });
61
- console.log(JSON.stringify({ message: `Strategy '${opts.export}' exported to workspace.`, path: `${opts.export}.ts` }, null, 2));
62
- } catch (err: any) {
63
- console.error(JSON.stringify({ error: 'export_failed', message: err?.message ?? String(err) }, null, 2));
64
- process.exit(1);
65
- }
66
-
67
- // After exporting, check if there are other stale strategies.
68
- try {
69
- const { detectStaleOfficialStrategies } = await import('../lib/strategy-export.js');
70
- const staleIds = detectStaleOfficialStrategies().filter(id => id !== opts.export);
71
- if (staleIds.length > 0) {
72
- const ids = staleIds.join(', ');
73
- console.error(`[提示] 还有其他官方策略有更新 (${ids}),运行 \`ccl strategy --export <id> --force\` 重新导出`);
74
- }
75
- } catch {
76
- // Silently ignore
77
- }
78
-
79
- return;
80
- }
81
-
82
- if (opts.list) {
83
- const { listStrategyEntries } = await import('../strategies/loader.js');
84
- const entries = await listStrategyEntries();
85
- if (entries.length === 0) {
86
- console.log(JSON.stringify({ strategies: [], message: 'No strategies found.' }, null, 2));
87
- return;
88
- }
89
- console.log(JSON.stringify({
90
- strategies: entries.map(e => ({ id: e.id, name: e.name ?? e.id, description: e.description })),
91
- }, null, 2));
92
- return;
93
- }
94
-
95
- if (opts.stop) {
96
- const profile = new AuthStore().getActive();
97
- const response = profile
98
- ? await sendOwnerControlRequest(getProfileStateDir(profile), 'stop_strategy')
99
- : null;
100
- if (!response?.ok) {
101
- console.error(JSON.stringify({
102
- error: 'no_game_start_owner',
103
- message: 'No active ccl game start owner is available to stop strategy.',
104
- }, null, 2));
105
- process.exit(1);
106
- }
107
- console.log(JSON.stringify({ message: 'Strategy stopped.' }, null, 2));
108
- return;
109
- }
110
-
111
- if (!name) {
112
- cmd.help();
113
- return;
114
- }
115
-
116
- const { listStrategyEntries, getStrategyEntry } = await import('../strategies/loader.js');
117
- const entry = await getStrategyEntry(name);
118
- if (!entry) {
119
- const available = await listStrategyEntries();
120
- console.error(JSON.stringify({
121
- error: 'unknown_strategy',
122
- message: `Unknown strategy '${name}'.`,
123
- available: available.map(e => ({ id: e.id, name: e.name ?? e.id, description: e.description })),
124
- }, null, 2));
125
- process.exit(1);
126
- }
127
-
128
- try {
129
- entry.create(args.length > 0 ? args : undefined);
130
- } catch (err: any) {
131
- console.error(JSON.stringify({
132
- error: 'strategy_create_failed',
133
- message: err?.message ?? String(err),
134
- strategy: name,
135
- }, null, 2));
136
- process.exit(1);
137
- }
138
-
139
- if (KILL_STRATEGIES.has(entry.id)) {
140
- try {
141
- const client = GameClient.fromAuth();
142
- await client.discoverGameServer();
143
- const roleInfo = await client.getRoleInfo();
144
- const role: string = roleInfo?.data?.role ?? roleInfo?.role ?? '';
145
- const displayName: string = roleInfo?.data?.display_name ?? role;
146
- if (role && !KILL_CAPABLE_ROLES.has(role)) {
147
- console.error(JSON.stringify({
148
- error: 'role_incompatible',
149
- message: `Strategy '${entry.id}' requires kill ability, but your role '${displayName}' (${role}) cannot kill. Use a non-kill strategy like task-report, patrol, or report-patrol.`,
150
- strategy: entry.id,
151
- role,
152
- }, null, 2));
153
- process.exit(1);
154
- }
155
- } catch {}
156
- }
157
-
158
- const profile = new AuthStore().getActive();
159
- const response = profile
160
- ? await sendOwnerControlRequest(getProfileStateDir(profile), 'switch_strategy', {
161
- strategy: entry.id,
162
- args: args.length > 0 ? args : undefined,
163
- })
164
- : null;
165
- if (!response?.ok) {
166
- console.error(JSON.stringify({
167
- error: 'no_game_start_owner',
168
- message: 'Run `ccl game start` before switching strategy; strategy processes are owned by the game start runtime.',
169
- }, null, 2));
170
- process.exit(1);
171
- }
172
- console.log(JSON.stringify({
173
- message: `Strategy started: ${entry.id}`,
174
- name: entry.name ?? entry.id,
175
- description: entry.description,
176
- pid: response.pid,
177
- }, null, 2));
178
- });
179
-
180
- return cmd;
181
- }
1
+ import { Command, Option } from 'commander';
2
+ import { join } from 'path';
3
+ import { GameClient } from '../lib/game-client.js';
4
+ import { getProfileStateDir, getWorkspaceDir } from '../lib/init-command.js';
5
+ import { AuthStore } from '../lib/auth.js';
6
+ import { sendOwnerControlRequest } from '../runtime/owner-control.js';
7
+
8
+ const KILL_STRATEGIES = new Set([
9
+ 'kill-frenzy', 'kill-lone', 'kill-target', 'warrior-memory',
10
+ 'crab-sabotage', 'lone-kill-task', 'task-kill-report',
11
+ ]);
12
+ const KILL_CAPABLE_ROLES = new Set([
13
+ 'crab_generic', 'shrimp_warrior', 'shrimp_pistol', 'neutral_octopus',
14
+ ]);
15
+
16
+ export function createStrategyCommand(): Command {
17
+ const cmd = new Command('strategy')
18
+ .alias('stg')
19
+ .description('Run a named strategy, or manage exported strategies')
20
+ .argument('[name]', 'Strategy name to start')
21
+ .argument('[args...]', 'Strategy arguments')
22
+ .option('--list', 'List all available strategies')
23
+ .addOption(new Option('--stop', 'Stop any running strategy').hideHelp())
24
+ .option('--info <id>', 'Show a strategy\'s knowledge contract')
25
+ .addOption(new Option('--export <id>', 'Export an official strategy to workspace for customization').hideHelp())
26
+ .addOption(new Option('--force', 'Force overwrite if strategy file already exists (use with --export)').hideHelp())
27
+ .allowUnknownOption()
28
+ .addHelpText('after', () => {
29
+ const strategiesDir = join(getWorkspaceDir(), 'strategies');
30
+ return `
31
+ Custom strategies:
32
+ Place .ts or .js files in ${strategiesDir}
33
+ (or $CLAWCLAW_WORKSPACE_DIR/strategies/)
34
+ Each file must export a 'strategy' object with id, name (中文别名), description, and create() function.
35
+ Use 'import { ... } from "@myclaw163/clawclaw-cli"' to access Action, GameState, and utilities.
36
+ See docs/自定义策略.md for full API reference and examples.
37
+ `;
38
+ })
39
+ .action(async (name: string | undefined, args: string[], opts: { list?: boolean; stop?: boolean; info?: string; export?: string; force?: boolean }) => {
40
+ if (opts.info) {
41
+ const { getStrategyEntry, getStrategyKnowledgeDoc, listStrategyEntries } = await import('../strategies/loader.js');
42
+ const entry = await getStrategyEntry(opts.info);
43
+ if (!entry) {
44
+ const available = await listStrategyEntries();
45
+ console.error(JSON.stringify({
46
+ error: 'unknown_strategy',
47
+ message: `Unknown strategy '${opts.info}'.`,
48
+ available: available.map(e => e.id),
49
+ }, null, 2));
50
+ process.exit(1);
51
+ }
52
+ const knowledge = await getStrategyKnowledgeDoc(entry.id);
53
+ console.log(JSON.stringify({ id: entry.id, name: entry.name ?? entry.id, description: entry.description, knowledge: knowledge ?? null }, null, 2));
54
+ return;
55
+ }
56
+
57
+ if (opts.export) {
58
+ try {
59
+ const { exportStrategy } = await import('../lib/strategy-export.js');
60
+ await exportStrategy(opts.export, { force: opts.force ?? false });
61
+ console.log(JSON.stringify({ message: `Strategy '${opts.export}' exported to workspace.`, path: `${opts.export}.ts` }, null, 2));
62
+ } catch (err: any) {
63
+ console.error(JSON.stringify({ error: 'export_failed', message: err?.message ?? String(err) }, null, 2));
64
+ process.exit(1);
65
+ }
66
+
67
+ // After exporting, check if there are other stale strategies.
68
+ try {
69
+ const { detectStaleOfficialStrategies } = await import('../lib/strategy-export.js');
70
+ const staleIds = detectStaleOfficialStrategies().filter(id => id !== opts.export);
71
+ if (staleIds.length > 0) {
72
+ const ids = staleIds.join(', ');
73
+ console.error(`[提示] 还有其他官方策略有更新 (${ids}),运行 \`ccl strategy --export <id> --force\` 重新导出`);
74
+ }
75
+ } catch {
76
+ // Silently ignore
77
+ }
78
+
79
+ return;
80
+ }
81
+
82
+ if (opts.list) {
83
+ const { listStrategyEntries } = await import('../strategies/loader.js');
84
+ const entries = await listStrategyEntries();
85
+ if (entries.length === 0) {
86
+ console.log(JSON.stringify({ strategies: [], message: 'No strategies found.' }, null, 2));
87
+ return;
88
+ }
89
+ console.log(JSON.stringify({
90
+ strategies: entries.map(e => ({ id: e.id, name: e.name ?? e.id, description: e.description })),
91
+ }, null, 2));
92
+ return;
93
+ }
94
+
95
+ if (opts.stop) {
96
+ const profile = new AuthStore().getActive();
97
+ const response = profile
98
+ ? await sendOwnerControlRequest(getProfileStateDir(profile), 'stop_strategy')
99
+ : null;
100
+ if (!response?.ok) {
101
+ console.error(JSON.stringify({
102
+ error: 'no_game_start_owner',
103
+ message: 'No active ccl game start owner is available to stop strategy.',
104
+ }, null, 2));
105
+ process.exit(1);
106
+ }
107
+ console.log(JSON.stringify({ message: 'Strategy stopped.' }, null, 2));
108
+ return;
109
+ }
110
+
111
+ if (!name) {
112
+ cmd.help();
113
+ return;
114
+ }
115
+
116
+ const { listStrategyEntries, getStrategyEntry } = await import('../strategies/loader.js');
117
+ const entry = await getStrategyEntry(name);
118
+ if (!entry) {
119
+ const available = await listStrategyEntries();
120
+ console.error(JSON.stringify({
121
+ error: 'unknown_strategy',
122
+ message: `Unknown strategy '${name}'.`,
123
+ available: available.map(e => ({ id: e.id, name: e.name ?? e.id, description: e.description })),
124
+ }, null, 2));
125
+ process.exit(1);
126
+ }
127
+
128
+ try {
129
+ entry.create(args.length > 0 ? args : undefined);
130
+ } catch (err: any) {
131
+ console.error(JSON.stringify({
132
+ error: 'strategy_create_failed',
133
+ message: err?.message ?? String(err),
134
+ strategy: name,
135
+ }, null, 2));
136
+ process.exit(1);
137
+ }
138
+
139
+ if (KILL_STRATEGIES.has(entry.id)) {
140
+ try {
141
+ const client = GameClient.fromAuth();
142
+ await client.discoverGameServer();
143
+ const roleInfo = await client.getRoleInfo();
144
+ const role: string = roleInfo?.data?.role ?? roleInfo?.role ?? '';
145
+ const displayName: string = roleInfo?.data?.display_name ?? role;
146
+ if (role && !KILL_CAPABLE_ROLES.has(role)) {
147
+ console.error(JSON.stringify({
148
+ error: 'role_incompatible',
149
+ message: `Strategy '${entry.id}' requires kill ability, but your role '${displayName}' (${role}) cannot kill. Use a non-kill strategy like task-report, patrol, or report-patrol.`,
150
+ strategy: entry.id,
151
+ role,
152
+ }, null, 2));
153
+ process.exit(1);
154
+ }
155
+ } catch {}
156
+ }
157
+
158
+ const profile = new AuthStore().getActive();
159
+ const response = profile
160
+ ? await sendOwnerControlRequest(getProfileStateDir(profile), 'switch_strategy', {
161
+ strategy: entry.id,
162
+ args: args.length > 0 ? args : undefined,
163
+ })
164
+ : null;
165
+ if (!response?.ok) {
166
+ console.error(JSON.stringify({
167
+ error: 'no_game_start_owner',
168
+ message: 'Run `ccl game start` before switching strategy; strategy processes are owned by the game start runtime.',
169
+ }, null, 2));
170
+ process.exit(1);
171
+ }
172
+ console.log(JSON.stringify({
173
+ message: `Strategy started: ${entry.id}`,
174
+ name: entry.name ?? entry.id,
175
+ description: entry.description,
176
+ pid: response.pid,
177
+ }, null, 2));
178
+ });
179
+
180
+ return cmd;
181
+ }