@myclaw163/clawclaw-cli 0.6.74 → 0.6.76
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/README.md +14 -4
- package/package.json +1 -1
- package/skills/clawclaw/SKILL.md +5 -3
- package/skills/clawclaw/references/COMMANDS.md +11 -6
- package/skills/clawclaw/references/STRATEGIES.md +1 -1
- package/src/cli.ts +2 -0
- package/src/commands/data.test.ts +33 -0
- package/src/commands/data.ts +22 -0
- package/src/commands/events.test.ts +29 -0
- package/src/commands/events.ts +30 -1
- package/src/commands/hub.ts +2 -2
- package/src/commands/strategy.test.ts +13 -5
- package/src/commands/strategy.ts +6 -4
- package/src/lib/auth.test.ts +12 -0
- package/src/lib/auth.ts +69 -32
- package/src/lib/hub-install.test.ts +2 -2
- package/src/lib/hub-install.ts +53 -14
- package/src/lib/hub-reminder.ts +5 -2
- package/src/lib/init-command.ts +12 -2
- package/src/lib/load-context.test.ts +3 -3
- package/src/lib/server-registry.ts +1 -1
- package/src/lib/strategy-export.test.ts +17 -9
- package/src/lib/strategy-export.ts +11 -6
- package/src/lib/user-data.test.ts +96 -0
- package/src/lib/user-data.ts +400 -0
- package/src/pipeline/player-projection.test.ts +49 -0
- package/src/pipeline/player-projection.ts +1 -11
- package/src/strategies/loader.test.ts +3 -3
- package/src/strategies/loader.ts +3 -1
package/src/lib/hub-install.ts
CHANGED
|
@@ -4,15 +4,21 @@ import { dirname, extname, join } from 'path';
|
|
|
4
4
|
import { getWorkspaceDir } from './init-command.js';
|
|
5
5
|
import type { ResourceType, ResourceDetail } from './hub-client.js';
|
|
6
6
|
import { unzip } from './hub-unzip.js';
|
|
7
|
+
import {
|
|
8
|
+
ensureUserDataLayout,
|
|
9
|
+
getUserDataDir,
|
|
10
|
+
getUserDataHubStrategyLockfile,
|
|
11
|
+
getUserDataStrategiesDir,
|
|
12
|
+
} from './user-data.js';
|
|
7
13
|
|
|
8
14
|
export interface InstalledEntry { type: ResourceType; id: string; title: string; path: string; installedAt: string; }
|
|
9
15
|
export type Lockfile = Record<string, InstalledEntry>; // keyed by "<type>/<id>"
|
|
10
16
|
|
|
11
17
|
export function hubDir(): string { return join(getWorkspaceDir(), 'hub'); }
|
|
12
18
|
export function lockfilePath(): string { return join(hubDir(), 'installed.json'); }
|
|
19
|
+
export function strategyLockfilePath(): string { return getUserDataHubStrategyLockfile(getWorkspaceDir()); }
|
|
13
20
|
|
|
14
|
-
|
|
15
|
-
const p = lockfilePath();
|
|
21
|
+
function readJsonLockfile(p: string): Lockfile {
|
|
16
22
|
if (!existsSync(p)) return {};
|
|
17
23
|
try {
|
|
18
24
|
return JSON.parse(readFileSync(p, 'utf8')) as Lockfile;
|
|
@@ -22,11 +28,39 @@ export function readLockfile(): Lockfile {
|
|
|
22
28
|
}
|
|
23
29
|
}
|
|
24
30
|
|
|
25
|
-
export function
|
|
31
|
+
export function readSkillLockfile(): Lockfile {
|
|
32
|
+
return Object.fromEntries(
|
|
33
|
+
Object.entries(readJsonLockfile(lockfilePath())).filter(([, entry]) => entry.type === 'skill'),
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function readStrategyLockfile(): Lockfile {
|
|
38
|
+
ensureUserDataLayout(getWorkspaceDir());
|
|
39
|
+
return Object.fromEntries(
|
|
40
|
+
Object.entries(readJsonLockfile(strategyLockfilePath())).filter(([, entry]) => entry.type === 'strategy'),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function readLockfile(): Lockfile {
|
|
45
|
+
return { ...readSkillLockfile(), ...readStrategyLockfile() };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function writeSkillLockfile(lock: Lockfile): void {
|
|
26
49
|
mkdirSync(hubDir(), { recursive: true });
|
|
27
50
|
writeFileSync(lockfilePath(), JSON.stringify(lock, null, 2) + '\n', 'utf8');
|
|
28
51
|
}
|
|
29
52
|
|
|
53
|
+
function writeStrategyLockfile(lock: Lockfile): void {
|
|
54
|
+
const p = strategyLockfilePath();
|
|
55
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
56
|
+
writeFileSync(p, JSON.stringify(lock, null, 2) + '\n', 'utf8');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function writeLockfile(lock: Lockfile): void {
|
|
60
|
+
writeSkillLockfile(Object.fromEntries(Object.entries(lock).filter(([, entry]) => entry.type === 'skill')));
|
|
61
|
+
writeStrategyLockfile(Object.fromEntries(Object.entries(lock).filter(([, entry]) => entry.type === 'strategy')));
|
|
62
|
+
}
|
|
63
|
+
|
|
30
64
|
function assertSafeSegment(label: string, s: string): void {
|
|
31
65
|
if (!s || s === '.' || s === '..' || /[\\/:\0]/.test(s)) {
|
|
32
66
|
throw new Error(`unsafe ${label} segment: '${s}'`);
|
|
@@ -39,7 +73,7 @@ export function slugify(title: string): string {
|
|
|
39
73
|
|
|
40
74
|
const STRATEGY_EXTS = new Set(['.ts', '.tsx', '.js']);
|
|
41
75
|
|
|
42
|
-
/**
|
|
76
|
+
/** Install path relative to user-data for strategies and workspace for skills. */
|
|
43
77
|
export function placementRelPath(detail: { type: ResourceType; id: string; title: string; fileName?: string }): string {
|
|
44
78
|
assertSafeSegment('id', detail.id);
|
|
45
79
|
if (detail.type === 'strategy') {
|
|
@@ -60,31 +94,31 @@ export interface InstallResult { ref: string; type: ResourceType; id: string; ti
|
|
|
60
94
|
|
|
61
95
|
export function installResource(detail: ResourceDetail, artifact: Buffer, opts: { force?: boolean } = {}): InstallResult {
|
|
62
96
|
const ws = getWorkspaceDir();
|
|
97
|
+
ensureUserDataLayout(ws);
|
|
63
98
|
const ref = makeRef(detail.type, detail.id);
|
|
64
99
|
const relPath = placementRelPath(detail);
|
|
65
|
-
const
|
|
66
|
-
const
|
|
100
|
+
const rootDir = detail.type === 'strategy' ? getUserDataDir(ws) : ws;
|
|
101
|
+
const absPath = join(rootDir, relPath);
|
|
102
|
+
const lock = detail.type === 'strategy' ? readStrategyLockfile() : readSkillLockfile();
|
|
67
103
|
const tracked = lock[ref] !== undefined;
|
|
68
104
|
|
|
69
105
|
if (detail.type === 'strategy') {
|
|
70
106
|
if (existsSync(absPath) && !tracked && !opts.force) {
|
|
71
107
|
throw new Error(`${relPath} already exists and is not tracked by hub; re-run with --force to overwrite`);
|
|
72
108
|
}
|
|
73
|
-
mkdirSync(
|
|
109
|
+
mkdirSync(getUserDataStrategiesDir(ws), { recursive: true });
|
|
74
110
|
writeFileSync(absPath, artifact);
|
|
75
111
|
} else {
|
|
76
112
|
if (existsSync(absPath) && !tracked && !opts.force) {
|
|
77
113
|
throw new Error(`skills/${slugify(detail.title) || detail.id} already exists and is not tracked by hub; re-run with --force to overwrite`);
|
|
78
114
|
}
|
|
79
115
|
const entries = unzip(artifact);
|
|
80
|
-
// Validate ALL entry paths before any filesystem mutation (no partial/unsafe writes).
|
|
81
116
|
for (const e of entries) {
|
|
82
117
|
if (e.path.includes('..') || e.path.startsWith('/') || /^[A-Za-z]:/.test(e.path)) {
|
|
83
118
|
throw new Error(`unsafe path in skill archive: ${e.path}`);
|
|
84
119
|
}
|
|
85
120
|
}
|
|
86
|
-
//
|
|
87
|
-
// error mid-write could leave a partial skill dir. Acceptable for v1.
|
|
121
|
+
// Non-atomic by design: entries are decoded and validated before writing.
|
|
88
122
|
if (existsSync(absPath)) rmSync(absPath, { recursive: true, force: true });
|
|
89
123
|
mkdirSync(absPath, { recursive: true });
|
|
90
124
|
for (const e of entries) {
|
|
@@ -101,18 +135,23 @@ export function installResource(detail: ResourceDetail, artifact: Buffer, opts:
|
|
|
101
135
|
path: relPath.split('\\').join('/'),
|
|
102
136
|
installedAt: new Date().toISOString(),
|
|
103
137
|
};
|
|
104
|
-
|
|
138
|
+
if (detail.type === 'strategy') writeStrategyLockfile(lock);
|
|
139
|
+
else writeSkillLockfile(lock);
|
|
105
140
|
return { ref, type: detail.type, id: detail.id, title: detail.title, installedPath: absPath };
|
|
106
141
|
}
|
|
107
142
|
|
|
108
143
|
export function uninstallResource(ref: string): { removed: boolean; path?: string } {
|
|
109
|
-
const
|
|
144
|
+
const type = ref.split('/')[0] as ResourceType;
|
|
145
|
+
const lock = type === 'strategy' ? readStrategyLockfile() : readSkillLockfile();
|
|
110
146
|
const entry = lock[ref];
|
|
111
147
|
if (!entry) return { removed: false };
|
|
112
|
-
const
|
|
148
|
+
const ws = getWorkspaceDir();
|
|
149
|
+
const rootDir = type === 'strategy' ? getUserDataDir(ws) : ws;
|
|
150
|
+
const abs = join(rootDir, entry.path);
|
|
113
151
|
if (existsSync(abs)) rmSync(abs, { recursive: true, force: true });
|
|
114
152
|
delete lock[ref];
|
|
115
|
-
|
|
153
|
+
if (type === 'strategy') writeStrategyLockfile(lock);
|
|
154
|
+
else writeSkillLockfile(lock);
|
|
116
155
|
return { removed: true, path: abs };
|
|
117
156
|
}
|
|
118
157
|
|
package/src/lib/hub-reminder.ts
CHANGED
|
@@ -7,19 +7,22 @@ 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
|
+
import { ensureUserDataLayout, getUserDataStrategiesDir } from './user-data.js';
|
|
10
11
|
|
|
11
12
|
export const HUB_URL = 'https://myclaw.163.com/hub';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* 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
|
+
* hub-installed strategy and no user strategy file in `<workspace>/user-data/strategies`.
|
|
16
17
|
*/
|
|
17
18
|
export function usesOnlyBuiltinStrategies(): boolean {
|
|
18
19
|
try {
|
|
19
20
|
if (listInstalled().some((e) => e.type === 'strategy')) return false;
|
|
20
21
|
} catch {}
|
|
21
22
|
try {
|
|
22
|
-
const
|
|
23
|
+
const workspaceDir = getWorkspaceDir();
|
|
24
|
+
ensureUserDataLayout(workspaceDir);
|
|
25
|
+
const dir = getUserDataStrategiesDir(workspaceDir);
|
|
23
26
|
if (existsSync(dir)) {
|
|
24
27
|
const hasUserStrategy = readdirSync(dir).some((f) => {
|
|
25
28
|
if (f.startsWith('_')) return false;
|
package/src/lib/init-command.ts
CHANGED
|
@@ -2,6 +2,13 @@ import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
|
2
2
|
import { isAbsolute, join, resolve, win32 } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
4
|
import type { AuthProfile } from './auth.js';
|
|
5
|
+
import {
|
|
6
|
+
ensureUserDataLayout,
|
|
7
|
+
getRuntimeDir,
|
|
8
|
+
getUserDataAccountDir,
|
|
9
|
+
getUserDataMemoryFile,
|
|
10
|
+
getUserDataPersonaFile,
|
|
11
|
+
} from './user-data.js';
|
|
5
12
|
|
|
6
13
|
const ACCOUNT_DIRS = ['state', 'logs', 'games'];
|
|
7
14
|
|
|
@@ -52,6 +59,7 @@ export function cmdInit(force = false): { created: boolean; path: string } {
|
|
|
52
59
|
|
|
53
60
|
mkdirSync(workspaceDir, { recursive: true });
|
|
54
61
|
mkdirSync(join(workspaceDir, 'accounts'), { recursive: true });
|
|
62
|
+
ensureUserDataLayout(workspaceDir);
|
|
55
63
|
|
|
56
64
|
if (!alreadyExists) {
|
|
57
65
|
writeFileSync(
|
|
@@ -80,6 +88,8 @@ export function getAccountId(profile: Pick<AuthProfile, 'apiKey'>): string {
|
|
|
80
88
|
|
|
81
89
|
export function createProfileDirs(profile: Pick<AuthProfile, 'apiKey'>): void {
|
|
82
90
|
createAccountDirs(getAccountId(profile));
|
|
91
|
+
mkdirSync(getUserDataAccountDir(getWorkspaceDir(), profile), { recursive: true });
|
|
92
|
+
mkdirSync(getRuntimeDir(getWorkspaceDir()), { recursive: true });
|
|
83
93
|
}
|
|
84
94
|
|
|
85
95
|
export function getStateDir(accountId: string): string {
|
|
@@ -111,10 +121,10 @@ export function getProfileGamesDir(profile: Pick<AuthProfile, 'apiKey'>): string
|
|
|
111
121
|
|
|
112
122
|
export function getProfileMemoryFile(profile: Pick<AuthProfile, 'apiKey'>): string {
|
|
113
123
|
createProfileDirs(profile);
|
|
114
|
-
return
|
|
124
|
+
return getUserDataMemoryFile(getWorkspaceDir(), profile);
|
|
115
125
|
}
|
|
116
126
|
|
|
117
127
|
export function getProfilePersonaFile(profile: Pick<AuthProfile, 'apiKey'>): string {
|
|
118
128
|
createProfileDirs(profile);
|
|
119
|
-
return
|
|
129
|
+
return getUserDataPersonaFile(getWorkspaceDir(), profile);
|
|
120
130
|
}
|
|
@@ -40,13 +40,13 @@ describe('loadContext', () => {
|
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
it('returns persona and memory snapshots for the active account', () => {
|
|
43
|
-
const store = new AuthStore(
|
|
43
|
+
const store = new AuthStore();
|
|
44
44
|
const snapshot = loadContext(store);
|
|
45
45
|
expect(snapshot.persona.content).toBe('voice');
|
|
46
46
|
expect(snapshot.persona.exists).toBe(true);
|
|
47
47
|
expect(snapshot.memory.content).toBe('notes');
|
|
48
48
|
expect(snapshot.memory.exists).toBe(true);
|
|
49
|
-
expect(snapshot.persona.path).
|
|
50
|
-
expect(snapshot.memory.path).
|
|
49
|
+
expect(snapshot.persona.path.split('\\').join('/')).toContain('user-data/accounts/');
|
|
50
|
+
expect(snapshot.memory.path.split('\\').join('/')).toContain('user-data/accounts/');
|
|
51
51
|
});
|
|
52
52
|
});
|
|
@@ -142,7 +142,7 @@ export class ServerRegistry {
|
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
/** Persist gameServerUrl to auth
|
|
145
|
+
/** Persist gameServerUrl to runtime auth state (best-effort). */
|
|
146
146
|
private persistGameServerUrl(): void {
|
|
147
147
|
if (!this.agentName) return;
|
|
148
148
|
try {
|
|
@@ -30,6 +30,14 @@ describe('strategy-export', () => {
|
|
|
30
30
|
rmSync(testWorkspace, { recursive: true, force: true });
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
+
function userStrategiesDir(): string {
|
|
34
|
+
return join(testWorkspace, 'user-data', 'strategies');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function officialManifestFile(): string {
|
|
38
|
+
return join(testWorkspace, 'runtime', 'official-strategies', 'manifest.json');
|
|
39
|
+
}
|
|
40
|
+
|
|
33
41
|
describe('rewriteBuiltinImports', () => {
|
|
34
42
|
it('should rewrite ALL relative imports to @myclaw163/clawclaw-cli', () => {
|
|
35
43
|
const source = `
|
|
@@ -75,7 +83,7 @@ export const strategy = { id: 'test', description: 'test', create: () => ({ name
|
|
|
75
83
|
it('should export a formerly goal-based strategy with its knowledge sidecar', async () => {
|
|
76
84
|
await exportStrategy('kill-lone');
|
|
77
85
|
|
|
78
|
-
const strategiesDir =
|
|
86
|
+
const strategiesDir = userStrategiesDir();
|
|
79
87
|
expect(existsSync(join(strategiesDir, 'kill-lone.ts'))).toBe(true);
|
|
80
88
|
expect(existsSync(join(strategiesDir, 'kill-lone.knowledge.md'))).toBe(true);
|
|
81
89
|
});
|
|
@@ -83,8 +91,8 @@ export const strategy = { id: 'test', description: 'test', create: () => ({ name
|
|
|
83
91
|
it('should export a valid strategy', async () => {
|
|
84
92
|
await exportStrategy('task-only');
|
|
85
93
|
|
|
86
|
-
const strategyFile = join(
|
|
87
|
-
const manifestFile =
|
|
94
|
+
const strategyFile = join(userStrategiesDir(), 'task-only.ts');
|
|
95
|
+
const manifestFile = officialManifestFile();
|
|
88
96
|
|
|
89
97
|
expect(existsSync(strategyFile)).toBe(true);
|
|
90
98
|
expect(existsSync(manifestFile)).toBe(true);
|
|
@@ -130,7 +138,7 @@ export const strategy = {
|
|
|
130
138
|
});
|
|
131
139
|
|
|
132
140
|
it('should refuse to overwrite untracked files without --force', async () => {
|
|
133
|
-
const strategiesDir =
|
|
141
|
+
const strategiesDir = userStrategiesDir();
|
|
134
142
|
mkdirSync(strategiesDir, { recursive: true });
|
|
135
143
|
writeFileSync(join(strategiesDir, 'task-only.ts'), '// user file', 'utf8');
|
|
136
144
|
|
|
@@ -141,7 +149,7 @@ export const strategy = {
|
|
|
141
149
|
});
|
|
142
150
|
|
|
143
151
|
it('should overwrite untracked files with --force', async () => {
|
|
144
|
-
const strategiesDir =
|
|
152
|
+
const strategiesDir = userStrategiesDir();
|
|
145
153
|
mkdirSync(strategiesDir, { recursive: true });
|
|
146
154
|
writeFileSync(join(strategiesDir, 'task-only.ts'), '// user file', 'utf8');
|
|
147
155
|
|
|
@@ -155,7 +163,7 @@ export const strategy = {
|
|
|
155
163
|
// First export
|
|
156
164
|
await exportStrategy('task-only');
|
|
157
165
|
|
|
158
|
-
const strategiesDir =
|
|
166
|
+
const strategiesDir = userStrategiesDir();
|
|
159
167
|
const strategyFile = join(strategiesDir, 'task-only.ts');
|
|
160
168
|
|
|
161
169
|
// Modify the file
|
|
@@ -189,7 +197,7 @@ export const strategy = {
|
|
|
189
197
|
await exportStrategy('task-only');
|
|
190
198
|
|
|
191
199
|
// Manually modify the manifest to simulate a stale hash
|
|
192
|
-
const manifestFile =
|
|
200
|
+
const manifestFile = officialManifestFile();
|
|
193
201
|
const manifest = JSON.parse(readFileSync(manifestFile, 'utf8'));
|
|
194
202
|
manifest['task-only'].sourceHash = 'stale-hash-value';
|
|
195
203
|
writeFileSync(manifestFile, JSON.stringify(manifest, null, 2), 'utf8');
|
|
@@ -202,7 +210,7 @@ export const strategy = {
|
|
|
202
210
|
await exportStrategy('kill-lone');
|
|
203
211
|
|
|
204
212
|
// Make both stale
|
|
205
|
-
const manifestFile =
|
|
213
|
+
const manifestFile = officialManifestFile();
|
|
206
214
|
const manifest = JSON.parse(readFileSync(manifestFile, 'utf8'));
|
|
207
215
|
manifest['task-only'].sourceHash = 'stale-1';
|
|
208
216
|
manifest['kill-lone'].sourceHash = 'stale-2';
|
|
@@ -218,7 +226,7 @@ export const strategy = {
|
|
|
218
226
|
await exportStrategy('kill-lone');
|
|
219
227
|
|
|
220
228
|
// Verify kill-lone has a sidecar hash (it has a .knowledge.md)
|
|
221
|
-
const manifestFile =
|
|
229
|
+
const manifestFile = officialManifestFile();
|
|
222
230
|
const manifest = JSON.parse(readFileSync(manifestFile, 'utf8'));
|
|
223
231
|
expect(manifest['kill-lone'].sidecarHash).toBeDefined();
|
|
224
232
|
|
|
@@ -6,12 +6,15 @@ import { tmpdir } from 'os';
|
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
7
|
import { createHash } from 'crypto';
|
|
8
8
|
import { getWorkspaceDir } from './init-command.js';
|
|
9
|
+
import {
|
|
10
|
+
ensureUserDataLayout,
|
|
11
|
+
getRuntimeOfficialStrategiesDir,
|
|
12
|
+
getRuntimeOfficialStrategiesManifestFile,
|
|
13
|
+
getUserDataStrategiesDir,
|
|
14
|
+
} from './user-data.js';
|
|
9
15
|
import { importUserFile } from '../strategies/loader.js';
|
|
10
16
|
|
|
11
17
|
// ── Constants ──────────────────────────────────────────────────────
|
|
12
|
-
const OFFICIAL_DIR = '.official';
|
|
13
|
-
const MANIFEST_FILE = 'manifest.json';
|
|
14
|
-
|
|
15
18
|
// ── Types ──────────────────────────────────────────────────────────
|
|
16
19
|
export interface ManifestEntry {
|
|
17
20
|
sourceHash: string;
|
|
@@ -22,15 +25,17 @@ export type Manifest = Record<string, ManifestEntry>;
|
|
|
22
25
|
|
|
23
26
|
// ── Helpers ────────────────────────────────────────────────────────
|
|
24
27
|
function strategiesDir(): string {
|
|
25
|
-
|
|
28
|
+
const workspaceDir = getWorkspaceDir();
|
|
29
|
+
ensureUserDataLayout(workspaceDir);
|
|
30
|
+
return getUserDataStrategiesDir(workspaceDir);
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
function officialDir(): string {
|
|
29
|
-
return
|
|
34
|
+
return getRuntimeOfficialStrategiesDir(getWorkspaceDir());
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
function manifestPath(): string {
|
|
33
|
-
return
|
|
38
|
+
return getRuntimeOfficialStrategiesManifestFile(getWorkspaceDir());
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
export function readManifest(): Manifest {
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import {
|
|
6
|
+
ensureUserDataLayout,
|
|
7
|
+
getRuntimeAuthStateFile,
|
|
8
|
+
getUserDataAccountHash,
|
|
9
|
+
getUserDataAuthFile,
|
|
10
|
+
getUserDataHubStrategyLockfile,
|
|
11
|
+
getUserDataMemoryFile,
|
|
12
|
+
getUserDataPersonaFile,
|
|
13
|
+
getUserDataStrategiesDir,
|
|
14
|
+
} from './user-data.js';
|
|
15
|
+
|
|
16
|
+
describe('user-data layout migration', () => {
|
|
17
|
+
let ws: string;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
ws = mkdtempSync(join(tmpdir(), 'ccl-user-data-'));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
rmSync(ws, { recursive: true, force: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('migrates only portable user data from the legacy workspace layout', () => {
|
|
28
|
+
const apiKey = 'claw_secret_123';
|
|
29
|
+
writeFileSync(join(ws, '.auth.json'), JSON.stringify({
|
|
30
|
+
activeProfile: 'lobster-1',
|
|
31
|
+
profiles: {
|
|
32
|
+
'lobster-1': {
|
|
33
|
+
agentName: 'lobster-1',
|
|
34
|
+
apiKey,
|
|
35
|
+
serverUrl: 'https://example.com/claw',
|
|
36
|
+
gameServerUrl: 'http://game.example.com',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
neteaseTtsKey: 'tts-secret',
|
|
40
|
+
}, null, 2));
|
|
41
|
+
|
|
42
|
+
mkdirSync(join(ws, 'accounts', apiKey), { recursive: true });
|
|
43
|
+
writeFileSync(join(ws, 'accounts', apiKey, 'persona.md'), 'persona old', 'utf8');
|
|
44
|
+
writeFileSync(join(ws, 'accounts', apiKey, 'memory.md'), 'memory old', 'utf8');
|
|
45
|
+
|
|
46
|
+
mkdirSync(join(ws, 'strategies', '.official'), { recursive: true });
|
|
47
|
+
writeFileSync(join(ws, 'strategies', 'custom.ts'), 'export const strategy = {};', 'utf8');
|
|
48
|
+
writeFileSync(join(ws, 'strategies', '.official', 'manifest.json'), JSON.stringify({ 'task-only': { sourceHash: 'x' } }), 'utf8');
|
|
49
|
+
|
|
50
|
+
mkdirSync(join(ws, 'hub'), { recursive: true });
|
|
51
|
+
writeFileSync(join(ws, 'hub', 'installed.json'), JSON.stringify({
|
|
52
|
+
'strategy/abc': { type: 'strategy', id: 'abc', title: 'Strategy', path: 'strategies/custom.ts', installedAt: 'now' },
|
|
53
|
+
'skill/def': { type: 'skill', id: 'def', title: 'Skill', path: 'skills/helper', installedAt: 'now' },
|
|
54
|
+
}, null, 2), 'utf8');
|
|
55
|
+
|
|
56
|
+
ensureUserDataLayout(ws);
|
|
57
|
+
|
|
58
|
+
const auth = JSON.parse(readFileSync(getUserDataAuthFile(ws), 'utf8'));
|
|
59
|
+
expect(auth.activeProfile).toBe('lobster-1');
|
|
60
|
+
expect(auth.profiles['lobster-1'].apiKey).toBe(apiKey);
|
|
61
|
+
expect(auth.profiles['lobster-1'].gameServerUrl).toBeUndefined();
|
|
62
|
+
expect(auth.profiles['lobster-1'].tts.keys.leihuo).toBe('tts-secret');
|
|
63
|
+
|
|
64
|
+
const runtimeAuth = JSON.parse(readFileSync(getRuntimeAuthStateFile(ws), 'utf8'));
|
|
65
|
+
expect(runtimeAuth.profiles['lobster-1'].gameServerUrl).toBe('http://game.example.com');
|
|
66
|
+
|
|
67
|
+
expect(getUserDataAccountHash(apiKey)).toHaveLength(16);
|
|
68
|
+
expect(readFileSync(getUserDataPersonaFile(ws, { apiKey }), 'utf8')).toBe('persona old');
|
|
69
|
+
expect(readFileSync(getUserDataMemoryFile(ws, { apiKey }), 'utf8')).toBe('memory old');
|
|
70
|
+
|
|
71
|
+
expect(readFileSync(join(getUserDataStrategiesDir(ws), 'custom.ts'), 'utf8')).toContain('strategy');
|
|
72
|
+
expect(existsSync(join(getUserDataStrategiesDir(ws), '.official'))).toBe(false);
|
|
73
|
+
|
|
74
|
+
const strategyLock = JSON.parse(readFileSync(getUserDataHubStrategyLockfile(ws), 'utf8'));
|
|
75
|
+
expect(Object.keys(strategyLock)).toEqual(['strategy/abc']);
|
|
76
|
+
expect(strategyLock['skill/def']).toBeUndefined();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('does not overwrite existing user-data account files', () => {
|
|
80
|
+
const apiKey = 'claw_keep_new';
|
|
81
|
+
writeFileSync(join(ws, '.auth.json'), JSON.stringify({
|
|
82
|
+
activeProfile: 'lobster-1',
|
|
83
|
+
profiles: {
|
|
84
|
+
'lobster-1': { agentName: 'lobster-1', apiKey, serverUrl: 'https://example.com/claw' },
|
|
85
|
+
},
|
|
86
|
+
}), 'utf8');
|
|
87
|
+
mkdirSync(join(ws, 'accounts', apiKey), { recursive: true });
|
|
88
|
+
writeFileSync(join(ws, 'accounts', apiKey, 'persona.md'), 'old persona', 'utf8');
|
|
89
|
+
mkdirSync(join(ws, 'user-data', 'accounts', getUserDataAccountHash(apiKey)), { recursive: true });
|
|
90
|
+
writeFileSync(getUserDataPersonaFile(ws, { apiKey }), 'new persona', 'utf8');
|
|
91
|
+
|
|
92
|
+
ensureUserDataLayout(ws);
|
|
93
|
+
|
|
94
|
+
expect(readFileSync(getUserDataPersonaFile(ws, { apiKey }), 'utf8')).toBe('new persona');
|
|
95
|
+
});
|
|
96
|
+
});
|