wangchuan 5.3.0 → 5.6.0

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 (75) hide show
  1. package/.wangchuan/config.example.json +1 -1
  2. package/README.md +46 -10
  3. package/README.zh-CN.md +46 -10
  4. package/dist/bin/wangchuan.js +1 -1
  5. package/dist/src/agents/claude.d.ts.map +1 -1
  6. package/dist/src/agents/claude.js +5 -0
  7. package/dist/src/agents/claude.js.map +1 -1
  8. package/dist/src/agents/codebuddy.d.ts.map +1 -1
  9. package/dist/src/agents/codebuddy.js +2 -1
  10. package/dist/src/agents/codebuddy.js.map +1 -1
  11. package/dist/src/agents/codex.d.ts.map +1 -1
  12. package/dist/src/agents/codex.js +5 -0
  13. package/dist/src/agents/codex.js.map +1 -1
  14. package/dist/src/agents/cursor.d.ts.map +1 -1
  15. package/dist/src/agents/cursor.js +4 -1
  16. package/dist/src/agents/cursor.js.map +1 -1
  17. package/dist/src/agents/gemini.d.ts.map +1 -1
  18. package/dist/src/agents/gemini.js +4 -0
  19. package/dist/src/agents/gemini.js.map +1 -1
  20. package/dist/src/agents/workbuddy.d.ts.map +1 -1
  21. package/dist/src/agents/workbuddy.js +6 -1
  22. package/dist/src/agents/workbuddy.js.map +1 -1
  23. package/dist/src/commands/env.js +4 -0
  24. package/dist/src/commands/env.js.map +1 -1
  25. package/dist/src/commands/init.d.ts.map +1 -1
  26. package/dist/src/commands/init.js +67 -22
  27. package/dist/src/commands/init.js.map +1 -1
  28. package/dist/src/commands/snapshot.d.ts.map +1 -1
  29. package/dist/src/commands/snapshot.js +7 -1
  30. package/dist/src/commands/snapshot.js.map +1 -1
  31. package/dist/src/commands/watch.js +1 -1
  32. package/dist/src/commands/watch.js.map +1 -1
  33. package/dist/src/core/config.js +1 -1
  34. package/dist/src/core/config.js.map +1 -1
  35. package/dist/src/core/crypto.d.ts.map +1 -1
  36. package/dist/src/core/crypto.js +3 -2
  37. package/dist/src/core/crypto.js.map +1 -1
  38. package/dist/src/core/migrate.d.ts.map +1 -1
  39. package/dist/src/core/migrate.js +69 -8
  40. package/dist/src/core/migrate.js.map +1 -1
  41. package/dist/src/core/sync-restore.d.ts +19 -0
  42. package/dist/src/core/sync-restore.d.ts.map +1 -0
  43. package/dist/src/core/sync-restore.js +339 -0
  44. package/dist/src/core/sync-restore.js.map +1 -0
  45. package/dist/src/core/sync-shared.d.ts +30 -0
  46. package/dist/src/core/sync-shared.d.ts.map +1 -0
  47. package/dist/src/core/sync-shared.js +397 -0
  48. package/dist/src/core/sync-shared.js.map +1 -0
  49. package/dist/src/core/sync-stage.d.ts +57 -0
  50. package/dist/src/core/sync-stage.d.ts.map +1 -0
  51. package/dist/src/core/sync-stage.js +429 -0
  52. package/dist/src/core/sync-stage.js.map +1 -0
  53. package/dist/src/core/sync.d.ts +22 -46
  54. package/dist/src/core/sync.d.ts.map +1 -1
  55. package/dist/src/core/sync.js +64 -1267
  56. package/dist/src/core/sync.js.map +1 -1
  57. package/dist/src/i18n.d.ts.map +1 -1
  58. package/dist/src/i18n.js +31 -2
  59. package/dist/src/i18n.js.map +1 -1
  60. package/dist/test/crypto.test.js +2 -2
  61. package/dist/test/crypto.test.js.map +1 -1
  62. package/dist/test/git.test.d.ts +5 -0
  63. package/dist/test/git.test.d.ts.map +1 -0
  64. package/dist/test/git.test.js +90 -0
  65. package/dist/test/git.test.js.map +1 -0
  66. package/dist/test/migrate.test.d.ts +9 -0
  67. package/dist/test/migrate.test.d.ts.map +1 -0
  68. package/dist/test/migrate.test.js +133 -0
  69. package/dist/test/migrate.test.js.map +1 -0
  70. package/dist/test/sync-lock.test.d.ts +5 -0
  71. package/dist/test/sync-lock.test.d.ts.map +1 -0
  72. package/dist/test/sync-lock.test.js +94 -0
  73. package/dist/test/sync-lock.test.js.map +1 -0
  74. package/package.json +2 -2
  75. package/skill/SKILL.md +81 -9
@@ -0,0 +1,133 @@
1
+ /**
2
+ * migrate.test.ts — ensureMigrated unit tests
3
+ *
4
+ * Tests profile reconciliation behavior: verifies that user customizations
5
+ * (enabled state, workspacePath) are preserved while sync definitions are
6
+ * updated from latest agent definitions.
7
+ */
8
+ import { describe, it, after, before } from 'node:test';
9
+ import assert from 'node:assert/strict';
10
+ import fs from 'fs';
11
+ import os from 'os';
12
+ import path from 'path';
13
+ import { ensureMigrated } from '../src/core/migrate.js';
14
+ import { config, CONFIG_VERSION } from '../src/core/config.js';
15
+ import { buildDefaultProfiles, buildDefaultShared } from '../src/agents/index.js';
16
+ import { AGENT_NAMES } from '../src/types.js';
17
+ const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'wc-test-migrate-'));
18
+ // Create a synthetic ~/.wangchuan structure for config.save() calls inside ensureMigrated
19
+ const FAKE_HOME = path.join(TMP, 'home');
20
+ const WANGCHUAN_DIR = path.join(FAKE_HOME, '.wangchuan');
21
+ const REPO_DIR = path.join(WANGCHUAN_DIR, 'repo');
22
+ // Backup real paths and override
23
+ const REAL_SAVE = config.save.bind(config);
24
+ let savedCfg = null;
25
+ before(() => {
26
+ fs.mkdirSync(REPO_DIR, { recursive: true });
27
+ fs.mkdirSync(path.join(REPO_DIR, 'agents'), { recursive: true });
28
+ // Monkey-patch config.save to capture output without writing to real ~/.wangchuan
29
+ config.save = (cfg) => {
30
+ savedCfg = cfg;
31
+ };
32
+ });
33
+ after(() => {
34
+ // Restore real save
35
+ config.save = REAL_SAVE;
36
+ fs.rmSync(TMP, { recursive: true, force: true });
37
+ });
38
+ /** Build a minimal v2 config for testing */
39
+ function buildTestConfig(profileOverrides) {
40
+ const defaults = buildDefaultProfiles();
41
+ const profiles = {};
42
+ for (const name of AGENT_NAMES) {
43
+ const def = defaults[name];
44
+ const override = profileOverrides?.[name];
45
+ profiles[name] = { ...def, ...override };
46
+ }
47
+ return {
48
+ repo: 'git@github.com:test/test.git',
49
+ branch: 'main',
50
+ localRepoPath: REPO_DIR,
51
+ keyPath: path.join(WANGCHUAN_DIR, 'master.key'),
52
+ hostname: 'test-host',
53
+ version: CONFIG_VERSION,
54
+ profiles: { default: profiles },
55
+ shared: buildDefaultShared(),
56
+ };
57
+ }
58
+ describe('ensureMigrated — v2 config reconciliation', () => {
59
+ it('already-current v2 config passes through without error', () => {
60
+ const cfg = buildTestConfig();
61
+ const result = ensureMigrated(cfg);
62
+ assert.equal(result.version, CONFIG_VERSION);
63
+ });
64
+ it('preserves user enabled=true when reconciling', () => {
65
+ const cfg = buildTestConfig({ claude: { enabled: true } });
66
+ const result = ensureMigrated(cfg);
67
+ const claude = result.profiles.default.claude;
68
+ assert.equal(claude.enabled, true);
69
+ });
70
+ it('preserves user enabled=false when reconciling', () => {
71
+ const cfg = buildTestConfig({ openclaw: { enabled: false } });
72
+ const result = ensureMigrated(cfg);
73
+ const oc = result.profiles.default.openclaw;
74
+ assert.equal(oc.enabled, false);
75
+ });
76
+ it('preserves user workspacePath customization', () => {
77
+ const customPath = '/custom/workspace/path';
78
+ const cfg = buildTestConfig({ gemini: { workspacePath: customPath } });
79
+ const result = ensureMigrated(cfg);
80
+ assert.equal(result.profiles.default.gemini.workspacePath, customPath);
81
+ });
82
+ it('updates syncFiles from latest definitions', () => {
83
+ const defaults = buildDefaultProfiles();
84
+ const latestClaudeFiles = defaults.claude.syncFiles;
85
+ // Start with an outdated syncFiles (empty array)
86
+ const cfg = buildTestConfig({ claude: { syncFiles: [] } });
87
+ const result = ensureMigrated(cfg);
88
+ assert.deepEqual(result.profiles.default.claude.syncFiles, latestClaudeFiles);
89
+ });
90
+ it('result contains all agent names', () => {
91
+ const cfg = buildTestConfig();
92
+ const result = ensureMigrated(cfg);
93
+ for (const name of AGENT_NAMES) {
94
+ assert.ok(result.profiles.default[name] !== undefined, `agent ${name} should be present in reconciled profiles`);
95
+ }
96
+ });
97
+ it('result has shared config', () => {
98
+ const cfg = buildTestConfig();
99
+ const result = ensureMigrated(cfg);
100
+ assert.ok(result.shared !== undefined);
101
+ assert.ok(result.shared.skills !== undefined);
102
+ assert.ok(result.shared.mcp !== undefined);
103
+ });
104
+ });
105
+ describe('ensureMigrated — new agent detection', () => {
106
+ it('adds new agents from definitions with enabled=false', () => {
107
+ // Build a config missing the last agent to simulate an upgrade
108
+ const defaults = buildDefaultProfiles();
109
+ const profiles = {};
110
+ const allNames = [...AGENT_NAMES];
111
+ const removedAgent = allNames[allNames.length - 1];
112
+ for (const name of allNames) {
113
+ if (name === removedAgent)
114
+ continue;
115
+ profiles[name] = defaults[name];
116
+ }
117
+ const cfg = {
118
+ repo: 'git@github.com:test/test.git',
119
+ branch: 'main',
120
+ localRepoPath: REPO_DIR,
121
+ keyPath: path.join(WANGCHUAN_DIR, 'master.key'),
122
+ hostname: 'test-host',
123
+ version: CONFIG_VERSION,
124
+ profiles: { default: profiles },
125
+ shared: buildDefaultShared(),
126
+ };
127
+ const result = ensureMigrated(cfg);
128
+ const added = result.profiles.default[removedAgent];
129
+ assert.ok(added !== undefined, `${removedAgent} should be added`);
130
+ assert.equal(added.enabled, false, 'newly added agent should be disabled');
131
+ });
132
+ });
133
+ //# sourceMappingURL=migrate.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrate.test.js","sourceRoot":"","sources":["../../test/migrate.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * sync-lock.test.ts — syncLock unit tests
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=sync-lock.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-lock.test.d.ts","sourceRoot":"","sources":["../../test/sync-lock.test.ts"],"names":[],"mappings":"AAAA;;GAEG"}
@@ -0,0 +1,94 @@
1
+ /**
2
+ * sync-lock.test.ts — syncLock unit tests
3
+ */
4
+ import { describe, it, after } from 'node:test';
5
+ import assert from 'node:assert/strict';
6
+ import fs from 'fs';
7
+ import os from 'os';
8
+ import path from 'path';
9
+ import { execSync } from 'child_process';
10
+ import { syncLock } from '../src/core/sync-lock.js';
11
+ const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'wc-test-lock-'));
12
+ // Create a bare repo + work clone so acquire() has a valid repoPath for cleanDirtyState
13
+ const BARE = path.join(TMP, 'bare.git');
14
+ const REPO = path.join(TMP, 'repo');
15
+ execSync(`git init --bare "${BARE}"`);
16
+ // Use a seed clone to create initial commit, then re-clone for REPO
17
+ const SEED = path.join(TMP, 'seed');
18
+ execSync(`git clone "${BARE}" "${SEED}"`);
19
+ execSync('git config user.name "Test"', { cwd: SEED });
20
+ execSync('git config user.email "test@test.com"', { cwd: SEED });
21
+ fs.writeFileSync(path.join(SEED, 'init.txt'), 'seed');
22
+ execSync('git add . && git commit -m "seed"', { cwd: SEED });
23
+ // Push to whatever default branch was created
24
+ const branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: SEED, encoding: 'utf-8' }).trim();
25
+ execSync(`git push origin ${branch}`, { cwd: SEED });
26
+ // Clone fresh for test use
27
+ execSync(`git clone "${BARE}" "${REPO}"`);
28
+ after(() => {
29
+ // Always clean up lock to avoid polluting the real ~/.wangchuan/
30
+ syncLock.release();
31
+ fs.rmSync(TMP, { recursive: true, force: true });
32
+ });
33
+ describe('syncLock.acquire + release', () => {
34
+ it('basic cycle: lock file created then deleted', async () => {
35
+ await syncLock.acquire(REPO);
36
+ assert.ok(fs.existsSync(syncLock.lockPath), 'lock file should exist after acquire');
37
+ syncLock.release();
38
+ assert.ok(!fs.existsSync(syncLock.lockPath), 'lock file should be gone after release');
39
+ });
40
+ });
41
+ describe('syncLock.exists', () => {
42
+ it('returns true when lock present', async () => {
43
+ await syncLock.acquire(REPO);
44
+ assert.equal(syncLock.exists(), true);
45
+ syncLock.release();
46
+ });
47
+ it('returns false when absent', () => {
48
+ assert.equal(syncLock.exists(), false);
49
+ });
50
+ });
51
+ describe('syncLock.read', () => {
52
+ it('returns parsed lock with pid and startedAt', async () => {
53
+ await syncLock.acquire(REPO);
54
+ const lock = syncLock.read();
55
+ assert.ok(lock !== null);
56
+ assert.equal(lock.pid, process.pid);
57
+ assert.ok(typeof lock.startedAt === 'string');
58
+ syncLock.release();
59
+ });
60
+ it('returns null for corrupt lock file', () => {
61
+ // Write garbage to the lock path
62
+ fs.mkdirSync(path.dirname(syncLock.lockPath), { recursive: true });
63
+ fs.writeFileSync(syncLock.lockPath, '{{{not json', 'utf-8');
64
+ const lock = syncLock.read();
65
+ assert.equal(lock, null);
66
+ fs.unlinkSync(syncLock.lockPath);
67
+ });
68
+ });
69
+ describe('syncLock.acquire stale detection', () => {
70
+ it('cleans stale lock from dead PID', async () => {
71
+ // Write a lock with a PID that almost certainly does not exist
72
+ fs.mkdirSync(path.dirname(syncLock.lockPath), { recursive: true });
73
+ fs.writeFileSync(syncLock.lockPath, JSON.stringify({
74
+ startedAt: new Date().toISOString(),
75
+ pid: 999999,
76
+ }), 'utf-8');
77
+ // acquire should detect dead PID and succeed
78
+ await syncLock.acquire(REPO);
79
+ assert.ok(syncLock.exists());
80
+ const lock = syncLock.read();
81
+ assert.equal(lock.pid, process.pid);
82
+ syncLock.release();
83
+ });
84
+ it('throws when live PID holds lock', async () => {
85
+ await syncLock.acquire(REPO);
86
+ // Attempting a second acquire with live PID should throw
87
+ await assert.rejects(() => syncLock.acquire(REPO), (err) => {
88
+ assert.ok(err.message.includes(String(process.pid)));
89
+ return true;
90
+ });
91
+ syncLock.release();
92
+ });
93
+ });
94
+ //# sourceMappingURL=sync-lock.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-lock.test.js","sourceRoot":"","sources":["../../test/sync-lock.test.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wangchuan",
3
- "version": "5.3.0",
3
+ "version": "5.6.0",
4
4
  "description": "忘川 · AI 记忆同步系统 — 智能体记忆永不遗失",
5
5
  "bin": {
6
6
  "wangchuan": "./dist/bin/wangchuan.js"
@@ -17,7 +17,7 @@
17
17
  "postbuild": "chmod +x dist/bin/wangchuan.js",
18
18
  "prepublishOnly": "npm run build",
19
19
  "dev": "tsx bin/wangchuan.ts",
20
- "test": "node --import tsx/esm --test test/crypto.test.ts test/json-field.test.ts test/sync.test.ts test/sync-history.test.ts test/merge.test.ts",
20
+ "test": "node --import tsx/esm --test test/crypto.test.ts test/json-field.test.ts test/sync.test.ts test/sync-history.test.ts test/merge.test.ts test/git.test.ts test/sync-lock.test.ts test/migrate.test.ts",
21
21
  "typecheck": "tsc --noEmit"
22
22
  },
23
23
  "keywords": [
package/skill/SKILL.md CHANGED
@@ -113,12 +113,12 @@ Supported by: `sync`, `status`, `watch`, `memory`.
113
113
  | Value | Description |
114
114
  |-------|-------------|
115
115
  | `openclaw` | MEMORY.md (enc), AGENTS.md, SOUL.md, TOOLS.md, IDENTITY.md, USER.md (enc), HEARTBEAT.md, BOOTSTRAP.md, memory/ (enc), openclaw.json → agents/skills/ui (enc), skills/ |
116
- | `claude` | CLAUDE.md, settings.json (enc), .claude.json → mcpServers (enc) |
117
- | `gemini` | settings.internal.json → security + model + general (enc) |
118
- | `codebuddy` | MEMORY.md (enc), CODEBUDDY.md, mcp.json → mcpServers (enc), settings.json → enabledPlugins (enc) |
119
- | `workbuddy` | MEMORY.md (enc), IDENTITY.md, SOUL.md, USER.md (enc), BOOTSTRAP.md, mcp.json → mcpServers (enc), settings.json → enabledPlugins (enc) |
120
- | `cursor` | rules/ dir, mcp.json → mcpServers (enc), cli-config.json → permissions + model + enabledPlugins (enc) |
121
- | `codex` | AGENTS.md, instructions.md |
116
+ | `claude` | CLAUDE.md, settings.json (enc), .claude.json → mcpServers (enc), commands/ (dir), plugins/ (installed + marketplaces) |
117
+ | `gemini` | settings.internal.json → security + model + general (enc), skills/ (dir) |
118
+ | `codebuddy` | MEMORY.md (enc), CODEBUDDY.md, mcp.json → mcpServers (enc), settings.json → enabledPlugins + hooks (enc), plugins/ (marketplaces) |
119
+ | `workbuddy` | MEMORY.md (enc), IDENTITY.md, SOUL.md, USER.md (enc), BOOTSTRAP.md, mcp.json → mcpServers (enc), settings.json → enabledPlugins + hooks (enc), skills/ (dir), extensions/ |
120
+ | `cursor` | rules/ dir, mcp.json → mcpServers (enc), cli-config.json → permissions + model + enabledPlugins (enc), extensions/, hooks.json |
121
+ | `codex` | MEMORY.md (enc), instructions.md, config.toml (enc), skills/ (dir), memories/ (enc) |
122
122
 
123
123
  When omitted, operates on all enabled agents plus the shared tier (skills/MCP/custom agents/shared memory).
124
124
 
@@ -162,6 +162,78 @@ The watch daemon auto-detects file changes and triggers sync, serving as a safet
162
162
  ## Prerequisites
163
163
 
164
164
  1. Node.js ≥ 18
165
- 2. `wangchuan init` has been run (~/.wangchuan/config.json exists)
166
- 3. Local SSH key has access to the target git repo
167
- 4. Copy `~/.wangchuan/master.key` manually when migrating across machines (or use `wangchuan doctor --key-export`)
165
+ 2. Git installed and configured (SSH key or HTTPS credentials)
166
+ 3. A **private** Git repo on any hosting platform (GitHub, GitLab, Gitee, Bitbucket, Gitea, or self-hosted)
167
+
168
+ ## Installation
169
+
170
+ ### Install wangchuan CLI
171
+
172
+ ```bash
173
+ npm install -g wangchuan
174
+ ```
175
+
176
+ ### First-time setup
177
+
178
+ ```bash
179
+ # Interactive — auto-detects installed agents, creates repo via GitHub CLI if available
180
+ wangchuan init
181
+
182
+ # Or specify a repo URL directly (any Git hosting):
183
+ wangchuan init --repo git@github.com:you/brain.git
184
+ wangchuan init --repo git@gitlab.com:you/brain.git
185
+ wangchuan init --repo git@gitee.com:you/brain.git
186
+ ```
187
+
188
+ ### Install this skill to an agent
189
+
190
+ Copy the `wangchuan/` skill folder to your agent's skills directory:
191
+
192
+ ```bash
193
+ # Claude
194
+ cp -r wangchuan/ ~/.claude/skills/wangchuan/
195
+
196
+ # OpenClaw
197
+ cp -r wangchuan/ ~/.openclaw/workspace/skills/wangchuan/
198
+
199
+ # Codex
200
+ cp -r wangchuan/ ~/.codex/skills/wangchuan/
201
+
202
+ # Or let wangchuan sync distribute it to all agents automatically:
203
+ wangchuan sync
204
+ ```
205
+
206
+ ### Setting up Git repo (if you don't have one)
207
+
208
+ Create a **private** repo on your preferred platform:
209
+
210
+ | Platform | How to create |
211
+ |----------|--------------|
212
+ | **GitHub** | `wangchuan init` auto-creates via `gh` CLI, or: github.com → New repository → Private |
213
+ | **GitLab** | gitlab.com → New project → Private |
214
+ | **Gitee** | gitee.com → New repo → Private |
215
+ | **Bitbucket** | bitbucket.org → Create repository → Private |
216
+ | **Gitea** | Your instance → New Repository → Private |
217
+
218
+ Then: `wangchuan init --repo <ssh-url>`
219
+
220
+ ### New machine setup
221
+
222
+ ```bash
223
+ npm install -g wangchuan
224
+ wangchuan init --repo <your-repo-url> --key <master-key-hex>
225
+ ```
226
+
227
+ Get the master key from your original machine: `wangchuan doctor --key-export`
228
+
229
+ ### Migrating the master key
230
+
231
+ ⚠️ **`master.key` is the ONLY thing that cannot be recovered.** Back it up securely.
232
+
233
+ ```bash
234
+ # On the source machine:
235
+ wangchuan doctor --key-export # prints wangchuan_<hex>
236
+
237
+ # On the target machine:
238
+ wangchuan init --repo <url> --key wangchuan_<hex>
239
+ ```