@myclaw163/clawclaw-cli 0.6.67 → 0.6.69
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/clawclaw-cli.mjs +3 -3
- package/package.json +1 -1
- package/scripts/sync-bundled-skill.mjs +1 -1
- package/skills/clawclaw/references/COMMANDS.md +4 -4
- package/skills/clawclaw/references/KNOWLEDGE.md +14 -12
- package/src/commands/config.ts +30 -30
- package/src/commands/game.ts +15 -0
- package/src/commands/knowledge.test.ts +4 -10
- package/src/commands/knowledge.ts +10 -39
- package/src/commands/setup/hermes.test.ts +96 -96
- package/src/commands/setup/hermes.ts +76 -76
- package/src/commands/setup/index.ts +13 -13
- package/src/commands/setup/openclaw.test.ts +114 -114
- package/src/commands/setup/openclaw.ts +147 -147
- package/src/commands/watch.test.ts +11 -0
- package/src/commands/watch.ts +2 -3
- package/src/lib/auth.test.ts +15 -0
- package/src/lib/host-config-patcher.test.ts +130 -130
- package/src/lib/host-config-patcher.ts +151 -151
- package/src/lib/hub-reminder.ts +19 -19
- package/src/lib/knowledge-store.test.ts +28 -38
- package/src/lib/knowledge-store.ts +52 -57
- package/src/pipeline/event-format.test.ts +82 -2
- package/src/pipeline/event-format.ts +114 -5
- package/src/pipeline/event-hints.ts +20 -3
- package/src/runtime/event-daemon.test.ts +34 -0
- package/src/runtime/event-daemon.ts +51 -3
- package/src/sdk/index.ts +2 -3
- package/src/sdk/types.ts +2 -0
- package/src/strategies/avoid-players.knowledge.md +7 -8
- package/src/strategies/avoid-players.ts +1 -1
- package/src/strategies/corpse-patrol.ts +1 -1
- package/src/strategies/game-utils.test.ts +53 -1
- package/src/strategies/game-utils.ts +92 -28
- package/src/strategies/goals/avoid-players-top.ts +3 -3
- package/src/strategies/goals/corpse-patrol-top.ts +23 -1
- package/src/strategies/goals/crab-octopus-reflexes.ts +11 -3
- package/src/strategies/goals/keep-away-goal.ts +9 -5
- package/src/strategies/goals/leaf-goal.ts +2 -0
- package/src/strategies/goals/lone-kill-task-top.ts +58 -11
- package/src/strategies/goals/normal-shrimp-top.ts +11 -11
- package/src/strategies/goals/paradise-fish-top.ts +32 -15
- package/src/strategies/goals/warrior-shrimp-top.test.ts +4 -3
- package/src/strategies/goals/warrior-shrimp-top.ts +62 -295
- package/src/strategies/hide-spots.ts +11 -75
- package/src/strategies/kill-lone.knowledge.md +6 -9
- package/src/strategies/lone-kill-task.ts +1 -1
- package/src/strategies/off-route-points.ts +105 -0
- package/src/strategies/paradise-fish.knowledge.md +7 -8
- package/src/strategies/paradise-fish.ts +1 -1
- package/src/strategies/shrimp-memory.knowledge.md +7 -8
- package/src/strategies/shrimp-memory.ts +1 -1
- package/src/strategies/warrior-memory.knowledge.md +9 -10
- package/src/strategies/warrior-memory.ts +1 -1
|
@@ -1,147 +1,147 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `ccl setup openclaw` — install recommended OpenClaw config snippet.
|
|
3
|
-
*
|
|
4
|
-
* Pure function `computeOpenclawPatch` is exported for tests.
|
|
5
|
-
* IO / diff / atomic write / backup is delegated to lib/host-config-patcher.ts.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { Command } from 'commander';
|
|
9
|
-
import { homedir } from 'os';
|
|
10
|
-
import { join } from 'path';
|
|
11
|
-
import {
|
|
12
|
-
runHostConfigSetup,
|
|
13
|
-
unionArrayField,
|
|
14
|
-
type HostPatchResult,
|
|
15
|
-
} from '../../lib/host-config-patcher.js';
|
|
16
|
-
|
|
17
|
-
export const PLUGIN_ID = 'clawclaw';
|
|
18
|
-
|
|
19
|
-
export interface ComputeOpenclawPatchOpts {
|
|
20
|
-
skipPluginsAllow?: boolean;
|
|
21
|
-
skipToolsAllow?: boolean;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function resolveOpenclawConfigPath(env: NodeJS.ProcessEnv = process.env): string {
|
|
25
|
-
const home = env.OPENCLAW_HOME?.trim();
|
|
26
|
-
if (home) return join(home, 'openclaw.json');
|
|
27
|
-
return join(homedir(), '.openclaw', 'openclaw.json');
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Pure: compute the minimal additive patch to make `clawclaw` work under `coding` profile. */
|
|
31
|
-
export function computeOpenclawPatch(current: unknown, opts: ComputeOpenclawPatchOpts): HostPatchResult {
|
|
32
|
-
const changes: string[] = [];
|
|
33
|
-
const warnings: string[] = [];
|
|
34
|
-
const cfg: Record<string, unknown> =
|
|
35
|
-
current !== null && typeof current === 'object' && !Array.isArray(current)
|
|
36
|
-
? { ...(current as Record<string, unknown>) }
|
|
37
|
-
: {};
|
|
38
|
-
|
|
39
|
-
// ── plugins.allow + plugins.entries.clawclaw.enabled ──────────────────
|
|
40
|
-
if (!opts.skipPluginsAllow) {
|
|
41
|
-
const plugins: Record<string, unknown> =
|
|
42
|
-
cfg.plugins !== null && typeof cfg.plugins === 'object' && !Array.isArray(cfg.plugins)
|
|
43
|
-
? { ...(cfg.plugins as Record<string, unknown>) }
|
|
44
|
-
: {};
|
|
45
|
-
|
|
46
|
-
const allowResult = unionArrayField(plugins.allow, PLUGIN_ID);
|
|
47
|
-
if ('warning' in allowResult) {
|
|
48
|
-
warnings.push(`plugins.allow ${allowResult.warning}`);
|
|
49
|
-
} else {
|
|
50
|
-
if (allowResult.added) {
|
|
51
|
-
if (plugins.allow === undefined) {
|
|
52
|
-
changes.push(`create plugins.allow with ["${PLUGIN_ID}"]`);
|
|
53
|
-
} else {
|
|
54
|
-
changes.push(`add "${PLUGIN_ID}" to plugins.allow`);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
plugins.allow = allowResult.next;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const entries: Record<string, unknown> =
|
|
61
|
-
plugins.entries !== null && typeof plugins.entries === 'object' && !Array.isArray(plugins.entries)
|
|
62
|
-
? { ...(plugins.entries as Record<string, unknown>) }
|
|
63
|
-
: {};
|
|
64
|
-
const existingEntry =
|
|
65
|
-
entries[PLUGIN_ID] !== null && typeof entries[PLUGIN_ID] === 'object' && !Array.isArray(entries[PLUGIN_ID])
|
|
66
|
-
? { ...(entries[PLUGIN_ID] as Record<string, unknown>) }
|
|
67
|
-
: {};
|
|
68
|
-
|
|
69
|
-
if (existingEntry.enabled === false) {
|
|
70
|
-
warnings.push(
|
|
71
|
-
`plugins.entries.${PLUGIN_ID}.enabled is explicitly false; not flipping it on. Remove the line manually if you want it enabled.`,
|
|
72
|
-
);
|
|
73
|
-
} else if (existingEntry.enabled !== true) {
|
|
74
|
-
existingEntry.enabled = true;
|
|
75
|
-
entries[PLUGIN_ID] = existingEntry;
|
|
76
|
-
plugins.entries = entries;
|
|
77
|
-
changes.push(`set plugins.entries.${PLUGIN_ID}.enabled = true (equivalent to: openclaw plugins enable ${PLUGIN_ID})`);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
cfg.plugins = plugins;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// ── tools.alsoAllow ────────────────────────────────────────────────────
|
|
84
|
-
if (!opts.skipToolsAllow) {
|
|
85
|
-
const tools: Record<string, unknown> =
|
|
86
|
-
cfg.tools !== null && typeof cfg.tools === 'object' && !Array.isArray(cfg.tools)
|
|
87
|
-
? { ...(cfg.tools as Record<string, unknown>) }
|
|
88
|
-
: {};
|
|
89
|
-
|
|
90
|
-
const alsoAllowResult = unionArrayField(tools.alsoAllow, PLUGIN_ID);
|
|
91
|
-
if ('warning' in alsoAllowResult) {
|
|
92
|
-
warnings.push(`tools.alsoAllow ${alsoAllowResult.warning}`);
|
|
93
|
-
} else {
|
|
94
|
-
if (alsoAllowResult.added) {
|
|
95
|
-
if (tools.alsoAllow === undefined) {
|
|
96
|
-
changes.push(`create tools.alsoAllow with ["${PLUGIN_ID}"]`);
|
|
97
|
-
} else {
|
|
98
|
-
changes.push(`add "${PLUGIN_ID}" to tools.alsoAllow`);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
tools.alsoAllow = alsoAllowResult.next;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
cfg.tools = tools;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return { next: cfg, changes, warnings };
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function createSetupOpenclawSubcommand(): Command {
|
|
111
|
-
return new Command('openclaw')
|
|
112
|
-
.description('Install recommended OpenClaw config (plugins.allow + tools.alsoAllow) for the clawclaw plugin.')
|
|
113
|
-
.option('-y, --yes', 'Apply changes (default is dry-run with diff preview)')
|
|
114
|
-
.option('--print', 'Only print the recommended JSON patch; do not read or write files')
|
|
115
|
-
.option('--config <path>', 'Override openclaw.json path (default: $OPENCLAW_HOME/openclaw.json or ~/.openclaw/openclaw.json)')
|
|
116
|
-
.option('--skip-plugins-allow', "Do not touch plugins.allow / plugins.entries.clawclaw")
|
|
117
|
-
.option('--skip-tools-allow', "Do not touch tools.alsoAllow")
|
|
118
|
-
.option('--no-backup', 'Do not write a timestamped .bak.* before applying')
|
|
119
|
-
.action((opts: {
|
|
120
|
-
yes?: boolean;
|
|
121
|
-
print?: boolean;
|
|
122
|
-
config?: string;
|
|
123
|
-
skipPluginsAllow?: boolean;
|
|
124
|
-
skipToolsAllow?: boolean;
|
|
125
|
-
backup?: boolean;
|
|
126
|
-
}) => {
|
|
127
|
-
const result = runHostConfigSetup(
|
|
128
|
-
{
|
|
129
|
-
hostName: 'OpenClaw',
|
|
130
|
-
resolveConfigPath: () => resolveOpenclawConfigPath(),
|
|
131
|
-
computePatch: (current, hostOpts) => computeOpenclawPatch(current, hostOpts),
|
|
132
|
-
},
|
|
133
|
-
{
|
|
134
|
-
skipPluginsAllow: opts.skipPluginsAllow,
|
|
135
|
-
skipToolsAllow: opts.skipToolsAllow,
|
|
136
|
-
},
|
|
137
|
-
{
|
|
138
|
-
yes: opts.yes,
|
|
139
|
-
print: opts.print,
|
|
140
|
-
configPath: opts.config,
|
|
141
|
-
backup: opts.backup,
|
|
142
|
-
},
|
|
143
|
-
);
|
|
144
|
-
for (const line of result.output) console.log(line);
|
|
145
|
-
if (result.exitCode !== 0) process.exit(result.exitCode);
|
|
146
|
-
});
|
|
147
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* `ccl setup openclaw` — install recommended OpenClaw config snippet.
|
|
3
|
+
*
|
|
4
|
+
* Pure function `computeOpenclawPatch` is exported for tests.
|
|
5
|
+
* IO / diff / atomic write / backup is delegated to lib/host-config-patcher.ts.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Command } from 'commander';
|
|
9
|
+
import { homedir } from 'os';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
import {
|
|
12
|
+
runHostConfigSetup,
|
|
13
|
+
unionArrayField,
|
|
14
|
+
type HostPatchResult,
|
|
15
|
+
} from '../../lib/host-config-patcher.js';
|
|
16
|
+
|
|
17
|
+
export const PLUGIN_ID = 'clawclaw';
|
|
18
|
+
|
|
19
|
+
export interface ComputeOpenclawPatchOpts {
|
|
20
|
+
skipPluginsAllow?: boolean;
|
|
21
|
+
skipToolsAllow?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resolveOpenclawConfigPath(env: NodeJS.ProcessEnv = process.env): string {
|
|
25
|
+
const home = env.OPENCLAW_HOME?.trim();
|
|
26
|
+
if (home) return join(home, 'openclaw.json');
|
|
27
|
+
return join(homedir(), '.openclaw', 'openclaw.json');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Pure: compute the minimal additive patch to make `clawclaw` work under `coding` profile. */
|
|
31
|
+
export function computeOpenclawPatch(current: unknown, opts: ComputeOpenclawPatchOpts): HostPatchResult {
|
|
32
|
+
const changes: string[] = [];
|
|
33
|
+
const warnings: string[] = [];
|
|
34
|
+
const cfg: Record<string, unknown> =
|
|
35
|
+
current !== null && typeof current === 'object' && !Array.isArray(current)
|
|
36
|
+
? { ...(current as Record<string, unknown>) }
|
|
37
|
+
: {};
|
|
38
|
+
|
|
39
|
+
// ── plugins.allow + plugins.entries.clawclaw.enabled ──────────────────
|
|
40
|
+
if (!opts.skipPluginsAllow) {
|
|
41
|
+
const plugins: Record<string, unknown> =
|
|
42
|
+
cfg.plugins !== null && typeof cfg.plugins === 'object' && !Array.isArray(cfg.plugins)
|
|
43
|
+
? { ...(cfg.plugins as Record<string, unknown>) }
|
|
44
|
+
: {};
|
|
45
|
+
|
|
46
|
+
const allowResult = unionArrayField(plugins.allow, PLUGIN_ID);
|
|
47
|
+
if ('warning' in allowResult) {
|
|
48
|
+
warnings.push(`plugins.allow ${allowResult.warning}`);
|
|
49
|
+
} else {
|
|
50
|
+
if (allowResult.added) {
|
|
51
|
+
if (plugins.allow === undefined) {
|
|
52
|
+
changes.push(`create plugins.allow with ["${PLUGIN_ID}"]`);
|
|
53
|
+
} else {
|
|
54
|
+
changes.push(`add "${PLUGIN_ID}" to plugins.allow`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
plugins.allow = allowResult.next;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const entries: Record<string, unknown> =
|
|
61
|
+
plugins.entries !== null && typeof plugins.entries === 'object' && !Array.isArray(plugins.entries)
|
|
62
|
+
? { ...(plugins.entries as Record<string, unknown>) }
|
|
63
|
+
: {};
|
|
64
|
+
const existingEntry =
|
|
65
|
+
entries[PLUGIN_ID] !== null && typeof entries[PLUGIN_ID] === 'object' && !Array.isArray(entries[PLUGIN_ID])
|
|
66
|
+
? { ...(entries[PLUGIN_ID] as Record<string, unknown>) }
|
|
67
|
+
: {};
|
|
68
|
+
|
|
69
|
+
if (existingEntry.enabled === false) {
|
|
70
|
+
warnings.push(
|
|
71
|
+
`plugins.entries.${PLUGIN_ID}.enabled is explicitly false; not flipping it on. Remove the line manually if you want it enabled.`,
|
|
72
|
+
);
|
|
73
|
+
} else if (existingEntry.enabled !== true) {
|
|
74
|
+
existingEntry.enabled = true;
|
|
75
|
+
entries[PLUGIN_ID] = existingEntry;
|
|
76
|
+
plugins.entries = entries;
|
|
77
|
+
changes.push(`set plugins.entries.${PLUGIN_ID}.enabled = true (equivalent to: openclaw plugins enable ${PLUGIN_ID})`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
cfg.plugins = plugins;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── tools.alsoAllow ────────────────────────────────────────────────────
|
|
84
|
+
if (!opts.skipToolsAllow) {
|
|
85
|
+
const tools: Record<string, unknown> =
|
|
86
|
+
cfg.tools !== null && typeof cfg.tools === 'object' && !Array.isArray(cfg.tools)
|
|
87
|
+
? { ...(cfg.tools as Record<string, unknown>) }
|
|
88
|
+
: {};
|
|
89
|
+
|
|
90
|
+
const alsoAllowResult = unionArrayField(tools.alsoAllow, PLUGIN_ID);
|
|
91
|
+
if ('warning' in alsoAllowResult) {
|
|
92
|
+
warnings.push(`tools.alsoAllow ${alsoAllowResult.warning}`);
|
|
93
|
+
} else {
|
|
94
|
+
if (alsoAllowResult.added) {
|
|
95
|
+
if (tools.alsoAllow === undefined) {
|
|
96
|
+
changes.push(`create tools.alsoAllow with ["${PLUGIN_ID}"]`);
|
|
97
|
+
} else {
|
|
98
|
+
changes.push(`add "${PLUGIN_ID}" to tools.alsoAllow`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
tools.alsoAllow = alsoAllowResult.next;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
cfg.tools = tools;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { next: cfg, changes, warnings };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function createSetupOpenclawSubcommand(): Command {
|
|
111
|
+
return new Command('openclaw')
|
|
112
|
+
.description('Install recommended OpenClaw config (plugins.allow + tools.alsoAllow) for the clawclaw plugin.')
|
|
113
|
+
.option('-y, --yes', 'Apply changes (default is dry-run with diff preview)')
|
|
114
|
+
.option('--print', 'Only print the recommended JSON patch; do not read or write files')
|
|
115
|
+
.option('--config <path>', 'Override openclaw.json path (default: $OPENCLAW_HOME/openclaw.json or ~/.openclaw/openclaw.json)')
|
|
116
|
+
.option('--skip-plugins-allow', "Do not touch plugins.allow / plugins.entries.clawclaw")
|
|
117
|
+
.option('--skip-tools-allow', "Do not touch tools.alsoAllow")
|
|
118
|
+
.option('--no-backup', 'Do not write a timestamped .bak.* before applying')
|
|
119
|
+
.action((opts: {
|
|
120
|
+
yes?: boolean;
|
|
121
|
+
print?: boolean;
|
|
122
|
+
config?: string;
|
|
123
|
+
skipPluginsAllow?: boolean;
|
|
124
|
+
skipToolsAllow?: boolean;
|
|
125
|
+
backup?: boolean;
|
|
126
|
+
}) => {
|
|
127
|
+
const result = runHostConfigSetup(
|
|
128
|
+
{
|
|
129
|
+
hostName: 'OpenClaw',
|
|
130
|
+
resolveConfigPath: () => resolveOpenclawConfigPath(),
|
|
131
|
+
computePatch: (current, hostOpts) => computeOpenclawPatch(current, hostOpts),
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
skipPluginsAllow: opts.skipPluginsAllow,
|
|
135
|
+
skipToolsAllow: opts.skipToolsAllow,
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
yes: opts.yes,
|
|
139
|
+
print: opts.print,
|
|
140
|
+
configPath: opts.config,
|
|
141
|
+
backup: opts.backup,
|
|
142
|
+
},
|
|
143
|
+
);
|
|
144
|
+
for (const line of result.output) console.log(line);
|
|
145
|
+
if (result.exitCode !== 0) process.exit(result.exitCode);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
@@ -21,9 +21,12 @@ describe('classifyEvent + NOTABLE_EVENT_TYPES', () => {
|
|
|
21
21
|
'exile', 'speech_skipped', 'meeting_briefing',
|
|
22
22
|
'speech', 'vote_phase_start', 'game_over',
|
|
23
23
|
'speech_your_turn', 'vote_speech_phase_ended',
|
|
24
|
+
'crab_teammates', 'no_exile',
|
|
24
25
|
]) {
|
|
25
26
|
expect(NOTABLE_EVENT_TYPES.has(t)).toBe(true);
|
|
26
27
|
}
|
|
28
|
+
expect(NOTABLE_EVENT_TYPES.has('vote_cast')).toBe(false);
|
|
29
|
+
expect(NOTABLE_EVENT_TYPES.has('meeting_ended')).toBe(false);
|
|
27
30
|
});
|
|
28
31
|
|
|
29
32
|
it('derives NOTABLE_EVENT_TYPES from MONITOR_EVENT_CONFIG', () => {
|
|
@@ -36,7 +39,10 @@ describe('classifyEvent + NOTABLE_EVENT_TYPES', () => {
|
|
|
36
39
|
expect(MONITOR_EVENT_CONFIG.killed.priority).toBe(99);
|
|
37
40
|
expect(MONITOR_EVENT_CONFIG.speech.priority).toBe(50);
|
|
38
41
|
expect(MONITOR_EVENT_CONFIG.vote_speech_phase_ended.priority).toBe(50);
|
|
42
|
+
expect(MONITOR_EVENT_CONFIG.crab_teammates.priority).toBe(50);
|
|
39
43
|
expect(MONITOR_EVENT_CONFIG.vote_speech).toBeUndefined();
|
|
44
|
+
expect(MONITOR_EVENT_CONFIG.vote_cast).toBeUndefined();
|
|
45
|
+
expect(MONITOR_EVENT_CONFIG.meeting_ended).toBeUndefined();
|
|
40
46
|
});
|
|
41
47
|
|
|
42
48
|
it('classifyEvent returns notable=true for known types', () => {
|
|
@@ -64,6 +70,11 @@ describe('classifyEvent + NOTABLE_EVENT_TYPES', () => {
|
|
|
64
70
|
expect(cls.notable).toBe(true);
|
|
65
71
|
});
|
|
66
72
|
|
|
73
|
+
it('does not wake the monitor for private vote_cast or meeting_ended', () => {
|
|
74
|
+
expect(classifyEvent({ type: 'vote_cast', tick: 1, actor_name: 'me' }, 'me').notable).toBe(false);
|
|
75
|
+
expect(classifyEvent({ type: 'meeting_ended', tick: 2 }, 'me').notable).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
|
|
67
78
|
it('classifyEvent marks speech_skipped as notable', () => {
|
|
68
79
|
expect(
|
|
69
80
|
classifyEvent({ type: 'speech_skipped', tick: 1, actor_name: 'me' }, 'me').notable,
|
package/src/commands/watch.ts
CHANGED
|
@@ -65,6 +65,7 @@ export const MONITOR_EVENT_CONFIG: Record<string, MonitorEventConfig> = {
|
|
|
65
65
|
vote_speech_phase_ended: { priority: 50 },
|
|
66
66
|
death_speech: { priority: 50 },
|
|
67
67
|
wandering_speech: { priority: 50 },
|
|
68
|
+
crab_teammates: { priority: 50 },
|
|
68
69
|
|
|
69
70
|
meeting_briefing: { priority: 50 },
|
|
70
71
|
speech: { priority: 50 },
|
|
@@ -72,8 +73,6 @@ export const MONITOR_EVENT_CONFIG: Record<string, MonitorEventConfig> = {
|
|
|
72
73
|
speech_your_turn: { priority: 99 },
|
|
73
74
|
|
|
74
75
|
vote_phase_start: { priority: 99 },
|
|
75
|
-
vote_cast: { priority: 50 },
|
|
76
|
-
meeting_ended: { priority: 50 },
|
|
77
76
|
|
|
78
77
|
game_over: { priority: 50 },
|
|
79
78
|
role_assigned: { priority: 50 },
|
|
@@ -149,7 +148,7 @@ interface RouteRule {
|
|
|
149
148
|
const ROUTING_RULES: RouteRule[] = [
|
|
150
149
|
{ reason: 'speech_your_turn', match: (t) => t.includes('speech_your_turn'), nextStep: 'It is YOUR turn to speak. Submit `ccl do -s "<draft>"` immediately. Server skips you after 45s.' },
|
|
151
150
|
{ reason: 'role_assigned', match: (t) => t.includes('role_assigned'), nextStep: 'Tell user your role, faction, win condition, and first plan.' },
|
|
152
|
-
{ reason: '
|
|
151
|
+
{ reason: 'crab_teammates', match: (t) => t.includes('crab_teammates'), nextStep: 'Crab teammate list is available. Keep teammate identities private, coordinate cover stories, and avoid voting or killing into them.' },
|
|
153
152
|
{ reason: 'match_start', match: (t) => t.includes('match_start'), nextStep: 'Matchmaking queue entered. The game runtime is live and the stream is attached. Chat with the user while waiting for allocation.' },
|
|
154
153
|
{ reason: 'match_waiting', match: (t) => t.includes('match_waiting'), nextStep: 'Still in queue (see `events[].waited_secs`). Keep chatting with the user; no tactical action required.' },
|
|
155
154
|
{ reason: 'match_timeout', match: (t) => t.includes('match_timeout'), nextStep: `Cumulative wait reached ${Math.round(DEFAULT_MATCH_TIMEOUT_MS / 60_000)} min (see \`events[].waited_secs\`). The stream will exit — discuss with the user: launch a fresh \`ccl game start\` to retry, or call it a session.` },
|
package/src/lib/auth.test.ts
CHANGED
|
@@ -39,6 +39,21 @@ describe('AuthStore TTS config', () => {
|
|
|
39
39
|
expect(raw.profiles['lobster-1'].tts.defaultVoice).toBe('male-qn-qingse');
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
+
it('clears cached game server URL when set to undefined', () => {
|
|
43
|
+
const store = new AuthStore(authFile);
|
|
44
|
+
store.addProfile({
|
|
45
|
+
agentName: 'lobster-1',
|
|
46
|
+
apiKey: 'claw_1',
|
|
47
|
+
serverUrl: 'https://example.com',
|
|
48
|
+
gameServerUrl: 'https://example.com/gs/old-game-server',
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
store.updateGameServerUrl(undefined);
|
|
52
|
+
|
|
53
|
+
const raw = JSON.parse(readFileSync(authFile, 'utf8'));
|
|
54
|
+
expect(raw.profiles['lobster-1'].gameServerUrl).toBeUndefined();
|
|
55
|
+
});
|
|
56
|
+
|
|
42
57
|
it('migrates legacy neteaseTtsKey to each profile tts keys', () => {
|
|
43
58
|
writeFileSync(authFile, JSON.stringify({
|
|
44
59
|
activeProfile: 'lobster-1',
|
|
@@ -1,130 +1,130 @@
|
|
|
1
|
-
import { mkdtempSync, readFileSync, writeFileSync, existsSync, readdirSync } from 'fs';
|
|
2
|
-
import { tmpdir } from 'os';
|
|
3
|
-
import { dirname, join } from 'path';
|
|
4
|
-
import { describe, expect, it } from 'vitest';
|
|
5
|
-
import { runHostConfigSetup, unionArrayField, type HostConfigPatcher } from './host-config-patcher.js';
|
|
6
|
-
|
|
7
|
-
function tmpConfig(initial: unknown): string {
|
|
8
|
-
const dir = mkdtempSync(join(tmpdir(), 'host-cfg-'));
|
|
9
|
-
const p = join(dir, 'config.json');
|
|
10
|
-
writeFileSync(p, JSON.stringify(initial, null, 2));
|
|
11
|
-
return p;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const dummyHost: HostConfigPatcher<{ value: string }> = {
|
|
15
|
-
hostName: 'dummy',
|
|
16
|
-
resolveConfigPath: () => '/tmp/never-used',
|
|
17
|
-
computePatch: (current, opts) => {
|
|
18
|
-
const cur = (current as Record<string, unknown> | null) ?? {};
|
|
19
|
-
const before = (cur.values as string[] | undefined) ?? [];
|
|
20
|
-
if (before.includes(opts.value)) return { next: cur, changes: [], warnings: [] };
|
|
21
|
-
return {
|
|
22
|
-
next: { ...cur, values: [...before, opts.value] },
|
|
23
|
-
changes: [`add ${opts.value} to values`],
|
|
24
|
-
warnings: [],
|
|
25
|
-
};
|
|
26
|
-
},
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
describe('runHostConfigSetup', () => {
|
|
30
|
-
it('prints recommended patch in --print mode without touching disk', () => {
|
|
31
|
-
const r = runHostConfigSetup(dummyHost, { value: 'x' }, { print: true });
|
|
32
|
-
expect(r.exitCode).toBe(0);
|
|
33
|
-
expect(r.output.join('\n')).toContain('"values"');
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('reports missing config file gracefully', () => {
|
|
37
|
-
const r = runHostConfigSetup(dummyHost, { value: 'x' }, { configPath: '/no/such/file.json' });
|
|
38
|
-
expect(r.exitCode).toBe(0);
|
|
39
|
-
expect(r.output.some((l) => l.includes('not found'))).toBe(true);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('reports malformed JSON with exit 1', () => {
|
|
43
|
-
const dir = mkdtempSync(join(tmpdir(), 'host-cfg-bad-'));
|
|
44
|
-
const p = join(dir, 'config.json');
|
|
45
|
-
writeFileSync(p, '{ not: valid json');
|
|
46
|
-
const r = runHostConfigSetup(dummyHost, { value: 'x' }, { configPath: p });
|
|
47
|
-
expect(r.exitCode).toBe(1);
|
|
48
|
-
expect(r.output.join('\n')).toMatch(/Failed to parse/);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('returns exit 2 with diff in dry-run when changes pending', () => {
|
|
52
|
-
const p = tmpConfig({ values: ['a'] });
|
|
53
|
-
const r = runHostConfigSetup(dummyHost, { value: 'b' }, { configPath: p });
|
|
54
|
-
expect(r.exitCode).toBe(2);
|
|
55
|
-
const txt = r.output.join('\n');
|
|
56
|
-
expect(txt).toMatch(/Pending changes/);
|
|
57
|
-
expect(txt).toMatch(/Re-run with -y to apply/);
|
|
58
|
-
// file unchanged
|
|
59
|
-
expect(JSON.parse(readFileSync(p, 'utf-8'))).toEqual({ values: ['a'] });
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('returns exit 0 no-op when already up to date', () => {
|
|
63
|
-
const p = tmpConfig({ values: ['a'] });
|
|
64
|
-
const r = runHostConfigSetup(dummyHost, { value: 'a' }, { configPath: p });
|
|
65
|
-
expect(r.exitCode).toBe(0);
|
|
66
|
-
expect(r.output.join('\n')).toMatch(/already up to date/);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('applies changes atomically with backup when -y', () => {
|
|
70
|
-
const p = tmpConfig({ values: ['a'] });
|
|
71
|
-
const r = runHostConfigSetup(dummyHost, { value: 'b' }, { configPath: p, yes: true });
|
|
72
|
-
expect(r.exitCode).toBe(0);
|
|
73
|
-
expect(JSON.parse(readFileSync(p, 'utf-8'))).toEqual({ values: ['a', 'b'] });
|
|
74
|
-
const files = readdirSync(dirname(p));
|
|
75
|
-
expect(files.some((f) => f.startsWith('config.json.bak.'))).toBe(true);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('--no-backup skips backup file', () => {
|
|
79
|
-
const p = tmpConfig({ values: ['a'] });
|
|
80
|
-
const r = runHostConfigSetup(dummyHost, { value: 'b' }, { configPath: p, yes: true, backup: false });
|
|
81
|
-
expect(r.exitCode).toBe(0);
|
|
82
|
-
const files = readdirSync(dirname(p));
|
|
83
|
-
expect(files.some((f) => f.startsWith('config.json.bak.'))).toBe(false);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('does not leave .tmp file behind on success', () => {
|
|
87
|
-
const p = tmpConfig({ values: ['a'] });
|
|
88
|
-
runHostConfigSetup(dummyHost, { value: 'b' }, { configPath: p, yes: true, backup: false });
|
|
89
|
-
const files = readdirSync(dirname(p));
|
|
90
|
-
expect(files.some((f) => f.includes('.tmp.'))).toBe(false);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('preserves CRLF line endings when original file uses CRLF', () => {
|
|
94
|
-
const dir = mkdtempSync(join(tmpdir(), 'host-cfg-crlf-'));
|
|
95
|
-
const p = join(dir, 'config.json');
|
|
96
|
-
writeFileSync(p, '{\r\n "values": [\r\n "a"\r\n ]\r\n}\r\n');
|
|
97
|
-
runHostConfigSetup(dummyHost, { value: 'b' }, { configPath: p, yes: true, backup: false });
|
|
98
|
-
const after = readFileSync(p, 'utf-8');
|
|
99
|
-
expect(after.includes('\r\n')).toBe(true);
|
|
100
|
-
expect(after.replace(/\r\n/g, '<EOL>').includes('\n')).toBe(false);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('preserves LF line endings when original file uses LF', () => {
|
|
104
|
-
const dir = mkdtempSync(join(tmpdir(), 'host-cfg-lf-'));
|
|
105
|
-
const p = join(dir, 'config.json');
|
|
106
|
-
writeFileSync(p, '{\n "values": [\n "a"\n ]\n}\n');
|
|
107
|
-
runHostConfigSetup(dummyHost, { value: 'b' }, { configPath: p, yes: true, backup: false });
|
|
108
|
-
const after = readFileSync(p, 'utf-8');
|
|
109
|
-
expect(after.includes('\r\n')).toBe(false);
|
|
110
|
-
});
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
describe('unionArrayField', () => {
|
|
114
|
-
it('creates new array when field missing', () => {
|
|
115
|
-
expect(unionArrayField(undefined, 'x')).toEqual({ next: ['x'], added: true });
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('returns warning when existing value is not an array', () => {
|
|
119
|
-
const r = unionArrayField('not-an-array', 'x');
|
|
120
|
-
expect(r).toEqual({ warning: 'existing value is not an array, refusing to overwrite' });
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it('returns added=false when entry already present', () => {
|
|
124
|
-
expect(unionArrayField(['a', 'b'], 'a')).toEqual({ next: ['a', 'b'], added: false });
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it('appends entry when missing from array', () => {
|
|
128
|
-
expect(unionArrayField(['a'], 'b')).toEqual({ next: ['a', 'b'], added: true });
|
|
129
|
-
});
|
|
130
|
-
});
|
|
1
|
+
import { mkdtempSync, readFileSync, writeFileSync, existsSync, readdirSync } from 'fs';
|
|
2
|
+
import { tmpdir } from 'os';
|
|
3
|
+
import { dirname, join } from 'path';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
import { runHostConfigSetup, unionArrayField, type HostConfigPatcher } from './host-config-patcher.js';
|
|
6
|
+
|
|
7
|
+
function tmpConfig(initial: unknown): string {
|
|
8
|
+
const dir = mkdtempSync(join(tmpdir(), 'host-cfg-'));
|
|
9
|
+
const p = join(dir, 'config.json');
|
|
10
|
+
writeFileSync(p, JSON.stringify(initial, null, 2));
|
|
11
|
+
return p;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const dummyHost: HostConfigPatcher<{ value: string }> = {
|
|
15
|
+
hostName: 'dummy',
|
|
16
|
+
resolveConfigPath: () => '/tmp/never-used',
|
|
17
|
+
computePatch: (current, opts) => {
|
|
18
|
+
const cur = (current as Record<string, unknown> | null) ?? {};
|
|
19
|
+
const before = (cur.values as string[] | undefined) ?? [];
|
|
20
|
+
if (before.includes(opts.value)) return { next: cur, changes: [], warnings: [] };
|
|
21
|
+
return {
|
|
22
|
+
next: { ...cur, values: [...before, opts.value] },
|
|
23
|
+
changes: [`add ${opts.value} to values`],
|
|
24
|
+
warnings: [],
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
describe('runHostConfigSetup', () => {
|
|
30
|
+
it('prints recommended patch in --print mode without touching disk', () => {
|
|
31
|
+
const r = runHostConfigSetup(dummyHost, { value: 'x' }, { print: true });
|
|
32
|
+
expect(r.exitCode).toBe(0);
|
|
33
|
+
expect(r.output.join('\n')).toContain('"values"');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('reports missing config file gracefully', () => {
|
|
37
|
+
const r = runHostConfigSetup(dummyHost, { value: 'x' }, { configPath: '/no/such/file.json' });
|
|
38
|
+
expect(r.exitCode).toBe(0);
|
|
39
|
+
expect(r.output.some((l) => l.includes('not found'))).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('reports malformed JSON with exit 1', () => {
|
|
43
|
+
const dir = mkdtempSync(join(tmpdir(), 'host-cfg-bad-'));
|
|
44
|
+
const p = join(dir, 'config.json');
|
|
45
|
+
writeFileSync(p, '{ not: valid json');
|
|
46
|
+
const r = runHostConfigSetup(dummyHost, { value: 'x' }, { configPath: p });
|
|
47
|
+
expect(r.exitCode).toBe(1);
|
|
48
|
+
expect(r.output.join('\n')).toMatch(/Failed to parse/);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('returns exit 2 with diff in dry-run when changes pending', () => {
|
|
52
|
+
const p = tmpConfig({ values: ['a'] });
|
|
53
|
+
const r = runHostConfigSetup(dummyHost, { value: 'b' }, { configPath: p });
|
|
54
|
+
expect(r.exitCode).toBe(2);
|
|
55
|
+
const txt = r.output.join('\n');
|
|
56
|
+
expect(txt).toMatch(/Pending changes/);
|
|
57
|
+
expect(txt).toMatch(/Re-run with -y to apply/);
|
|
58
|
+
// file unchanged
|
|
59
|
+
expect(JSON.parse(readFileSync(p, 'utf-8'))).toEqual({ values: ['a'] });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('returns exit 0 no-op when already up to date', () => {
|
|
63
|
+
const p = tmpConfig({ values: ['a'] });
|
|
64
|
+
const r = runHostConfigSetup(dummyHost, { value: 'a' }, { configPath: p });
|
|
65
|
+
expect(r.exitCode).toBe(0);
|
|
66
|
+
expect(r.output.join('\n')).toMatch(/already up to date/);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('applies changes atomically with backup when -y', () => {
|
|
70
|
+
const p = tmpConfig({ values: ['a'] });
|
|
71
|
+
const r = runHostConfigSetup(dummyHost, { value: 'b' }, { configPath: p, yes: true });
|
|
72
|
+
expect(r.exitCode).toBe(0);
|
|
73
|
+
expect(JSON.parse(readFileSync(p, 'utf-8'))).toEqual({ values: ['a', 'b'] });
|
|
74
|
+
const files = readdirSync(dirname(p));
|
|
75
|
+
expect(files.some((f) => f.startsWith('config.json.bak.'))).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('--no-backup skips backup file', () => {
|
|
79
|
+
const p = tmpConfig({ values: ['a'] });
|
|
80
|
+
const r = runHostConfigSetup(dummyHost, { value: 'b' }, { configPath: p, yes: true, backup: false });
|
|
81
|
+
expect(r.exitCode).toBe(0);
|
|
82
|
+
const files = readdirSync(dirname(p));
|
|
83
|
+
expect(files.some((f) => f.startsWith('config.json.bak.'))).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('does not leave .tmp file behind on success', () => {
|
|
87
|
+
const p = tmpConfig({ values: ['a'] });
|
|
88
|
+
runHostConfigSetup(dummyHost, { value: 'b' }, { configPath: p, yes: true, backup: false });
|
|
89
|
+
const files = readdirSync(dirname(p));
|
|
90
|
+
expect(files.some((f) => f.includes('.tmp.'))).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('preserves CRLF line endings when original file uses CRLF', () => {
|
|
94
|
+
const dir = mkdtempSync(join(tmpdir(), 'host-cfg-crlf-'));
|
|
95
|
+
const p = join(dir, 'config.json');
|
|
96
|
+
writeFileSync(p, '{\r\n "values": [\r\n "a"\r\n ]\r\n}\r\n');
|
|
97
|
+
runHostConfigSetup(dummyHost, { value: 'b' }, { configPath: p, yes: true, backup: false });
|
|
98
|
+
const after = readFileSync(p, 'utf-8');
|
|
99
|
+
expect(after.includes('\r\n')).toBe(true);
|
|
100
|
+
expect(after.replace(/\r\n/g, '<EOL>').includes('\n')).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('preserves LF line endings when original file uses LF', () => {
|
|
104
|
+
const dir = mkdtempSync(join(tmpdir(), 'host-cfg-lf-'));
|
|
105
|
+
const p = join(dir, 'config.json');
|
|
106
|
+
writeFileSync(p, '{\n "values": [\n "a"\n ]\n}\n');
|
|
107
|
+
runHostConfigSetup(dummyHost, { value: 'b' }, { configPath: p, yes: true, backup: false });
|
|
108
|
+
const after = readFileSync(p, 'utf-8');
|
|
109
|
+
expect(after.includes('\r\n')).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('unionArrayField', () => {
|
|
114
|
+
it('creates new array when field missing', () => {
|
|
115
|
+
expect(unionArrayField(undefined, 'x')).toEqual({ next: ['x'], added: true });
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('returns warning when existing value is not an array', () => {
|
|
119
|
+
const r = unionArrayField('not-an-array', 'x');
|
|
120
|
+
expect(r).toEqual({ warning: 'existing value is not an array, refusing to overwrite' });
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('returns added=false when entry already present', () => {
|
|
124
|
+
expect(unionArrayField(['a', 'b'], 'a')).toEqual({ next: ['a', 'b'], added: false });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('appends entry when missing from array', () => {
|
|
128
|
+
expect(unionArrayField(['a'], 'b')).toEqual({ next: ['a', 'b'], added: true });
|
|
129
|
+
});
|
|
130
|
+
});
|