Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

All notable changes to the XMemo CLI client will be documented in this file.

## [Unreleased]
## [0.4.193] - 2026-10-28

### Added
- **Muse Code (Meta) MCP Client Support**:
Expand All @@ -11,9 +11,13 @@ All notable changes to the XMemo CLI client will be documented in this file.
- Automatically initializes `schema_version: 1` when creating a new settings file while preserving existing settings and schema versions.
- Seamlessly migrates legacy `mcp_servers` to `mcpServers` while failing closed if both coexist to prevent Muse loader member drop.
- Detects pre-existing lowercase `xmemo` or legacy `memory-os` server entries to prevent duplicate definitions.
- Uses atomic write via temporary file and rename to prevent file corruption.
- Scopes atomic write via temporary file and rename to `muse-code` while preserving symlinks via `fs.realpath` across all JSON clients.
- Config path defaults to `$XDG_CONFIG_HOME/muse/settings.json` or `~/.config/muse/settings.json`.
- Next steps guidance printed on `xmemo mcp add muse-code --write` and `xmemo setup muse`.

### Changed
- **Skill Version Pin Synchronization**:
- Updated pinned XMemo Skill version (`PINNED_SKILL_VERSION` and `PINNED_OPENCLAW_SKILL_VERSION`) in `src/core/pins.js` from `1.1.45` to `1.1.48` with dual-verified SHA-512 SRI integrity (`sha512-GlAxEoo1eOq0GBcVem+hbqEKrBegpRuIhv9asblgFvrAGz79cZM7FcOXLjz2R9DaK9lexL89yEIYTqnuaeKiig==`).

## [0.4.192] - 2026-10-19

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ The CLI installs the verified XMemo Skill locally into agent skill folders or a
- **Codex**: `~/.codex/skills/xmemo-memory`
- **OpenClaw**: `~/.openclaw/skills/xmemo-memory`
- All other 21 clients remain `null` until officially documented.
- Defaults to the pinned `@xmemo/skill@1.1.45` release from npm and verifies tarball integrity (sha512 SRI) before extraction.
- Defaults to the pinned `@xmemo/skill@1.1.48` release from npm and verifies tarball integrity (sha512 SRI) before extraction.
- Override version with `--version <semver>` or explicitly opt into the latest release via `--version latest`.
- For air-gapped or offline installations, install from a local directory or packed tarball with `--from <dir|tgz>` (optional `--integrity <sha512>`).
- Manage client skills with `skill status`, `skill update`, and `skill remove`.
Expand Down Expand Up @@ -359,7 +359,7 @@ curl -fsSL https://xmemo.dev/skill/install | XMEMO_SKILL_AGENT=claude-code sh
curl -fsSL https://xmemo.dev/skill/install | XMEMO_SKILL_AGENT=codex sh

# OpenClaw: install via OpenClaw CLI
openclaw skills install @xmemo/xmemo --version 1.1.45
openclaw skills install @xmemo/xmemo --version 1.1.48

# Windows (PowerShell):
# $env:XMEMO_SKILL_AGENT="claude-code"; irm https://xmemo.dev/skill/install.ps1 | iex
Expand Down
4 changes: 2 additions & 2 deletions README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ CLI 支持将经过官方完整性校验的 XMemo Skill 安装至各智能体专
- **Codex**: `~/.codex/skills/xmemo-memory`
- **OpenClaw**: `~/.openclaw/skills/xmemo-memory`
- 其余 21 款客户端在官方文档明确前均设为 `null`
- 默认安装 npm 上锁定的 `@xmemo/skill@1.1.45` 版本,并在解压前严格校验 tarball 完整性哈希(sha512 SRI)。
- 默认安装 npm 上锁定的 `@xmemo/skill@1.1.48` 版本,并在解压前严格校验 tarball 完整性哈希(sha512 SRI)。
- 可通过 `--version <semver>` 指定特定版本,或显式传入 `--version latest` 获取最新版本。
- 在离线或内网隔离环境中,可通过 `--from <dir|tgz>` 从本地目录或打好的 tarball 进行离线安装(支持 `--integrity <sha512>`)。
- 支持通过 `skill status` 查看安装状态,通过 `skill update` 快捷更新,通过 `skill remove` 干净移除。
Expand Down Expand Up @@ -344,7 +344,7 @@ curl -fsSL https://xmemo.dev/skill/install | XMEMO_SKILL_AGENT=claude-code sh
curl -fsSL https://xmemo.dev/skill/install | XMEMO_SKILL_AGENT=codex sh

# OpenClaw: 建议通过 OpenClaw 官方 CLI 安装
openclaw skills install @xmemo/xmemo --version 1.1.45
openclaw skills install @xmemo/xmemo --version 1.1.48

# Windows (PowerShell):
# $env:XMEMO_SKILL_AGENT="claude-code"; irm https://xmemo.dev/skill/install.ps1 | iex
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@xmemo/client",
"version": "0.4.192",
"version": "0.4.193",
"description": "Official XMemo client: CLI, MCP setup and stdio server for XMemo, the user-owned Memory OS for AI agents.",
"mcpName": "io.github.yonro/xmemo",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion server.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
{
"registryType": "npm",
"identifier": "@xmemo/client",
"version": "0.4.192",
"version": "0.4.193",
"runtimeHint": "npx",
"transport": {
"type": "stdio"
Expand Down
6 changes: 3 additions & 3 deletions src/core/pins.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,14 @@ export const PINNED_OPENCLAW_PLUGIN_VERSION = '1.0.18';
export const PINNED_OPENCLAW_PLUGIN_SPEC = `clawhub:@xmemo/openclaw-memory@${PINNED_OPENCLAW_PLUGIN_VERSION}`;
export const PINNED_OPENCLAW_PLUGIN_NAME = '@xmemo/openclaw-memory';

export const PINNED_OPENCLAW_SKILL_VERSION = '1.1.45';
export const PINNED_OPENCLAW_SKILL_VERSION = '1.1.48';
export const PINNED_OPENCLAW_SKILL_NAME = '@xmemo/xmemo';
export const PINNED_OPENCLAW_SKILL_SPEC = `${PINNED_OPENCLAW_SKILL_NAME}@${PINNED_OPENCLAW_SKILL_VERSION}`;

export const PINNED_HERMES_PLUGIN_VERSION = '1.1.3';
export const PINNED_HERMES_CATALOG_ID = 'xmemo';
export const HERMES_CATALOG_ID = PINNED_HERMES_CATALOG_ID;

export const PINNED_SKILL_VERSION = '1.1.45';
export const PINNED_SKILL_INTEGRITY = 'sha512-UGUKUlorT+YQs6/hb6sHHTrQqkm9n1aKtxgOUpKrhRONeDFHTceijpRHRvptKdpODOlA0fvwqDCoo3vw8l1Zng==';
export const PINNED_SKILL_VERSION = '1.1.48';
export const PINNED_SKILL_INTEGRITY = 'sha512-GlAxEoo1eOq0GBcVem+hbqEKrBegpRuIhv9asblgFvrAGz79cZM7FcOXLjz2R9DaK9lexL89yEIYTqnuaeKiig==';

14 changes: 7 additions & 7 deletions test/cli-skill-install.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,13 @@ function spawnStub(calls, { code = 0, stdout = '', stderr = '', error = null, ta
let callCode = code;
if (code === 0) {
if (args.includes('view')) {
callStdout = JSON.stringify('sha512-UGUKUlorT+YQs6/hb6sHHTrQqkm9n1aKtxgOUpKrhRONeDFHTceijpRHRvptKdpODOlA0fvwqDCoo3vw8l1Zng==');
callStdout = JSON.stringify('sha512-GlAxEoo1eOq0GBcVem+hbqEKrBegpRuIhv9asblgFvrAGz79cZM7FcOXLjz2R9DaK9lexL89yEIYTqnuaeKiig==');
} else if (args.includes('pack')) {
const destIdx = args.indexOf('--pack-destination');
if (destIdx !== -1 && args[destIdx + 1]) {
const destDir = args[destIdx + 1];
const fixturePath = path.resolve(__dirname, 'fixtures', 'xmemo-skill-1.1.45.fixture');
const tgzPath = path.join(destDir, 'xmemo-skill-1.1.45.tgz');
const fixturePath = path.resolve(__dirname, 'fixtures', 'xmemo-skill-1.1.48.fixture');
const tgzPath = path.join(destDir, 'xmemo-skill-1.1.48.tgz');
try {
if (tarballContent !== null) {
fsSync.writeFileSync(tgzPath, tarballContent);
Expand All @@ -71,7 +71,7 @@ function spawnStub(calls, { code = 0, stdout = '', stderr = '', error = null, ta
}
} catch {}
}
callStdout = JSON.stringify([{ filename: 'xmemo-skill-1.1.45.tgz' }]);
callStdout = JSON.stringify([{ filename: 'xmemo-skill-1.1.48.tgz' }]);
}
}
if (callStdout) {
Expand Down Expand Up @@ -113,7 +113,7 @@ test('CLI skill install: default delegates to @xmemo/skill@pinned without shell'
const packDest1 = calls[0].args[calls[0].args.indexOf('--pack-destination') + 1];
const expectedPackArgs1 = [
'pack',
'@xmemo/skill@1.1.45',
'@xmemo/skill@1.1.48',
'--pack-destination',
packDest1,
'--json'
Expand Down Expand Up @@ -158,7 +158,7 @@ test('CLI skill install: default delegates to @xmemo/skill@pinned without shell'

const report = JSON.parse(result.stdout);
assert.equal(report.source, 'npm');
assert.equal(report.spec, '1.1.45');
assert.equal(report.spec, '1.1.48');
assert.equal(report.networkUsed, true);
assert.equal(report.tokenSent, false);
assert.equal(report.package, '@xmemo/skill');
Expand Down Expand Up @@ -313,7 +313,7 @@ test('CLI skill install: non-JSON human readable output', async () => {

assert.equal(result.code, 0);
assert.match(result.stdout, /Installed XMemo Skill 1\.1\.25 to/);
assert.match(result.stdout, /Source: npm \(@xmemo\/skill@1\.1\.45\) \(no credential used\)/);
assert.match(result.stdout, /Source: npm \(@xmemo\/skill@1\.1\.48\) \(no credential used\)/);
});

test('CLI skill install: end-to-end with --from <dir> installs byte-identical skill', async () => {
Expand Down
6 changes: 3 additions & 3 deletions test/cli-status-client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ test('status codex with skill installed reports skill installed and version', as
await fs.mkdir(codexSkillDir, { recursive: true });
await fs.writeFile(
path.join(codexSkillDir, 'package.json'),
JSON.stringify({ name: '@xmemo/skill', version: '1.1.45' }, null, 2),
JSON.stringify({ name: '@xmemo/skill', version: '1.1.48' }, null, 2),
'utf8'
);

Expand All @@ -278,7 +278,7 @@ test('status codex with skill installed reports skill installed and version', as
const report = JSON.parse(jsonResult.stdout);
assert.equal(report.clients[0].id, 'codex');
assert.equal(report.clients[0].resources.skill.installed, true);
assert.equal(report.clients[0].resources.skill.version, '1.1.45');
assert.equal(report.clients[0].resources.skill.version, '1.1.48');
assert.ok(report.clients[0].resources.skill.path.includes('xmemo-memory'));

// Status codex human-readable
Expand All @@ -288,7 +288,7 @@ test('status codex with skill installed reports skill installed and version', as
});
assert.equal(humanResult.code, 0);
assert.match(humanResult.stdout, /Codex \(codex\):/);
assert.match(humanResult.stdout, /Skill: installed 1\.1\.45/);
assert.match(humanResult.stdout, /Skill: installed 1\.1\.48/);
});

test('status with no client arguments and nothing detected reports service probes only', async (t) => {
Expand Down
12 changes: 6 additions & 6 deletions test/cli.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ test('skill install delegates to @xmemo/skill npm package', async () => {
const packDest = calls[0].args[calls[0].args.indexOf('--pack-destination') + 1];
const expectedPackArgs = [
'pack',
'@xmemo/skill@1.1.45',
'@xmemo/skill@1.1.48',
'--pack-destination',
packDest,
'--json'
Expand Down Expand Up @@ -97,7 +97,7 @@ test('skill install delegates to @xmemo/skill npm package', async () => {
assert.equal(report.installed, false);
assert.equal(report.networkUsed, true);
assert.equal(report.source, 'npm');
assert.equal(report.spec, '1.1.45');
assert.equal(report.spec, '1.1.48');
assert.equal(report.tokenSent, false);
} finally {
await fs.rm(tempDir, { recursive: true, force: true });
Expand Down Expand Up @@ -1900,7 +1900,7 @@ test('setup openclaw installs native plugin and skill without hosted MCP by defa
assert.equal(plan.selectedClient.status.connected, true);
assert.deepEqual(calls.map((call) => call.args), [
['plugins', 'install', 'clawhub:@xmemo/openclaw-memory@1.0.18'],
['skills', 'install', '@xmemo/xmemo', '--version', '1.1.45'],
['skills', 'install', '@xmemo/xmemo', '--version', '1.1.48'],
['xmemo', 'status', '--json']
]);
});
Expand Down Expand Up @@ -2758,14 +2758,14 @@ function spawnStub(calls, { code = 0, stdout = '', stderr = '' } = {}) {
const destIdx = args.indexOf('--pack-destination');
if (destIdx !== -1 && args[destIdx + 1]) {
const destDir = args[destIdx + 1];
const fixturePath = path.resolve(__dirname, 'fixtures', 'xmemo-skill-1.1.45.fixture');
const tgzPath = path.join(destDir, 'xmemo-skill-1.1.45.tgz');
const fixturePath = path.resolve(__dirname, 'fixtures', 'xmemo-skill-1.1.48.fixture');
const tgzPath = path.join(destDir, 'xmemo-skill-1.1.48.tgz');
try {
const bytes = readFileSync(fixturePath);
writeFileSync(tgzPath, bytes);
} catch {}
}
callStdout = JSON.stringify([{ filename: 'xmemo-skill-1.1.45.tgz' }]);
callStdout = JSON.stringify([{ filename: 'xmemo-skill-1.1.48.tgz' }]);
}
if (callStdout) {
child.stdout.emit('data', callStdout);
Expand Down
Binary file removed test/fixtures/xmemo-skill-1.1.45.fixture
Binary file not shown.
Binary file added test/fixtures/xmemo-skill-1.1.48.fixture
Binary file not shown.
8 changes: 4 additions & 4 deletions test/pinned-installs.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,13 @@ function spawnStub(calls, { code = 0, stdout = '', stderr = '', error = null, ta
let callCode = code;
if (code === 0) {
if (args.includes('view')) {
callStdout = JSON.stringify('sha512-UGUKUlorT+YQs6/hb6sHHTrQqkm9n1aKtxgOUpKrhRONeDFHTceijpRHRvptKdpODOlA0fvwqDCoo3vw8l1Zng==');
callStdout = JSON.stringify('sha512-GlAxEoo1eOq0GBcVem+hbqEKrBegpRuIhv9asblgFvrAGz79cZM7FcOXLjz2R9DaK9lexL89yEIYTqnuaeKiig==');
} else if (args.includes('pack')) {
const destIdx = args.indexOf('--pack-destination');
if (destIdx !== -1 && args[destIdx + 1]) {
const destDir = args[destIdx + 1];
const fixturePath = path.resolve(__dirname, 'fixtures', 'xmemo-skill-1.1.45.fixture');
const tgzPath = path.join(destDir, 'xmemo-skill-1.1.45.tgz');
const fixturePath = path.resolve(__dirname, 'fixtures', 'xmemo-skill-1.1.48.fixture');
const tgzPath = path.join(destDir, 'xmemo-skill-1.1.48.tgz');
try {
if (tarballContent !== null) {
fsSync.writeFileSync(tgzPath, tarballContent);
Expand All @@ -113,7 +113,7 @@ function spawnStub(calls, { code = 0, stdout = '', stderr = '', error = null, ta
}
} catch {}
}
callStdout = JSON.stringify([{ filename: 'xmemo-skill-1.1.45.tgz' }]);
callStdout = JSON.stringify([{ filename: 'xmemo-skill-1.1.48.tgz' }]);
}
}
if (callStdout) {
Expand Down
44 changes: 22 additions & 22 deletions test/release-check.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ const packageJson = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json
const CLI_VERSION = packageJson.version;

test('check-release-version: parseSemver parses valid semver and rejects invalid strings', () => {
const v1 = parseSemver('1.1.45');
const v1 = parseSemver('1.1.48');
assert.equal(v1.major, 1);
assert.equal(v1.minor, 1);
assert.equal(v1.patch, 45);
assert.equal(v1.patch, 48);
assert.equal(v1.prerelease, null);

const v2 = parseSemver('v2.0.1-beta.2');
Expand All @@ -37,45 +37,45 @@ test('check-release-version: parseSemver parses valid semver and rejects invalid
});

test('check-release-version: compareSemver correctly orders semver numerically', () => {
assert.ok(compareSemver('1.1.45', '1.1.35') > 0);
assert.ok(compareSemver('1.1.35', '1.1.45') < 0);
assert.equal(compareSemver('1.1.45', '1.1.45'), 0);
assert.ok(compareSemver('1.1.45', '1.1.9') > 0, '1.1.45 must be greater than 1.1.9');
assert.ok(compareSemver('1.1.48', '1.1.35') > 0);
assert.ok(compareSemver('1.1.35', '1.1.48') < 0);
assert.equal(compareSemver('1.1.48', '1.1.48'), 0);
assert.ok(compareSemver('1.1.48', '1.1.9') > 0, '1.1.48 must be greater than 1.1.9');
assert.ok(compareSemver('2.0.0', '1.9.99') > 0);
});

test('check-release-version: getLatestSkillTag finds the highest semver skill tag', () => {
const sampleTags = [
'skill-v1.1.9',
'skill-v1.1.35',
'skill-v1.1.45',
'skill-v1.1.48',
'skill-v1.1.10',
'cli-v0.4.191',
'random-tag'
];
const latest = getLatestSkillTag(sampleTags);
assert.ok(latest);
assert.equal(latest.tag, 'skill-v1.1.45');
assert.equal(latest.version, '1.1.45');
assert.equal(latest.tag, 'skill-v1.1.48');
assert.equal(latest.version, '1.1.48');
});

test('check-release-version: verifySkillPinGate passes when pinned version is current or newer', () => {
const tags = ['skill-v1.1.35', 'skill-v1.1.45'];
const tags = ['skill-v1.1.35', 'skill-v1.1.48'];

const passEqual = verifySkillPinGate({ pinnedVersion: '1.1.45', tagList: tags });
const passEqual = verifySkillPinGate({ pinnedVersion: '1.1.48', tagList: tags });
assert.equal(passEqual.ok, true);
assert.equal(passEqual.latestVersion, '1.1.45');
assert.equal(passEqual.latestVersion, '1.1.48');

const passNewer = verifySkillPinGate({ pinnedVersion: '1.1.46', tagList: tags });
const passNewer = verifySkillPinGate({ pinnedVersion: '1.1.49', tagList: tags });
assert.equal(passNewer.ok, true);
});

test('check-release-version: verifySkillPinGate fails closed when pinned version is older than latest skill tag', () => {
const tags = ['skill-v1.1.35', 'skill-v1.1.45'];
const tags = ['skill-v1.1.35', 'skill-v1.1.48'];

const failOlder = verifySkillPinGate({ pinnedVersion: '1.1.35', tagList: tags });
assert.equal(failOlder.ok, false);
assert.equal(failOlder.latestVersion, '1.1.45');
assert.equal(failOlder.latestVersion, '1.1.48');
assert.match(failOlder.error, /older than the latest skill tag/);
});

Expand Down Expand Up @@ -105,26 +105,26 @@ test('check-release-version CLI gate: fails closed with exit code 1 when pinned
});

test('check-release-version: compareSemver throws when either version is invalid', () => {
assert.throws(() => compareSemver('invalid', '1.1.45'), /Cannot compare invalid semver/);
assert.throws(() => compareSemver('1.1.45', 'bad'), /Cannot compare invalid semver/);
assert.throws(() => compareSemver('', '1.1.45'), /Cannot compare invalid semver/);
assert.throws(() => compareSemver(null, '1.1.45'), /Cannot compare invalid semver/);
assert.throws(() => compareSemver('invalid', '1.1.48'), /Cannot compare invalid semver/);
assert.throws(() => compareSemver('1.1.48', 'bad'), /Cannot compare invalid semver/);
assert.throws(() => compareSemver('', '1.1.48'), /Cannot compare invalid semver/);
assert.throws(() => compareSemver(null, '1.1.48'), /Cannot compare invalid semver/);
});

test('check-release-version: verifySkillPinGate fails when pinnedVersion is invalid semver', () => {
const res = verifySkillPinGate({ pinnedVersion: 'invalid', tagList: ['skill-v1.1.45'] });
const res = verifySkillPinGate({ pinnedVersion: 'invalid', tagList: ['skill-v1.1.48'] });
assert.equal(res.ok, false);
assert.match(res.error, /not a valid semver/);
});

test('check-release-version: verifySkillPinGate fails closed when requireTags is true and no tags found', () => {
const res = verifySkillPinGate({ pinnedVersion: '1.1.45', tagList: [], requireTags: true });
const res = verifySkillPinGate({ pinnedVersion: '1.1.48', tagList: [], requireTags: true });
assert.equal(res.ok, false);
assert.match(res.error, /No skill-v tags found/);
});

test('check-release-version: verifySkillPinGate skips with explicit warning when requireTags is false and no tags found', () => {
const res = verifySkillPinGate({ pinnedVersion: '1.1.45', tagList: [], requireTags: false });
const res = verifySkillPinGate({ pinnedVersion: '1.1.48', tagList: [], requireTags: false });
assert.equal(res.ok, true);
assert.equal(res.skipped, true);
assert.match(res.warning, /No skill-v tags found in git repository; skill pin gate check was skipped/);
Expand Down
Loading
Loading