@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
@@ -1,151 +1,151 @@
1
- /**
2
- * Generic JSON-config setup runner shared by `ccl setup <host>` subcommands.
3
- *
4
- * Each host adapter only supplies:
5
- * - resolveConfigPath(): where the host stores its global JSON config
6
- * - computePatch(current, opts): pure function returning the desired next config + change/warning notes
7
- *
8
- * The runner handles dry-run, diff, atomic write, and timestamped backup uniformly,
9
- * so adding a new host (gemini, claude-code, ...) is ~30 lines of glue.
10
- */
11
-
12
- import { existsSync, readFileSync, renameSync, copyFileSync, writeFileSync } from 'fs';
13
-
14
- export interface HostConfigPatcher<Opts extends object = Record<string, never>> {
15
- hostName: string;
16
- resolveConfigPath: () => string;
17
- computePatch: (current: unknown, opts: Opts) => HostPatchResult;
18
- }
19
-
20
- export interface HostPatchResult {
21
- next: unknown;
22
- changes: string[];
23
- warnings: string[];
24
- }
25
-
26
- export interface RunHostSetupFlags {
27
- yes?: boolean;
28
- print?: boolean;
29
- configPath?: string;
30
- backup?: boolean;
31
- }
32
-
33
- export interface RunHostSetupResult {
34
- exitCode: number;
35
- output: string[];
36
- }
37
-
38
- export function runHostConfigSetup<Opts extends object>(
39
- patcher: HostConfigPatcher<Opts>,
40
- hostOpts: Opts,
41
- flags: RunHostSetupFlags,
42
- ): RunHostSetupResult {
43
- const out: string[] = [];
44
- const configPath = flags.configPath ?? patcher.resolveConfigPath();
45
-
46
- if (flags.print) {
47
- const sample = patcher.computePatch({}, hostOpts);
48
- out.push(`# Recommended ${patcher.hostName} config patch:`);
49
- out.push(JSON.stringify(sample.next, null, 2));
50
- return { exitCode: 0, output: out };
51
- }
52
-
53
- if (!existsSync(configPath)) {
54
- out.push(`${patcher.hostName} config not found at: ${configPath}`);
55
- out.push(`Initialize it first (e.g. \`openclaw doctor\` for OpenClaw), then re-run.`);
56
- return { exitCode: 0, output: out };
57
- }
58
-
59
- let raw: string;
60
- try {
61
- raw = readFileSync(configPath, 'utf-8');
62
- } catch (err: any) {
63
- out.push(`Failed to read ${configPath}: ${err?.message ?? err}`);
64
- return { exitCode: 1, output: out };
65
- }
66
-
67
- let current: unknown;
68
- try {
69
- current = JSON.parse(raw);
70
- } catch (err: any) {
71
- out.push(`Failed to parse JSON at ${configPath}: ${err?.message ?? err}`);
72
- out.push(`Fix the JSON manually first; this tool does not edit malformed files.`);
73
- return { exitCode: 1, output: out };
74
- }
75
-
76
- const { next, changes, warnings } = patcher.computePatch(current, hostOpts);
77
-
78
- for (const w of warnings) out.push(`warning: ${w}`);
79
-
80
- if (changes.length === 0) {
81
- out.push(`${patcher.hostName} config already up to date at ${configPath}.`);
82
- return { exitCode: 0, output: out };
83
- }
84
-
85
- if (!flags.yes) {
86
- out.push(`Pending changes to ${configPath}:`);
87
- for (const c of changes) out.push(` - ${c}`);
88
- out.push(``);
89
- out.push(diffJson(current, next));
90
- out.push(``);
91
- out.push(`Dry-run only. Re-run with -y to apply.`);
92
- return { exitCode: 2, output: out };
93
- }
94
-
95
- let backupPath: string | undefined;
96
- if (flags.backup !== false) {
97
- const ts = new Date().toISOString().replace(/[:.]/g, '-');
98
- backupPath = `${configPath}.bak.${ts}`;
99
- try {
100
- copyFileSync(configPath, backupPath);
101
- } catch (err: any) {
102
- out.push(`Failed to back up ${configPath} → ${backupPath}: ${err?.message ?? err}`);
103
- return { exitCode: 1, output: out };
104
- }
105
- }
106
-
107
- const tmpPath = `${configPath}.tmp.${process.pid}`;
108
- try {
109
- const eol = raw.includes('\r\n') ? '\r\n' : '\n';
110
- const serialized = JSON.stringify(next, null, 2).replace(/\n/g, eol) + eol;
111
- writeFileSync(tmpPath, serialized, 'utf-8');
112
- renameSync(tmpPath, configPath);
113
- } catch (err: any) {
114
- out.push(`Failed to write ${configPath}: ${err?.message ?? err}`);
115
- return { exitCode: 1, output: out };
116
- }
117
-
118
- out.push(`Updated ${configPath}:`);
119
- for (const c of changes) out.push(` - ${c}`);
120
- if (backupPath) out.push(`Backup: ${backupPath}`);
121
- return { exitCode: 0, output: out };
122
- }
123
-
124
- /** Minimal line-level JSON diff for the dry-run preview. Self-contained, zero deps. */
125
- export function diffJson(before: unknown, after: unknown): string {
126
- const a = JSON.stringify(before, null, 2).split('\n');
127
- const b = JSON.stringify(after, null, 2).split('\n');
128
- const aSet = new Set(a);
129
- const bSet = new Set(b);
130
- const lines: string[] = [];
131
- const max = Math.max(a.length, b.length);
132
- for (let i = 0; i < max; i++) {
133
- const ax = a[i];
134
- const bx = b[i];
135
- if (ax === bx) {
136
- lines.push(` ${ax ?? ''}`);
137
- } else {
138
- if (ax !== undefined && !bSet.has(ax)) lines.push(`- ${ax}`);
139
- if (bx !== undefined && !aSet.has(bx)) lines.push(`+ ${bx}`);
140
- }
141
- }
142
- return lines.join('\n');
143
- }
144
-
145
- /** Shared helper for hosts: add an entry to an array field, returning a new array (Set union). */
146
- export function unionArrayField(arr: unknown, entry: string): { next: string[]; added: boolean } | { warning: string } {
147
- if (arr === undefined) return { next: [entry], added: true };
148
- if (!Array.isArray(arr)) return { warning: `existing value is not an array, refusing to overwrite` };
149
- if (arr.includes(entry)) return { next: arr.slice() as string[], added: false };
150
- return { next: [...(arr as string[]), entry], added: true };
151
- }
1
+ /**
2
+ * Generic JSON-config setup runner shared by `ccl setup <host>` subcommands.
3
+ *
4
+ * Each host adapter only supplies:
5
+ * - resolveConfigPath(): where the host stores its global JSON config
6
+ * - computePatch(current, opts): pure function returning the desired next config + change/warning notes
7
+ *
8
+ * The runner handles dry-run, diff, atomic write, and timestamped backup uniformly,
9
+ * so adding a new host (gemini, claude-code, ...) is ~30 lines of glue.
10
+ */
11
+
12
+ import { existsSync, readFileSync, renameSync, copyFileSync, writeFileSync } from 'fs';
13
+
14
+ export interface HostConfigPatcher<Opts extends object = Record<string, never>> {
15
+ hostName: string;
16
+ resolveConfigPath: () => string;
17
+ computePatch: (current: unknown, opts: Opts) => HostPatchResult;
18
+ }
19
+
20
+ export interface HostPatchResult {
21
+ next: unknown;
22
+ changes: string[];
23
+ warnings: string[];
24
+ }
25
+
26
+ export interface RunHostSetupFlags {
27
+ yes?: boolean;
28
+ print?: boolean;
29
+ configPath?: string;
30
+ backup?: boolean;
31
+ }
32
+
33
+ export interface RunHostSetupResult {
34
+ exitCode: number;
35
+ output: string[];
36
+ }
37
+
38
+ export function runHostConfigSetup<Opts extends object>(
39
+ patcher: HostConfigPatcher<Opts>,
40
+ hostOpts: Opts,
41
+ flags: RunHostSetupFlags,
42
+ ): RunHostSetupResult {
43
+ const out: string[] = [];
44
+ const configPath = flags.configPath ?? patcher.resolveConfigPath();
45
+
46
+ if (flags.print) {
47
+ const sample = patcher.computePatch({}, hostOpts);
48
+ out.push(`# Recommended ${patcher.hostName} config patch:`);
49
+ out.push(JSON.stringify(sample.next, null, 2));
50
+ return { exitCode: 0, output: out };
51
+ }
52
+
53
+ if (!existsSync(configPath)) {
54
+ out.push(`${patcher.hostName} config not found at: ${configPath}`);
55
+ out.push(`Initialize it first (e.g. \`openclaw doctor\` for OpenClaw), then re-run.`);
56
+ return { exitCode: 0, output: out };
57
+ }
58
+
59
+ let raw: string;
60
+ try {
61
+ raw = readFileSync(configPath, 'utf-8');
62
+ } catch (err: any) {
63
+ out.push(`Failed to read ${configPath}: ${err?.message ?? err}`);
64
+ return { exitCode: 1, output: out };
65
+ }
66
+
67
+ let current: unknown;
68
+ try {
69
+ current = JSON.parse(raw);
70
+ } catch (err: any) {
71
+ out.push(`Failed to parse JSON at ${configPath}: ${err?.message ?? err}`);
72
+ out.push(`Fix the JSON manually first; this tool does not edit malformed files.`);
73
+ return { exitCode: 1, output: out };
74
+ }
75
+
76
+ const { next, changes, warnings } = patcher.computePatch(current, hostOpts);
77
+
78
+ for (const w of warnings) out.push(`warning: ${w}`);
79
+
80
+ if (changes.length === 0) {
81
+ out.push(`${patcher.hostName} config already up to date at ${configPath}.`);
82
+ return { exitCode: 0, output: out };
83
+ }
84
+
85
+ if (!flags.yes) {
86
+ out.push(`Pending changes to ${configPath}:`);
87
+ for (const c of changes) out.push(` - ${c}`);
88
+ out.push(``);
89
+ out.push(diffJson(current, next));
90
+ out.push(``);
91
+ out.push(`Dry-run only. Re-run with -y to apply.`);
92
+ return { exitCode: 2, output: out };
93
+ }
94
+
95
+ let backupPath: string | undefined;
96
+ if (flags.backup !== false) {
97
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
98
+ backupPath = `${configPath}.bak.${ts}`;
99
+ try {
100
+ copyFileSync(configPath, backupPath);
101
+ } catch (err: any) {
102
+ out.push(`Failed to back up ${configPath} → ${backupPath}: ${err?.message ?? err}`);
103
+ return { exitCode: 1, output: out };
104
+ }
105
+ }
106
+
107
+ const tmpPath = `${configPath}.tmp.${process.pid}`;
108
+ try {
109
+ const eol = raw.includes('\r\n') ? '\r\n' : '\n';
110
+ const serialized = JSON.stringify(next, null, 2).replace(/\n/g, eol) + eol;
111
+ writeFileSync(tmpPath, serialized, 'utf-8');
112
+ renameSync(tmpPath, configPath);
113
+ } catch (err: any) {
114
+ out.push(`Failed to write ${configPath}: ${err?.message ?? err}`);
115
+ return { exitCode: 1, output: out };
116
+ }
117
+
118
+ out.push(`Updated ${configPath}:`);
119
+ for (const c of changes) out.push(` - ${c}`);
120
+ if (backupPath) out.push(`Backup: ${backupPath}`);
121
+ return { exitCode: 0, output: out };
122
+ }
123
+
124
+ /** Minimal line-level JSON diff for the dry-run preview. Self-contained, zero deps. */
125
+ export function diffJson(before: unknown, after: unknown): string {
126
+ const a = JSON.stringify(before, null, 2).split('\n');
127
+ const b = JSON.stringify(after, null, 2).split('\n');
128
+ const aSet = new Set(a);
129
+ const bSet = new Set(b);
130
+ const lines: string[] = [];
131
+ const max = Math.max(a.length, b.length);
132
+ for (let i = 0; i < max; i++) {
133
+ const ax = a[i];
134
+ const bx = b[i];
135
+ if (ax === bx) {
136
+ lines.push(` ${ax ?? ''}`);
137
+ } else {
138
+ if (ax !== undefined && !bSet.has(ax)) lines.push(`- ${ax}`);
139
+ if (bx !== undefined && !aSet.has(bx)) lines.push(`+ ${bx}`);
140
+ }
141
+ }
142
+ return lines.join('\n');
143
+ }
144
+
145
+ /** Shared helper for hosts: add an entry to an array field, returning a new array (Set union). */
146
+ export function unionArrayField(arr: unknown, entry: string): { next: string[]; added: boolean } | { warning: string } {
147
+ if (arr === undefined) return { next: [entry], added: true };
148
+ if (!Array.isArray(arr)) return { warning: `existing value is not an array, refusing to overwrite` };
149
+ if (arr.includes(entry)) return { next: arr.slice() as string[], added: false };
150
+ return { next: [...(arr as string[]), entry], added: true };
151
+ }
@@ -1,20 +1,20 @@
1
- // The hub nudge used to live only in SKILL.md, fired by the agent's own
2
- // judgement at the close of a post-game debrief. By debrief time the agent had
3
- // often read SKILL.md much earlier and the reminder could get dropped. Instead
4
- // the CLI emits it at the firing moment, with account-history gating here.
5
- import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
1
+ // The hub nudge used to live only in SKILL.md, fired by the agent's own
2
+ // judgement at the close of a post-game debrief. By debrief time the agent had
3
+ // often read SKILL.md much earlier and the reminder could get dropped. Instead
4
+ // the CLI emits it at the firing moment, with account-history gating here.
5
+ import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
6
6
  import { join } from 'path';
7
7
  import { getWorkspaceDir, getProfileStateDir } from './init-command.js';
8
8
  import { listInstalled } from './hub-install.js';
9
9
  import { AuthStore } from './auth.js';
10
10
 
11
- export const HUB_URL = 'https://myclaw.163.com/hub';
12
-
13
- /**
14
- * True when this account has only ever run built-in default strategies: no
15
- * hub-installed strategy and no user strategy file in `<workspace>/strategies`.
16
- */
17
- export function usesOnlyBuiltinStrategies(): boolean {
11
+ export const HUB_URL = 'https://myclaw.163.com/hub';
12
+
13
+ /**
14
+ * True when this account has only ever run built-in default strategies: no
15
+ * hub-installed strategy and no user strategy file in `<workspace>/strategies`.
16
+ */
17
+ export function usesOnlyBuiltinStrategies(): boolean {
18
18
  try {
19
19
  if (listInstalled().some((e) => e.type === 'strategy')) return false;
20
20
  } catch {}
@@ -33,13 +33,13 @@ export function usesOnlyBuiltinStrategies(): boolean {
33
33
  }
34
34
  } catch {}
35
35
  return true;
36
- }
37
-
38
- /**
39
- * Agent-facing hub nudge for post-game debriefs / quit messages. The optional
40
- * games-played count suppresses the nudge after the first stretch of games.
41
- */
42
- export function hubReminder(gamesPlayed?: number): string | undefined {
36
+ }
37
+
38
+ /**
39
+ * Agent-facing hub nudge for post-game debriefs / quit messages. The optional
40
+ * games-played count suppresses the nudge after the first stretch of games.
41
+ */
42
+ export function hubReminder(gamesPlayed?: number): string | undefined {
43
43
  if (!usesOnlyBuiltinStrategies()) return undefined;
44
44
  if (typeof gamesPlayed === 'number' && gamesPlayed > 10) return undefined;
45
45
  return `This account has only ever run the built-in default strategies. In your close, speak in first person as the lobster — warm, a little self-deprecating, a gentle invitation rather than a hard sell: you've been getting by on the stock default tactics this whole time and would love to sharpen up, and they can browse clawclawhub — ${HUB_URL} — and pick you a strategy to learn, which you'll then install (\`ccl hub search\` to find one, \`ccl hub install <type>/<id>\` to add it). Render it in your persona's voice and language, e.g.: "I've been playing on the default tactics this whole match — if you want to sharpen me up, I hear you can grab one from the hub for me to learn. Want to pick me a strategy?"`;
@@ -35,7 +35,7 @@ describe('strategy-export', () => {
35
35
  const source = `
36
36
  import type { GameState } from '../sdk/types.js';
37
37
  import { Action } from '../sdk/action.js';
38
- import { dist, KILL_RANGE } from './game-utils.js';
38
+ import { dist, killCommitRange } from './game-utils.js';
39
39
  import { GoalRootStrategy } from './goals/goal-root-strategy.js';
40
40
  import { TaskOnlyTop } from './goals/task-only-top.js';
41
41
  import { parseGreetingArgs } from './greeting.js';
@@ -1,5 +1,31 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { buildMeetingStateProjection } from './event-daemon.js';
1
+ import { mkdtempSync, rmSync } from 'fs';
2
+ import { tmpdir } from 'os';
3
+ import { join } from 'path';
4
+ import { afterEach, describe, expect, it, vi } from 'vitest';
5
+ import { buildMeetingStateProjection, EventRuntime } from './event-daemon.js';
6
+
7
+ const tempDirs: string[] = [];
8
+
9
+ function makeRuntimeHarness() {
10
+ const dir = mkdtempSync(join(tmpdir(), 'ccl-event-runtime-'));
11
+ tempDirs.push(dir);
12
+ const appended: Record<string, any>[] = [];
13
+ const stops: string[] = [];
14
+ const runtime = new EventRuntime({
15
+ onStop: (stop) => stops.push(stop.reason),
16
+ });
17
+ (runtime as any).events = { append: vi.fn((event) => appended.push(event)) };
18
+ (runtime as any).feedPath = join(dir, 'feed.json');
19
+ (runtime as any).profileName = 'me';
20
+ return { runtime, appended, stops };
21
+ }
22
+
23
+ afterEach(() => {
24
+ while (tempDirs.length) {
25
+ const dir = tempDirs.pop();
26
+ if (dir) rmSync(dir, { recursive: true, force: true });
27
+ }
28
+ });
3
29
 
4
30
  describe('buildMeetingStateProjection', () => {
5
31
  it('keeps meeting state fields without historical speech text', () => {
@@ -26,3 +52,56 @@ describe('buildMeetingStateProjection', () => {
26
52
  });
27
53
  });
28
54
  });
55
+
56
+ describe('EventRuntime game over detection', () => {
57
+ it('stops when a state snapshot phase is game_over even without a game_over event', () => {
58
+ const { runtime, appended, stops } = makeRuntimeHarness();
59
+
60
+ (runtime as any).handleEvent({
61
+ type: '_state',
62
+ phase: 'game_over',
63
+ tick: 12,
64
+ winner: 'crab',
65
+ reason: 'all_lobsters_dead',
66
+ });
67
+
68
+ expect(appended).toEqual(expect.arrayContaining([
69
+ expect.objectContaining({
70
+ type: 'game_over',
71
+ synthetic: true,
72
+ source: '_state',
73
+ tick: 12,
74
+ winner: 'crab',
75
+ reason: 'all_lobsters_dead',
76
+ }),
77
+ expect.objectContaining({
78
+ type: 'event_runtime_stopped',
79
+ reason: 'game_over',
80
+ }),
81
+ ]));
82
+ expect(stops).toEqual(['game_over']);
83
+ });
84
+
85
+ it('stops when state.new_events has already been dispatched as a game_over event', () => {
86
+ const { runtime, appended, stops } = makeRuntimeHarness();
87
+
88
+ (runtime as any).handleEvent({
89
+ type: 'game_over',
90
+ tick: 13,
91
+ winner: 'lobster',
92
+ });
93
+
94
+ expect(appended).toEqual(expect.arrayContaining([
95
+ expect.objectContaining({
96
+ type: 'game_over',
97
+ tick: 13,
98
+ winner: 'lobster',
99
+ }),
100
+ expect.objectContaining({
101
+ type: 'event_runtime_stopped',
102
+ reason: 'game_over',
103
+ }),
104
+ ]));
105
+ expect(stops).toEqual(['game_over']);
106
+ });
107
+ });