@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,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
|
+
}
|
package/src/lib/hub-reminder.ts
CHANGED
|
@@ -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?"`;
|
|
@@ -46,8 +46,7 @@ describe('KnowledgeStore', () => {
|
|
|
46
46
|
selector: '5',
|
|
47
47
|
key: 'role',
|
|
48
48
|
value: 'impostor',
|
|
49
|
-
tags: ['
|
|
50
|
-
confidence: 0.8,
|
|
49
|
+
tags: ['kill_if_armed'],
|
|
51
50
|
note: 'vent read',
|
|
52
51
|
});
|
|
53
52
|
|
|
@@ -55,9 +54,7 @@ describe('KnowledgeStore', () => {
|
|
|
55
54
|
|
|
56
55
|
expect(file?.gameId).toBe('game-1');
|
|
57
56
|
expect(file?.subjects['player:5']?.facts?.role.value).toBe('impostor');
|
|
58
|
-
expect(file?.subjects['player:5']?.
|
|
59
|
-
expect(file?.subjects['player:5']?.tags?.suspect.note).toBe('vent read');
|
|
60
|
-
expect(file?.subjects['player:5']?.tags?.kill_if_armed.confidence).toBe(0.8);
|
|
57
|
+
expect(file?.subjects['player:5']?.tags?.kill_if_armed.note).toBe('vent read');
|
|
61
58
|
});
|
|
62
59
|
|
|
63
60
|
it('preserves explicit null fact values', () => {
|
|
@@ -79,16 +76,8 @@ describe('KnowledgeStore', () => {
|
|
|
79
76
|
expect(readKnowledgeFileResult(path).status).toBe('invalid');
|
|
80
77
|
});
|
|
81
78
|
|
|
82
|
-
it('rejects invalid confidence values', () => {
|
|
83
|
-
const s = store();
|
|
84
|
-
|
|
85
|
-
expect(() => s.set({ type: 'player', selector: '5', tags: ['avoid'], confidence: Number.NaN })).toThrow(/confidence/);
|
|
86
|
-
expect(() => s.set({ type: 'player', selector: '5', tags: ['avoid'], confidence: -0.1 })).toThrow(/confidence/);
|
|
87
|
-
expect(() => s.set({ type: 'player', selector: '5', tags: ['avoid'], confidence: 1.1 })).toThrow(/confidence/);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
79
|
it('returns an empty file when loading knowledge from another game', () => {
|
|
91
|
-
store('old-game').set({ type: 'player', selector: '5', tags: ['
|
|
80
|
+
store('old-game').set({ type: 'player', selector: '5', tags: ['hostile'] });
|
|
92
81
|
|
|
93
82
|
expect(store('new-game').list()).toEqual({
|
|
94
83
|
version: 1,
|
|
@@ -97,14 +86,30 @@ describe('KnowledgeStore', () => {
|
|
|
97
86
|
});
|
|
98
87
|
});
|
|
99
88
|
|
|
89
|
+
it('treats unmarked players as suspected by default', () => {
|
|
90
|
+
const s = store();
|
|
91
|
+
s.set({ type: 'player', selector: '5', tags: ['hostile'] });
|
|
92
|
+
|
|
93
|
+
const v = view({ '5': 'Bob', '9': 'Zed' });
|
|
94
|
+
|
|
95
|
+
// 显式标记的两档
|
|
96
|
+
expect(v.isHostile({ seat: 5 })).toBe(true);
|
|
97
|
+
expect(v.isSuspect({ seat: 5 })).toBe(false);
|
|
98
|
+
// 完全没标记的人 = 被怀疑(markOf 返回 undefined)
|
|
99
|
+
expect(v.markOf({ seat: 9 })).toBeUndefined();
|
|
100
|
+
expect(v.isSuspect({ name: 'Zed' })).toBe(true);
|
|
101
|
+
expect(v.isHostile({ seat: 9 })).toBe(false);
|
|
102
|
+
expect(v.isTrusted({ seat: 9 })).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
|
|
100
105
|
it('aggregates player marks written by seat and by name', () => {
|
|
101
106
|
const s = store();
|
|
102
|
-
s.set({ type: 'player', selector: '5', tags: ['
|
|
107
|
+
s.set({ type: 'player', selector: '5', tags: ['hostile'] });
|
|
103
108
|
|
|
104
109
|
const v = view({ '5': 'Bob' });
|
|
105
110
|
|
|
106
|
-
expect(v.
|
|
107
|
-
expect(v.markOf({ seat: 5 })).toBe('
|
|
111
|
+
expect(v.isHostile({ name: 'Bob' })).toBe(true);
|
|
112
|
+
expect(v.markOf({ seat: 5 })).toBe('hostile');
|
|
108
113
|
});
|
|
109
114
|
|
|
110
115
|
it('uses the latest mark across seat and name selectors', () => {
|
|
@@ -127,12 +132,12 @@ describe('KnowledgeStore', () => {
|
|
|
127
132
|
it('keeps role facts independent from strategy marks', () => {
|
|
128
133
|
const s = store();
|
|
129
134
|
s.set({ type: 'player', selector: '5', key: 'role', value: 'impostor' });
|
|
130
|
-
s.set({ type: 'player', selector: '5', tags: ['
|
|
135
|
+
s.set({ type: 'player', selector: '5', tags: ['trusted'] });
|
|
131
136
|
|
|
132
137
|
const v = view();
|
|
133
138
|
|
|
134
139
|
expect(v.roleOf({ seat: 5 })).toBe('impostor');
|
|
135
|
-
expect(v.markOf({ seat: 5 })).toBe('
|
|
140
|
+
expect(v.markOf({ seat: 5 })).toBe('trusted');
|
|
136
141
|
expect(v.isHostile({ seat: 5 })).toBe(false);
|
|
137
142
|
});
|
|
138
143
|
|
|
@@ -140,38 +145,23 @@ describe('KnowledgeStore', () => {
|
|
|
140
145
|
const s = store();
|
|
141
146
|
s.set({ type: 'player', selector: '5', tags: ['trusted'] });
|
|
142
147
|
tick();
|
|
143
|
-
s.set({ type: 'player', selector: 'Bob', tags: ['
|
|
148
|
+
s.set({ type: 'player', selector: 'Bob', tags: ['hostile'] });
|
|
144
149
|
|
|
145
150
|
const v = view({ '5': 'Bob' });
|
|
146
151
|
|
|
147
|
-
expect(v.
|
|
152
|
+
expect(v.isHostile({ seat: 5 })).toBe(true);
|
|
148
153
|
expect(v.isTrusted({ seat: 5 })).toBe(false);
|
|
149
154
|
expect(v.selectorsWithTag('player', 'trusted')).toEqual([]);
|
|
150
|
-
expect(v.selectorsWithTag('player', '
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('applies confidence thresholds to facts and tags', () => {
|
|
154
|
-
const s = store();
|
|
155
|
-
s.set({ type: 'player', selector: '5', tags: ['suspect'], confidence: 0.5 });
|
|
156
|
-
s.set({ type: 'player', selector: '6', tags: ['hostile'], confidence: 0.7 });
|
|
157
|
-
|
|
158
|
-
const v = view();
|
|
159
|
-
|
|
160
|
-
expect(v.isSuspect({ seat: 5 }, 0.4)).toBe(true);
|
|
161
|
-
expect(v.isSuspect({ seat: 5 }, 0.6)).toBe(false);
|
|
162
|
-
expect(v.isHostile({ seat: 6 }, 0.6)).toBe(true);
|
|
163
|
-
expect(v.isHostile({ seat: 6 }, 0.8)).toBe(false);
|
|
155
|
+
expect(v.selectorsWithTag('player', 'hostile')).toEqual(['Bob']);
|
|
164
156
|
});
|
|
165
157
|
|
|
166
|
-
it('maps legacy tags onto the
|
|
158
|
+
it('maps legacy tags onto the two canonical marks', () => {
|
|
167
159
|
const s = store();
|
|
168
|
-
s.set({ type: 'player', selector: '5', tags: ['avoid'] });
|
|
169
160
|
s.set({ type: 'player', selector: '6', tags: ['kill_if_armed'] });
|
|
170
161
|
s.set({ type: 'player', selector: '7', tags: ['protected'] });
|
|
171
162
|
|
|
172
163
|
const v = view();
|
|
173
164
|
|
|
174
|
-
expect(v.markOf({ seat: 5 })).toBe('suspect');
|
|
175
165
|
expect(v.markOf({ seat: 6 })).toBe('hostile');
|
|
176
166
|
expect(v.markOf({ seat: 7 })).toBe('trusted');
|
|
177
167
|
expect(v.hasTag({ seat: 6 }, 'hostile')).toBe(true);
|