Sign In

@xmemo/client

Package Overview
Dependencies
Maintainers
1
Versions
51
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@xmemo/client - npm Package Compare versions

Comparing version
0.4.178
to
0.4.179
+88
docs/assets/xmemo-cli-architecture.svg
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="440" viewBox="0 0 1200 440" role="img" aria-labelledby="title desc">
<title id="title">XMemo CLI architecture</title>
<desc id="desc">AI clients connect through XMemo CLI using native integrations, hosted MCP, or a local stdio server, while credentials remain outside project files.</desc>
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" class="arrow-head"/>
</marker>
<style>
.canvas { fill: #f8fafc; stroke: #e2e8f0; }
.card { fill: #ffffff; stroke: #cbd5e1; }
.core { fill: #f5f3ff; stroke: #8b5cf6; }
.endpoint { fill: #fdf4ff; stroke: #d946ef; }
.title { fill: #0f172a; font: 700 22px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
.label { fill: #0f172a; font: 650 17px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
.body { fill: #475569; font: 14px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
.caption { fill: #64748b; font: 13px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
.line { fill: none; stroke: #94a3b8; stroke-width: 2; marker-end: url(#arrow); }
.arrow-head { fill: #94a3b8; }
.accent-blue { fill: #06b6d4; }
.accent-violet { fill: #8b5cf6; }
.accent-pink { fill: #ec4899; }
@media (prefers-color-scheme: dark) {
.canvas { fill: #0d1117; stroke: #30363d; }
.card { fill: #161b22; stroke: #3d444d; }
.core { fill: #211a36; stroke: #a78bfa; }
.endpoint { fill: #2a1830; stroke: #e879f9; }
.title, .label { fill: #f0f6fc; }
.body { fill: #b1bac4; }
.caption { fill: #8b949e; }
.line { stroke: #6e7681; }
.arrow-head { fill: #6e7681; }
}
</style>
</defs>
<rect class="canvas" x="1" y="1" width="1198" height="438" rx="22"/>
<text class="title" x="48" y="56">One CLI, three connection paths</text>
<text class="caption" x="48" y="82">Choose the integration that fits the client without moving credentials into the repository.</text>
<rect class="card" x="48" y="126" width="246" height="234" rx="16"/>
<rect class="accent-blue" x="48" y="126" width="7" height="234" rx="3"/>
<text class="label" x="80" y="168">AI clients</text>
<text class="body" x="80" y="204">Codex · Cursor · Gemini</text>
<text class="body" x="80" y="232">Copilot · Kiro · Grok</text>
<text class="body" x="80" y="260">OpenClaw · Hermes</text>
<text class="body" x="80" y="288">and other MCP clients</text>
<text class="caption" x="80" y="328">Editors, CLIs, and agents</text>
<rect class="core" x="366" y="126" width="246" height="234" rx="16"/>
<rect class="accent-violet" x="366" y="126" width="7" height="234" rx="3"/>
<text class="label" x="398" y="168">XMemo CLI</text>
<text class="body" x="398" y="204">Install and update</text>
<text class="body" x="398" y="232">Authenticate and diagnose</text>
<text class="body" x="398" y="260">Generate safe client config</text>
<text class="body" x="398" y="288">Install behavior profiles</text>
<text class="caption" x="398" y="328">npm package: @xmemo/client</text>
<rect class="card" x="684" y="112" width="228" height="78" rx="14"/>
<text class="label" x="712" y="144">Native adapters</text>
<text class="caption" x="712" y="169">OpenClaw · Hermes</text>
<rect class="card" x="684" y="204" width="228" height="78" rx="14"/>
<text class="label" x="712" y="236">Hosted MCP</text>
<text class="caption" x="712" y="261">https://xmemo.dev/mcp</text>
<rect class="card" x="684" y="296" width="228" height="78" rx="14"/>
<text class="label" x="712" y="328">Local stdio</text>
<text class="caption" x="712" y="353">xmemo-mcp</text>
<rect class="endpoint" x="984" y="126" width="168" height="234" rx="16"/>
<rect class="accent-pink" x="984" y="126" width="7" height="234" rx="3"/>
<text class="label" x="1016" y="168">XMemo</text>
<text class="body" x="1016" y="204">Durable memory</text>
<text class="body" x="1016" y="232">Search and recall</text>
<text class="body" x="1016" y="260">Project context</text>
<text class="body" x="1016" y="288">Governance</text>
<text class="caption" x="1016" y="328">User-owned context</text>
<path class="line" d="M294 243 H354"/>
<path class="line" d="M612 243 H650 V151 H672"/>
<path class="line" d="M612 243 H672"/>
<path class="line" d="M612 243 H650 V335 H672"/>
<path class="line" d="M912 151 H948 V210 H972"/>
<path class="line" d="M912 243 H972"/>
<path class="line" d="M912 335 H948 V276 H972"/>
<text class="caption" x="48" y="405">Credentials stay in user-scoped storage, environment variables, OAuth, or an enterprise secret manager.</text>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="270" viewBox="0 0 1200 270" role="img" aria-labelledby="title desc">
<title id="title">XMemo CLI setup workflow</title>
<desc id="desc">Install, authenticate, diagnose, connect a client, then recall and remember with XMemo.</desc>
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" class="arrow-head"/>
</marker>
<style>
.canvas { fill: #f8fafc; stroke: #e2e8f0; }
.step { fill: #ffffff; stroke: #cbd5e1; }
.number { fill: #7c3aed; }
.number-text { fill: #ffffff; font: 700 13px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
.label { fill: #0f172a; font: 650 17px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
.code { fill: #475569; font: 13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
.line { fill: none; stroke: #94a3b8; stroke-width: 2; marker-end: url(#arrow); }
.arrow-head { fill: #94a3b8; }
@media (prefers-color-scheme: dark) {
.canvas { fill: #0d1117; stroke: #30363d; }
.step { fill: #161b22; stroke: #3d444d; }
.number { fill: #8b5cf6; }
.label { fill: #f0f6fc; }
.code { fill: #b1bac4; }
.line { stroke: #6e7681; }
.arrow-head { fill: #6e7681; }
}
</style>
</defs>
<rect class="canvas" x="1" y="1" width="1198" height="268" rx="22"/>
<rect class="step" x="42" y="72" width="190" height="126" rx="15"/>
<circle class="number" cx="70" cy="101" r="16"/>
<text class="number-text" x="66" y="106">1</text>
<text class="label" x="94" y="108">Install</text>
<text class="code" x="64" y="153">npm install -g</text>
<text class="code" x="64" y="175">@xmemo/client</text>
<rect class="step" x="274" y="72" width="190" height="126" rx="15"/>
<circle class="number" cx="302" cy="101" r="16"/>
<text class="number-text" x="298" y="106">2</text>
<text class="label" x="326" y="108">Authenticate</text>
<text class="code" x="296" y="164">xmemo login</text>
<rect class="step" x="506" y="72" width="190" height="126" rx="15"/>
<circle class="number" cx="534" cy="101" r="16"/>
<text class="number-text" x="530" y="106">3</text>
<text class="label" x="558" y="108">Diagnose</text>
<text class="code" x="528" y="164">xmemo doctor</text>
<rect class="step" x="738" y="72" width="190" height="126" rx="15"/>
<circle class="number" cx="766" cy="101" r="16"/>
<text class="number-text" x="762" y="106">4</text>
<text class="label" x="790" y="108">Connect</text>
<text class="code" x="760" y="153">xmemo setup</text>
<text class="code" x="760" y="175">&lt;client&gt;</text>
<rect class="step" x="970" y="72" width="188" height="126" rx="15"/>
<circle class="number" cx="998" cy="101" r="16"/>
<text class="number-text" x="994" y="106">5</text>
<text class="label" x="1022" y="108">Use XMemo</text>
<text class="code" x="992" y="153">recall · remember</text>
<text class="code" x="992" y="175">context · timeline</text>
<path class="line" d="M232 135 H262"/>
<path class="line" d="M464 135 H494"/>
<path class="line" d="M696 135 H726"/>
<path class="line" d="M928 135 H958"/>
</svg>

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

# XMemo Skill Change Log
## 1.1.0
- `scripts/xmemo-skill.mjs`: align the advertised and runtime version at `1.1.0` while preserving the `XMemo Memory` package identity and formal-account-first login policy.
- `scripts/xmemo-skill.mjs`: add the discovery-compatible `auth-status` alias and `auth claim-deny` for the server's two-phase temporary-account bind flow.
- `scripts/xmemo-skill.mjs`: read temporary item/expiry limits from `/.well-known/xmemo-agent.json`, disclose them immediately after registration, and use the documented production limits as a non-blocking fallback when discovery is unavailable.
- `scripts/xmemo-skill.mjs`: route temporary `search` to `/v1/memories/search`, keep `recall` on `/v1/recall`, and retain temporary access only for `remember`, `recall`, and `search`.
- `scripts/xmemo-skill.mjs`: parse `--metadata` as a JSON object, parse `--explain` and `--prefer_working` as strict booleans, and validate state `--ttl_seconds` against the hosted `0..604800` contract.
- `scripts/xmemo-skill.mjs`: retain the established formal device-login scopes, including `ledger:read`; no server API contract or destructive memory command was added.
- `SKILL.md` and references: document the formal-account default, temporary limits, status alias, bind-denial flow, and typed argument examples without exposing credential values.
- Tests: cover dynamic temporary limits, temporary search routing, bind denial and pending-token cleanup, typed arguments, the `auth-status` alias, version output, and documentation invariants.
## 1.0.9
- Removed the non-runtime `skill-card.md` file. No user-facing, documentation, or runtime behavior changed in this marketplace release.
## 1.0.8
- `scripts/xmemo-skill.mjs`: advance the standalone runtime to `1.0.8` while preserving the existing REST operations, formal-login flow, temporary sandbox, and explicit plaintext fallback.
- `scripts/xmemo-skill.mjs`: stop displaying token prefixes and prevent `logout` from revoking an externally managed `XMEMO_KEY` unless `--revoke-environment-token` is explicitly supplied.
- `scripts/xmemo-skill.mjs`: add `doctor --anonymous`, command-specific login/register/logout help, `--version`, strict command parameter allowlists, required-argument validation, and sensitive command-line option rejection.
- `scripts/xmemo-skill.mjs`: require HTTPS for remote custom origins while retaining loopback HTTP for local development, warn before authenticated custom-origin requests, and add bounded request timeouts plus an 8 MiB response limit.
- `scripts/xmemo-skill.mjs`: honor device-login expiry, preserve the established formal-account memory and ledger scope set, redact sensitive fields from every JSON operation response, and sanitize human-readable server content for terminal safety.
- `SKILL.md` and references: document the compatible logout/anonymous-doctor behavior, timeout and origin boundaries, Node.js requirement, and copyable POSIX/PowerShell token-input examples.
- Tests: cover anonymous diagnostics, external environment-token logout, token-prefix suppression, unsafe origin and secret-option rejection, timeout/response limits, JSON redaction, the established formal-login scope set, device-login expiry, command help, and version output.
- `scripts/xmemo-skill.mjs`: keep `XMEMO_KEY` as the highest-priority credential source and never copy an environment token into local storage.
- `scripts/xmemo-skill.mjs`: require explicit `--allow-plaintext` consent before `login`, `auth add`, or temporary registration writes any bearer credential; replace the inaccurate “stored securely” claim with the exact storage path and an unencrypted-storage warning.
- `scripts/xmemo-skill.mjs`: restrict the XMemo credential directory/file to `0700`/`0600` where POSIX permissions are supported, record consent metadata, and warn when reading a legacy unmarked plaintext credential.
- `scripts/xmemo-skill.mjs`: minimize temporary credential metadata, redact token-shaped fields from JSON claim/error output, and clear pending confirmation data after handoff.
- `SKILL.md` and references: document credential precedence, explicit plaintext consent, temporary bind-URL handling, and migration guidance while keeping formal account login recommended.
- `scripts/xmemo-skill.mjs`: add an explicit, policy-gated `register --reason unattended|declined` fallback for the server's unauthenticated agent registration. Formal `login` remains the primary path.
- `scripts/xmemo-skill.mjs`: persist temporary credentials locally, route their allowed `remember`/`recall`/`search` requests to the temporary REST sandbox, reject unsupported commands clearly, and support claim-status/claim-confirm formal-token handoff.
- `SKILL.md` and references: document the temporary sandbox limits, required user disclosure, bind URL, and formal-account upgrade path.
- `scripts/xmemo-skill.mjs`: normalize successful list payloads (`result.results`, `result.todos`, or a bare array), so `recall`, `search`, and `todo-list` never call `forEach` on an API wrapper object.
- `scripts/xmemo-skill.mjs`: extract IDs from object or string results for `remember` and `expense-add`, preventing `[object Object]` output.
- `scripts/xmemo-skill.mjs`: parse every REST response through one guarded JSON helper. Empty or non-JSON gateway responses now include the HTTP status and a bounded server-response preview.
- `scripts/xmemo-skill.mjs`: add global and command-level `--help`, clear unknown-command errors, and `--compact` rendering for recall/search.
- `SKILL.md` and `references/*.md`: make every command relative to the Skill root (`node scripts/xmemo-skill.mjs ...`) and document compact output and help.
- `test/xmemo-standalone-skill.test.js`: add regression coverage for wrapped list payloads, object IDs, help output, and non-JSON responses.
+6
-4
{
"name": "@xmemo/client",
"version": "0.4.178",
"version": "0.4.179",
"description": "Privacy-first CLI and MCP setup helper for XMemo.",

@@ -8,8 +8,9 @@ "mcpName": "io.github.yonro/xmemo",

"bin": {
"xmemo-mcp": "bin/mcp-stdio.js",
"xmemo": "bin/memory-os.js",
"memory-os": "bin/memory-os.js",
"xmemo-mcp": "bin/mcp-stdio.js"
"memory-os": "bin/memory-os.js"
},
"files": [
"bin",
"docs/assets",
"src",

@@ -26,3 +27,4 @@ "skills",

"pack:dry-run": "npm pack --dry-run",
"prepublishOnly": "npm run lint && npm test && npm run pack:dry-run"
"release:check": "node scripts/check-release-version.mjs",
"prepublishOnly": "npm run release:check && npm run lint && npm test && npm run pack:dry-run"
},

@@ -29,0 +31,0 @@ "engines": {

+1
-1

@@ -14,3 +14,3 @@ {

"license": "UNLICENSED",
"logo": "assets/logo.svg",
"logo": "assets/logo.png",
"keywords": [

@@ -17,0 +17,0 @@ "kiro",

@@ -8,3 +8,3 @@ # XMemo Kiro Power

- `mcp.json` adds the hosted XMemo MCP server at `https://xmemo.dev/mcp`.
- `assets/logo.svg` reuses the canonical XMemo marketplace icon source used by the existing ChatGPT/Claude listing assets.
- `assets/logo.png` is the canonical XMemo product mark used by the hosted application and marketplace listings.
- `steering/AGENTS.md` tells Kiro when to use XMemo memory.

@@ -11,0 +11,0 @@

@@ -45,3 +45,3 @@ # XMemo Kiro Power Setup Guide

```bash
printf '%s\n' 'your-token' | xmemo token add --from-stdin
printf '%s\n' 'your-token' | xmemo token add --from-stdin --allow-plaintext
```

@@ -48,0 +48,0 @@

@@ -14,3 +14,3 @@ {

"license": "UNLICENSED",
"logo": "assets/logo.svg",
"logo": "assets/logo.png",
"keywords": [

@@ -17,0 +17,0 @@ "cursor",

@@ -8,3 +8,3 @@ # XMemo Cursor Plugin

- `mcp.json` adds the hosted XMemo MCP server at `https://xmemo.dev/mcp`.
- `assets/logo.svg` reuses the canonical XMemo marketplace icon source used by the existing ChatGPT/Claude listing assets.
- `assets/logo.png` is the canonical XMemo product mark used by the hosted application and marketplace listings.
- `rules/AGENTS.mdc` tells Cursor when to use XMemo memory.

@@ -11,0 +11,0 @@ - `skills/agents/SKILL.md` gives Cursor a safe workflow for recall, writes, TODOs, and destructive memory actions.

+338
-531

@@ -1,708 +0,515 @@

# XMemo CLI
<div align="center">
<a href="https://xmemo.dev">
<img src="./plugins/xmemo/assets/logo.png" width="132" alt="XMemo logo">
</a>
[![smithery badge](https://smithery.ai/badge/xmemo/xmemo)](https://smithery.ai/servers/xmemo/xmemo)
[![MCP Badge](https://lobehub.com/badge/mcp/yonro-memory-os-cli)](https://lobehub.com/mcp/yonro-memory-os-cli)
<h1>XMemo CLI</h1>
`@xmemo/client` is the privacy-first command line entry point for XMemo client
setup. It is intentionally small: the npm package contains only the CLI and
setup/helper assets needed on a user's machine: the CLI runtime, client setup
profiles, XMemo skills, and marketplace plugin metadata.
<p><strong>One private memory layer for every AI agent.</strong></p>
<p>
Install, authenticate, diagnose, and connect XMemo across editors,
CLIs, and autonomous agents from one production-ready command line.
</p>
`@yonro/xmemo-client` is reserved as a Yonro fallback package. The CLI exposes
`xmemo` as the primary command and keeps `memory-os` as a compatibility alias.
<p>
<a href="https://github.com/yonro/memory-os-cli/actions/workflows/ci.yml"><img alt="CI" src="https://img.shields.io/github/actions/workflow/status/yonro/memory-os-cli/ci.yml?branch=main&style=flat-square&logo=githubactions&logoColor=white&label=CI"></a>
<a href="https://www.npmjs.com/package/@xmemo/client"><img alt="npm version" src="https://img.shields.io/npm/v/@xmemo/client?style=flat-square&logo=npm&logoColor=white&label=npm"></a>
<a href="https://www.npmjs.com/package/@xmemo/client"><img alt="npm downloads" src="https://img.shields.io/npm/dm/@xmemo/client?style=flat-square&logo=npm&logoColor=white&label=downloads"></a>
<a href="https://www.npmjs.com/package/@xmemo/client"><img alt="Node.js version" src="https://img.shields.io/node/v/@xmemo/client?style=flat-square&logo=nodedotjs&logoColor=white&label=node"></a>
<a href="./LICENSE"><img alt="MIT license" src="https://img.shields.io/npm/l/@xmemo/client?style=flat-square&label=license"></a>
<a href="https://github.com/yonro/memory-os-cli/stargazers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/yonro/memory-os-cli?style=flat-square&logo=github&label=stars"></a>
</p>
The XMemo server, database, token registry, deployment files, logs, and
internal scripts are not part of this npm package.
<p>
<a href="https://modelcontextprotocol.io/"><img alt="MCP compatible" src="https://img.shields.io/badge/MCP-compatible-2563eb?style=flat-square"></a>
<a href="https://xmemo.dev"><img alt="XMemo Cloud" src="https://img.shields.io/badge/XMemo-Cloud-7c3aed?style=flat-square"></a>
<a href="#security-by-default"><img alt="Privacy first" src="https://img.shields.io/badge/privacy-first-334155?style=flat-square&logo=shield&logoColor=white"></a>
<a href="https://lobehub.com/mcp/yonro-memory-os-cli"><img alt="LobeHub" src="https://img.shields.io/badge/LobeHub-MCP-ec4899?style=flat-square"></a>
<a href="https://glama.ai/mcp/servers/yonro/memory-os-cli"><img alt="Glama quality score" src="https://glama.ai/mcp/servers/yonro/memory-os-cli/badges/score.svg"></a>
</p>
> **XMemo CLI is the top-level control plane** — use `xmemo login`, `xmemo doctor`, `xmemo setup <client>`, and smoke checks before hand-editing MCP config. Hosted MCP remains the universal runtime path for clients that do not have a native integration.
<p>
<a href="#quick-start">Quick start</a> ·
<a href="#supported-integrations">Integrations</a> ·
<a href="#connection-modes">Connection modes</a> ·
<a href="#command-reference">Commands</a> ·
<a href="#security-by-default">Security</a>
</p>
</div>
## XMemo Runtime Overview
---
**XMemo** is a user-owned memory system that lets AI agents persistently store, search, recall, update, and manage notes and memory fragments across sessions, projects, and tools.
`@xmemo/client` is the official control plane for connecting AI tools to
[XMemo](https://xmemo.dev). It makes setup repeatable, keeps credentials out of
project files, and gives every supported client a consistent path to durable,
user-owned memory.
- **Top-level CLI**: `xmemo` from `@xmemo/client`
- **Native integrations**: OpenClaw XMemo memory plugin and Hermes `hermes-xmemo` provider
- **MCP Endpoint**: `https://xmemo.dev/mcp` (Streamable HTTP)
- **Auth**: Bearer Token (`XMEMO_KEY`) or MCP OAuth
- **Tools** (20): `get_mcp_identity`, `remember`, `recall`, `recall_context`, `memory_stats`, `update_memory`, `explain_memory`, `restore_memory`, `add_expense`, `list_ledger_transactions`, `get_monthly_ledger_summary`, `forget`, `create_memory_todo`, `list_memory_todos`, `complete_memory_todo`, `list_memory_versions`, `get_timeline`, `record_event`, `update_state`, `get_project_context`
- **Clients**: Kimi, Claude, Cursor, Copilot, Gemini, Grok, Windsurf, Cline, Trae, Zed, Qwen, and more
The package is deliberately small: the CLI runtime, safe client configuration,
behavior profiles, XMemo skills, and marketplace metadata. Server code,
databases, deployment files, logs, and internal operations remain outside the
npm distribution.
```json
{
"mcpServers": {
"XMemo": {
"type": "streamable-http",
"url": "https://xmemo.dev/mcp",
"headers": {
"Authorization": "Bearer ${XMEMO_KEY}"
}
}
}
}
```
## Architecture
See [MCP Setup](#mcp-setup) for detailed client configuration.
<p align="center">
<img src="./docs/assets/xmemo-cli-architecture.svg" width="100%" alt="XMemo CLI architecture">
</p>
## Install
| | |
| --- | --- |
| **Package** | [`@xmemo/client`](https://www.npmjs.com/package/@xmemo/client) |
| **Primary command** | `xmemo` |
| **Local MCP command** | `xmemo-mcp` |
| **Hosted MCP** | `https://xmemo.dev/mcp` |
| **Runtime** | Node.js 20 or later |
| **License** | MIT |
```bash
npm install -g @xmemo/client
```
## Why XMemo CLI
Upgrade an existing global install:
- **One control plane** — login, diagnostics, configuration, profiles, updates,
and smoke checks share one predictable interface.
- **Private by design** — generated project configuration references a
credential; it never embeds the credential value.
- **Native where it matters** — OpenClaw and Hermes use dedicated memory
integrations instead of duplicating the same capability through MCP.
- **Portable everywhere else** — hosted Streamable HTTP MCP and local stdio
cover modern editors, terminals, and agent runtimes.
- **Safe automation** — supported setup and removal paths offer preview,
dry-run, or explicit confirmation before making changes.
- **Small supply-chain surface** — the npm package is governed by an explicit
file allowlist and release provenance.
```bash
xmemo update
```
## Quick start
This runs `npm install -g @xmemo/client@latest`. Use `xmemo update --dry-run`
to print the exact command without changing anything.
## Commands
```bash
xmemo update
npm install -g @xmemo/client
xmemo login
xmemo doctor
xmemo setup codex
xmemo setup codex --dry-run
xmemo setup cursor
xmemo setup cursor --dry-run
xmemo setup copilot
xmemo setup copilot --dry-run
xmemo setup openclaw
xmemo setup openclaw --dry-run
xmemo setup openclaw --with-mcp
xmemo setup openclaw --mcp-only
xmemo setup hermes
xmemo setup hermes --with-mcp
xmemo setup hermes --mcp-only
xmemo setup gemini
xmemo setup gemini --dry-run
xmemo setup antigravity
xmemo setup antigravity --dry-run
xmemo setup kiro
xmemo setup kiro --dry-run
xmemo setup grok
xmemo setup grok --dry-run
xmemo mcp add antigravity2
xmemo mcp add antigravity2 --write
xmemo doctor
xmemo discovery show
xmemo setup
xmemo login
xmemo auth status
xmemo status
xmemo token status
xmemo token add --from-stdin
xmemo env example --shell bash
xmemo mcp list
xmemo mcp config --client generic
xmemo mcp config --client antigravity
xmemo mcp add antigravity --write
xmemo profile status codex
xmemo profile install gemini
xmemo profile install antigravity
xmemo smoke --client codex
xmemo privacy
```
## Enterprise privacy and security defaults
Replace `codex` with your client. Preview a configuration before writing it:
- No telemetry or analytics.
- `xmemo doctor`, `xmemo discovery show`, and `xmemo status` do not send tokens.
- MCP config generated by the CLI references `XMEMO_KEY` or uses the client's
MCP OAuth flow; it does not write token values into project files.
- The CLI generates one stable non-secret `XMEMO_AGENT_INSTANCE_ID` per device
and stores it in user-scoped config outside git. All agents on the same device
share this instance ID.
- `xmemo setup <client>` can install a marker-scoped XMemo memory behavior
profile for the selected agent. The profile contains instructions only; it
never embeds token values.
- `xmemo login` stores the issued credential in the user-scoped XMemo CLI
config directory, shows the approved account when the server provides it,
and does not require extra token configuration afterward.
- `xmemo token add` remains available for existing tokens and still avoids
project files, shell history, and printed token values.
- Legacy `xmemo token set` refuses plaintext credential storage unless
`--allow-plaintext` is explicitly provided.
- The npm package uses a `files` whitelist so only `bin`, `src`, `skills`,
published plugin metadata/assets, `README.md`, and `LICENSE` are published.
## Token flow
Recommended personal-user flow:
```bash
xmemo login
xmemo auth status
xmemo token status --verify
xmemo setup cursor --dry-run
```
`xmemo login` uses the hosted device-login flow when the service advertises it:
the CLI shows a browser URL and one-time code, the user authorizes in XMemo, and
the CLI stores the issued MCP token in the user-scoped credential file. When the
service returns approved account metadata, the CLI prints the account label so
users can confirm which XMemo account was connected. No manual token setup is
needed after a successful `xmemo login`; `xmemo token status --verify` is only
an optional connectivity check. The CLI waits for the full browser authorization
window by default; use `--timeout-ms` only to shorten or extend that approval
window, and `--http-timeout-ms` only for individual service requests.
<p align="center">
<img src="./docs/assets/xmemo-cli-workflow.svg" width="100%" alt="XMemo CLI setup workflow">
</p>
Users who already have a token can configure it directly without shell profiles:
> [!TIP]
> Start with `xmemo login`, `xmemo doctor`, and `xmemo setup <client>`.
> Hand-edit MCP configuration only when a client has no verified setup path.
```bash
printf '%s\n' 'your-token' | xmemo token add --from-stdin
xmemo token status --verify
```
## Supported integrations
This is the preferred fallback while a hosted service is rolling out device
login. It still avoids project files, MCP config files, logs, and chat
transcripts.
| Client | Recommended command | Connection |
| --- | --- | --- |
| **Codex** | `xmemo setup codex` | Hosted MCP + behavior profile |
| **Cursor** | `xmemo setup cursor` | Hosted MCP + behavior profile |
| **Copilot CLI** | `xmemo setup copilot` | Local authenticated proxy |
| **Gemini CLI** | `xmemo setup gemini` | Hosted MCP + OAuth |
| **Antigravity** | `xmemo setup antigravity` | Hosted MCP + OAuth |
| **OpenClaw** | `xmemo setup openclaw` | Native memory plugin + Skill |
| **Hermes** | `xmemo setup hermes` | Native memory provider |
| **Kiro** | `xmemo setup kiro` | Hosted MCP |
| **Grok** | `xmemo setup grok` | Hosted MCP |
| **Other MCP clients** | `xmemo mcp config --client generic` | Generated template |
Tokens should be created by the XMemo website or enterprise console, then
stored with `xmemo login`, `xmemo token add`, a user environment variable, or an
enterprise secret manager:
The client registry also covers Windsurf, Cline, Continue, Claude Desktop,
Claude Code, Kimi Code, Zed, JetBrains, OpenCode, Qwen, Trae, and compatible
MCP hosts. Run `xmemo mcp list` for the current machine-readable catalog.
```bash
export XMEMO_KEY="your-token"
```
## Connection modes
PowerShell:
### Hosted MCP
```powershell
[Environment]::SetEnvironmentVariable("XMEMO_KEY", "your-token", "User")
```
The recommended universal path is the XMemo Streamable HTTP endpoint:
Do not commit tokens to source control, MCP config files, `.env` files, logs, or
chat transcripts.
## Hosted discovery setup
Hosted setup uses the XMemo public discovery contracts. The CLI reads
secret-free discovery and onboarding status documents, then tells the user where
the API, MCP endpoint, docs, and any server-advertised onboarding links are.
```bash
xmemo doctor
xmemo discovery show
xmemo setup
```
Discovery requests do not send `XMEMO_KEY` or any Authorization
header. Token creation still happens in the website or enterprise console; the
public service discovery document does not return token values.
The hosted default service/base URL is `https://xmemo.dev`, so normal users do
not need to type a service address. The MCP endpoint is discovered from that
base URL and written as `https://xmemo.dev/mcp`; `https://mcp.xmemo.dev` is not
the current canonical setup URL. Use `--url <service-url>` or `XMEMO_URL` only
for private, enterprise, or self-hosted deployments. `MEMORY_OS_URL` remains
accepted as a compatibility alias.
Generate and write a client config from discovery:
```bash
xmemo setup codex
xmemo setup codex --url "https://your-private-service.example"
xmemo setup cursor
xmemo setup copilot
xmemo setup openclaw
xmemo setup hermes
xmemo setup gemini
xmemo setup antigravity
```
`xmemo setup <client>` is the unified setup entry point. For write-capable
clients, it applies the user-scoped config directly; use `--dry-run` to preview
without writing. Codex/Cursor configs reference `XMEMO_KEY`; OAuth-native
clients such as Gemini CLI and Antigravity use the client's MCP OAuth flow
instead. `xmemo setup openclaw` is a custom OpenClaw installer: it installs or
updates the native `@xmemo/openclaw-memory` plugin and the XMemo Skill, and does
not add the hosted MCP server unless `--with-mcp` is passed. No generated config
embeds a token value. Write-capable client configs also include stable
non-secret agent identity headers where the client format supports them.
`--yes` remains accepted for Codex and Cursor as a compatibility no-op.
After writing MCP config, `xmemo setup <client>` prompts:
```text
Write XMemo memory behavior profile to <path>? [Y/n]
https://xmemo.dev/mcp
```
The default is `Y`, so pressing Enter writes a marker-scoped profile that nudges
the agent to recall/search XMemo at the start of non-trivial work and remember
high-signal decisions after meaningful changes. Use `n` or `--no-profile` to
configure MCP only. Use `--dry-run` to preview without writing config or profile
files, and `--profile-target <path>` to choose a different behavior profile
target.
OAuth-capable clients complete authentication in the browser. Other clients
reference `XMEMO_KEY` without copying its value into repository files.
### Uninstall
Generic configuration shape:
Remove the XMemo MCP server entry from one or all detected client configs:
```bash
xmemo uninstall --all --dry-run
xmemo uninstall --all --yes
xmemo uninstall cursor --yes
xmemo uninstall --all --yes --profiles
```json
{
"mcpServers": {
"XMemo": {
"type": "streamable-http",
"url": "https://xmemo.dev/mcp",
"headers": {
"Authorization": "Bearer ${XMEMO_KEY}"
}
}
}
}
```
`xmemo uninstall --all` scans the same clients as `setup --all` and removes only
the `XMemo` entry (and legacy names such as `memory_os`) from each detected
config file. Other MCP servers are preserved. By default it shows a summary and
asks for confirmation; pass `--yes` (or `-y`) to skip the prompt, or `--dry-run`
to preview without modifying files.
Client configuration keys differ; prefer `xmemo setup <client>` over copying
this generic example directly.
Pass `--profiles` to also remove installed behavior profiles (Codex `AGENTS.md`,
Cursor memory profile, etc.). Identity files and credentials are not removed, so
a later `xmemo setup --all` can re-enable XMemo with the same agent instance ID.
### Local stdio MCP
Default behavior profile targets:
`xmemo-mcp` is the dedicated stdio entry point for marketplaces and clients
that launch a local process. Safe discovery exposes 20 tools, three prompts,
and two documentation resources without a token. Tool execution still requires
authentication.
```text
codex ./AGENTS.md
cursor ~/.cursor/memory-profile.md
gemini ~/.gemini/GEMINI.md
antigravity ~/.gemini/antigravity/MEMORY.md
```
After a global installation:
Antigravity 2.0 currently uses the lower-level MCP writer because its stable
user config path is separate from the original Antigravity profile. Preview the
generated OAuth-first config with:
```bash
xmemo mcp add antigravity2 --url https://xmemo.dev
xmemo-mcp
```
Write it to the default Antigravity 2.0 config path with:
Install-free MCP configuration:
```bash
xmemo mcp add antigravity2 --url https://xmemo.dev --write
```json
{
"mcpServers": {
"XMemo": {
"command": "npx",
"args": [
"-y",
"--package",
"@xmemo/client@latest",
"xmemo-mcp"
]
}
}
}
```
The generated config uses `https://xmemo.dev/mcp`, contains no bearer token, and
expects Antigravity 2.0 to complete MCP OAuth in the browser on first use.
`XMEMO_AGENT_INSTANCE_ID` is a device-level identifier shared by all agents on
the same machine, generated and stored when `--write` is used.
`xmemo mcp serve` is equivalent when the CLI is already installed.
## MCP setup
### Native integrations
List supported client generators:
OpenClaw and Hermes have dedicated memory providers. Their default setup avoids
installing a second, duplicate XMemo tool surface.
```bash
xmemo mcp list
```
# Native OpenClaw plugin + XMemo Skill
xmemo setup openclaw
Current write-capable clients:
```text
codex ~/.codex/config.toml
cursor ~/.cursor/mcp.json
copilot ~/.copilot/mcp-config.json
gemini ~/.gemini/settings.json
antigravity ~/.gemini/antigravity/mcp_config.json
antigravity2 ~/.antigravity2/mcp.json
kiro ~/.kiro/settings/mcp.json
# Native Hermes memory provider
xmemo setup hermes
```
For clients without a verified user-scoped write path, generate a read-only
template and apply it manually after review:
Add hosted MCP only when an explicit fallback is desired:
```bash
xmemo mcp config --client generic --base-url "https://your-private-service.example" --json
xmemo setup openclaw --with-mcp
xmemo setup hermes --with-mcp
```
Codex, Cursor, Copilot CLI, Gemini CLI, Antigravity, OpenClaw, Hermes, and Kiro
have write-capable setup helpers. Antigravity 2.0 is write-capable through
`xmemo mcp add antigravity2 --write`.
Other client writes should only be added after their official user-scoped config
format is verified.
Use `--mcp-only` to skip the native integration and install only the hosted MCP
fallback.
OpenClaw and Hermes use the same setup modes:
## Authentication
| Command | Result |
|---------|--------|
| `xmemo setup <openclaw|hermes>` | Install/update the native integration and sync credentials. MCP is not installed. |
| `xmemo setup <openclaw|hermes> --with-mcp` | Install/update the native integration, sync credentials, and add hosted MCP fallback. |
| `xmemo setup <openclaw|hermes> --mcp-only` | Add hosted MCP fallback only. Native plugin/Skill install and native credential sync are skipped. |
### Browser login
### OpenClaw
Recommended for personal accounts:
Recommended OpenClaw setup:
```bash
xmemo login
xmemo setup openclaw
openclaw xmemo status
xmemo auth status
```
`xmemo setup openclaw` installs or updates OpenClaw's native XMemo memory plugin
from `@xmemo/openclaw-memory`, installs the XMemo Skill with `openclaw skills
install xmemo --force`, and then runs `openclaw xmemo status --json` so the user
can see whether credentials are available. The native plugin reads the same
user-scoped XMemo credential used by `xmemo login` and `xmemo token add
--from-stdin`, so normal users do not need to separately configure an OpenClaw
API key.
The CLI uses the hosted device-login flow, waits for browser approval, and
asks once before storing the issued credential unencrypted in the current
user's XMemo config directory. The exact path is shown before approval, file
permissions are restricted where the operating system supports it, and the
credential value is never printed. Prefer `XMEMO_KEY` or a managed secret store
on shared systems.
Hosted MCP is not installed by default because it creates a second XMemo tool
surface beside the native memory plugin. If you intentionally want that fallback
too, run:
For non-interactive automation, record the same decision explicitly:
```bash
xmemo setup openclaw --with-mcp
xmemo login --allow-plaintext
```
The fallback MCP entry references `Authorization: Bearer ${XMEMO_KEY}` and does
not embed the token value. To install only the hosted MCP fallback without the
native plugin or Skill, run:
### Existing token
Pipe an existing token through stdin so it does not appear in command history:
```bash
xmemo setup openclaw --mcp-only
printf '%s\n' 'your-token' | xmemo token add --from-stdin --allow-plaintext
xmemo token status --verify
```
Use `--no-skill` only when the XMemo Skill is managed by another deployment
path. Use `--openclaw-bin <path>` for custom OpenClaw binary locations.
PowerShell:
### Hermes
Recommended Hermes setup:
```bash
xmemo login
xmemo setup hermes
```powershell
$xmemoToken = Read-Host "XMemo token"
$xmemoToken | xmemo token add --from-stdin --allow-plaintext
Remove-Variable xmemoToken
```
`xmemo setup hermes` is Hermes-aware rather than a raw MCP writer. It installs
or updates the native `hermes-xmemo` package with `python -m pip install -U
hermes-xmemo`, runs `hermes-xmemo install`, then resolves the XMemo token from,
in order, process environment (`XMEMO_KEY`, `MEMORY_OS_API_KEY`, or
`MEMORY_OS_MCP_TOKEN`), the shared `@xmemo/client` credential written by
`xmemo login` / `xmemo token add --from-stdin`, and an existing
`$HERMES_HOME/.env`. It syncs the token to Hermes' `$HERMES_HOME/.env` as
`XMEMO_KEY`, which keeps the native `hermes-xmemo` plugin and any optional
Hermes MCP fallback on the same credential.
For CI and managed workstations, expose `XMEMO_KEY` through the platform's
secret manager. Do not commit it to `.env`, MCP configuration, logs, issue
reports, or chat transcripts.
Hosted MCP is not installed by default because it creates a second XMemo tool
surface beside the native memory provider. If you intentionally want that
fallback too, run:
## Command reference
```bash
xmemo setup hermes --with-mcp
```
<details>
<summary><strong>Lifecycle and diagnostics</strong></summary>
The fallback MCP entry references `Authorization:Bearer ${XMEMO_KEY}` and does
not embed the token value. To install only the hosted MCP fallback without the
native plugin or native credential sync, run:
```bash
xmemo setup hermes --mcp-only
xmemo --version
xmemo update
xmemo update --dry-run
xmemo doctor
xmemo discovery show
xmemo status
xmemo privacy
```
If an older Hermes plugin setup already created `$HERMES_HOME/.env`,
`xmemo setup hermes` can backfill the same token into the shared `@xmemo/client`
credential file so future XMemo CLI and MCP flows reuse it. Use `--no-plugin`
only when the native Hermes plugin is managed by another deployment path. Use
`--hermes-home <path>` for non-default Hermes homes. The legacy
`hermes memory setup xmemo` flow remains supported by the plugin and can reuse
the shared credential when present.
</details>
### Copilot CLI
<details>
<summary><strong>Authentication</strong></summary>
Copilot CLI has `/mcp` management and reads user MCP configuration from
`~/.copilot/mcp-config.json` (or `$COPILOT_HOME/mcp-config.json`). XMemo writes
a local proxy server entry there:
```bash
xmemo login
xmemo setup copilot
xmemo mcp proxy
xmemo auth status
xmemo auth-status --verify
xmemo token status --verify
xmemo token add --from-stdin --allow-plaintext
xmemo env example --shell bash
```
`xmemo setup copilot` writes `XMemo` to Copilot CLI's user MCP config and
does not include token or identity headers. Use `xmemo setup copilot --dry-run`
to preview without writing. `xmemo mcp proxy` reads the token saved by
`xmemo login` or `xmemo token add --from-stdin`, adds the XMemo bearer token and
local agent identity, then forwards requests to `https://xmemo.dev/mcp`. If
Copilot CLI is already open, reload MCP config or restart Copilot CLI after
setup.
If you specifically want the older environment-variable template, run:
</details>
<details>
<summary><strong>Client setup</strong></summary>
```bash
xmemo mcp config --client copilot-cli --remote-env
xmemo setup <client>
xmemo setup <client> --dry-run
xmemo setup --all
xmemo setup openclaw [--with-mcp|--mcp-only]
xmemo setup hermes [--with-mcp|--mcp-only]
```
### Codex
</details>
Recommended Codex setup:
<details>
<summary><strong>MCP and behavior profiles</strong></summary>
```bash
xmemo setup codex
xmemo mcp serve
xmemo mcp list
xmemo mcp config --client generic
xmemo mcp add <client> --write
xmemo mcp proxy
xmemo profile install <client>
xmemo profile status <client>
xmemo profile uninstall <client>
xmemo smoke --client codex
```
`setup codex` writes the MCP config to user-scoped Codex config and, by default,
installs the XMemo Codex behavior profile into the current project's `AGENTS.md`
between these markers. Use `xmemo setup codex --dry-run` to preview without
writing or `xmemo setup codex --no-profile` to skip the behavior profile.
</details>
```html
<!-- memory-os:codex-profile:start -->
<!-- memory-os:codex-profile:end -->
```
<details>
<summary><strong>Safe removal</strong></summary>
Repeat installs update only that marker block. Remove it with:
```bash
xmemo profile uninstall codex
xmemo uninstall <client> --dry-run
xmemo uninstall <client> --yes
xmemo uninstall --all --dry-run
xmemo uninstall --all --yes --profiles
```
Advanced: generate a Codex MCP config snippet without touching files:
Only XMemo-owned entries and marker-scoped behavior profiles are removed.
Unrelated MCP servers, credentials, and device identity remain intact.
```bash
xmemo mcp add codex --url "$XMEMO_URL"
```
</details>
Write it to the default Codex config path:
Run `xmemo help` or `xmemo <command> --help` for complete, version-matched
options.
```bash
xmemo mcp add codex --url "$XMEMO_URL" --write
```
## Client notes
The generated config references `XMEMO_KEY`, includes the non-secret
`X-Memory-OS-Agent-ID` / `X-Memory-OS-Agent-Instance-ID` attribution headers,
and does not include the token value.
<details>
<summary><strong>Codex and Cursor</strong></summary>
Codex MCP-depth checks:
```bash
xmemo mcp profile codex
xmemo profile install codex --dry-run
xmemo profile install codex
xmemo profile status codex
xmemo setup codex
xmemo smoke --client codex
```
`xmemo mcp profile codex` prints the recommended memory behavior profile:
recall/search at the start of non-trivial tasks, write back high-signal
decisions and fixes, and never store secrets. `xmemo smoke --client codex`
checks the local Codex TOML config for the `XMemo` MCP server,
`bearer_token_env_var = "XMEMO_KEY"`, token presence in the environment, and
absence of embedded token values.
### Cursor
Cursor marketplace plugin assets live in `.cursor-plugin/marketplace.json` and
`plugins/xmemo/`. The marketplace plugin is OAuth-first and its `mcp.json` stores
only `https://xmemo.dev/mcp`; it must not contain `Authorization`, `Bearer`, or
`XMEMO_KEY`.
Recommended Cursor setup:
```bash
xmemo setup cursor
```
`setup cursor` merges the Cursor MCP config into the default Cursor user config
path. Use `xmemo setup cursor --dry-run` to preview without writing. The
lower-level equivalent remains:
Both setup paths write a user-scoped MCP entry and can install a marker-scoped
memory behavior profile. Use `--no-profile` to configure MCP only. Cursor's
public marketplace plugin remains OAuth-first and contains no bearer-token
configuration.
```bash
xmemo mcp add cursor --url "$XMEMO_URL" --write
```
</details>
The CLI refuses to overwrite an existing `XMemo`, `memory_os`, or `memory-os`
MCP server entry. Edit the config manually if you need to rotate the endpoint.
Cursor configs include
`X-Memory-OS-Agent-ID` and `X-Memory-OS-Agent-Instance-ID`; the instance ID is
non-secret and stored under the user's XMemo CLI config directory. By default,
the setup prompt also installs a Cursor behavior profile at
`~/.cursor/memory-profile.md`; answer `n` or pass `--no-profile` to skip it.
Use this direct-key setup only for local/manual installs where Cursor OAuth is
unavailable; public plugin submission should use the OAuth-first plugin config.
<details>
<summary><strong>Gemini CLI and Antigravity</strong></summary>
### Gemini CLI
Recommended Gemini CLI setup:
```bash
xmemo setup gemini
xmemo setup antigravity
```
`setup gemini` merges an XMemo MCP server into Gemini CLI's user settings at
`~/.gemini/settings.json`. It writes a remote HTTP server using Gemini's
`httpUrl` key plus `X-Memory-OS-Agent-ID` and `X-Memory-OS-Agent-Instance-ID`
headers. Use `xmemo setup gemini --dry-run` to preview without writing.
These clients use hosted MCP OAuth. Their generated configuration carries no
token value; restart the client and complete the browser login on first use.
Unlike Codex/Cursor, the Gemini config carries **no token**: authentication uses
Gemini CLI's built-in MCP OAuth flow (a one-time browser login on first use).
This is deliberate — Gemini redacts environment variables matching
`*KEY*`/`*TOKEN*`/`*AUTH*` during header expansion, so an `${XMEMO_KEY}`
reference would not survive. OAuth avoids storing any secret in the config and
still grants the full XMemo tool profile. After setup, restart Gemini CLI and
run `/mcp` (or the first XMemo tool call) to complete the OAuth login. By
default, the setup prompt also installs a Gemini behavior profile at
`~/.gemini/GEMINI.md`; answer `n` or pass `--no-profile` to skip it.
</details>
The CLI refuses to overwrite an existing `XMemo`, `memory_os`, or `memory-os`
MCP server entry. Edit the config manually if you need to rotate the endpoint.
<details>
<summary><strong>OpenClaw</strong></summary>
### Antigravity
Recommended Antigravity setup:
```bash
xmemo setup antigravity
xmemo login
xmemo setup openclaw
openclaw xmemo status
```
`setup antigravity` merges an XMemo MCP server into Antigravity's user MCP config
at `~/.gemini/antigravity/mcp_config.json`. It writes Antigravity's
`serverUrl` shape plus `X-Memory-OS-Agent-ID` and
`X-Memory-OS-Agent-Instance-ID` headers. Like Gemini CLI, the config carries
**no token**: restart Antigravity and complete the MCP OAuth flow on first use.
By default, the setup prompt also installs an Antigravity behavior profile at
`~/.gemini/antigravity/MEMORY.md`; answer `n` or pass `--no-profile` to skip it.
The setup command installs or updates `@xmemo/openclaw-memory`, installs the
XMemo Skill, reuses the shared XMemo credential, and checks plugin status.
The lower-level equivalent is:
</details>
<details>
<summary><strong>Hermes</strong></summary>
```bash
xmemo mcp add antigravity --write
xmemo login
xmemo setup hermes
```
Use `xmemo setup antigravity` for normal installs because it performs discovery
and chooses the recommended Antigravity path automatically. Use
`xmemo mcp add antigravity --write` when you want the generic MCP writer
directly, for example with `--url` or `--config` in advanced/multi-client setup.
The CLI refuses to overwrite an existing `XMemo`, `memory_os`, or `memory-os`
MCP server entry. Edit the config manually if you need to rotate the endpoint.
The setup command installs or updates `hermes-xmemo`, configures the native
provider, and synchronizes the user-scoped XMemo credential with Hermes.
### Kiro
</details>
Recommended Kiro setup:
<details>
<summary><strong>Copilot CLI</strong></summary>
```bash
xmemo setup kiro
xmemo login
xmemo setup copilot
xmemo mcp proxy
```
`setup kiro` merges an XMemo MCP server into Kiro's user MCP config at
`~/.kiro/settings/mcp.json`. It writes a remote HTTP server using Kiro's
standard `url` key plus `X-Memory-OS-Agent-ID` and `X-Memory-OS-Agent-Instance-ID`
headers. Use `xmemo setup kiro --dry-run` to preview without writing.
Copilot CLI receives a local proxy entry. The proxy reads the credential from
user-scoped storage, adds identity metadata, and forwards requests to hosted
MCP without writing secrets into Copilot configuration.
The Kiro config includes `Authorization: Bearer ${env:XMEMO_KEY}` so
authentication uses the environment variable approach. Set your XMemo token with:
</details>
```bash
xmemo login
# or
printf '%s\n' 'your-token' | xmemo token add --from-stdin
```
## Security by default
After setup, restart Kiro or reload MCP servers for the changes to take effect.
The CLI refuses to overwrite an existing `XMemo`, `memory_os`, or `memory-os`
MCP server entry. Edit the config manually if you need to rotate the endpoint.
| Control | Default behavior |
| --- | --- |
| **Telemetry** | No CLI analytics or usage telemetry |
| **Credential output** | Token values are never printed |
| **Project files** | Generated configuration references secrets; it does not embed them |
| **Discovery** | `doctor`, `discovery show`, and public capability discovery send no token |
| **Identity** | One stable, non-secret agent-instance ID is stored outside git |
| **Writes** | Setup supports preview/dry-run; broad removal requires confirmation |
| **Local credential storage** | Interactive login asks first; non-interactive writes require `--allow-plaintext`; stored tokens are unencrypted |
| **Package contents** | An npm `files` allowlist excludes tests, operations, logs, and server code |
### Grok (xAI)
Credential precedence and compatibility aliases are documented by:
Recommended Grok setup:
```bash
xmemo setup grok
xmemo env example --shell bash
xmemo privacy
```
`setup grok` appends an XMemo MCP server to Grok's user config at
`~/.grok/config.toml` using Grok's TOML `[mcp_servers.XMemo]` format with `url`,
`bearer_token_env_var = "XMEMO_KEY"`, and an `[mcp_servers.XMemo.http_headers]`
table carrying `X-Memory-OS-Agent-ID`. Use `xmemo setup grok --dry-run` to
preview without writing.
For private or self-hosted deployments, set `XMEMO_URL` or pass
`--url <service-url>`. `MEMORY_OS_URL` remains a compatibility alias.
Set your XMemo token with:
## Package boundary
```bash
xmemo login
# or
printf '%s\n' 'your-token' | xmemo token add --from-stdin
Published to npm:
```text
bin/
docs/assets/
src/
skills/
plugins/kiro/
plugins/xmemo/
README.md
LICENSE
```
Grok is also fully compatible with Claude Code: it automatically reads Claude
Code marketplaces, plugins, skills, and MCP servers. So `xmemo setup claude-code`
plus the repo's `.claude-plugin/`, `.mcp.json`, and `skills/` are discovered by
Grok with zero extra configuration. Use `xmemo setup grok` when you want the
server written directly into Grok's own `~/.grok/config.toml`.
Not published:
After setup, restart Grok or run `/mcps` to reload MCP servers. The CLI refuses
to overwrite an existing `XMemo` MCP server entry; edit the config manually if
you need to rotate the endpoint.
```text
.github/
docs/analysis/
docs/architecture/
test/
coverage/
server code
database migrations
deployment files
logs and local state
```
### Antigravity 2.0
## Development
Recommended Antigravity 2.0 setup:
```bash
xmemo mcp add antigravity2 --write
npm install
npm run release:check
npm run lint
npm test
npm run pack:dry-run
```
Use a dry preview first if you want to inspect the exact JSON before writing:
Before proposing a release, run the complete package gate:
```bash
xmemo mcp add antigravity2
npm run prepublishOnly
```
Antigravity 2.0 uses a separate config path from the original Antigravity
profile. The default write target is `~/.antigravity2/mcp.json`; on Windows the
server-side config contract also documents `%APPDATA%\\Antigravity 2.0\\mcp.json`
as the Antigravity 2.0 user config location. Pass `--config <path>` when you
want to write to a specific file.
The local stdio server can be inspected directly:
The generated `XMemo` entry uses hosted HTTP MCP:
```json
{
"mcpServers": {
"XMemo": {
"type": "http",
"url": "https://xmemo.dev/mcp"
}
}
}
```bash
node bin/mcp-stdio.js
```
No token value is written. Restart Antigravity 2.0 after setup and complete the
MCP OAuth browser flow on first use. If you use `--write`, the CLI also prepares
a device-level `XMEMO_AGENT_INSTANCE_ID` shared by all agents on the same
machine so XMemo can attribute activity consistently without embedding secrets.
## Release model
This repository is the source for the `@xmemo/client` npm package. Releases
should be published from GitHub Actions on tags or GitHub Releases, not from a
developer workstation.
Normal releases are produced by GitHub Actions from the exact tagged commit,
not from a mutable branch checkout or a developer workstation:
Recommended flow:
```text
develop -> test -> tag/release -> GitHub Actions -> npm publish --provenance
develop → version sync → test → tag → GitHub Actions → npm publish --provenance
```
## Package boundary
Version-bearing files must stay synchronized:
Included in npm:
- `package.json`
- `package-lock.json`
- `server.json`
- `lhm.plugin.json`
```text
bin/
src/
README.md
LICENSE
```
`node scripts/check-release-version.mjs --tag vX.Y.Z` verifies the tag and every
version-bearing file before publication. The separate npm publish workflow is
manual recovery only, so creating a GitHub Release cannot publish twice.
Excluded from npm:
## Documentation and support
```text
.github/
test/
coverage/
server code
database migrations
deployment files
logs
local state
secrets
```
- [XMemo](https://xmemo.dev)
- [XMemo MCP guide](https://xmemo.dev/product/mcp)
- [MCP server reference](./MCP-README.md)
- [Issues](https://github.com/yonro/memory-os-cli/issues)
- [Releases](https://github.com/yonro/memory-os-cli/releases)
## License
[MIT](./LICENSE) © 2025–2026 Yonro

@@ -6,2 +6,12 @@ # XMemo Skill Operations

## Contents
- Runtime selection
- Account policy and temporary fallback
- Command matrix
- Examples
- Direct execution details
- Output and terminal safety
- Limitations
## Runtime Selection

@@ -11,7 +21,57 @@

1. **Bundled Skill script** (`node skills/xmemo/scripts/xmemo-skill.mjs <command>`), which directly integrates with the XMemo REST API using stored credentials.
1. **Bundled Skill script** (`node scripts/xmemo-skill.mjs <command>`), which directly integrates with the XMemo REST API using stored credentials. Run commands from the Skill root with Node.js 20 or newer.
2. **XMemo MCP tools** (when running in environments that natively host the XMemo MCP server).
If no credential is stored, the script reports the login or token add command as the repair path. Never paste a raw token into chat.
Credential resolution is `XMEMO_KEY` first, then the user-scoped credential
file. An environment token is never copied into that file. If no credential is
available, the script reports the formal login or token-add repair path. Never
paste a raw token into chat.
The zero-dependency runtime cannot provide one portable operating-system
keychain implementation. Commands that create or replace a local credential
therefore require `--allow-plaintext`. This flag explicitly permits an
unencrypted user-file credential; the script prints a warning and applies
private POSIX permissions where supported. Prefer `XMEMO_KEY` or a managed
secret store when this local trust boundary is not acceptable.
## Account policy and temporary fallback
Use `login` or `auth add` by default. They provide a formal, account-backed
credential and the full command set. Do not automatically choose a temporary
token just because it is convenient.
Only use the fallback after the human explicitly declines formal registration,
or in unattended automation with no human available:
```text
node scripts/xmemo-skill.mjs register --reason declined --allow-plaintext
node scripts/xmemo-skill.mjs register --reason unattended --allow-plaintext
```
The fallback stores its token in the explicitly approved user credential file and can use only
`remember`, `recall`, and `search` in an isolated temporary memory space. Show
the returned bind URL only to the intended user; do not publish or log it. The
script reads `/.well-known/xmemo-agent.json` and discloses the current cap and
expiry immediately after registration. The current policy is 100 items, expiry
after 14 days without successful memory activity, and an absolute maximum of
30 days from registration. Formal registration removes these sandbox limits.
After their web claim, complete the
one-time formal-token handoff with:
```text
node scripts/xmemo-skill.mjs auth claim-status
node scripts/xmemo-skill.mjs auth claim-confirm
```
If the user does not approve the pending bind, reject it as the temporary-token
holder and keep the isolated temporary credential:
```text
node scripts/xmemo-skill.mjs auth claim-deny
```
For a legacy temporary credential that predates recorded consent, append
`--allow-plaintext` to the claim command once. Successful handoff overwrites the
temporary credential and removes pending confirmation data.
## Command matrix

@@ -30,3 +90,6 @@

| `expense-add` | Record a ledger expense |
| `doctor` | Check service health and auth status |
| `doctor` | Check service health and auth status; add `--anonymous` to omit credentials |
| `auth status` / `auth-status` | Show local auth state; add `--verify` for server validation |
| `auth claim-status` / `auth claim-confirm` / `auth claim-deny` | Inspect, approve, or reject the two-phase temporary bind |
| `logout` | Revoke/remove a local credential; externally managed `XMEMO_KEY` requires explicit revocation |

@@ -38,5 +101,3 @@ ## Examples

```text
node skills/xmemo/scripts/xmemo-skill.mjs remember \
--content "Use pnpm for package management in this repo" \
--path "projects/memory-os-cli/conventions"
node scripts/xmemo-skill.mjs remember --content "Use pnpm for package management in this repo" --path "projects/memory-os-cli/conventions"
```

@@ -47,16 +108,27 @@

```text
node skills/xmemo/scripts/xmemo-skill.mjs recall \
--query "package manager convention for memory-os-cli"
node scripts/xmemo-skill.mjs recall --query "package manager convention for memory-os-cli" --compact
```
Structured arguments are parsed before transmission. Pass metadata as a JSON
object and boolean query controls as the literal values `true` or `false`:
```text
node scripts/xmemo-skill.mjs remember --content "Verified decision" --path "projects/demo/decisions" --metadata '{"source":"review"}'
node scripts/xmemo-skill.mjs search --query "active implementation" --explain true --prefer_working false --compact
```
### Save handoff state
```text
node skills/xmemo/scripts/xmemo-skill.mjs save-state --key active_task
node scripts/xmemo-skill.mjs save-state --key active_task
```
`--ttl_seconds` accepts `0` through `604800` (seven days), matching the hosted
state-operation contract. A value of `0` requests the server's non-expiring
state behavior for that item.
### Restore handoff state
```text
node skills/xmemo/scripts/xmemo-skill.mjs restore-state --key active_task
node scripts/xmemo-skill.mjs restore-state --key active_task
```

@@ -67,4 +139,3 @@

```text
node skills/xmemo/scripts/xmemo-skill.mjs todo-add \
--content "Add unit tests for ledger expense command"
node scripts/xmemo-skill.mjs todo-add --content "Add unit tests for ledger expense command"
```

@@ -75,14 +146,45 @@

```text
node skills/xmemo/scripts/xmemo-skill.mjs expense-add \
--item "team lunch" --amount 42.5 --currency USD
node scripts/xmemo-skill.mjs expense-add --item "team lunch" --amount 42.5 --currency USD
```
### Add an existing token without command-line exposure
POSIX shell:
```text
printf '%s' "$XMEMO_KEY" | node scripts/xmemo-skill.mjs auth add --from-stdin --allow-plaintext
```
PowerShell:
```powershell
$env:XMEMO_KEY | node scripts/xmemo-skill.mjs auth add --from-stdin --allow-plaintext
```
## Direct Skill execution details
If the Skill file itself is not available, you can still run direct operations by calling the XMemo REST API using tools like curl or any HTTP client.
Use the bundled script or an available XMemo MCP/native integration. Do not
improvise REST calls when the Skill artifact is missing; restore the package or
use the documented hosted MCP path so authentication, redaction, and argument
validation remain intact.
## Output and terminal safety
`remember` and `expense-add` print the server-returned memory or ledger ID.
`recall` and `search` accept `--compact` to render each memory on one shortened
line; use `--json` when a caller needs the complete redacted response payload.
Human-readable output removes terminal control sequences. For the exact accepted
parameters of any command, run
`node scripts/xmemo-skill.mjs <command> --help`; use `--version` to identify the
runtime and `--timeout-ms <ms>` to bound each network request.
## Limitations
- The commands call the hosted endpoints on `xmemo.dev`. They require a network connection and a valid credential.
- Custom HTTPS origins are supported and receive the credential used by
authenticated commands. Use only trusted origins. Plain HTTP is accepted only
for localhost/loopback development.
- Responses larger than 8 MiB are rejected, and requests default to a 30-second
timeout.
- `save-state` / `restore-state` map to `update_state` / `_get_active_state_item` under the hood; they capture/resume server-side active task state.
- Offline memory storage or local sync is not implemented.

@@ -11,3 +11,3 @@ # XMemo Skill Troubleshooting

```text
node skills/xmemo/scripts/xmemo-skill.mjs doctor
node scripts/xmemo-skill.mjs doctor
```

@@ -20,3 +20,5 @@

```text
node skills/xmemo/scripts/xmemo-skill.mjs auth status
node scripts/xmemo-skill.mjs auth status
# equivalent discovery-compatible alias
node scripts/xmemo-skill.mjs auth-status
```

@@ -27,7 +29,35 @@

```text
node skills/xmemo/scripts/xmemo-skill.mjs login
node scripts/xmemo-skill.mjs login --allow-plaintext
# or
echo "$XMEMO_KEY" | node skills/xmemo/scripts/xmemo-skill.mjs auth add --from-stdin
printf '%s' "$XMEMO_KEY" | node scripts/xmemo-skill.mjs auth add --from-stdin --allow-plaintext
```
PowerShell token-add equivalent:
```powershell
$env:XMEMO_KEY | node scripts/xmemo-skill.mjs auth add --from-stdin --allow-plaintext
```
`XMEMO_KEY` remains the preferred credential source and is never copied to the
local credential file. The commands above include `--allow-plaintext` because
device login and `auth add` must retain a token for later standalone commands.
The flag explicitly permits unencrypted storage in the current user's XMemo
directory; the script prints the exact path and a warning before writing it.
Formal login is recommended. If and only if a human is unavailable or has
explicitly declined registration for now, create a limited temporary sandbox:
```text
node scripts/xmemo-skill.mjs register --reason unattended --allow-plaintext
```
Temporary credentials work only for `remember`, `recall`, and `search`. Give
the displayed bind URL to the user, then use `auth claim-confirm` after their
claim to receive the formal credential. The script displays the current
temporary item and time limits immediately after registration. The current
policy is 100 items, 14 days without successful memory activity, and 30 days
maximum from registration. Do not share the bind URL publicly. If the user
rejects a pending bind, run `node scripts/xmemo-skill.mjs auth claim-deny` to
reject it server-side and clear the local pending confirmation value.
New users should create or sign in to an XMemo account at `https://xmemo.dev`

@@ -44,3 +74,4 @@ before approving the device-login code. The browser page must show the same

```text
node skills/xmemo/scripts/xmemo-skill.mjs auth status --verify
node scripts/xmemo-skill.mjs auth status --verify
node scripts/xmemo-skill.mjs auth-status --verify
```

@@ -55,8 +86,16 @@

Check the hosted service without sending a token:
Check the hosted service and current credential together:
```text
node skills/xmemo/scripts/xmemo-skill.mjs doctor
node scripts/xmemo-skill.mjs doctor
```
When a credential is available, `doctor` sends it so the service can report
authentication validity. To check service health without any Authorization
header, run:
```text
node scripts/xmemo-skill.mjs doctor --anonymous
```
If this fails:

@@ -66,3 +105,8 @@

- Check DNS, VPN, or corporate proxy settings.
- Try an explicit base URL: `node skills/xmemo/scripts/xmemo-skill.mjs doctor --base-url https://xmemo.dev`.
- Try an explicit base URL: `node scripts/xmemo-skill.mjs doctor --base-url https://xmemo.dev`.
- Increase the per-request timeout only when the service is known to be slow:
`node scripts/xmemo-skill.mjs doctor --timeout-ms 60000`.
- Custom service origins must use HTTPS. Plain HTTP is accepted only for
localhost/loopback development, and authenticated commands warn before sending
a credential to a non-default origin.

@@ -73,5 +117,11 @@ ## 5. Common errors

|---------|--------------|--------|
| `No XMemo credential found` | Not logged in | Run the `login` command |
| `No XMemo credential found` | Not logged in | Set `XMEMO_KEY`, or run `node scripts/xmemo-skill.mjs login --allow-plaintext` |
| `Refusing unencrypted credential storage` | Missing explicit consent | Prefer `XMEMO_KEY`, or rerun the credential-writing command with `--allow-plaintext` |
| `Authentication failed (HTTP 401)` | Token invalid/expired | Run `login` or add a new token |
| `Remote XMemo server is not reachable` | Network or service outage | Check network/VPN/proxy |
| `XMemo base URL must use HTTPS` | Insecure non-loopback service URL | Use HTTPS, or localhost HTTP only for local development |
| `Request timed out` | Service/network exceeded the request deadline | Retry after checking service health, or set a bounded `--timeout-ms` |
| `Unknown option` | Unsupported or misspelled command parameter | Run the command with `--help`; do not pass tokens as flags |
| `--metadata must be a JSON object` | Metadata is invalid JSON, an array, or a scalar | Pass one JSON object, for example `'{"source":"review"}'` |
| `--explain must be true or false` | A boolean parameter used another spelling | Pass the literal `true` or `false` |
| `Method not found` | Server does not expose the requested operation | Server-side capability gap |

@@ -84,2 +134,9 @@

- Prefer `login` for interactive authentication.
- Prefer `XMEMO_KEY` or a managed secret store over plaintext file storage.
- `auth status` reports the credential source but never prints a token prefix.
- `logout` leaves externally managed `XMEMO_KEY` unchanged by default. Unset the
variable to stop using it; pass `--revoke-environment-token` only when remote
revocation is explicitly intended.
- `--allow-plaintext` means the local token is unencrypted and may be read by
processes running as the same operating-system user.
- Treat `X-Memory-OS-Agent-ID` as an attribution signal, not authorization proof.

@@ -14,5 +14,47 @@ #!/usr/bin/env node

import readline from 'node:readline';
import { randomUUID } from 'node:crypto';
const SKILL_VERSION = '1.0.0';
const SKILL_VERSION = '1.1.0';
const credentialsPath = path.join(os.homedir(), '.xmemo', 'skill-credentials.json');
const registrationPath = path.join(os.homedir(), '.xmemo', 'skill-registration.json');
const SCRIPT_COMMAND = 'node scripts/xmemo-skill.mjs';
const PLAINTEXT_STORAGE = 'plaintext-user-file';
const DEFAULT_BASE_URL = 'https://xmemo.dev';
const DEFAULT_TIMEOUT_MS = 30_000;
const MAX_TIMEOUT_MS = 300_000;
const MAX_RESPONSE_BYTES = 8_388_608;
const DEFAULT_TEMPORARY_LIMITS = Object.freeze({
max_items: 100,
ttl_seconds: 1_209_600,
max_lifetime_seconds: 2_592_000,
});
const warnedCredentialOrigins = new Set();
const REST_COMMANDS = new Set([
'remember', 'recall', 'search', 'save-state', 'restore-state', 'state-save', 'state-restore',
'todo-add', 'todo-list', 'todo-done', 'expense-add', 'doctor',
]);
const COMMAND_FLAGS = {
login: new Set(),
register: new Set(['reason']),
logout: new Set(),
doctor: new Set(),
remember: new Set(['content', 'path', 'metadata', 'logic_path', 'bucket', 'scope', 'team_id']),
recall: new Set(['query', 'limit', 'threshold', 'path', 'bucket', 'scope', 'team_id', 'memory_type', 'explain', 'prefer_working']),
search: new Set(['query', 'limit', 'threshold', 'path', 'bucket', 'scope', 'team_id', 'memory_type', 'explain', 'prefer_working']),
'save-state': new Set(['key', 'state_key', 'content', 'current_task', 'next_action', 'blocked_reason', 'ttl_seconds', 'bucket', 'scope']),
'state-save': new Set(['key', 'state_key', 'content', 'current_task', 'next_action', 'blocked_reason', 'ttl_seconds', 'bucket', 'scope']),
'restore-state': new Set(['key', 'state_key', 'bucket', 'scope']),
'state-restore': new Set(['key', 'state_key', 'bucket', 'scope']),
'todo-add': new Set(['content', 'due_at', 'bucket', 'scope', 'path']),
'todo-list': new Set(['bucket', 'scope', 'status']),
'todo-done': new Set(['id', 'todo_id', 'note']),
'expense-add': new Set(['item', 'amount', 'currency', 'transaction_date', 'date', 'path', 'bucket', 'scope']),
};
const AUTH_FLAGS = {
status: new Set(),
add: new Set(['from-stdin']),
'claim-status': new Set(),
'claim-confirm': new Set(),
'claim-deny': new Set(),
};

@@ -23,4 +65,11 @@ // Helper to parse arguments

json: false,
baseUrl: process.env.XMEMO_BASE_URL || 'https://xmemo.dev',
baseUrl: process.env.XMEMO_BASE_URL || DEFAULT_BASE_URL,
timeoutMs: process.env.XMEMO_TIMEOUT_MS || String(DEFAULT_TIMEOUT_MS),
verify: false,
compact: false,
help: false,
version: false,
allowPlaintext: false,
anonymous: false,
revokeEnvironmentToken: false,
};

@@ -33,15 +82,45 @@ const positionals = [];

if (arg.startsWith('--')) {
const key = arg.slice(2);
const rawKey = arg.slice(2);
const equalsIndex = rawKey.indexOf('=');
const key = equalsIndex === -1 ? rawKey : rawKey.slice(0, equalsIndex);
const inlineValue = equalsIndex === -1 ? undefined : rawKey.slice(equalsIndex + 1);
if (key === 'json') {
rejectBooleanValue(key, inlineValue);
options.json = true;
} else if (key === 'verify') {
rejectBooleanValue(key, inlineValue);
options.verify = true;
} else if (key === 'compact') {
rejectBooleanValue(key, inlineValue);
options.compact = true;
} else if (key === 'help') {
rejectBooleanValue(key, inlineValue);
options.help = true;
} else if (key === 'version') {
rejectBooleanValue(key, inlineValue);
options.version = true;
} else if (key === 'allow-plaintext') {
rejectBooleanValue(key, inlineValue);
options.allowPlaintext = true;
} else if (key === 'from-stdin') {
rejectBooleanValue(key, inlineValue);
flags[key] = true;
} else if (key === 'anonymous') {
rejectBooleanValue(key, inlineValue);
options.anonymous = true;
} else if (key === 'revoke-environment-token') {
rejectBooleanValue(key, inlineValue);
options.revokeEnvironmentToken = true;
} else if (key === 'base-url') {
options.baseUrl = args[++i];
} else if (key.includes('=')) {
const [k, v] = key.split('=', 2);
flags[k] = v;
const parsed = readOptionValue(args, i, key, inlineValue);
options.baseUrl = parsed.value;
i = parsed.index;
} else if (key === 'timeout-ms') {
const parsed = readOptionValue(args, i, key, inlineValue);
options.timeoutMs = parsed.value;
i = parsed.index;
} else {
// next arg is the value
flags[key] = args[++i];
const parsed = readOptionValue(args, i, key, inlineValue);
flags[key] = parsed.value;
i = parsed.index;
}

@@ -54,2 +133,6 @@ } else if (arg.startsWith('-')) {

options.verify = true;
} else if (key === 'h') {
options.help = true;
} else {
throw new Error(`Unknown short option: -${key}`);
}

@@ -63,4 +146,258 @@ } else {

function rejectBooleanValue(key, inlineValue) {
if (inlineValue !== undefined) {
throw new Error(`--${key} does not accept a value; pass it as a bare flag.`);
}
}
function readOptionValue(args, index, key, inlineValue) {
if (inlineValue !== undefined) {
if (!inlineValue) throw new Error(`--${key} requires a value.`);
return { value: inlineValue, index };
}
const value = args[index + 1];
if (value === undefined || value.startsWith('--')) {
throw new Error(`--${key} requires a value.`);
}
return { value, index: index + 1 };
}
function printUsage(command) {
const commonOptions = '[--json] [--base-url <url>] [--timeout-ms <ms>]';
if (command === 'auth') {
console.log(`Usage:\n ${SCRIPT_COMMAND} auth status [--verify] ${commonOptions}\n ${SCRIPT_COMMAND} auth add --from-stdin --allow-plaintext\n ${SCRIPT_COMMAND} auth claim-status [--allow-plaintext]\n ${SCRIPT_COMMAND} auth claim-confirm [--allow-plaintext]\n ${SCRIPT_COMMAND} auth claim-deny [--allow-plaintext]\n\nAlias: ${SCRIPT_COMMAND} auth-status [--verify]\nXMEMO_KEY remains the preferred non-file credential source. --allow-plaintext explicitly permits unencrypted user-file storage.\nRun \`${SCRIPT_COMMAND} --help\` to list all commands.`);
return;
}
const directUsage = {
login: `login --allow-plaintext ${commonOptions}`,
register: `register --reason <unattended|declined> --allow-plaintext ${commonOptions}`,
logout: `logout [--revoke-environment-token] ${commonOptions}`,
};
if (directUsage[command]) {
console.log(`Usage:\n ${SCRIPT_COMMAND} ${directUsage[command]}`);
if (command === 'logout') {
console.log('\nXMEMO_KEY is externally managed and is not revoked unless --revoke-environment-token is explicitly passed.');
}
return;
}
if (REST_COMMANDS.has(command)) {
const commandUsage = {
remember: 'remember --content <text> [--path <path>] [--metadata <json-object>]',
recall: 'recall --query <text> [--limit <n>] [--explain <true|false>] [--prefer_working <true|false>] [--compact]',
search: 'search --query <text> [--limit <n>] [--explain <true|false>] [--prefer_working <true|false>] [--compact]',
'save-state': 'save-state --key <key> [--content <text>] [--ttl_seconds <0..604800>]',
'restore-state': 'restore-state --key <key>',
'state-save': 'state-save --key <key> [--content <text>] [--ttl_seconds <0..604800>] (legacy alias)',
'state-restore': 'state-restore --key <key> (legacy alias)',
'todo-add': 'todo-add --content <text>',
'todo-list': 'todo-list',
'todo-done': 'todo-done --id <todo_id>',
'expense-add': 'expense-add --item <text> --amount <number> --currency <code>',
doctor: 'doctor [--anonymous]',
};
console.log(`Usage:\n ${SCRIPT_COMMAND} ${commandUsage[command]} ${commonOptions}`);
return;
}
console.log(`XMemo Standalone Skill Runtime\n\nUsage:\n ${SCRIPT_COMMAND} <command> [options]\n\nCommands:\n login --allow-plaintext Start formal device login and explicitly permit local token storage\n register --reason <unattended|declined> --allow-plaintext\n Start limited temporary memory only when formal login is unavailable\n logout Revoke and remove a local credential\n auth status [--verify] Show local or verified auth status\n auth-status [--verify] Alias for auth status\n auth add --from-stdin --allow-plaintext\n Store a formal token read from standard input\n auth claim-status [--allow-plaintext]\n Check temporary-account claim status\n auth claim-confirm [--allow-plaintext]\n Confirm a pending human claim and accept formal token handoff\n auth claim-deny [--allow-plaintext]\n Decline a pending bind and keep isolated temporary access\n remember --content <text> --path <path>\n recall --query <text> [--limit <n>] [--compact]\n search --query <text> [--limit <n>] [--compact]\n save-state --key <key> [--content <text>] (aliases: state-save)\n restore-state --key <key> (aliases: state-restore)\n todo-add --content <text>\n todo-list\n todo-done --id <todo_id>\n expense-add --item <text> --amount <number> --currency <code>\n doctor [--anonymous]\n\nCredential resolution:\n XMEMO_KEY Preferred; never copied to the local credential file\n User credential file Read only as a fallback\n\nGlobal options:\n --json Print the API response as JSON\n --base-url <url> Override ${DEFAULT_BASE_URL}; HTTPS or loopback HTTP only\n --timeout-ms <ms> Per-request timeout (default: ${DEFAULT_TIMEOUT_MS})\n --compact Shorten recall/search content for terminals\n --allow-plaintext Explicitly permit unencrypted user-file credential storage\n --version Show the Skill runtime version\n --help, -h Show this help\n\nRun \`${SCRIPT_COMMAND} <command> --help\` for command-specific usage.`);
}
function parsePositiveInteger(value, name, max = Number.MAX_SAFE_INTEGER) {
if (!/^\d+$/.test(String(value ?? ''))) {
throw new Error(`${name} must be a positive integer.`);
}
const parsed = Number(value);
if (!Number.isSafeInteger(parsed) || parsed <= 0 || parsed > max) {
throw new Error(`${name} must be between 1 and ${max}.`);
}
return parsed;
}
function parseIntegerInRange(value, name, min, max) {
if (!/^\d+$/.test(String(value ?? ''))) {
throw new Error(`${name} must be an integer between ${min} and ${max}.`);
}
const parsed = Number(value);
if (!Number.isSafeInteger(parsed) || parsed < min || parsed > max) {
throw new Error(`${name} must be between ${min} and ${max}.`);
}
return parsed;
}
function parseJsonObject(value, name) {
let parsed;
try {
parsed = JSON.parse(String(value));
} catch {
throw new Error(`${name} must be a valid JSON object.`);
}
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error(`${name} must be a JSON object.`);
}
return parsed;
}
function parseStrictBoolean(value, name) {
if (value === true || value === 'true') return true;
if (value === false || value === 'false') return false;
throw new Error(`${name} must be true or false.`);
}
function isLoopbackHostname(hostname) {
const normalized = String(hostname || '').toLowerCase();
return normalized === 'localhost'
|| normalized === '127.0.0.1'
|| normalized === '::1'
|| normalized === '[::1]';
}
function normalizeBaseUrl(value) {
let url;
try {
url = new URL(value);
} catch {
throw new Error(`Invalid XMemo base URL: ${value}`);
}
if (url.username || url.password) {
throw new Error('XMemo base URL must not contain embedded credentials.');
}
if (url.protocol !== 'https:' && !(url.protocol === 'http:' && isLoopbackHostname(url.hostname))) {
throw new Error('XMemo base URL must use HTTPS. Plain HTTP is allowed only for localhost/loopback development.');
}
url.hash = '';
url.search = '';
return url.toString().replace(/\/$/, '');
}
function validateCommandInput(command, subcommand, positionals, options, flags) {
const expectedPositionals = command === 'auth' ? 2 : 1;
if (positionals.length > expectedPositionals) {
throw new Error(`Unexpected positional argument: ${positionals[expectedPositionals]}`);
}
if (options.anonymous && command !== 'doctor') {
throw new Error('--anonymous is supported only by doctor.');
}
if (options.revokeEnvironmentToken && command !== 'logout') {
throw new Error('--revoke-environment-token is supported only by logout.');
}
const allowedFlags = command === 'auth'
? AUTH_FLAGS[subcommand] || new Set()
: COMMAND_FLAGS[command] || new Set();
for (const key of Object.keys(flags)) {
if (/token|api[-_]?key|bearer|authorization|cookie|secret/i.test(key) && key !== 'from-stdin') {
throw new Error(`Refusing sensitive command-line option --${key}. Use XMEMO_KEY or --from-stdin where documented.`);
}
if (!allowedFlags.has(key)) {
throw new Error(`Unknown option for ${command}${subcommand ? ` ${subcommand}` : ''}: --${key}`);
}
}
const required = {
remember: ['content'],
recall: ['query'],
search: ['query'],
'todo-add': ['content'],
'todo-done': ['id|todo_id'],
'expense-add': ['item', 'amount'],
};
for (const requirement of required[command] || []) {
const alternatives = requirement.split('|');
if (!alternatives.some((key) => flags[key] !== undefined && String(flags[key]).trim())) {
throw new Error(`${command} requires --${alternatives.join(' or --')}.`);
}
}
if (flags.limit !== undefined) parsePositiveInteger(flags.limit, '--limit', 100);
if (flags.ttl_seconds !== undefined) parseIntegerInRange(flags.ttl_seconds, '--ttl_seconds', 0, 604_800);
if (flags.metadata !== undefined) flags.metadata = parseJsonObject(flags.metadata, '--metadata');
if (flags.explain !== undefined) flags.explain = parseStrictBoolean(flags.explain, '--explain');
if (flags.prefer_working !== undefined) flags.prefer_working = parseStrictBoolean(flags.prefer_working, '--prefer_working');
if (flags.threshold !== undefined) {
const threshold = Number(flags.threshold);
if (!Number.isFinite(threshold) || threshold < 0 || threshold > 1) {
throw new Error('--threshold must be a number between 0 and 1.');
}
}
if (flags.amount !== undefined && !Number.isFinite(Number(flags.amount))) {
throw new Error('--amount must be numeric.');
}
}
function parseJsonResponse(res, context) {
const body = typeof res.body === 'string' ? res.body.trim() : '';
if (!body) {
throw new Error(`${context}: server returned an empty response (HTTP ${res.statusCode}).`);
}
try {
return JSON.parse(body);
} catch {
const safeBody = sanitizeTerminalText(body);
const preview = safeBody.length > 2_000 ? `${safeBody.slice(0, 2_000)}…` : safeBody;
throw new Error(`${context}: server returned a non-JSON response (HTTP ${res.statusCode}): ${preview}`);
}
}
function extractList(result) {
if (Array.isArray(result)) return result;
if (Array.isArray(result?.results)) return result.results;
if (Array.isArray(result?.todos)) return result.todos;
return [];
}
function extractId(result) {
if (typeof result === 'string') return result;
if (result?.id) return result.id;
if (result?.memory_id) return result.memory_id;
return JSON.stringify(result) ?? String(result ?? '');
}
function apiErrorMessage(data, fallback = 'Operation failed') {
const candidate = data?.error?.message || data?.error_description || data?.detail || data?.error;
if (typeof candidate === 'string') return sanitizeTerminalText(candidate);
if (candidate !== undefined && candidate !== null) return safeJson(candidate);
return fallback;
}
function redactSensitiveResponse(value) {
if (value === null || typeof value !== 'object') return value;
if (Array.isArray(value)) return value.map(redactSensitiveResponse);
const sensitiveKeys = new Set([
'access_token', 'refresh_token', 'id_token', 'temporary_token', 'formal_token',
'confirmation_token', 'pending_confirmation_token', 'device_code', 'token',
'authorization', 'api_key', 'apikey', 'cookie', 'set-cookie',
]);
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
key,
sensitiveKeys.has(key.toLowerCase()) ? '[REDACTED]' : redactSensitiveResponse(item),
]));
}
function safeJson(value) {
return JSON.stringify(redactSensitiveResponse(value));
}
function sanitizeTerminalText(value) {
return String(value ?? '')
.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '')
.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g, '');
}
function formatMemoryContent(content, compact) {
const value = sanitizeTerminalText(content);
const rendered = compact ? value.replace(/\s+/g, ' ').trim() : value;
const limit = compact ? 280 : 2_000;
return rendered.length > limit ? `${rendered.slice(0, limit)}… (truncated)` : rendered;
}
function formatDuration(seconds) {
if (seconds % 86_400 === 0) return `${seconds / 86_400} days`;
if (seconds % 3_600 === 0) return `${seconds / 3_600} hours`;
return `${seconds} seconds`;
}
// HTTP request helper
function makeHttpRequest(baseUrl, apiPath, method, body = null, headers = {}) {
function makeHttpRequest(baseUrl, apiPath, method, body = null, headers = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
return new Promise((resolve, reject) => {

@@ -82,9 +419,35 @@ try {

};
const authorizationHeader = Object.entries(reqHeaders)
.find(([key]) => key.toLowerCase() === 'authorization')?.[1];
if (authorizationHeader && url.origin !== new URL(DEFAULT_BASE_URL).origin && !warnedCredentialOrigins.has(url.origin)) {
warnedCredentialOrigins.add(url.origin);
console.error(`⚠️ Sending an XMemo credential to custom origin ${url.origin}. Continue only if this host is trusted.`);
}
let settled = false;
const settleResolve = (value) => {
if (settled) return;
settled = true;
resolve(value);
};
const settleReject = (error) => {
if (settled) return;
settled = true;
reject(error);
};
const req = client.request(url, options, (res) => {
let data = '';
let responseBytes = 0;
res.on('data', (chunk) => {
responseBytes += Buffer.byteLength(chunk);
if (responseBytes > MAX_RESPONSE_BYTES) {
const error = new Error(`Server response exceeded the ${MAX_RESPONSE_BYTES}-byte safety limit.`);
settleReject(error);
res.destroy();
return;
}
data += chunk;
});
res.on('end', () => {
resolve({
settleResolve({
statusCode: res.statusCode,

@@ -95,6 +458,9 @@ headers: res.headers,

});
res.on('error', settleReject);
res.on('aborted', () => settleReject(new Error('Server response was interrupted.')));
});
req.on('error', (err) => {
reject(err);
req.setTimeout(timeoutMs, () => {
req.destroy(new Error(`Request timed out after ${timeoutMs} ms.`));
});
req.on('error', settleReject);
if (bodyStr) {

@@ -110,6 +476,31 @@ req.write(bodyStr);

async function fetchTemporaryLimits(baseUrl, timeoutMs) {
try {
const res = await makeHttpRequest(baseUrl, '/.well-known/xmemo-agent.json', 'GET', null, {}, timeoutMs);
if (res.statusCode < 200 || res.statusCode >= 300) return { ...DEFAULT_TEMPORARY_LIMITS };
const data = parseJsonResponse(res, 'Temporary-memory policy discovery');
const limits = data?.temporary_token?.limits;
const max_items = Number(limits?.max_items);
const ttl_seconds = Number(limits?.ttl_seconds);
const max_lifetime_seconds = Number(limits?.max_lifetime_seconds);
if (![max_items, ttl_seconds, max_lifetime_seconds].every(Number.isSafeInteger)
|| max_items <= 0 || ttl_seconds <= 0 || max_lifetime_seconds <= 0) {
return { ...DEFAULT_TEMPORARY_LIMITS };
}
return { max_items, ttl_seconds, max_lifetime_seconds };
} catch {
// Discovery must not make an otherwise available registration endpoint unusable.
return { ...DEFAULT_TEMPORARY_LIMITS };
}
}
// Read credential helper
async function getStoredToken() {
const credential = await getStoredCredential();
return credential?.token || null;
}
async function getStoredCredential() {
if (process.env.XMEMO_KEY) {
return process.env.XMEMO_KEY;
return { token: process.env.XMEMO_KEY, credential_type: 'environment', storage: 'environment' };
}

@@ -119,3 +510,7 @@ try {

const parsed = JSON.parse(data);
return parsed.token || null;
if (!parsed.token) return null;
if (parsed.storage !== PLAINTEXT_STORAGE || parsed.plaintext_storage_consent !== true) {
console.error(`⚠️ Legacy plaintext XMemo credential detected at ${credentialsPath}. Rotate it with XMEMO_KEY, or explicitly recreate it with --allow-plaintext.`);
}
return parsed;
} catch {

@@ -126,12 +521,165 @@ return null;

// Save credential helper
async function saveToken(token) {
await fs.mkdir(path.dirname(credentialsPath), { recursive: true });
async function bestEffortChmod(targetPath, mode) {
try {
await fs.chmod(targetPath, mode);
} catch {
// Some platforms do not implement POSIX permission bits. Never claim this is encryption.
}
}
function plaintextStorageAllowed(options, credential = null) {
return options?.allowPlaintext === true
|| (credential?.storage === PLAINTEXT_STORAGE && credential?.plaintext_storage_consent === true);
}
function requirePlaintextStorageConsent(options, action) {
if (options?.allowPlaintext === true) return;
throw new Error(`${action} needs to persist a token between commands. XMEMO_KEY is preferred and is never copied to disk. To explicitly permit unencrypted storage in ${credentialsPath}, rerun with --allow-plaintext.`);
}
function warnPlaintextStorage() {
console.error(`⚠️ Plaintext credential storage explicitly enabled. The token will be stored unencrypted at ${credentialsPath} and may be read by processes running as your OS user. Prefer XMEMO_KEY or a managed secret store; never share or commit this file.`);
}
// Save credential helper. Every caller must prove explicit consent or carry forward recorded consent.
async function saveToken(token, details = {}, { allowPlaintext = false, warn = false } = {}) {
if (!allowPlaintext) {
throw new Error(`Refusing unencrypted credential storage without --allow-plaintext. Prefer XMEMO_KEY.`);
}
if (warn) warnPlaintextStorage();
const credentialDir = path.dirname(credentialsPath);
await fs.mkdir(credentialDir, { recursive: true, mode: 0o700 });
await bestEffortChmod(credentialDir, 0o700);
const {
token: _discardToken,
created_at: _discardCreatedAt,
storage: _discardStorage,
plaintext_storage_consent: _discardConsent,
plaintext_storage_consent_at: _discardConsentAt,
claim_code: _discardClaimCode,
...safeDetails
} = details;
const data = JSON.stringify({
token,
created_at: new Date().toISOString(),
credential_type: 'formal',
...safeDetails,
storage: PLAINTEXT_STORAGE,
plaintext_storage_consent: true,
plaintext_storage_consent_at: new Date().toISOString(),
}, null, 2);
await fs.writeFile(credentialsPath, data, 'utf8');
await fs.writeFile(credentialsPath, `${data}\n`, { encoding: 'utf8', mode: 0o600 });
await bestEffortChmod(credentialsPath, 0o600);
}
async function getInstallationFingerprint() {
try {
const data = JSON.parse(await fs.readFile(registrationPath, 'utf8'));
if (typeof data.installation_fingerprint === 'string' && data.installation_fingerprint) {
return data.installation_fingerprint;
}
} catch {
// Create a non-secret stable ID below when no local registration file exists.
}
const installation_fingerprint = randomUUID();
const registrationDir = path.dirname(registrationPath);
await fs.mkdir(registrationDir, { recursive: true, mode: 0o700 });
await bestEffortChmod(registrationDir, 0o700);
await fs.writeFile(registrationPath, `${JSON.stringify({ installation_fingerprint, created_at: new Date().toISOString() }, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
await bestEffortChmod(registrationPath, 0o600);
return installation_fingerprint;
}
function printMemoryResults(result, compact) {
const results = extractList(result);
if (results.length === 0) {
console.log('No matching memories found.');
return;
}
results.forEach((item, index) => {
console.log(`[${index + 1}] ID: ${sanitizeTerminalText(item?.id || item?.memory_id || '(unknown)')} | Path: ${sanitizeTerminalText(item?.path || '(unknown)')}`);
console.log(`Content: ${formatMemoryContent(item?.content, compact)}`);
console.log('---');
});
}
async function requestTemporaryMemoryOperation(command, options, flags, credential) {
const headers = { Authorization: `Bearer ${credential.token}` };
let res;
if (command === 'remember') {
const body = Object.fromEntries(Object.entries(flags).filter(([, value]) => value !== undefined));
body.content = flags.content || '';
body.path = flags.path || 'memories';
res = await makeHttpRequest(options.baseUrl, '/v1/remember', 'POST', body, headers, options.timeoutMs);
} else {
const params = new URLSearchParams({ query: flags.query || '', limit: String(flags.limit || 5) });
for (const key of ['threshold', 'path', 'bucket', 'scope', 'team_id', 'memory_type', 'explain', 'prefer_working']) {
if (flags[key] !== undefined) params.set(key, String(flags[key]));
}
const apiPath = command === 'search' ? '/v1/memories/search' : '/v1/recall';
res = await makeHttpRequest(options.baseUrl, `${apiPath}?${params}`, 'GET', null, headers, options.timeoutMs);
}
const data = parseJsonResponse(res, `Temporary ${command} request`);
if (res.statusCode < 200 || res.statusCode >= 300) {
const challenge = data?.detail;
if (res.statusCode === 428 && challenge?.errorType === 'binding_confirmation_required') {
const allowPlaintext = plaintextStorageAllowed(options, credential);
const pending = {
credential_type: 'temporary',
agent_id: credential.agent_id,
bind_url: credential.bind_url,
registration_reason: credential.registration_reason,
pending_confirmation_token: challenge.confirmation_token,
};
await saveToken(credential.token, pending, { allowPlaintext, warn: options.allowPlaintext && !credential.plaintext_storage_consent });
if (options.json) {
console.log(safeJson(data));
} else {
console.error('Your human account has a pending bind confirmation. Run "auth claim-confirm" to finish the formal-token handoff. Do not share the bind URL or confirmation value.');
}
} else {
console.error(`Temporary ${command} failed: ${apiErrorMessage(data, safeJson(data))}`);
}
process.exit(1);
}
if (options.json) {
console.log(safeJson(data));
process.exit(0);
}
if (command === 'remember') {
console.log(`✅ Saved to temporary XMemo memory.\nID: ${sanitizeTerminalText(extractId(data.result || data))}`);
} else {
printMemoryResults(data.result || data, options.compact);
}
}
async function claimStatus(baseUrl, credential, options) {
const res = await makeHttpRequest(baseUrl, '/v1/agents/status', 'GET', null, {
Authorization: `Bearer ${credential.token}`,
}, options.timeoutMs);
const data = parseJsonResponse(res, 'Claim status request');
if (res.statusCode < 200 || res.statusCode >= 300) {
throw new Error(`Claim status request failed: ${apiErrorMessage(data, safeJson(data))}`);
}
if (typeof data.formal_token === 'string' && data.formal_token) {
const allowPlaintext = plaintextStorageAllowed(options, credential);
await saveToken(data.formal_token, { credential_type: 'formal', agent_id: credential.agent_id }, {
allowPlaintext,
warn: options.allowPlaintext && !credential.plaintext_storage_consent,
});
console.log('✅ Formal XMemo credential received and stored in the explicitly approved user credential file. Temporary access has been replaced.');
return data;
}
if (options.json) {
console.log(safeJson(data));
} else {
console.log(`Claim status: ${sanitizeTerminalText(data.status || 'unknown')}`);
}
return data;
}
// Read stdin helper

@@ -152,12 +700,39 @@ async function readStdin() {

async function main() {
const { command, subcommand, options, flags } = parseArgs(process.argv.slice(2));
let { command, subcommand, positionals, options, flags } = parseArgs(process.argv.slice(2));
if (command === 'auth-status') {
command = 'auth';
subcommand = 'status';
positionals = ['auth', 'status', ...positionals.slice(1)];
}
if (options.help) {
printUsage(command);
process.exit(0);
}
if (options.version) {
console.log(SKILL_VERSION);
process.exit(0);
}
if (!command) {
console.log('XMemo Standalone Skill Runtime. Use one of: login, logout, auth status, auth add, remember, recall, search, state-save, state-restore, todo-add, todo-list, todo-done, expense-add, doctor.');
printUsage();
process.exit(0);
}
if (!['login', 'register', 'logout', 'auth'].includes(command) && !REST_COMMANDS.has(command)) {
console.error(`Unknown command: ${command}`);
printUsage();
process.exit(1);
}
options.baseUrl = normalizeBaseUrl(options.baseUrl);
options.timeoutMs = parsePositiveInteger(options.timeoutMs, '--timeout-ms', MAX_TIMEOUT_MS);
validateCommandInput(command, subcommand, positionals, options, flags);
// 1. LOGIN
if (command === 'login') {
try {
requirePlaintextStorageConsent(options, 'Device login');
const res = await makeHttpRequest(options.baseUrl, '/v1/auth/device/start', 'POST', {

@@ -169,17 +744,34 @@ client_id: 'xmemo-skill',

scopes: ['memory:read', 'memory:write', 'memory:restore', 'ledger:write', 'ledger:read']
});
}, {}, options.timeoutMs);
const data = parseJsonResponse(res, 'Device login start');
if (res.statusCode !== 200) {
console.error('Failed to start device login:', res.body);
console.error(`Failed to start device login: ${apiErrorMessage(data, safeJson(data))}`);
process.exit(1);
}
const data = JSON.parse(res.body);
const verificationUrl = data.verification_uri_complete || data.verification_uri;
if (!data.device_code || !verificationUrl) {
console.error('Failed to start device login: the service response omitted the device code or verification URL.');
process.exit(1);
}
console.log(`To verify this device, open the following URL in your browser:\n`);
console.log(` ${data.verification_uri_complete}\n`);
console.log(`Or enter the code: ${data.user_code}`);
console.log(` ${sanitizeTerminalText(verificationUrl)}\n`);
console.log(`Or enter the code: ${sanitizeTerminalText(data.user_code)}`);
console.log(`\nWaiting for authorization...`);
const deviceCode = data.device_code;
const interval = (data.interval || 5) * 1000;
const intervalSeconds = Number(data.interval);
const expiresInSeconds = Number(data.expires_in);
let pollInterval = Number.isFinite(intervalSeconds) && intervalSeconds > 0
? Math.max(1, intervalSeconds * 1000)
: 5000;
const expiresInMs = Number.isFinite(expiresInSeconds) && expiresInSeconds > 0
? Math.max(1, expiresInSeconds * 1000)
: 600_000;
const loginDeadline = Date.now() + expiresInMs;
const poll = async () => {
if (Date.now() >= loginDeadline) {
console.error('Login failed: the device authorization code expired before approval.');
process.exit(1);
}
try {

@@ -189,11 +781,12 @@ const pollRes = await makeHttpRequest(options.baseUrl, '/v1/auth/device/token', 'POST', {

grant_type: 'urn:ietf:params:oauth:grant-type:device_code'
});
const pollData = JSON.parse(pollRes.body);
}, {}, options.timeoutMs);
const pollData = parseJsonResponse(pollRes, 'Device login polling');
if (pollData.error) {
if (pollData.error === 'authorization_pending') {
setTimeout(poll, interval);
setTimeout(poll, Math.min(pollInterval, Math.max(1, loginDeadline - Date.now())));
} else if (pollData.error === 'slow_down') {
setTimeout(poll, interval + 5000);
pollInterval += 5000;
setTimeout(poll, Math.min(pollInterval, Math.max(1, loginDeadline - Date.now())));
} else {
console.error(`Login failed: ${pollData.error_description || pollData.error}`);
console.error(`Login failed: ${sanitizeTerminalText(pollData.error_description || pollData.error)}`);
process.exit(1);

@@ -203,4 +796,5 @@ }

try {
await saveToken(pollData.access_token);
console.log('✅ Authorization successful. Credentials stored securely.');
await saveToken(pollData.access_token, { credential_type: 'formal' }, { allowPlaintext: options.allowPlaintext, warn: true });
console.log(`✅ Authorization successful. Token stored in the explicitly approved user credential file: ${credentialsPath}`);
console.log('Token value was not printed. Project files were not modified.');
process.exit(0);

@@ -211,9 +805,16 @@ } catch (err) {

}
} else {
console.error('Login failed: the token endpoint returned neither an access token nor a recognized pending status.');
process.exit(1);
}
} catch (e) {
console.error('Network error during polling:', e.message);
setTimeout(poll, interval);
if (Date.now() >= loginDeadline) {
console.error('Login failed: the device authorization window expired after repeated polling errors.');
process.exit(1);
}
console.error('Login polling error:', e.message);
setTimeout(poll, Math.min(pollInterval, Math.max(1, loginDeadline - Date.now())));
}
};
setTimeout(poll, interval);
setTimeout(poll, Math.min(pollInterval, expiresInMs));
} catch (e) {

@@ -226,5 +827,57 @@ console.error('Login error:', e.message);

// 1b. LIMITED NO-ACCOUNT-START REGISTRATION (explicit fallback only)
if (command === 'register') {
const reason = flags.reason;
if (!['unattended', 'declined'].includes(reason)) {
console.error(`Temporary registration is a conditional fallback. Use "${SCRIPT_COMMAND} register --reason unattended --allow-plaintext" when no human can log in, or "--reason declined --allow-plaintext" after the human explicitly declines formal registration.`);
process.exit(1);
}
try {
requirePlaintextStorageConsent(options, 'Temporary registration');
} catch (e) {
console.error(`Temporary registration refused: ${e.message}`);
process.exit(1);
}
if (await getStoredToken()) {
console.error(`A credential is already configured. Formal login is the recommended path; use "${SCRIPT_COMMAND} login" to refresh it instead of creating temporary access.`);
process.exit(1);
}
try {
const limits = await fetchTemporaryLimits(options.baseUrl, options.timeoutMs);
const installation_fingerprint = await getInstallationFingerprint();
const res = await makeHttpRequest(options.baseUrl, '/v1/agents/register', 'POST', {
entry_type: 'skill',
client_name: 'xmemo-skill',
client_version: SKILL_VERSION,
installation_fingerprint,
runtime: `node ${process.version}`,
skill_package_id: 'xmemo-memory',
metadata: { registration_reason: reason },
}, {}, options.timeoutMs);
const data = parseJsonResponse(res, 'Temporary registration');
if (res.statusCode < 200 || res.statusCode >= 300 || !data.temporary_token) {
throw new Error(apiErrorMessage(data, safeJson(data)));
}
await saveToken(data.temporary_token, {
credential_type: 'temporary',
agent_id: data.agent_id,
bind_url: data.bind_url,
registration_reason: reason,
}, { allowPlaintext: options.allowPlaintext, warn: true });
if (options.json) {
console.log(safeJson({ agent_id: data.agent_id, bind_url: data.bind_url, status: data.status, limits }));
} else {
console.log(`✅ Temporary XMemo memory enabled for this installation.\nThis is a limited sandbox, not a formal account.\nTemporary limits: up to ${limits.max_items} items; expires after ${formatDuration(limits.ttl_seconds)} without successful memory activity; maximum ${formatDuration(limits.max_lifetime_seconds)} from registration.\nComplete formal registration (recommended): ${sanitizeTerminalText(data.bind_url)}\nDo not share this bind URL publicly. After the human claim, run "${SCRIPT_COMMAND} auth claim-confirm" to accept the formal credential.`);
}
process.exit(0);
} catch (e) {
console.error('Temporary registration failed:', e.message);
process.exit(1);
}
}
// 2. LOGOUT
if (command === 'logout') {
const token = await getStoredToken();
const credential = await getStoredCredential();
const token = credential?.token;
if (!token) {

@@ -234,13 +887,59 @@ console.log('No active login found.');

}
if (credential.storage === 'environment' && !options.revokeEnvironmentToken) {
const result = {
status: 'environment_credential_unchanged',
credential_source: 'XMEMO_KEY',
remote_revoked: false,
local_file_removed: false,
};
if (options.json) {
console.log(safeJson(result));
} else {
console.log('XMEMO_KEY is externally managed. No token was revoked and no local credential file was changed.');
console.log('Unset XMEMO_KEY in the launching environment to log out, or pass --revoke-environment-token to explicitly revoke that token.');
}
process.exit(0);
}
let remoteRevoked = false;
let revokeError = null;
try {
await makeHttpRequest(options.baseUrl, '/v1/auth/token/revoke-self', 'POST', {}, {
const revokeRes = await makeHttpRequest(options.baseUrl, '/v1/auth/token/revoke-self', 'POST', {}, {
'Authorization': `Bearer ${token}`
});
} catch {
// Ignored: delete local credentials anyway
}, options.timeoutMs);
remoteRevoked = revokeRes.statusCode >= 200 && revokeRes.statusCode < 300;
if (!remoteRevoked) revokeError = `HTTP ${revokeRes.statusCode}`;
} catch (error) {
revokeError = sanitizeTerminalText(error.message);
}
try {
await fs.unlink(credentialsPath);
} catch {}
console.log('✅ Logged out successfully.');
let localFileRemoved = false;
if (credential.storage !== 'environment') {
try {
await fs.unlink(credentialsPath);
localFileRemoved = true;
} catch (error) {
if (error?.code !== 'ENOENT') throw error;
}
}
const result = {
status: remoteRevoked ? 'logged_out' : 'local_logout_completed',
credential_source: credential.storage === 'environment' ? 'XMEMO_KEY' : 'user-credential-file',
remote_revoked: remoteRevoked,
local_file_removed: localFileRemoved,
...(revokeError ? { remote_revoke_error: revokeError } : {}),
};
if (options.json) {
console.log(safeJson(result));
} else if (credential.storage === 'environment') {
console.log(remoteRevoked
? '✅ The externally managed XMEMO_KEY token was explicitly revoked. Unset XMEMO_KEY in the launching environment.'
: `The XMEMO_KEY token could not be revoked (${revokeError}). It remains externally managed.`);
} else if (remoteRevoked) {
console.log('✅ Logged out successfully. The remote token was revoked and the local credential file was removed.');
} else {
console.log(`Local credential file removed. Remote revocation could not be confirmed${revokeError ? ` (${revokeError})` : ''}.`);
}
process.exit(0);

@@ -252,3 +951,4 @@ }

if (subcommand === 'status') {
const token = await getStoredToken();
const credential = await getStoredCredential();
const token = credential?.token;
if (!token) {

@@ -263,3 +963,7 @@ if (options.json) {

const maskedToken = token.includes(':') ? `${token.split(':')[0]}:***` : '***';
const credentialSource = credential?.storage === 'environment'
? 'XMEMO_KEY'
: credential?.credential_type === 'temporary'
? 'temporary-user-credential-file'
: 'formal-user-credential-file';
if (options.verify) {

@@ -269,15 +973,16 @@ try {

'Authorization': `Bearer ${token}`
});
}, options.timeoutMs);
const data = parseJsonResponse(res, 'Token verification');
if (res.statusCode === 200) {
const data = JSON.parse(res.body);
if (options.json) {
console.log(JSON.stringify({ status: 'valid', scopes: data.scopes, setup_state: data.setup_state }));
console.log(safeJson({ status: 'valid', credential_source: credentialSource, scopes: data.scopes, setup_state: data.setup_state }));
} else {
console.log(`Status: Logged in (verified)\nToken Prefix: ${maskedToken}\nScopes: ${data.scopes.join(', ')}`);
const scopes = Array.isArray(data.scopes) ? data.scopes : [];
console.log(`Status: Logged in (verified)\nCredential Source: ${credentialSource}\nScopes: ${scopes.join(', ')}`);
}
} else {
if (options.json) {
console.log(JSON.stringify({ status: 'invalid' }));
console.log(safeJson({ status: 'invalid', credential_source: credentialSource }));
} else {
console.error('Status: Invalid or expired token.');
console.error(`Status: Invalid or expired token.${data ? ` ${apiErrorMessage(data, '')}` : ''}`);
}

@@ -292,5 +997,6 @@ process.exit(1);

if (options.json) {
console.log(JSON.stringify({ status: 'logged_in', token_prefix: maskedToken }));
console.log(safeJson({ status: 'logged_in', credential_source: credentialSource }));
} else {
console.log(`Status: Logged in\nToken Prefix: ${maskedToken}`);
const kind = credential?.credential_type === 'temporary' ? 'Temporary access' : 'Logged in';
console.log(`Status: ${kind}\nCredential Source: ${credentialSource}`);
}

@@ -303,2 +1009,8 @@ }

if (flags['from-stdin'] !== undefined || process.argv.includes('--from-stdin')) {
try {
requirePlaintextStorageConsent(options, 'auth add');
} catch (e) {
console.error(`Credential storage refused: ${e.message}`);
process.exit(1);
}
const token = await readStdin();

@@ -310,4 +1022,5 @@ if (!token) {

try {
await saveToken(token);
console.log('✅ Credentials saved.');
await saveToken(token, { credential_type: 'formal' }, { allowPlaintext: options.allowPlaintext, warn: true });
console.log(`✅ Credential stored in the explicitly approved user credential file: ${credentialsPath}`);
console.log('Token value was not printed. Project files were not modified.');
process.exit(0);

@@ -319,8 +1032,70 @@ } catch (err) {

} else {
console.error('Error: Run "auth add --from-stdin" to supply token.');
console.error(`Error: Run "${SCRIPT_COMMAND} auth add --from-stdin --allow-plaintext" to supply and explicitly store a token.`);
process.exit(1);
}
}
if (subcommand === 'claim-status' || subcommand === 'claim-confirm' || subcommand === 'claim-deny') {
const credential = await getStoredCredential();
if (!credential?.token || credential.credential_type !== 'temporary') {
console.error('Error: Claim commands require a locally stored temporary credential from "register".');
process.exit(1);
}
try {
if (subcommand === 'claim-deny') {
const denyRes = await makeHttpRequest(options.baseUrl, '/v1/agents/bind/deny-current-user', 'POST', {}, {
Authorization: `Bearer ${credential.token}`,
}, options.timeoutMs);
const denyData = parseJsonResponse(denyRes, 'Claim denial');
if (denyRes.statusCode < 200 || denyRes.statusCode >= 300) {
throw new Error(apiErrorMessage(denyData, safeJson(denyData)));
}
const allowPlaintext = plaintextStorageAllowed(options, credential);
await saveToken(credential.token, {
credential_type: 'temporary',
agent_id: credential.agent_id,
bind_url: credential.bind_url,
registration_reason: credential.registration_reason,
}, { allowPlaintext, warn: options.allowPlaintext && !credential.plaintext_storage_consent });
if (options.json) {
console.log(safeJson(denyData));
} else {
console.log('Pending account binding declined. The credential remains limited to isolated temporary memory; formal account login is still recommended.');
}
process.exit(0);
}
const status = await claimStatus(options.baseUrl, credential, options);
if (subcommand === 'claim-confirm' && !status.formal_token) {
const confirmation_token = status.confirmation_token || credential.pending_confirmation_token;
if (!confirmation_token) {
console.error(`No pending human claim confirmation is available. Current status: ${sanitizeTerminalText(status.status || 'unknown')}. Open the stored bind URL first: ${sanitizeTerminalText(credential.bind_url || '(unavailable)')}`);
process.exit(1);
}
const confirmRes = await makeHttpRequest(options.baseUrl, '/v1/agents/bind/confirm-current-user', 'POST', { confirmation_token }, {
Authorization: `Bearer ${credential.token}`,
}, options.timeoutMs);
const confirmData = parseJsonResponse(confirmRes, 'Claim confirmation');
if (confirmRes.statusCode < 200 || confirmRes.statusCode >= 300) {
throw new Error(apiErrorMessage(confirmData, safeJson(confirmData)));
}
if (credential.pending_confirmation_token) {
const allowPlaintext = plaintextStorageAllowed(options, credential);
await saveToken(credential.token, {
credential_type: 'temporary',
agent_id: credential.agent_id,
bind_url: credential.bind_url,
registration_reason: credential.registration_reason,
}, { allowPlaintext, warn: options.allowPlaintext && !credential.plaintext_storage_consent });
}
await claimStatus(options.baseUrl, credential, options);
}
process.exit(0);
} catch (e) {
console.error('Claim flow failed:', e.message);
process.exit(1);
}
}
console.error('Unknown auth subcommand. Use "status" or "add".');
console.error(`Unknown auth subcommand: ${subcommand || '(missing)'}`);
printUsage('auth');
process.exit(1);

@@ -330,3 +1105,4 @@ }

// 4. REST OPERATIONS (remember, recall, search, update, forget, state-save, state-restore, todo-*, expense-*, doctor)
const token = await getStoredToken();
const credential = command === 'doctor' && options.anonymous ? null : await getStoredCredential();
const token = credential?.token;

@@ -339,6 +1115,10 @@ // Doctor can be anonymous

arguments: {}
});
const data = JSON.parse(res.body);
}, {}, options.timeoutMs);
const data = parseJsonResponse(res, 'Doctor health check');
if (res.statusCode < 200 || res.statusCode >= 300 || data.ok === false) {
console.error(`Doctor health check failed: ${apiErrorMessage(data, safeJson(data))}`);
process.exit(1);
}
if (options.json) {
console.log(JSON.stringify(data));
console.log(safeJson(data));
} else {

@@ -356,10 +1136,24 @@ console.log(`XMemo Service Status: OK\nAuthentication: Missing/Unauthenticated`);

if (!token) {
console.error('Error: No XMemo credential found. Please run "node xmemo-skill.mjs login" or set process.env.XMEMO_KEY.');
console.error(`Error: No XMemo credential found. Preferred: set XMEMO_KEY. For formal account login with explicit local storage consent, run "${SCRIPT_COMMAND} login --allow-plaintext". For a limited temporary sandbox only when permitted, run "${SCRIPT_COMMAND} register --reason unattended|declined --allow-plaintext".`);
process.exit(1);
}
if (credential?.credential_type === 'temporary') {
if (['remember', 'recall', 'search'].includes(command)) {
try {
await requestTemporaryMemoryOperation(command, options, flags, credential);
} catch (e) {
console.error('Temporary memory request failed:', e.message);
process.exit(1);
}
return;
}
console.error(`Temporary access supports only remember, recall, and search in its isolated sandbox. Complete formal registration at ${sanitizeTerminalText(credential.bind_url || 'the bind URL shown at registration')} to use ${command}.`);
process.exit(1);
}
// Normalize commands for operations mapping
let opName = command;
if (command === 'save-state') opName = 'state-save';
if (command === 'restore-state') opName = 'state-restore';
if (command === 'save-state' || command === 'state-save') opName = 'state-save';
if (command === 'restore-state' || command === 'state-restore') opName = 'state-restore';

@@ -372,12 +1166,13 @@ try {

'Authorization': `Bearer ${token}`
});
}, options.timeoutMs);
const data = parseJsonResponse(res, `${opName} request`);
const succeeded = res.statusCode >= 200 && res.statusCode < 300 && data.ok !== false;
if (options.json) {
console.log(res.body);
process.exit(res.statusCode === 200 ? 0 : 1);
console.log(safeJson(data));
process.exit(succeeded ? 0 : 1);
}
const data = JSON.parse(res.body);
if (!data.ok) {
console.error(`Error: ${data.error?.message || 'Operation failed'} (Code: ${data.error?.code || 'error'})`);
if (!succeeded) {
console.error(`Error: ${apiErrorMessage(data)} (Code: ${data.error?.code || `HTTP ${res.statusCode}`})`);
process.exit(1);

@@ -389,3 +1184,3 @@ }

if (isValid) {
console.log(`XMemo Service Status: OK\nAuthentication: Valid\nScopes: ${(data.result?.scopes || []).join(', ')}`);
console.log(`XMemo Service Status: OK\nAuthentication: Valid\nScopes: ${extractList(data.result?.scopes).join(', ')}`);
} else {

@@ -396,3 +1191,3 @@ console.log(`XMemo Service Status: OK\nAuthentication: Invalid`);

} else if (opName === 'recall' || opName === 'search') {
const results = data.result || [];
const results = extractList(data.result);
if (results.length === 0) {

@@ -402,4 +1197,4 @@ console.log('No matching memories found.');

results.forEach((item, index) => {
console.log(`[${index + 1}] ID: ${item.id} | Path: ${item.path}`);
console.log(`Content: ${item.content}`);
console.log(`[${index + 1}] ID: ${sanitizeTerminalText(item?.id || item?.memory_id || '(unknown)')} | Path: ${sanitizeTerminalText(item?.path || '(unknown)')}`);
console.log(`Content: ${formatMemoryContent(item?.content, options.compact)}`);
console.log(`---`);

@@ -409,3 +1204,3 @@ });

} else if (opName === 'todo-list') {
const todos = data.result || [];
const todos = extractList(data.result);
if (todos.length === 0) {

@@ -415,11 +1210,11 @@ console.log('No TODOs found.');

todos.forEach((todo) => {
console.log(`- [${todo.status === 'done' ? 'x' : ' '}] ${todo.content} (ID: ${todo.id})`);
console.log(`- [${todo?.status === 'done' ? 'x' : ' '}] ${sanitizeTerminalText(todo?.content || '')} (ID: ${sanitizeTerminalText(todo?.id || todo?.memory_id || '(unknown)')})`);
});
}
} else if (opName === 'state-restore') {
console.log(`Working State restored:\nKey: ${data.result?.state_key}\nContent: ${data.result?.content}`);
console.log(`Working State restored:\nKey: ${sanitizeTerminalText(data.result?.state_key)}\nContent: ${formatMemoryContent(data.result?.content, false)}`);
} else if (opName === 'remember') {
console.log(`✅ Saved to XMemo.\nID: ${data.result}`);
console.log(`✅ Saved to XMemo.\nID: ${sanitizeTerminalText(extractId(data.result))}`);
} else if (opName === 'expense-add') {
console.log(`✅ Expense recorded.\nID: ${data.result}`);
console.log(`✅ Expense recorded.\nID: ${sanitizeTerminalText(extractId(data.result))}`);
} else {

@@ -434,2 +1229,5 @@ console.log(`✅ Operation succeeded.`);

main();
main().catch((error) => {
console.error(`Error: ${sanitizeTerminalText(error?.message || error)}`);
process.exit(1);
});

@@ -14,9 +14,14 @@ ---

1. **Bundled Skill script** at `skills/xmemo/scripts/xmemo-skill.mjs` (Primary standalone direct REST API integration, fully self-contained and zero-dependency).
1. **Bundled Skill script** at `scripts/xmemo-skill.mjs` (primary standalone direct REST API integration, fully self-contained and zero-dependency).
2. **XMemo MCP tools** (when running in environments that natively host the XMemo MCP server).
If no credential is available, run:
Run bundled commands from the Skill root with Node.js 20 or newer.
Credential lookup always prefers the `XMEMO_KEY` environment variable. When it
is present, the script does not copy its value into a local credential file.
If no credential is available, use formal account login (recommended):
```text
node skills/xmemo/scripts/xmemo-skill.mjs login
node scripts/xmemo-skill.mjs login --allow-plaintext
```

@@ -29,8 +34,43 @@

or, if you already have a token:
The standalone zero-dependency script has no cross-platform operating-system
keychain integration. `--allow-plaintext` is therefore an explicit decision to
store the issued token unencrypted in the current user's XMemo directory so
later commands can use it. The script prints the exact path, restricts POSIX
permissions where supported, never prints the token, and never writes it into
the project. Prefer `XMEMO_KEY` or a managed secret store when plaintext local
storage is not acceptable.
Formal registration/login is the default and recommended path. It gives the
user account-backed memory and the full command set.
Only when no human can complete login (`unattended`) or the human explicitly
declines registration for now (`declined`), use the explicit temporary fallback:
```text
echo "TOKEN_VALUE" | node skills/xmemo/scripts/xmemo-skill.mjs auth add --from-stdin
node scripts/xmemo-skill.mjs register --reason unattended --allow-plaintext
```
Temporary access is an isolated, limited memory sandbox. It only supports
`remember`, `recall`, and `search`. The script reads the current public policy
before registration and immediately discloses its item cap, inactivity expiry,
and maximum lifetime (currently 100 items, 14 days of inactivity, and 30 days
from registration). Show the returned bind URL to the user and do not share
that URL publicly. Run
`node scripts/xmemo-skill.mjs auth claim-confirm` after they claim it. Temporary
and pending-confirmation values inherit the same explicit plaintext-storage
consent and are replaced or cleared during formal-token handoff.
or, if you already have a token, pipe it without putting the value in the
command line. POSIX shell:
```text
printf '%s' "$XMEMO_KEY" | node scripts/xmemo-skill.mjs auth add --from-stdin --allow-plaintext
```
PowerShell:
```powershell
$env:XMEMO_KEY | node scripts/xmemo-skill.mjs auth add --from-stdin --allow-plaintext
```
Never ask the user to paste a raw token into chat, logs, or project files.

@@ -48,4 +88,5 @@

purchase or income.
- **Confirm destructive actions.** Always confirm the exact target before
`forget`, overwrite, or broad cleanup operations.
- **Confirm destructive actions.** The bundled script does not expose memory
deletion or overwrite commands. Use an authorized product surface with an
explicit target and user confirmation if such an operation is required.
- **Read provenance correctly.** `agent_id`, `agent_instance_id`, and

@@ -57,15 +98,19 @@ `agent_boundary` are attribution signals, not authorization boundaries.

```text
node skills/xmemo/scripts/xmemo-skill.mjs remember --content "..." --path "..."
node skills/xmemo/scripts/xmemo-skill.mjs recall --query "..."
node skills/xmemo/scripts/xmemo-skill.mjs search --query "..." --limit 5
node skills/xmemo/scripts/xmemo-skill.mjs save-state --key active_task
node skills/xmemo/scripts/xmemo-skill.mjs restore-state --key active_task
node skills/xmemo/scripts/xmemo-skill.mjs todo-add --content "..."
node skills/xmemo/scripts/xmemo-skill.mjs todo-list
node skills/xmemo/scripts/xmemo-skill.mjs todo-done --id <todo_id>
node skills/xmemo/scripts/xmemo-skill.mjs expense-add --item "..." --amount 12.5 --currency USD
node skills/xmemo/scripts/xmemo-skill.mjs doctor
node scripts/xmemo-skill.mjs remember --content "..." --path "..."
node scripts/xmemo-skill.mjs recall --query "..." --compact
node scripts/xmemo-skill.mjs search --query "..." --limit 5 --compact
node scripts/xmemo-skill.mjs save-state --key active_task
node scripts/xmemo-skill.mjs restore-state --key active_task
node scripts/xmemo-skill.mjs todo-add --content "..."
node scripts/xmemo-skill.mjs todo-list
node scripts/xmemo-skill.mjs todo-done --id <todo_id>
node scripts/xmemo-skill.mjs expense-add --item "..." --amount 12.5 --currency USD
node scripts/xmemo-skill.mjs doctor
node scripts/xmemo-skill.mjs doctor --anonymous
node scripts/xmemo-skill.mjs register --reason <unattended|declined> --allow-plaintext
```
The script supports JSON output with --json. It never prints token values.
The script supports JSON output with `--json`, command-specific usage with
`--help`, `--version`, per-request timeouts with `--timeout-ms`, and compact
recall/search output with `--compact`. It never prints token values or prefixes.

@@ -77,8 +122,17 @@ ## Direct CLI Commands

```text
node skills/xmemo/scripts/xmemo-skill.mjs auth status [--verify]
node skills/xmemo/scripts/xmemo-skill.mjs auth add --from-stdin
node skills/xmemo/scripts/xmemo-skill.mjs logout
node skills/xmemo/scripts/xmemo-skill.mjs doctor
node scripts/xmemo-skill.mjs auth status [--verify]
node scripts/xmemo-skill.mjs auth-status [--verify]
node scripts/xmemo-skill.mjs auth add --from-stdin --allow-plaintext
node scripts/xmemo-skill.mjs auth claim-status [--allow-plaintext]
node scripts/xmemo-skill.mjs auth claim-confirm [--allow-plaintext]
node scripts/xmemo-skill.mjs auth claim-deny [--allow-plaintext]
node scripts/xmemo-skill.mjs logout [--revoke-environment-token]
node scripts/xmemo-skill.mjs doctor
```
`logout` revokes and removes a user credential file. When `XMEMO_KEY` supplies
the active credential, logout leaves that externally managed token unchanged
unless `--revoke-environment-token` is explicitly passed; unset the environment
variable in the launching environment to stop using it.
## Setup And Repair

@@ -89,6 +143,13 @@

```text
node skills/xmemo/scripts/xmemo-skill.mjs doctor
node skills/xmemo/scripts/xmemo-skill.mjs auth status --verify
node scripts/xmemo-skill.mjs doctor
node scripts/xmemo-skill.mjs doctor --anonymous
node scripts/xmemo-skill.mjs auth status --verify
node scripts/xmemo-skill.mjs auth-status --verify
node scripts/xmemo-skill.mjs auth claim-status
```
`doctor` retains authenticated diagnosis when a credential is available.
`doctor --anonymous` performs the same service-health check without sending an
Authorization header.
For detailed examples, read `references/operations.md`. For auth, network, and service diagnosis, read `references/troubleshooting.md`.

@@ -116,2 +177,8 @@

screenshots, repositories, issue comments, marketplace metadata, or shared logs.
- Prefer `XMEMO_KEY` or a managed secret store. Use `--allow-plaintext` only
after accepting that processes running as the same operating-system user may
read the local credential file.
- The default service is `https://xmemo.dev`. Custom HTTPS origins are supported
but receive credentials when an authenticated command runs; use only trusted
hosts. Plain HTTP is rejected except for localhost/loopback development.
- Use synthetic data for marketplace demos and screenshots.

@@ -118,0 +185,0 @@ - Do not claim a marketplace integration is certified unless there is explicit

@@ -72,2 +72,6 @@ import {

if (command === 'auth-status') {
return await authCommand(['status', ...args.slice(1)], io);
}
if (command === 'token') {

@@ -74,0 +78,0 @@ return await tokenCommand(args.slice(1), io);

@@ -10,3 +10,4 @@ import { hasFlag, optionValue, parsePositiveInteger } from '../core/args.js';

storeTokenValue,
validateToken
validateToken,
credentialsPath
} from '../network/auth.js';

@@ -24,4 +25,9 @@ import { baseUrlOption } from '../network/base-url.js';

import { readAll } from '../core/runtime.js';
import { createInterface } from 'node:readline/promises';
export async function loginCommand(args, io) {
if (hasHelpFlag(args)) {
writeLoginHelp(io);
return 0;
}
const outputJson = hasFlag(args, '--json');

@@ -35,3 +41,10 @@ const fromStdin = hasFlag(args, '--from-stdin') || hasFlag(args, '--token-stdin');

if (fromStdin) {
const result = await storeTokenFromStdin(io, { source: 'stdin' });
const consented = await authorizePlaintextStorage(args, io, {
action: 'Importing a token from stdin',
interactive: false
});
if (!consented) {
return 0;
}
const result = await storeTokenFromStdin(io, { source: 'stdin' }, { allowPlaintext: true });
if (outputJson) {

@@ -41,3 +54,4 @@ writeLine(io.stdout, JSON.stringify(result, null, 2));

writeLine(io.stdout, `${PRODUCT_NAME} login complete.`);
writeLine(io.stdout, `Stored token in user-scoped credential file: ${result.credentialPath}`);
writeLine(io.stdout, `Credential stored in the approved user file: ${result.credentialPath}`);
writeLine(io.stdout, 'Storage: unencrypted; file access is restricted to the current OS user where supported.');
writeLine(io.stdout, 'Token value was not printed. Project files were not modified.');

@@ -48,2 +62,10 @@ }

const consented = await authorizePlaintextStorage(args, io, {
action: 'Browser login',
interactive: !outputJson
});
if (!consented) {
return 0;
}
const start = await startDeviceLogin(baseUrl, httpTimeoutMs, io);

@@ -63,3 +85,8 @@ const loginTimeoutMs = loginTimeoutOption

const token = await pollDeviceLogin(baseUrl, start, loginTimeoutMs, httpTimeoutMs, io, { pollOnce });
const result = await storeTokenValue(token.accessToken, { source: 'device-login', account: token.account }, io.env);
const result = await storeTokenValue(
token.accessToken,
{ source: 'device-login', account: token.account },
io.env,
{ allowPlaintext: true }
);
const payload = {

@@ -76,9 +103,9 @@ ...result,

} else {
writeLine(io.stdout, 'Login complete. Token stored securely in the user-scoped XMemo CLI config directory.');
writeLine(io.stdout, `${PRODUCT_NAME} login complete.`);
if (token.account) {
writeLine(io.stdout, `Signed in as: ${formatAccount(token.account)}`);
}
writeLine(io.stdout, `Credential path: ${result.credentialPath}`);
writeLine(io.stdout, 'No extra token configuration is required.');
writeLine(io.stdout, `Optional check: ${COMMAND_NAME} token status --verify`);
writeLine(io.stdout, `Credential stored in the approved user file: ${result.credentialPath}`);
writeLine(io.stdout, 'Storage: unencrypted; token value was not printed.');
writeLine(io.stdout, `Optional check: ${COMMAND_NAME} auth status --verify`);
}

@@ -95,3 +122,3 @@ return 0;

writeLine(io.stdout, '');
writeLine(io.stdout, `Use \`${COMMAND_NAME} login\` to sign in and \`${COMMAND_NAME} token add --from-stdin\` to store an existing token.`);
writeLine(io.stdout, `Use \`${COMMAND_NAME} login\` to sign in and \`${COMMAND_NAME} token add --from-stdin --allow-plaintext\` to store an existing token.`);
return 0;

@@ -113,7 +140,7 @@ }

writeLine(io.stdout, ` ${COMMAND_NAME} token status [--verify]`);
writeLine(io.stdout, ` ${COMMAND_NAME} token add --from-stdin`);
writeLine(io.stdout, ` ${COMMAND_NAME} token add --from-stdin --allow-plaintext`);
writeLine(io.stdout, ` ${COMMAND_NAME} token set --from-stdin [--allow-plaintext]`);
writeLine(io.stdout, '');
writeLine(io.stdout, `${COMMAND_NAME} login is the recommended personal-user path.`);
writeLine(io.stdout, `${COMMAND_NAME} token add --from-stdin stores a token in the user-scoped XMemo CLI config directory.`);
writeLine(io.stdout, `${COMMAND_NAME} token add --from-stdin requires explicit consent to unencrypted user-file storage.`);
return 0;

@@ -130,7 +157,12 @@ }

}
const result = await storeTokenFromStdin(io, { source: 'token-add' });
await authorizePlaintextStorage(args, io, {
action: 'Adding an existing token',
interactive: false
});
const result = await storeTokenFromStdin(io, { source: 'token-add' }, { allowPlaintext: true });
if (hasFlag(args, '--json')) {
writeLine(io.stdout, JSON.stringify(result, null, 2));
} else {
writeLine(io.stdout, `Stored token in user-scoped credential file: ${result.credentialPath}`);
writeLine(io.stdout, `Credential stored in the approved user file: ${result.credentialPath}`);
writeLine(io.stdout, 'Storage: unencrypted; file access is restricted to the current OS user where supported.');
writeLine(io.stdout, 'Token value was not printed. Project files were not modified.');

@@ -145,13 +177,11 @@ }

}
await authorizePlaintextStorage(args, io, {
action: 'Setting a token',
interactive: false
});
const token = (await readAll(io.stdin)).trim();
validateToken(token);
if (!hasFlag(args, '--allow-plaintext')) {
writeLine(io.stderr, 'Token was read from stdin but was not stored.');
writeLine(io.stderr, 'Enterprise default refuses plaintext token storage without --allow-plaintext.');
writeLine(io.stderr, `Preferred personal-user path: ${COMMAND_NAME} login or ${COMMAND_NAME} token add --from-stdin.`);
return 2;
}
const result = await storeTokenValue(token, { source: 'token-set' }, io.env);
writeLine(io.stdout, `Stored token in user-scoped credential file: ${result.credentialPath}`);
const result = await storeTokenValue(token, { source: 'token-set' }, io.env, { allowPlaintext: true });
writeLine(io.stdout, `Credential stored in the approved user file: ${result.credentialPath}`);
writeLine(io.stdout, 'Storage: unencrypted; file access is restricted to the current OS user where supported.');
writeLine(io.stdout, 'Token value was not printed. Do not commit this file.');

@@ -182,3 +212,5 @@ return 0;

path: credential.path,
storage: credential.storage ?? null
storage: credential.storage ?? null,
encryption: credential.encryption ?? (hasUserCredential ? 'unknown' : null),
plaintextStorageConsent: credential.plaintextStorageConsent ?? false
},

@@ -199,3 +231,3 @@ account: credential.account ?? null,

writeCredentialStatus(report, io, { mode });
writeLine(io.stderr, `No token found. Run \`${COMMAND_NAME} login\` or \`${COMMAND_NAME} token add --from-stdin\`.`);
writeLine(io.stderr, `No token found. Run \`${COMMAND_NAME} login\` or \`${COMMAND_NAME} token add --from-stdin --allow-plaintext\`.`);
}

@@ -238,2 +270,5 @@ return 1;

writeLine(io.stdout, `User credential file: ${report.userCredentialFile.present ? 'present' : 'missing'} (${report.userCredentialFile.path})`);
if (report.userCredentialFile.present) {
writeLine(io.stdout, `Credential encryption: ${report.userCredentialFile.encryption}`);
}
if (report.account) {

@@ -245,1 +280,51 @@ writeLine(io.stdout, `Account: ${formatAccount(report.account)}`);

function hasHelpFlag(args) {
return hasFlag(args, '--help') || hasFlag(args, '-h');
}
function writeLoginHelp(io) {
writeLine(io.stdout, 'Login command:');
writeLine(io.stdout, ` ${COMMAND_NAME} login [--base-url <url>] [--allow-plaintext]`);
writeLine(io.stdout, ` ${COMMAND_NAME} login --from-stdin --allow-plaintext [--json]`);
writeLine(io.stdout, '');
writeLine(io.stdout, 'Interactive browser login asks once before storing the issued token unencrypted.');
writeLine(io.stdout, 'Use --allow-plaintext to record that consent non-interactively. XMEMO_KEY remains preferred for managed environments.');
}
async function authorizePlaintextStorage(args, io, { action, interactive }) {
const credentialPath = credentialsPath(io.env);
writeLine(io.stderr, `${action} will store the XMemo token unencrypted at:`);
writeLine(io.stderr, ` ${credentialPath}`);
writeLine(io.stderr, 'File access is restricted to the current OS user where supported. Prefer XMEMO_KEY or a managed secret store on shared systems.');
if (hasFlag(args, '--allow-plaintext')) {
return true;
}
if (!interactive) {
throw new UsageError('Unencrypted credential storage requires --allow-plaintext in non-interactive mode.');
}
let accepted;
if (typeof io.confirm === 'function') {
accepted = await io.confirm('Continue with unencrypted credential storage? [y/N] ');
} else {
if (!io.stdin?.isTTY) {
throw new UsageError('Interactive confirmation is unavailable. Re-run with --allow-plaintext after reviewing the storage notice.');
}
const prompt = createInterface({ input: io.stdin, output: io.stderr });
try {
const answer = await prompt.question('Continue with unencrypted credential storage? [y/N] ');
accepted = /^(y|yes)$/i.test(answer.trim());
} finally {
prompt.close();
}
}
if (!accepted) {
writeLine(io.stderr, 'Login cancelled. No credential was stored.');
return false;
}
return true;
}

@@ -196,3 +196,8 @@ import fs from 'node:fs/promises';

if (shouldBackfillSharedCredential && !mcpOnly) {
await storeTokenValue(credential.token, { source: 'hermes-env-sync' }, io.env);
await storeTokenValue(
credential.token,
{ source: 'hermes-env-sync', existingPlaintextSource: hermesEnvPath },
io.env,
{ allowPlaintext: true }
);
selectedClient.credential.sharedCredentialBackfilled = true;

@@ -199,0 +204,0 @@ }

@@ -90,3 +90,3 @@ import { hasFlag, optionValue, parsePositiveInteger } from '../core/args.js';

if (useLocalProxy) {
writeLine(io.stdout, `Requires credential: ${COMMAND_NAME} login or ${COMMAND_NAME} token add --from-stdin`);
writeLine(io.stdout, `Requires credential: ${COMMAND_NAME} login or ${COMMAND_NAME} token add --from-stdin --allow-plaintext`);
writeLine(io.stdout, `Run local proxy: ${template.requiresLocalCommand}`);

@@ -93,0 +93,0 @@ } else {

@@ -87,3 +87,3 @@ import {

},
requiresCredential: [`${COMMAND_NAME} login`, `${COMMAND_NAME} token add --from-stdin`],
requiresCredential: [`${COMMAND_NAME} login`, `${COMMAND_NAME} token add --from-stdin --allow-plaintext`],
requiresLocalCommand: `${COMMAND_NAME} mcp proxy --port ${new URL(proxyUrl).port || DEFAULT_PROXY_PORT}`,

@@ -90,0 +90,0 @@ agentIdentity: {

@@ -28,3 +28,3 @@ import http from 'node:http';

if (!token) {
throw new UsageError(`No token found. Run \`${COMMAND_NAME} login\` or \`${COMMAND_NAME} token add --from-stdin\` first.`);
throw new UsageError(`No token found. Run \`${COMMAND_NAME} login\` or \`${COMMAND_NAME} token add --from-stdin --allow-plaintext\` first.`);
}

@@ -113,2 +113,1 @@ validateToken(token);

}

@@ -145,3 +145,3 @@ /**

const STATIC_TOOLS = STATIC_TOOL_NAMES.map((name) => ({
export const STATIC_TOOLS = STATIC_TOOL_NAMES.map((name) => ({
name,

@@ -161,3 +161,3 @@ description: STATIC_TOOL_DESCRIPTIONS[name],

const STATIC_PROMPTS = [
export const STATIC_PROMPTS = [
{

@@ -187,2 +187,49 @@ name: 'remember',

const GETTING_STARTED_RESOURCE = `# XMemo MCP quick start
XMemo gives AI agents durable, user-owned memory across clients and sessions.
1. Install the CLI: \`npm install -g @xmemo/client\`
2. Sign in: \`xmemo login\`
3. Check the connection: \`xmemo doctor\`
4. Configure a client: \`xmemo setup <client>\`
For stdio MCP clients, run \`xmemo-mcp\` or \`xmemo mcp serve\`.
For Streamable HTTP clients, connect to \`https://xmemo.dev/mcp\`.
Documentation: https://xmemo.dev/product/mcp
`;
const SECURITY_RESOURCE = `# XMemo security and privacy
- Credentials are read from the user-scoped XMemo credential store or the
\`XMEMO_KEY\` environment variable.
- Generated project configuration never embeds token values.
- Discovery, tools, prompts, and these documentation resources are available
without a token; tool execution requires authentication.
- Never paste XMemo credentials into prompts, source files, logs, or public
issue reports.
- Destructive operations such as \`forget\` require an explicit user request.
`;
export const STATIC_RESOURCES = [
{
uri: 'xmemo://docs/getting-started',
name: 'XMemo MCP quick start',
description: 'Installation, authentication, and connection guidance for XMemo MCP.',
mimeType: 'text/markdown'
},
{
uri: 'xmemo://docs/security',
name: 'XMemo security and privacy',
description: 'Credential handling, privacy boundaries, and destructive-action guidance.',
mimeType: 'text/markdown'
}
];
const STATIC_RESOURCE_CONTENT = new Map([
['xmemo://docs/getting-started', GETTING_STARTED_RESOURCE],
['xmemo://docs/security', SECURITY_RESOURCE]
]);
function handlePromptsGet(id, params, ctx) {

@@ -215,5 +262,24 @@ const name = params?.name;

function handleResourcesRead(id, params) {
const uri = params?.uri;
const resource = STATIC_RESOURCES.find((item) => item.uri === uri);
const text = STATIC_RESOURCE_CONTENT.get(uri);
if (!resource || text === undefined) {
return makeError(id, -32002, `Resource not found: ${uri || '(missing uri)'}`);
}
return makeResult(id, {
contents: [
{
uri: resource.uri,
mimeType: resource.mimeType,
text
}
]
});
}
const SERVER_CAPABILITIES = {
tools: {},
prompts: {}
prompts: {},
resources: {}
};

@@ -305,3 +371,5 @@

case 'resources/list':
return makeResult(id, { resources: [] });
return makeResult(id, { resources: STATIC_RESOURCES });
case 'resources/read':
return handleResourcesRead(id, params);
case 'ping':

@@ -308,0 +376,0 @@ return makeResult(id, {});

@@ -29,3 +29,9 @@ import fs from 'node:fs/promises';

token_type: 'mcp_token',
scopes: ['memory:read', 'memory:write']
scopes: [
'memory:read',
'memory:write',
'memory:restore',
'ledger:write',
'ledger:read'
]
}, timeoutMs, io);

@@ -84,10 +90,13 @@

export async function storeTokenFromStdin(io, metadata = {}) {
export async function storeTokenFromStdin(io, metadata = {}, options = {}) {
const token = (await readAll(io.stdin)).trim();
validateToken(token);
return await storeTokenValue(token, metadata, io.env);
return await storeTokenValue(token, metadata, io.env, options);
}
export async function storeTokenValue(token, metadata, env) {
export async function storeTokenValue(token, metadata, env, options = {}) {
validateToken(token);
if (options.allowPlaintext !== true) {
throw new UsageError('Refusing to store an unencrypted token without explicit plaintext-storage consent.');
}
const credentialPath = credentialsPath(env);

@@ -100,3 +109,4 @@ await writePlaintextCredential(credentialPath, token, metadata);

projectFilesModified: false,
storage: 'user-scoped-credential-file'
storage: 'user-scoped-credential-file',
encryption: 'none'
};

@@ -116,2 +126,4 @@ }

storage: stringValue(parsed, ['storage']),
encryption: stringValue(parsed, ['encryption']),
plaintextStorageConsent: parsed.plaintextStorageConsent === true,
account: accountFromPayload(parsed.metadata)

@@ -183,2 +195,5 @@ };

storage: 'user-scoped-credential-file',
encryption: 'none',
plaintextStorageConsent: true,
plaintextStorageConsentAt: new Date().toISOString(),
createdAt: new Date().toISOString(),

@@ -185,0 +200,0 @@ metadata,

@@ -29,8 +29,10 @@ import {

writeLine(io.stdout, 'Authentication');
writeLine(io.stdout, ` ${COMMAND_NAME} login [--from-stdin] [--base-url <url>]`);
writeLine(io.stdout, ' Start browser device login or save a token from stdin.');
writeLine(io.stdout, ` ${COMMAND_NAME} login [--base-url <url>] [--allow-plaintext]`);
writeLine(io.stdout, ' Start browser login; interactive use asks before unencrypted storage.');
writeLine(io.stdout, ` ${COMMAND_NAME} auth status [--verify]`);
writeLine(io.stdout, ' Check login state and optionally verify the active credential.');
writeLine(io.stdout, ` ${COMMAND_NAME} token status [--verify]`);
writeLine(io.stdout, ' Check the local credential without printing secrets.');
writeLine(io.stdout, ` ${COMMAND_NAME} token add --from-stdin`);
writeLine(io.stdout, ' Store an existing XMemo token from stdin.');
writeLine(io.stdout, ` ${COMMAND_NAME} token add --from-stdin --allow-plaintext`);
writeLine(io.stdout, ' Store an existing token after explicit consent to unencrypted storage.');
writeLine(io.stdout, '');

@@ -63,3 +65,3 @@ writeLine(io.stdout, 'Operations');

writeLine(io.stdout, ' - Dry-run first for setup and uninstall flows.');
writeLine(io.stdout, ' - Tokens stay in XMEMO_KEY or user-scoped credentials; they are never written to project configs.');
writeLine(io.stdout, ' - XMEMO_KEY is preferred; approved local credentials are unencrypted and never written to project configs.');
writeLine(io.stdout, ' - Config writes preserve unrelated servers and settings.');

@@ -66,0 +68,0 @@ writeLine(io.stdout, '');

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" role="img" aria-label="XMemo Claude connector icon">
<defs>
<radialGradient id="halo" cx="28%" cy="18%" r="78%">
<stop offset="0" stop-color="#8df7d5"/>
<stop offset="0.36" stop-color="#22d3ee"/>
<stop offset="0.74" stop-color="#102033"/>
<stop offset="1" stop-color="#070b18"/>
</radialGradient>
<linearGradient id="mark" x1="180" x2="844" y1="238" y2="786" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#f2fff9"/>
<stop offset="0.42" stop-color="#8df7d5"/>
<stop offset="1" stop-color="#22d3ee"/>
</linearGradient>
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%" color-interpolation-filters="sRGB">
<feDropShadow dx="0" dy="24" stdDeviation="46" flood-color="#22d3ee" flood-opacity="0.36"/>
<feDropShadow dx="0" dy="0" stdDeviation="18" flood-color="#8df7d5" flood-opacity="0.46"/>
</filter>
</defs>
<rect width="1024" height="1024" rx="220" fill="#070b18"/>
<circle cx="272" cy="214" r="440" fill="url(#halo)" opacity="0.64"/>
<circle cx="842" cy="820" r="420" fill="#14b8a6" opacity="0.18"/>
<path fill="#ffffff" opacity="0.08" d="M126 746c160 70 310 88 448 56 138-31 246-110 324-235v235c-88 78-197 125-326 141-156 19-304-7-446-79Z"/>
<g filter="url(#glow)">
<path fill="url(#mark)" d="M224 724V300h118l170 230 170-230h118v424H672V502L552 670h-80L352 502v222Z"/>
<path fill="#e6fff8" opacity="0.92" d="M382 300h96l34 48 34-48h96L512 486Z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" role="img" aria-label="XMemo Claude connector icon">
<defs>
<radialGradient id="halo" cx="28%" cy="18%" r="78%">
<stop offset="0" stop-color="#8df7d5"/>
<stop offset="0.36" stop-color="#22d3ee"/>
<stop offset="0.74" stop-color="#102033"/>
<stop offset="1" stop-color="#070b18"/>
</radialGradient>
<linearGradient id="mark" x1="180" x2="844" y1="238" y2="786" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#f2fff9"/>
<stop offset="0.42" stop-color="#8df7d5"/>
<stop offset="1" stop-color="#22d3ee"/>
</linearGradient>
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%" color-interpolation-filters="sRGB">
<feDropShadow dx="0" dy="24" stdDeviation="46" flood-color="#22d3ee" flood-opacity="0.36"/>
<feDropShadow dx="0" dy="0" stdDeviation="18" flood-color="#8df7d5" flood-opacity="0.46"/>
</filter>
</defs>
<rect width="1024" height="1024" rx="220" fill="#070b18"/>
<circle cx="272" cy="214" r="440" fill="url(#halo)" opacity="0.64"/>
<circle cx="842" cy="820" r="420" fill="#14b8a6" opacity="0.18"/>
<path fill="#ffffff" opacity="0.08" d="M126 746c160 70 310 88 448 56 138-31 246-110 324-235v235c-88 78-197 125-326 141-156 19-304-7-446-79Z"/>
<g filter="url(#glow)">
<path fill="url(#mark)" d="M224 724V300h118l170 230 170-230h118v424H672V502L552 670h-80L352 502v222Z"/>
<path fill="#e6fff8" opacity="0.92" d="M382 300h96l34 48 34-48h96L512 486Z"/>
</g>
</svg>