diff --git a/.gitignore b/.gitignore index 2fc5a90..ccc0f0e 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ AGENTS.md mcp-publisher.exe # Internal planning/review artifacts. Keep product docs in README/docs instead. +docs/design/ *-PLAN.md *_PLAN.md *-REPORT.md @@ -33,6 +34,7 @@ mcp-publisher.exe # DevFlow engine .progress/ .npm-cache-review/ +.tmp-npm-cache/ # Agent registry & profiles .agents/ diff --git a/.npmignore b/.npmignore index d87bb29..d249b96 100644 --- a/.npmignore +++ b/.npmignore @@ -3,8 +3,6 @@ !bin/** !src/ !src/** -!plugins/kiro/ -!plugins/kiro/** !README.md !LICENSE !.npmignore diff --git a/MCP-README.md b/MCP-README.md index ad34a7f..267ca61 100644 --- a/MCP-README.md +++ b/MCP-README.md @@ -211,7 +211,7 @@ xmemo-mcp | 客户端 | 支持方式 | 配置命令 | |--------|----------|----------| | **Kimi Code** | Streamable HTTP + Bearer Token(`XMEMO_KEY`) | `xmemo setup kimi-code` | -| **Kiro** | `mcp-remote` + Bearer Token(`XMEMO_KEY`) | `xmemo setup kiro` | +| **Kiro** | 原生 HTTP OAuth(默认);`--auth key` 使用 `XMEMO_KEY` | `xmemo setup kiro` | | **Claude Desktop** | `mcp-remote` + Bearer Token(`XMEMO_KEY`) | `xmemo setup claude-desktop` | | **Cursor** | Streamable HTTP + Bearer Token(`XMEMO_KEY`) | `xmemo setup cursor` | | **Copilot CLI** | Local Proxy + Bearer Token | `xmemo setup copilot` | diff --git a/MCP-SETUP-GUIDE.md b/MCP-SETUP-GUIDE.md index d95ffc5..3fac984 100644 --- a/MCP-SETUP-GUIDE.md +++ b/MCP-SETUP-GUIDE.md @@ -67,13 +67,28 @@ set XMEMO_AGENT_INSTANCE_ID=random-guid-here 配置文件:`~/.kiro/settings/mcp.json` -Kiro 使用 `mcp-remote` 连接 Hosted MCP,并从 `XMEMO_KEY` 读取 Bearer Token。推荐运行: +Kiro 默认使用原生 HTTP OAuth,配置不包含固定 Authorization,也不再启动 `mcp-remote`: ```bash xmemo setup kiro +# 或选择 Key;启动 Kiro 的进程需要继承 XMEMO_KEY +xmemo setup kiro --auth key ``` -不要把 Kiro 与 MCP OAuth 客户端混为一谈:`xmemo login` 可以通过浏览器获取 CLI 凭据,但 Kiro 的 MCP 请求仍由环境变量认证。 +OAuth 默认只请求 `memory:read` 和 `knowledge:read`。需要写入时,在 Kiro OAuth 配置中加入对应 write scope 并重新授权。Key 模式使用 `Bearer ${XMEMO_KEY}`;Key 权限由服务端签发时确定。`xmemo login` 的 CLI 凭据不会自动导入 Kiro。 + +已有配置出现反复弹出授权页面、认证成功后仍超时,可先离线检查,再修复: + +```bash +xmemo doctor --client kiro --json +xmemo doctor --client kiro --fix +# 明确保留 Key 认证时: +xmemo doctor --client kiro --fix --auth key +``` + +检查默认不修改文件;`--fix` 对识别出的旧代理或认证冲突先创建同目录备份,再替换 XMemo 连接,保留其他 MCP、Power 配置、审批和禁用设置。可用 `--config ` 指定配置。未知自定义命令、无效 JSON、非 HTTPS MCP 地址不会自动修复。 + +修复后重新加载 Kiro;OAuth 需要完成正常授权,Key 需要启动环境中的凭证。doctor 不读取 OAuth 缓存、不终止代理进程、不访问服务、不验证真实认证和刷新;旧代理仍运行时重新启动 Kiro。原来的 `${env:XMEMO_KEY}` 代理环境赋值可能成为字面文本,其固定 Authorization 又会覆盖 OAuth 令牌,造成重复认证。 --- diff --git a/README.md b/README.md index c7b93d6..693d15f 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ xmemo setup cursor --dry-run | **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 + Bearer Token | +| **Kiro** | `xmemo setup kiro` | Native HTTP OAuth; `--auth key` for API Key | | **Grok** | `xmemo setup grok` | Hosted MCP | | **Other MCP clients** | `xmemo mcp config --client generic` | Generated template | @@ -492,7 +492,6 @@ bin/ docs/assets/ src/ skills/ -plugins/kiro/ plugins/xmemo/ README.md LICENSE @@ -504,6 +503,7 @@ Not published: .github/ docs/analysis/ docs/architecture/ +docs/design/ test/ coverage/ server code @@ -582,3 +582,12 @@ it connects to. ## License [MIT](./LICENSE) © 2025–2026 Yonro + +### Repairing an existing Kiro MCP configuration + +Run `xmemo doctor --client kiro --json` to inspect local configuration without network requests. +Use `xmemo doctor --client kiro --fix` to migrate recognized legacy proxy configurations to native +HTTP OAuth, or add `--auth key` for native HTTP with `Bearer ${XMEMO_KEY}`. Repairs create a +backup, retain unrelated servers and client preferences, and never copy credentials into the +replacement. Reload Kiro and verify a real tool call afterwards; a configuration pass is not an +authentication or token-refresh result. Fresh installs use `xmemo setup kiro [--auth oauth|key]`. diff --git a/docs/design/MOS_01_CLOUD_SKILL_SAFE_WRITE_HANDOFF.md b/docs/design/MOS_01_CLOUD_SKILL_SAFE_WRITE_HANDOFF.md deleted file mode 100644 index fb0decf..0000000 --- a/docs/design/MOS_01_CLOUD_SKILL_SAFE_WRITE_HANDOFF.md +++ /dev/null @@ -1,60 +0,0 @@ -# MOS-01: proposed Cloud Skill safe write contract handoff - -Owner: authorized `memory-os` project AI -Consumer: `@xmemo/client` Cloud Skill add/update adapters -Status: required proposal; absent from MemoryOS source/OpenAPI at the clean -`origin/master` snapshot `18d7a652de48181a43b10b8f771d3a8df5933884`. This is not an implemented, -reviewed, frozen, or deployed server contract. The CLI fails closed until the -service publishes compatible OpenAPI and response fixtures. - -## Required endpoints - -### `POST /v1/skills/create` - -Request fields: `markdown_content`, `sub_files`, optional `name`, optional -`slug`, `publish` (default false), and optional `team_id`. - -The operation must be atomic create-only. A duplicate slug in the effective -personal/team scope returns 409 and does not append a revision or move any -latest/published pointer. - -### `PUT /v1/skills/{skill_id}/content` - -Request fields: required `expected_revision_id`, optional `markdown_content`, -optional `sub_files`, `publish` (default false), and optional `team_id`. -Omitting content is allowed only to publish the reviewed revision. - -The operation must atomically compare the current maintenance head, merge the -provided files while preserving omitted files, append at most one revision, -and update latest/published or team-proposed state. A stale expected revision -returns 409 with no resource or pointer change. - -## Compatibility and errors - -- Existing upsert/import/component endpoints keep their current behavior. -- The new paths must not delegate to a saving importer before uniqueness/CAS - checks are acquired in the real storage transaction or lock. -- Distinguish duplicate slug, stale revision, missing skill, permission denial, - parse failure, and unsupported publication state. -- Preserve current memory read/write scopes, ownership, team role/approval, - audit, and sandbox policies. Do not invent `skills:*` scopes in this task. -- 404/405 on these paths is interpreted by the CLI as - `SERVER_CONTRACT_REQUIRED`; the CLI does not fall back. - -## Required service tests - -1. Two independent repository/service instances race the same scoped slug: - exactly one create succeeds and one receives 409. -2. Two independent instances update the same expected revision: exactly one - succeeds and one receives 409. -3. Reopen persisted storage and prove resource count, components, latest head, - and published/proposed head match the winning transaction. -4. Prove a failed parse, permission check, stale CAS, or duplicate create leaves - no orphan revision and changes no pointer. -5. Prove omitted sub-files survive update, explicit replacements change only - named paths, and path/case normalization is deterministic. -6. Exercise personal publish and team proposal/approval policy separately. - -After source review, record the deployed service SHA and run the CLI contract -fixtures against that deployment before declaring Cloud Skill add/update -available to users. diff --git a/docs/design/XMEMO_CLI_SERVICE_CLIENT_BASELINE_2026-09-03.md b/docs/design/XMEMO_CLI_SERVICE_CLIENT_BASELINE_2026-09-03.md deleted file mode 100644 index 3575078..0000000 --- a/docs/design/XMEMO_CLI_SERVICE_CLIENT_BASELINE_2026-09-03.md +++ /dev/null @@ -1,167 +0,0 @@ -# XMemo CLI service-client baseline (CLI-00) - -计划:`XMEMO-CLI-CLIENT-V1` -核验日期:2026-09-03 -状态:历史基线,已由 `XMEMO_CLI_SERVICE_CLIENT_IMPLEMENTATION_STATUS_2026-09-04.md` 取代。本文保留 2026-09-03 的契约快照,不代表当前实现、审核或部署状态。 - -## Scope and repository evidence - -| Item | Evidence | -| --- | --- | -| CLI worktree | Isolated worktree used for historical local validation | -| CLI baseline | detached `HEAD` `6a82011d202316997220e9183c2b309a7c5df61d` | -| CLI package | `@xmemo/client` `0.4.181` | -| CLI main checkout | Not modified by the isolated validation worktree | -| Service source checked | 历史值 `11fd8652a528b95de33310f20b2b6b70323b4f90`;当前值见实施状态文档 | -| Service ref checked | 历史值 `1fa16bfdb9c7c6db063e100d3ddbc00ec704a9f4`;不要用于当前判断 | -| Service deployment | Not checked; source/OpenAPI evidence is not deployment evidence | -| Independent Skill | `skills/xmemo/scripts/xmemo-skill.mjs`; no CLI import or shell-out added | - -The service checkout was already three commits behind its `origin/master` ref -when checked. The local source and checked OpenAPI file are the contract -evidence for this baseline; the remote ref must be refreshed before any -target-environment claim. - -## Frozen command registry - -The source of truth is -`src/api/contracts/command-registry.js`. It contains the seven basic calls and -exactly the twelve planned domain entries. Existing setup, authentication, -diagnostic, MCP, profile, update, privacy, and uninstall commands remain -outside this new registry and are not removed. - -| CLI entry | Service route(s) | Scope | Side effect | Status | -| --- | --- | --- | --- | --- | -| `memory add` | `POST /api/v1/remember` | `memory:write` | yes | current | -| `memory search` | `GET /api/v1/recall` | `memory:read` | no | current | -| `context recall` | `POST /api/v1/recall/context` | `memory:read` | no | current, read-only POST | -| `state save` | `POST /api/v1/update_state` | `memory:write` | yes | current | -| `state restore` | `POST /api/v1/skill/operations`, `operation=state-restore` | `memory:read` | no | current, read-only dispatcher operation | -| `restart snapshot` | `POST /api/v1/restart/snapshot` | `memory:write` | yes | current | -| `restart restore` | `POST /api/v1/restart/restore` | `memory:read`, `memory:restore` | yes | current; may restore remote state | -| `knowledge add` | base/document/item or item-from-document sequence | `knowledge:write` | yes | current | -| `knowledge search` | `POST /api/v1/knowledge/search` | `knowledge:read` | no | current, read-only POST | -| `knowledge read` | item metadata plus fixed revision read | `knowledge:read` | no | current | -| `knowledge update` | content, content-from-document, and metadata paths | `knowledge:write` | yes | current; CAS fields required by adapter | -| `dream preview` | settings read plus `POST /api/v1/me/dream/runs` | memory read/write | yes (run creation) | current | -| `dream show` | `GET /api/v1/me/dream/runs/{run_id}` | `memory:read` | no | current | -| `dream apply` | `POST /api/v1/me/dream/runs/{run_id}/confirm` | `memory:write` | yes | current | -| `cloud-skill add` | proposed `POST /v1/skills/create` | memory write | yes | contract-required | -| `cloud-skill list` | `GET /v1/skills` | memory read | no | current | -| `cloud-skill show` | skill detail plus components | memory read | no | current | -| `cloud-skill update` | proposed `PUT /v1/skills/{skill_id}/content` | memory write | yes | contract-required | -| `cloud-skill run` | `POST /v1/skills/{skill_id}/execute` | memory write | yes | current, no retry | - -The registry records route intent only. Implementations use the checked -OpenAPI/source facts and fail closed when a required contract is absent; they -do not infer a full schema from MCP tool descriptions. - -## Service contract facts - -The checked service source exposes both `/api/v1/...` and compatibility -`/v1/...` aliases for the memory, knowledge, document, Dream, and restart -routes. The published OpenAPI uses `/api/v1/...` for those domains and `/v1` -for Cloud Skill routes. The selected defaults therefore preserve the documented -OpenAPI route rather than relying on an undocumented alias. - -Relevant request facts from the checked OpenAPI/source: - -- `MemoryRememberRequest` requires `content` and `path`. -- `RecallContextRequest` requires `query`; `include_knowledge` defaults false. -- `MemoryStateUpdateRequest` accepts `state_key`, structured state fields, - `bucket`, `scope`, and bounded `ttl_seconds`. -- Restart snapshot/restore accept `state_key`, session selectors, restore - controls, and bounded TTL. Restore can return a `state_update`, so the CLI - treats it as a remote write even though it does not write local project files. -- Knowledge search accepts `query`, optional base, `limit` (1..100), and an - opaque `cursor`; results carry `next_cursor` in the page response. -- Knowledge content update requires `expected_current_revision_id`; document - update additionally requires `document_id` and `expected_document_version`. -- Document upload accepts `filename` and base64 content, requires the memory - data-plane write permission as well as document runtime availability, and the - checked route reports a 4 MiB raw-file limit. -- Dream preview uses `window_days` and optional `idempotency_key`; confirmation - requires `item_id`, `expected_run_version`, and - `expected_settings_version`. -- Cloud Skill execution accepts `script_path`, optional `revision_id` and - `input_args`, and `timeout_seconds` bounded to 1..60 seconds. - -The device-login implementation accepts the explicit scopes -`memory:read`, `memory:write`, `memory:restore`, `ledger:read`, -`ledger:write`, `knowledge:read`, and `knowledge:write`; when scopes are not -requested the service defaults to memory read/write. Dream checks the generic -REST read/write scope plus owner/entitlement rules. Cloud Skill currently uses -the generic memory read/write scope and owner/team checks; no new `dream:*` or -`skills:*` scope is introduced by this work. - -## Cloud Skill safety blocker - -The current source exposes: - -- `POST /v1/skills` and `POST /v1/skills/import`, both capable of saving into - an existing scoped slug; -- `PUT /v1/skills/{skill_id}/components`, which does not accept the planned - expected-revision CAS field; and -- detail, revision, component, publish, execute, render, and export routes. - -The planned safe paths `POST /v1/skills/create` and -`PUT /v1/skills/{skill_id}/content` are absent from the checked source and -OpenAPI. CLI-00 therefore marks only `cloud-skill.add` and -`cloud-skill.update` as `contract-required`. Future adapters must return a -stable server-contract error on 404/405 or incompatible response and must not -send an unsafe fallback request. MOS-01 remains a separate handoff to the -authorized `memory-os` project owner; no service files were modified here. - -## Frozen implementation constraints - -- New business commands will use the `schemaVersion/ok/command/data/meta/error` - envelope and single-object JSON stdout under `--json`; progress goes to - stderr. -- `Authorization` is attached only to the selected HTTPS service origin (with - explicit loopback development exception), never followed across origins. -- Writes are not retried unless a verified idempotency/CAS contract says so; - unknown outcomes remain unknown and are not replayed. -- `--from` read receipts bind update/apply/run to the resource, origin, scope, - and viewed revision/settings version. Dream receipts use - `confirmation_version` and candidate IDs; Cloud Skill receipts record - published/draft status and default execution accepts published receipts - only. A receipt is not an authorization token or approval proof. -- Credential-file tokens are origin-bound to login metadata. Legacy files with - no origin metadata are rejected unless the caller explicitly opts into the - default-service-only migration path with `--allow-legacy-credential`. -- Local file/input validation precedes Knowledge base creation. Document upload - extraction is bounded and multi-step failures return resumable partial - results; content update followed by publish uses the authoritative - post-update version. -- Read-only POST requests may use bounded retry; side-effect requests never - auto-replay. Response bodies are bounded before decoding completes, and new - service commands reject unknown options/input fields. -- `skills/xmemo` remains runtime- and credential-independent from the CLI. - -## Remediation evidence and review state - -Changed files in this worktree include: - -- `src/api/contracts/command-registry.js` — frozen registry and availability - metadata. -- `test/command-registry.test.js` — count, route, scope, and safety assertions. -- `src/api/*.js` — shared service transport, errors, envelopes, inputs, receipts, - upload safety, and command help schemas. -- `src/commands/service.js`, `knowledge.js`, `dream.js`, `cloud-skill.js` — - command adapters and safety gates. -- `test/service-client.test.js`, `test/service-command.test.js` — regression - coverage for the first review findings and command contracts. -- `docs/design/XMEMO_CLI_SERVICE_CLIENT_BASELINE_2026-09-03.md` — this audit - record. - -Historical validation at that checkpoint: `npm test` 166 passed / 0 failed, -`npm run lint` passed, `git diff --check` passed, and `npm pack --dry-run` -passed using a worktree-local npm cache. These counts do not describe the final -scope. The prior review request was -`824402e1-b80e-4012-85c2-891b2b006ce1` and was changes-requested; a new request -must include the immutable scope snapshot supplied by the current DevFlow -review helper. - -Next authorized unit: CLI-01, implementing the shared REST transport, -validated inputs, error/envelope mapping, read receipts, and help schema while -preserving the existing CLI command behavior. diff --git a/docs/design/XMEMO_CLI_SERVICE_CLIENT_IMPLEMENTATION_STATUS_2026-09-04.md b/docs/design/XMEMO_CLI_SERVICE_CLIENT_IMPLEMENTATION_STATUS_2026-09-04.md deleted file mode 100644 index 602112a..0000000 --- a/docs/design/XMEMO_CLI_SERVICE_CLIENT_IMPLEMENTATION_STATUS_2026-09-04.md +++ /dev/null @@ -1,99 +0,0 @@ -# XMemo CLI service-client implementation status - -Plan: `XMEMO-CLI-CLIENT-V1` -Audit date: 2026-09-04 -CLI base: `origin/main` at `49a959f5f3c1cd72b4d449209054e91bd158afe4` - -## Outcome - -The planned CLI code is implemented for the seven basic service calls and the -safe, currently available Knowledge, Dream, and Cloud Skill contracts. It is -not an "all features available in production" claim. The -two Cloud Skill write adapters are implemented but intentionally remain marked -`contract-required` until MemoryOS provides and deploys MOS-01. A new binary -file cannot yet replace the source of an existing file-backed Knowledge item, -because the checked service has no API that creates a new version under the -same Document ID; the CLI supports the safe existing-Document update route and -rejects unsupported source conversion. - -No merge, version bump, tag, package publish, service deployment, or production -write was performed. - -## Local validation - -- The targeted service suite passes. It includes child-process calls - through a real loopback HTTP server for all 19 frozen commands, delayed-body - timeouts, unknown writes, redaction, HTML 405 contract rejection, and a real - npm archive executed outside the repository. -- Package smoke: the archive was unpacked to a temporary directory; a CLI-only - copy called the HTTP fixture without `skills/`, while a Skill-only copy called - the fixture without CLI `src/`. -- Full-suite, lint, diff, and formal-review results are recorded only after they - actually run; this document does not treat the fixture as production E2E. -- Full repository suite, lint, package, and diff checks are rerun against the - final candidate before review; formal review is tracked outside this document. -- Server source rechecked at clean local/origin - `18d7a652de48181a43b10b8f771d3a8df5933884`; - both proposed MOS-01 paths remain absent. No deployed service was checked. - -## Work-unit coverage - -| Work unit | Status | Evidence / remaining condition | -| --- | --- | --- | -| CLI-00 | implemented | Registry freezes seven basic calls and exactly twelve domain entries. | -| CLI-01 | implemented locally | HTTPS/loopback origin gate, whole-response timeout/interruption, retry policy, envelopes, typed receipts/input, and command-specific JSON schema/examples. | -| CLI-02 | implemented locally | Optional explicit service scopes without changing default login scopes; origin-bound credentials; legacy migration gate; recursive error redaction; read-only `doctor --services`. | -| CLI-03 | implemented | Memory/context/state/restart adapters; full restart request models accepted through `--input`. | -| CLI-04 | implemented locally | Knowledge search/read, explicit first-page cursor, `--from` fixed-revision continuation, cursor propagation, unmodified citations/index state, and receipts. | -| CLI-05 | implemented locally; one server limitation | Text/document add, prevalidated base/Document, bounded upload/extraction, provenance-preserving CAS update, publish-only, partial/unknown outcomes. New binary replacement requires a same-Document version-upload server contract. | -| CLI-06 | implemented | Settings/entitlement preflight, stable idempotency key, apply availability, bounded wait, terminal failures, receipts, candidate validation, and single-item apply. | -| CLI-07 | implemented locally | Cloud Skill list/show/run, draft/published distinction, script selection, HTTP budget covering 1..60-second execution, business failure, and unknown outcome handling. | -| CLI-08 | adapter complete; service blocked | File/directory safety, create-only request, content CAS, publish confirmation, and no legacy fallback are implemented. MOS-01 must exist and be deployed. | -| CLI-09 | partial | Windows loopback HTTP and real-package isolation are covered. Unix execution and authorized test-tenant E2E remain pending. | -| CLI-10 | release-gated | Final immutable-scope review and release authorization remain external gates rather than implementation claims. | - -## Product boundary - -The public command surface remains limited to: - -- Basic calls: `memory add/search`, `context recall`, `state save/restore`, and - `restart snapshot/restore`. -- Knowledge: `add/search/read/update`. -- Dream: `preview/show/apply`. -- Cloud Skill: `add/list/show/update/run`. - -No revision, diff, export, rollback, schedule-management, batch, generic API, -or background-daemon command was added. - -## Safety and agent-use behavior - -- Every new business command supports `--json`; successful and failed machine - output is one envelope on stdout. -- Every business command accepts JSON input where the command contract permits - it. Cloud Skill run reserves `--input` for `input_args`, as frozen in the plan. -- Duplicate flag/JSON fields are rejected. High-impact actions require an - interactive confirmation or an explicit `--yes`; missing confirmation exits - with code 10. -- Read/show receipts bind origin, scope, resource, and viewed versions. Writes - never silently refresh a stale receipt. -- Side-effect requests are not automatically replayed. Disconnect or timeout - after a write is sent exits 11; known multi-step partial completion exits 12. -- Local SIGINT exits 130 for read/wait operations; interruption during an - already-sent write remains an unknown outcome. -- Human output includes the actual bounded service result instead of only an - acknowledgement line. -- Stored credentials are origin-bound. CLI and `skills/xmemo` retain separate - runtime code and credential stores; neither imports or shells out to the other. - -## Remaining external verification - -1. Implement, review, and deploy MOS-01 in `memory-os`, then run create/create - and update/update multi-instance races plus persisted reopen checks. -2. If direct replacement of a file-backed Knowledge source is required, add an - authorized same-Document version-upload contract in `memory-os`; the existing - `content/from-document` route already enforces that the Document ID matches. -3. Run authorized test-tenant E2E for one success and key failure path in each - domain, and verify the deployed service SHA, feature flags, extraction queue, - Dream entitlement, and Cloud Skill sandbox. -4. Repeat the package/HTTP checks on a Unix host. Do not infer production - readiness from the successful Windows fixture. diff --git a/package-lock.json b/package-lock.json index a082f09..de67a89 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@xmemo/client", - "version": "0.4.182", + "version": "0.4.183", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@xmemo/client", - "version": "0.4.182", + "version": "0.4.183", "license": "MIT", "bin": { "memory-os": "bin/memory-os.js", diff --git a/package.json b/package.json index 88c53a0..3c72a43 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@xmemo/client", - "version": "0.4.182", + "version": "0.4.183", "description": "Privacy-first CLI client and MCP setup tool for XMemo.", "mcpName": "io.github.yonro/xmemo", "type": "module", @@ -15,7 +15,6 @@ "scripts", "src", "skills", - "plugins/kiro", "plugins/xmemo", "README.md", "LICENSE" diff --git a/plugins/kiro/.kiro-plugin/power.json b/plugins/kiro/.kiro-plugin/power.json deleted file mode 100644 index 7e552f3..0000000 --- a/plugins/kiro/.kiro-plugin/power.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "xmemo", - "displayName": "XMemo", - "version": "0.1.0", - "description": "Connect Kiro to XMemo's hosted, user-owned memory layer for durable project context, coding preferences, decisions, and reusable agent knowledge.", - "author": { - "name": "XMemo", - "email": "support@xmemo.dev" - }, - "publisher": "XMemo", - "homepage": "https://xmemo.dev", - "repository": "https://github.com/yonro/memory-os-cli", - "license": "UNLICENSED", - "logo": "assets/logo.png", - "keywords": [ - "kiro", - "power", - "mcp", - "memory", - "agent-memory", - "developer-tools", - "oauth" - ], - "category": "Developer Tools", - "tags": [ - "mcp", - "memory", - "oauth", - "developer-tools" - ], - "steering": [ - "steering/AGENTS.md" - ], - "mcpServers": "mcp.json" -} diff --git a/plugins/kiro/CHANGELOG.md b/plugins/kiro/CHANGELOG.md deleted file mode 100644 index 87761fe..0000000 --- a/plugins/kiro/CHANGELOG.md +++ /dev/null @@ -1,9 +0,0 @@ -# Changelog - -## [0.1.0] - 2026-06-08 - -### Added -- Initial Kiro Power release -- XMemo MCP server configuration for Kiro -- Steering file for memory management workflow -- OAuth-first authentication support diff --git a/plugins/kiro/LICENSE b/plugins/kiro/LICENSE deleted file mode 100644 index ede87b1..0000000 --- a/plugins/kiro/LICENSE +++ /dev/null @@ -1,7 +0,0 @@ -Copyright (c) Yonro. - -All rights reserved. - -This package is published as a client distribution artifact for XMemo. -No license is granted to copy, modify, distribute, sublicense, or use the source -code except as expressly permitted by Yonro in a separate written agreement. diff --git a/plugins/kiro/POWER.md b/plugins/kiro/POWER.md deleted file mode 100644 index 58840bc..0000000 --- a/plugins/kiro/POWER.md +++ /dev/null @@ -1,148 +0,0 @@ -# XMemo for Kiro - -XMemo gives Kiro a hosted, user-owned memory layer for durable project context, coding preferences, decisions, TODOs, and reusable agent knowledge. - -## Overview - -This Kiro Power integrates XMemo's MCP server to provide persistent memory across your Kiro sessions. XMemo helps Kiro remember: - -- **Project context**: Repository conventions, architecture decisions, and verified commands -- **Coding preferences**: Your approved preferences for code review, testing, and documentation -- **Decisions**: Important technical decisions that should inform future work -- **TODOs**: Follow-up tasks and reminders for future sessions -- **Knowledge**: Reusable agent knowledge and learnings - -## Installation - -Install this power through Kiro's Powers panel or manually configure: - -```bash -xmemo setup kiro -``` - -This command: -1. Merges XMemo MCP server configuration into `~/.kiro/settings/mcp.json` -2. Configures Kiro to read Bearer authentication from the `XMEMO_KEY` environment variable -3. Adds agent identity headers for attribution - -## Authentication - -### Environment Variable (Required) - -Kiro requires the `XMEMO_KEY` environment variable to be set for MCP authentication. - -First, authenticate with XMemo: - -```bash -xmemo login -``` - -Then set the environment variable: - -```bash -# PowerShell (User-level, persistent) -$token = xmemo token show --format raw -[Environment]::SetEnvironmentVariable("XMEMO_KEY", $token, "User") - -# PowerShell (Session-only) -$env:XMEMO_KEY = xmemo token show --format raw - -# Bash/Zsh -export XMEMO_KEY=$(xmemo token show --format raw) -``` - -**Important**: Restart Kiro after setting the environment variable for the first time. - -### Why Environment Variable? - -Kiro currently has a known issue with MCP OAuth token persistence. `xmemo login` may use a browser to obtain the CLI credential, but Kiro itself still reads `XMEMO_KEY`; using the environment variable ensures reliable authentication across sessions. - -## Usage - -Once installed, Kiro will automatically use XMemo when appropriate. The steering file instructs Kiro to: - -1. **Recall first**: Search XMemo before making assumptions about preferences or past decisions -2. **Save durable information**: Remember important context that should persist across sessions -3. **Keep content useful**: Store concrete facts, decisions, and action items (not chat transcripts) -4. **Confirm destructive actions**: Ask before deleting or overwriting memory - -### Example Prompts - -- "Search XMemo for coding style preferences for this project" -- "Remember in XMemo: Always use TypeScript strict mode in this project" -- "Recall what I saved about API design patterns" -- "Create a TODO in XMemo to refactor the auth module next week" -- "List my XMemo TODOs" - -## What Gets Stored - -**Good candidates for XMemo:** -- Repository conventions and verified commands -- Architecture decisions and design patterns -- Release procedures and deployment notes -- User-approved preferences for development practices -- TODOs and follow-up actions - -**Never store:** -- Secrets, credentials, API keys, or tokens -- Private customer data or PII -- Temporary debugging output -- Chat transcripts or verbose logs - -## Configuration - -The power installs: -- **MCP server**: `https://xmemo.dev/mcp` -- **Steering file**: Auto-included guidance for when to use XMemo -- **Agent identity**: Headers for attribution (non-secret) - -### Manual Configuration - -Advanced users can generate a config snippet: - -```bash -xmemo mcp config --client kiro --json -``` - -Or see the dry-run preview: - -```bash -xmemo setup kiro --dry-run -``` - -## Troubleshooting - -### Connection Issues - -Check your token status: - -```bash -xmemo auth status -xmemo token status --verify -``` - -### Configuration Issues - -View your current MCP config: - -```bash -cat ~/.kiro/settings/mcp.json -``` - -### Reset Configuration - -To remove XMemo from Kiro, manually edit `~/.kiro/settings/mcp.json` and remove the `XMemo` entry from `mcpServers`. - -## Privacy & Security - -- No telemetry or analytics -- Tokens are managed by XMemo CLI and referenced via environment variable -- Agent identity headers are non-secret attribution IDs -- Environment variable approach keeps tokens out of config files -- All memory content is user-owned and controlled through your XMemo account -## License and support - -This power is licensed under [LicenseRef-Proprietary](./LICENSE). -This power integrates with XMemo MCP Server (LicenseRef-Proprietary). -- [Privacy Policy](https://xmemo.dev/legal/privacy) -- [Support](mailto:support@xmemo.dev) diff --git a/plugins/kiro/README.md b/plugins/kiro/README.md deleted file mode 100644 index ea30032..0000000 --- a/plugins/kiro/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# XMemo Kiro Power - -XMemo gives Kiro a hosted, user-owned memory layer for durable project context, coding preferences, decisions, TODOs, and reusable agent knowledge. - -## What it installs - -- `mcp.json` adds the hosted XMemo MCP server at `https://xmemo.dev/mcp`. -- `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. - -## Authentication - -The Kiro Power uses environment variable authentication due to a known Kiro IDE OAuth token persistence issue. The power metadata stores only the hosted MCP URL plus the `XMEMO_KEY` environment variable reference. - -Users should authenticate with: - -```bash -xmemo login -``` - -Then set the environment variable: - -```bash -# PowerShell (persistent) -$token = xmemo token show --format raw -[Environment]::SetEnvironmentVariable("XMEMO_KEY", $token, "User") -``` - -Then restart Kiro for the environment variable to take effect. - -Manual configuration is available through: - -```bash -xmemo mcp config --client kiro --json -``` - -## Reviewer smoke prompts - -1. "List the XMemo tools you can use in Kiro." -2. "Search XMemo for coding style preferences for this project." -3. "Remember in XMemo: For Kiro review, prefer small PRs with validation evidence." -4. "Recall what I saved about Kiro review PR preferences." -5. "Create a XMemo memory TODO to capture Kiro review screenshots tomorrow, then list my TODOs." - -Use a dedicated reviewer workspace with synthetic data only. Redact emails, OAuth codes, cookies, bearer tokens, trace IDs, internal account IDs, real memory content, and private local paths from evidence. diff --git a/plugins/kiro/SETUP.md b/plugins/kiro/SETUP.md deleted file mode 100644 index 7939089..0000000 --- a/plugins/kiro/SETUP.md +++ /dev/null @@ -1,254 +0,0 @@ -# XMemo Kiro Power Setup Guide - -This guide walks you through setting up XMemo for Kiro. - -## Quick Start - -Install and configure XMemo for Kiro in one command: - -```bash -xmemo setup kiro -``` - -This will: -1. Merge XMemo MCP server config into `~/.kiro/settings/mcp.json` -2. Configure authentication via `XMEMO_KEY` environment variable -3. Set up agent identity headers for attribution - -**Note**: Kiro uses environment variable authentication (`XMEMO_KEY`) as the recommended method. Make sure to set the environment variable before using XMemo. - -## Step-by-Step Setup - -### 1. Install XMemo CLI - -```bash -npm install -g @xmemo/client -``` - -### 2. Authenticate with XMemo - -Kiro requires the `XMEMO_KEY` environment variable to be set. Choose one of these methods: - -#### Option A: Browser Login for the CLI Credential (Recommended) - -```bash -xmemo login -``` - -This opens your browser for secure device login and stores the CLI credential. It does not change Kiro's MCP authentication mode: Kiro still reads `XMEMO_KEY` from its launch environment. - -#### Option B: Direct Token - -If you already have a token, add it to the token store: - -```bash -printf '%s\n' 'your-token' | xmemo token add --from-stdin --allow-plaintext -``` - -After authentication, export your token to the environment variable: - -**PowerShell (User-level, persistent):** -```powershell -$token = xmemo token show --format raw -[Environment]::SetEnvironmentVariable("XMEMO_KEY", $token, "User") -``` - -**PowerShell (Session-only):** -```powershell -$env:XMEMO_KEY = xmemo token show --format raw -``` - -**Bash/Zsh:** -```bash -export XMEMO_KEY=$(xmemo token show --format raw) -``` - -**Note**: The environment variable must be set before starting Kiro for the MCP server to authenticate successfully. - -### 3. Configure Kiro - -Preview the configuration first: - -```bash -xmemo setup kiro --dry-run -``` - -Then write the configuration: - -```bash -xmemo setup kiro -``` - -### 4. Verify Installation - -Check your MCP configuration: - -```bash -cat ~/.kiro/settings/mcp.json -``` - -You should see an `XMemo` entry in `mcpServers`. - -Verify token status: - -```bash -xmemo auth status -xmemo token status --verify -``` - -### 5. Restart Kiro - -After configuration, restart Kiro or reload MCP servers for changes to take effect. - -## Using XMemo in Kiro - -Once installed, you can use XMemo through natural language: - -### Search Memory -``` -Search XMemo for coding style preferences for this project -``` - -### Save Information -``` -Remember in XMemo: Always use TypeScript strict mode in this project -``` - -### Recall Decisions -``` -Recall what I saved about API design patterns -``` - -### Manage TODOs -``` -Create a TODO in XMemo to refactor the auth module next week -List my XMemo TODOs -``` - -## Configuration Details - -The setup command creates this configuration: - -```json -{ - "mcpServers": { - "XMemo": { - "url": "https://xmemo.dev/mcp", - "headers": { - "Authorization": "Bearer ${env:XMEMO_KEY}", - "X-Memory-OS-Agent-ID": "kiro", - "X-Memory-OS-Agent-Instance-ID": "${XMEMO_AGENT_INSTANCE_ID}" - } - } - } -} -``` - -### Configuration Components - -- **url**: XMemo MCP server endpoint -- **Authorization**: Bearer token from `XMEMO_KEY` environment variable -- **X-Memory-OS-Agent-ID**: Identifies the agent as Kiro -- **X-Memory-OS-Agent-Instance-ID**: Unique identifier for this installation - -## Troubleshooting - -### XMemo tools not available - -**Check MCP configuration:** -```bash -cat ~/.kiro/settings/mcp.json -``` - -**Verify the `XMemo` entry exists in `mcpServers`.** - -**Restart Kiro** or reload MCP servers. - -### Authentication errors - -**Check token status:** -```bash -xmemo auth status -xmemo token status --verify -``` - -**Verify environment variable is set:** - -PowerShell: -```powershell -$env:XMEMO_KEY -``` - -Bash/Zsh: -```bash -echo $XMEMO_KEY -``` - -**If the environment variable is not set, set it:** - -PowerShell (User-level, persistent): -```powershell -$token = xmemo token show --format raw -[Environment]::SetEnvironmentVariable("XMEMO_KEY", $token, "User") -``` - -**Then restart Kiro** for the environment variable to be loaded. - -**Re-authenticate if needed:** -```bash -xmemo login -``` - -### Duplicate server entries - -If you see an error about duplicate entries: - -``` -MCP config already contains mcpServers.XMemo -``` - -Edit `~/.kiro/settings/mcp.json` manually to remove or update the existing entry. - -## Advanced Configuration - -### Custom Service URL - -For self-hosted or enterprise XMemo: - -```bash -xmemo setup kiro --url https://your-xmemo-instance.example.com -``` - -### Manual Configuration - -Generate config without writing: - -```bash -xmemo mcp config --client kiro --json -``` - -Copy the output and manually merge into your Kiro config. - -## Uninstalling - -To remove XMemo from Kiro: - -1. Edit `~/.kiro/settings/mcp.json` -2. Remove the `XMemo` entry from `mcpServers` -3. Restart Kiro or reload MCP servers - -## Getting Help - -- **Documentation**: https://xmemo.dev -- **CLI Help**: `xmemo --help` -- **Setup Help**: `xmemo setup --help` -- **Issues**: https://github.com/yonro/memory-os-cli/issues -- **Email**: support@xmemo.dev - -## Privacy & Security - -- Configuration does not embed token values -- Tokens are stored securely outside project files -- Agent identity headers are non-secret attribution IDs -- No telemetry or analytics -- All memory content is user-owned diff --git a/plugins/kiro/assets/logo.png b/plugins/kiro/assets/logo.png deleted file mode 100644 index 645821f..0000000 Binary files a/plugins/kiro/assets/logo.png and /dev/null differ diff --git a/plugins/kiro/mcp.json b/plugins/kiro/mcp.json deleted file mode 100644 index cd387b7..0000000 --- a/plugins/kiro/mcp.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "mcpServers": { - "XMemo": { - "url": "https://xmemo.dev/mcp" - } - } -} diff --git a/plugins/kiro/steering/AGENTS.md b/plugins/kiro/steering/AGENTS.md deleted file mode 100644 index f6f7e32..0000000 --- a/plugins/kiro/steering/AGENTS.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -inclusion: auto ---- - -# XMemo Memory - -Use XMemo's hosted MCP memory tools for durable context, project preferences, decisions, reminders, and cross-session recall in Kiro. - -## When to Use - -Use XMemo when the task may depend on prior memory, durable project context, coding preferences, decisions, or follow-up actions. - -## Workflow - -1. **Recall first** when prior context could change the answer. Use XMemo search/recall/context tools before making assumptions about preferences or past decisions. -2. **Save only durable information** that the user asks to remember or that is clearly useful across future sessions. -3. **Keep memory content concise, scoped, and useful**. Prefer concrete facts, decisions, links to public docs, and action items over chat transcripts. -4. **For destructive memory actions**, confirm the exact target before deleting, forgetting, or overwriting. -5. **If authorization errors occur**, tell the user: "Run `xmemo login` or visit https://xmemo.dev to get your token, then set the `XMEMO_KEY` environment variable and restart Kiro." - -## Good Memory Candidates - -- Repository conventions and verified commands. -- Architecture decisions, release procedures, and deployment notes. -- User-approved preferences for code review, testing, documentation, or UX. -- TODOs and follow-ups that should be visible to future agents. - -## Avoid Saving - -- Secrets, credentials, OAuth codes, cookies, API keys, or token prefixes. -- Private customer data or sensitive personal data. -- Temporary debugging output that will not help future work. diff --git a/server.json b/server.json index 8a322b9..6aca5ef 100644 --- a/server.json +++ b/server.json @@ -51,14 +51,20 @@ { "registryType": "npm", "identifier": "@xmemo/client", - "version": "0.4.182", + "version": "0.4.183", "runtimeHint": "npx", "transport": { "type": "stdio" }, "packageArguments": [ - { "type": "positional", "value": "mcp" }, - { "type": "positional", "value": "serve" } + { + "type": "positional", + "value": "mcp" + }, + { + "type": "positional", + "value": "serve" + } ], "environmentVariables": [ { diff --git a/src/commands/diagnostics.js b/src/commands/diagnostics.js index 19bc7f7..90ec305 100644 --- a/src/commands/diagnostics.js +++ b/src/commands/diagnostics.js @@ -1,3 +1,4 @@ +import { kiroDoctor } from './kiro-doctor.js'; import { booleanValue, hasFlag, @@ -35,6 +36,12 @@ import { ServiceClientError, errorToExitCode } from '../api/errors.js'; import { writeFailure, writeSuccess } from '../api/envelope.js'; export async function doctorCommand(args, io) { + const client = optionValue(args, '--client'); + if (client === 'kiro') { + if (hasFlag(args, '--services')) throw new UsageError('--client kiro cannot be combined with --services.'); + return await kiroDoctor(args, io); + } + if (client || hasFlag(args, '--fix')) throw new UsageError('Local config repair requires --client kiro.'); if (hasFlag(args, '--services')) return await serviceDoctor(args, io); const baseUrl = normalizeBaseUrl(baseUrlOption(args, io.env)); const outputJson = hasFlag(args, '--json'); diff --git a/src/commands/kiro-doctor.js b/src/commands/kiro-doctor.js new file mode 100644 index 0000000..2885897 --- /dev/null +++ b/src/commands/kiro-doctor.js @@ -0,0 +1,72 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { randomUUID } from 'node:crypto'; +import { hasFlag, optionValue } from '../core/args.js'; +import { UsageError } from '../core/errors.js'; +import { isPlainObject, parseJsonConfig } from '../core/runtime.js'; +import { defaultKiroConfigPath } from '../mcp/identity/paths.js'; +import { jsonClientServerConfig } from '../mcp/formats/json.js'; +import { writeLine } from '../core/io.js'; + +// Offline configuration repair only: never read OAuth stores, send tokens, or kill processes. +export async function kiroDoctor(args, io) { + const seen = new Set(); + for (let i = 0; i < args.length; i++) { + const flag = args[i]; + if (!['--client', '--config', '--auth', '--fix', '--json'].includes(flag) || seen.has(flag)) throw new UsageError('Unsupported or duplicate Kiro doctor option. Use --client kiro [--config PATH] [--auth oauth|key] [--fix] [--json].'); + seen.add(flag); + if (['--client', '--config', '--auth'].includes(flag)) { optionValue(args, flag); i++; } + } + const configPath = optionValue(args, '--config') ?? defaultKiroConfigPath(io.env); + const auth = optionValue(args, '--auth'); + if (auth && !['oauth', 'key'].includes(auth)) throw new UsageError('--auth must be oauth or key.'); + const fix = hasFlag(args, '--fix'); + let raw; + try { raw = await fs.readFile(configPath, 'utf8'); } + catch (e) { if (e.code !== 'ENOENT') throw new UsageError('Cannot read Kiro config.'); } + let config; + try { config = raw === undefined ? {} : parseJsonConfig(raw, configPath); } + catch { throw new UsageError('Kiro config is invalid JSON; unchanged.'); } + if (!isPlainObject(config) || (config.mcpServers !== undefined && !isPlainObject(config.mcpServers))) throw new UsageError('Invalid Kiro MCP configuration; unchanged.'); + const server = config.mcpServers?.XMemo; + const issues = []; + if (isPlainObject(server) && ['headers', 'env', 'oauth'].some(k => server[k] !== undefined && !isPlainObject(server[k]))) throw new UsageError('Invalid Kiro authentication fields; config unchanged.'); + if (!isPlainObject(server)) issues.push('xmemo_server_missing'); + const legacy = isPlainObject(server) && Array.isArray(server.args) && server.args.includes('mcp-remote'); + if (legacy) issues.push('legacy_mcp_remote'); + if (server?.command && !legacy) issues.push('custom_command_requires_manual_review'); + const authorization = Object.entries(server?.headers ?? {}).find(([k]) => k.toLowerCase() === 'authorization')?.[1]; + if (server?.env?.XMEMO_KEY === '${env:XMEMO_KEY}' || (typeof authorization === 'string' && authorization.includes('${env:'))) issues.push('unsupported_key_interpolation'); + if (server?.oauth && authorization) issues.push('oauth_authorization_conflict'); + if (typeof authorization === 'string' && authorization !== 'Bearer ${XMEMO_KEY}') issues.push('nonstandard_authorization_header'); + let endpoint = server?.url ?? (legacy ? server.args.find(a => typeof a === 'string' && /^https?:\/\//.test(a)) : undefined); + let validEndpoint = false; + try { const u = new URL(endpoint); validEndpoint = u.protocol === 'https:' && !u.username && !u.password && !u.search && !u.hash && u.pathname === '/mcp'; } catch { /* reported below */ } + if (!validEndpoint) issues.push('invalid_or_missing_https_endpoint'); + const mode = auth ?? (authorization && !server?.oauth && !legacy ? 'key' : 'oauth'); + if (auth && isPlainObject(server) && ((auth === 'key' && !authorization) || (auth === 'oauth' && authorization))) issues.push('authentication_mode_change'); + const report = { client: 'kiro', configPath, ok: issues.length === 0, issues, fixed: false, authentication: mode, networkUsed: false, authenticationVerified: false }; + if (fix && issues.length) { + if (!server || !validEndpoint || (server.command && !legacy)) throw new UsageError('Cannot safely repair this Kiro entry. Run setup kiro or review its endpoint/command; config unchanged.'); + const identity = { agentId: server.headers?.['X-Memory-OS-Agent-ID'] ?? 'kiro', agentInstanceId: server.headers?.['X-Memory-OS-Agent-Instance-ID'] ?? server.env?.XMEMO_AGENT_INSTANCE_ID ?? `xmemo-kiro-${randomUUID()}` }; + const replacement = jsonClientServerConfig('kiro', endpoint, identity, { auth: mode }); + // Preserve transport timeouts, tool approvals, disabled state and unrelated client fields. + const { command, args: oldArgs, env, headers, oauth, type, ...retained } = server; + const safeHeaders = Object.fromEntries(Object.entries(headers ?? {}).filter(([k]) => k.toLowerCase() !== 'authorization')); + config.mcpServers.XMemo = { ...retained, ...replacement, headers: { ...safeHeaders, ...replacement.headers } }; + if (mode === 'oauth' && isPlainObject(oauth)) config.mcpServers.XMemo.oauth = oauth; + const backupPath = `${configPath}.xmemo-backup-${randomUUID()}`; + await fs.writeFile(backupPath, raw, { flag: 'wx', mode: 0o600 }); + // Refuse to overwrite a concurrent editor change after creating the backup. + if (await fs.readFile(configPath, 'utf8') !== raw) throw new UsageError('Kiro config changed during repair; retry after the editor finishes.'); + const temporary = path.join(path.dirname(configPath), `.xmemo-repair-${randomUUID()}.tmp`); + try { + await fs.writeFile(temporary, `${JSON.stringify(config, null, 2)}\n`, { flag: 'wx', mode: 0o600 }); + await fs.rename(temporary, configPath); + } finally { await fs.rm(temporary, { force: true }); } + Object.assign(report, { ok: true, fixed: true, backupPath }); + } + report.nextStep = report.fixed ? 'Reload Kiro. Complete OAuth consent or supply XMEMO_KEY in the launching environment. Verify a real tool call; config repair does not prove authentication.' : report.ok ? 'Configuration check passed; verify authentication in Kiro.' : 'Run xmemo doctor --client kiro --fix to repair a recognized configuration.'; + writeLine(io.stdout, hasFlag(args, '--json') ? JSON.stringify(report, null, 2) : `${report.ok ? 'PASS' : 'FAIL'} Kiro configuration: ${issues.join(', ') || 'native HTTP'}\n${report.nextStep}${report.backupPath ? `\nBackup: ${report.backupPath}` : ''}`); + return report.ok ? 0 : 1; +} diff --git a/src/commands/mcp.js b/src/commands/mcp.js index 12c3d1e..ea7bab2 100644 --- a/src/commands/mcp.js +++ b/src/commands/mcp.js @@ -40,6 +40,8 @@ export async function mcpCommand(args, io) { if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') { writeLine(io.stdout, 'MCP commands:'); writeLine(io.stdout, ` ${COMMAND_NAME} mcp serve`); + writeLine(io.stdout, ` ${COMMAND_NAME} mcp config --client kiro [--auth oauth|key] [--json]`); + writeLine(io.stdout, ` ${COMMAND_NAME} mcp add kiro [--auth oauth|key] [--write] [--force] [--config ]`); writeLine(io.stdout, ` ${COMMAND_NAME} mcp list`); writeLine(io.stdout, ` ${COMMAND_NAME} mcp config --client [--base-url ] [--json]`); writeLine(io.stdout, ` ${COMMAND_NAME} mcp proxy [--port ${DEFAULT_PROXY_PORT}] [--base-url ]`); @@ -75,7 +77,9 @@ export async function mcpCommand(args, io) { const useLocalProxy = clientId === 'copilot-cli' && !hasFlag(args, '--remote-env'); const proxyPort = parsePositiveInteger(optionValue(args, '--port') ?? String(DEFAULT_PROXY_PORT), '--port'); const proxyUrl = `http://${DEFAULT_PROXY_HOST}:${proxyPort}/mcp`; - const templateOptions = { mcpClients: MCP_CLIENTS }; + const auth = optionValue(args, '--auth'); + if (auth && (clientId !== 'kiro' || !['oauth', 'key'].includes(auth))) throw new UsageError('--auth oauth|key is supported only for Kiro.'); + const templateOptions = { mcpClients: MCP_CLIENTS, auth }; const template = useLocalProxy ? mcpLocalProxyTemplate(clientId, proxyUrl, templateOptions) : mcpConfigTemplate(clientId, mcpUrl, templateOptions); @@ -135,6 +139,8 @@ export async function mcpCommand(args, io) { } const target = args[1] ?? ''; + const auth = optionValue(args, '--auth'); + if (auth && (target !== 'kiro' || !['oauth', 'key'].includes(auth))) throw new UsageError('--auth oauth|key is supported only for Kiro.'); const client = MCP_CLIENTS.get(target); if (subcommand !== 'add' || !client) { @@ -147,7 +153,7 @@ export async function mcpCommand(args, io) { if (hasFlag(args, '--json')) { const identity = envReferenceIdentity(target); - const oauthClient = usesClientOAuth(target); + const oauthClient = (usesClientOAuth(target) && auth !== 'key'); writeLine(io.stdout, JSON.stringify({ client: target, label: client.label, @@ -168,9 +174,9 @@ export async function mcpCommand(args, io) { const identity = hasFlag(args, '--write') ? await agentIdentity(target, io.env) : envReferenceIdentity(target); if (hasFlag(args, '--write')) { - await client.writeConfig(configPath, mcpUrl, identity); + await client.writeConfig(configPath, mcpUrl, identity, { auth, force: hasFlag(args, '--force') }); writeLine(io.stdout, `Updated ${client.label} MCP config: ${configPath}`); - if (usesClientOAuth(target)) { + if ((usesClientOAuth(target) && auth !== 'key')) { writeLine(io.stdout, `Token value was not written. ${client.label} will complete MCP OAuth on first use.`); } else { writeLine(io.stdout, `Token value was not written. ${client.label} will read ${TOKEN_ENV_VAR} from the environment.`); @@ -179,12 +185,12 @@ export async function mcpCommand(args, io) { return 0; } - const snippet = client.buildSnippet(mcpUrl, identity); + const snippet = client.buildSnippet(mcpUrl, identity, { auth }); writeLine(io.stdout, `Add this to your ${client.label} config (${configPath}):`); writeLine(io.stdout, ''); writeLine(io.stdout, snippet.trimEnd()); writeLine(io.stdout, ''); - if (usesClientOAuth(target)) { + if ((usesClientOAuth(target) && auth !== 'key')) { writeLine(io.stdout, `Restart ${client.label} and complete its MCP OAuth flow. No token value is included here.`); } else { writeLine(io.stdout, `Set ${TOKEN_ENV_VAR} in your user environment or secret manager. The token value is not included here.`); diff --git a/src/commands/setup.js b/src/commands/setup.js index 69dbcd6..27059f3 100644 --- a/src/commands/setup.js +++ b/src/commands/setup.js @@ -71,6 +71,8 @@ export async function setupCommand(args, io) { throw new UsageError('Cannot specify both --all and a specific client.'); } + const auth = optionValue(optionArgs, '--auth'); + if (auth && (clientId !== 'kiro' || !['oauth', 'key'].includes(auth))) throw new UsageError('--auth oauth|key requires setup kiro.'); const dryRun = hasFlag(optionArgs, '--dry-run') || hasFlag(optionArgs, '--preview'); const force = hasFlag(optionArgs, '--force'); const writeConfig = !dryRun && (hasFlag(optionArgs, '--write') || hasFlag(optionArgs, '--yes') || shortClientSetup || (setupAll && (hasFlag(optionArgs, '--write') || hasFlag(optionArgs, '--yes')))); @@ -169,9 +171,9 @@ export async function setupCommand(args, io) { } const identity = writeConfig ? await agentIdentity(clientId, io.env) : envReferenceIdentity(clientId); - setupPlan.selectedClient = clientSetupPlan(clientId, client, setupPlan.mcpUrl, io.env, identity); + setupPlan.selectedClient = clientSetupPlan(clientId, client, setupPlan.mcpUrl, io.env, identity, { auth }); if (writeConfig) { - await client.writeConfig(setupPlan.selectedClient.configPath, setupPlan.mcpUrl, identity, { force }); + await client.writeConfig(setupPlan.selectedClient.configPath, setupPlan.mcpUrl, identity, { force, auth }); setupPlan.selectedClient.written = true; } diff --git a/src/mcp/clients/registry.js b/src/mcp/clients/registry.js index 73130ec..1364ed6 100644 --- a/src/mcp/clients/registry.js +++ b/src/mcp/clients/registry.js @@ -39,8 +39,8 @@ function jsonClient(definition, deps) { return { label: definition.label, defaultConfigPath: deps[definition.defaultConfigPath], - buildSnippet: (mcpUrl, identity) => deps.jsonClientSnippet(definition.id, mcpUrl, identity), - writeConfig: (configPath, mcpUrl, identity, options = {}) => deps.mergeJsonClientMcpConfig(definition.id, configPath, mcpUrl, identity, options.force), + buildSnippet: (mcpUrl, identity, options = {}) => deps.jsonClientSnippet(definition.id, mcpUrl, identity, options), + writeConfig: (configPath, mcpUrl, identity, options = {}) => deps.mergeJsonClientMcpConfig(definition.id, configPath, mcpUrl, identity, options.force, options), removeConfig: (configPath, options = {}) => deps.removeJsonClientMcpConfig(definition.id, configPath, options), configKind: definition.configKind, authentication: definition.authentication diff --git a/src/mcp/core/templates.js b/src/mcp/core/templates.js index 9eb607d..0cdb2c6 100644 --- a/src/mcp/core/templates.js +++ b/src/mcp/core/templates.js @@ -36,9 +36,9 @@ export function mcpConfigTemplate(clientId, mcpUrl, options = {}) { const jsonDefinition = jsonMcpClientDefinition(clientId); if (jsonDefinition) { const identityClientId = jsonDefinition.defaultIdentityId ?? clientId; - return jsonDefinition.authentication === 'oauth' - ? oauthJsonMcpTemplate(clientId, identityClientId, mcpUrl, jsonClientConfig(clientId, mcpUrl), options) - : bearerJsonMcpTemplate(clientId, identityClientId, mcpUrl, jsonClientConfig(clientId, mcpUrl), options); + return jsonDefinition.authentication === 'oauth' && !(clientId === 'kiro' && options.auth === 'key') + ? oauthJsonMcpTemplate(clientId, identityClientId, mcpUrl, jsonClientConfig(clientId, mcpUrl, undefined, options), options) + : bearerJsonMcpTemplate(clientId, identityClientId, mcpUrl, jsonClientConfig(clientId, mcpUrl, undefined, options), options); } return { diff --git a/src/mcp/formats/json.js b/src/mcp/formats/json.js index 8dbafa8..533dcb5 100644 --- a/src/mcp/formats/json.js +++ b/src/mcp/formats/json.js @@ -30,7 +30,7 @@ export const JSON_MCP_CLIENT_DEFINITIONS = Object.freeze([ nestedTransportClientDefinition('continue', 'Continue', 'defaultContinueConfigPath'), commandClientDefinition('claude-desktop', 'Claude Desktop', 'defaultClaudeConfigPath'), httpClientDefinition('openclaw', 'OpenClaw', 'defaultOpenclawConfigPath', { urlKey: 'url', authentication: 'env-bearer' }), - commandClientDefinition('kiro', 'Kiro', 'defaultKiroConfigPath'), + httpClientDefinition('kiro', 'Kiro', 'defaultKiroConfigPath', { urlKey: 'url', authentication: 'oauth' }), httpClientDefinition('kimi-code', 'Kimi Code', 'defaultKimiCodeConfigPath', { urlKey: 'url', authentication: 'bearer-token-env-var', bearerTokenEnvVar: 'XMEMO_KEY' }), commandClientDefinition('zed', 'Zed', 'defaultZedConfigPath', { section: 'context_servers' }), nestedTransportClientDefinition('jetbrains', 'JetBrains', 'defaultJetbrainsConfigPath'), @@ -103,24 +103,24 @@ export function jsonMcpClientIds() { return JSON_MCP_CLIENT_DEFINITIONS.map((definition) => definition.id); } -export function jsonClientConfig(clientId, mcpUrl, identity) { +export function jsonClientConfig(clientId, mcpUrl, identity, options = {}) { const definition = requireJsonMcpClientDefinition(clientId); - return sectionConfig(definition.section, jsonClientServerConfig(clientId, mcpUrl, identity)); + return sectionConfig(definition.section, jsonClientServerConfig(clientId, mcpUrl, identity, options)); } -export function jsonClientSnippet(clientId, mcpUrl, identity) { - return `${JSON.stringify(jsonClientConfig(clientId, mcpUrl, identity), null, 2)}\n`; +export function jsonClientSnippet(clientId, mcpUrl, identity, options = {}) { + return `${JSON.stringify(jsonClientConfig(clientId, mcpUrl, identity, options), null, 2)}\n`; } -export function jsonClientServerConfig(clientId, mcpUrl, identity) { +export function jsonClientServerConfig(clientId, mcpUrl, identity, options = {}) { const definition = requireJsonMcpClientDefinition(clientId); const resolvedIdentity = identity ?? envReferenceIdentity(definition.defaultIdentityId ?? definition.id); - return serverConfigFromDefinition(definition, mcpUrl, resolvedIdentity); + return serverConfigFromDefinition(definition, mcpUrl, resolvedIdentity, options); } -export async function mergeJsonClientMcpConfig(clientId, configPath, mcpUrl, identity, force = false) { +export async function mergeJsonClientMcpConfig(clientId, configPath, mcpUrl, identity, force = false, options = {}) { const definition = requireJsonMcpClientDefinition(clientId); - const serverConfig = serverConfigFromDefinition(definition, mcpUrl, identity); + const serverConfig = serverConfigFromDefinition(definition, mcpUrl, identity, options); await mergeJsonSectionConfig(configPath, definition.section, serverConfig, definition.section, (parsed) => { if (definition.mergeExperimentalModelContextProtocolServers && isPlainObject(parsed.experimental)) { mergeExperimentalModelContextProtocolServers(parsed, serverConfig, mcpUrl); @@ -144,7 +144,15 @@ function sectionConfig(sectionName, serverConfig) { }; } -function serverConfigFromDefinition(definition, mcpUrl, identity) { +function serverConfigFromDefinition(definition, mcpUrl, identity, options = {}) { + if (definition.id === 'kiro') { + const auth = options.auth ?? 'oauth'; + if (!['oauth', 'key'].includes(auth)) throw new UsageError('Kiro --auth must be oauth or key.'); + const headers = headersForDefinition(definition, identity); + return auth === 'key' + ? { url: mcpUrl, headers: { ...headers, Authorization: 'Bearer ${XMEMO_KEY}' } } + : { url: mcpUrl, headers, oauth: { oauthScopes: ['memory:read', 'knowledge:read'] } }; + } if (definition.serverKind === 'mcp-remote-command') { return mcpRemoteCommandJsonServerConfig(mcpUrl, identity); } diff --git a/src/ui/help.js b/src/ui/help.js index c82ef98..45ba78c 100644 --- a/src/ui/help.js +++ b/src/ui/help.js @@ -46,6 +46,8 @@ export function writeHelp(io) { writeLine(io.stdout, ' Store an existing token after explicit consent to unencrypted storage.'); writeLine(io.stdout, ''); writeLine(io.stdout, 'Operations'); + writeLine(io.stdout, ` ${COMMAND_NAME} doctor --client kiro [--config ] [--auth oauth|key] [--fix] [--json]`); + writeLine(io.stdout, ` ${COMMAND_NAME} setup kiro [--auth oauth|key]`); writeLine(io.stdout, ` ${COMMAND_NAME} doctor [--services [memory,dream,knowledge,cloud-skill]] [--base-url ] [--json]`); writeLine(io.stdout, ' Validate runtime, service reachability, and integration readiness.'); writeLine(io.stdout, ` ${COMMAND_NAME} status [--url ] [--json]`); diff --git a/src/ui/setup.js b/src/ui/setup.js index 20daeca..f99c766 100644 --- a/src/ui/setup.js +++ b/src/ui/setup.js @@ -87,7 +87,7 @@ export function normalizeSetupClientId(candidate, mcpClients) { return normalized; } -export function clientSetupPlan(clientId, client, mcpUrl, env, identity) { +export function clientSetupPlan(clientId, client, mcpUrl, env, identity, options = {}) { return { id: clientId, label: client.label, @@ -95,7 +95,8 @@ export function clientSetupPlan(clientId, client, mcpUrl, env, identity) { configPath: client.defaultConfigPath(env), serverName: MCP_SERVER_NAME, mcpUrl, - tokenEnvVar: TOKEN_ENV_VAR, + tokenEnvVar: usesClientOAuth(clientId) && options.auth !== 'key' ? null : TOKEN_ENV_VAR, + authentication: usesClientOAuth(clientId) && options.auth !== 'key' ? 'oauth' : 'env-bearer', agentId: identity.agentId, agentInstanceId: identity.agentInstanceId, agentInstanceIdPath: identity.path, @@ -297,7 +298,7 @@ export function writeSetupSummary(plan, io) { if (plan.tokenPortalUrl) { writeLine(io.stdout, ` (Token portal: ${plan.tokenPortalUrl})`); } - } else if (usesClientOAuth(cid)) { + } else if (usesClientOAuth(cid) && plan.selectedClient.authentication !== 'env-bearer') { writeLine(io.stdout, `💡 Next steps for ${plan.selectedClient.label}:`); writeLine(io.stdout, ' 1. When the agent starts or first makes an XMemo tool call, a browser window will automatically pop up requesting OAuth authorization.'); writeLine(io.stdout, ' 2. Follow the page prompts to sign in and click "Authorize".'); diff --git a/test/branding.test.js b/test/branding.test.js index a69f417..e0c9bae 100644 --- a/test/branding.test.js +++ b/test/branding.test.js @@ -11,7 +11,6 @@ const root = path.join(__dirname, '..'); test('official XMemo logo is consistent across published integrations', async () => { const logoPaths = [ 'plugins/xmemo/assets/logo.png', - 'plugins/kiro/assets/logo.png', 'extensions/vscode/media/icon.png' ]; const hashes = await Promise.all( @@ -24,20 +23,17 @@ test('official XMemo logo is consistent across published integrations', async () assert.equal(new Set(hashes).size, 1); - const [cursorManifest, kiroManifest, lobeManifest, readme] = await Promise.all([ + const [cursorManifest, lobeManifest, readme] = await Promise.all([ readJson('plugins/xmemo/.cursor-plugin/plugin.json'), - readJson('plugins/kiro/.kiro-plugin/power.json'), readJson('lhm.plugin.json'), readFile(path.join(root, 'README.md'), 'utf8') ]); assert.equal(cursorManifest.logo, 'assets/logo.png'); - assert.equal(kiroManifest.logo, 'assets/logo.png'); assert.match(lobeManifest.icon, /plugins\/xmemo\/assets\/logo\.png$/); assert.match(readme, /plugins\/xmemo\/assets\/logo\.png/); await assert.rejects(access(path.join(root, 'plugins/xmemo/assets/logo.svg'))); - await assert.rejects(access(path.join(root, 'plugins/kiro/assets/logo.svg'))); }); async function readJson(relativePath) { diff --git a/test/cli.test.js b/test/cli.test.js index 7a7146e..4e483a7 100644 --- a/test/cli.test.js +++ b/test/cli.test.js @@ -2138,20 +2138,47 @@ test('setup kiro shorthand writes config by default', async () => { assert.equal(plan.selectedClient.behaviorProfile.writesTokenValue, false); const config = JSON.parse(await fs.readFile(path.join(tempDir, '.kiro', 'settings', 'mcp.json'), 'utf8')); - assert.equal(config.mcpServers.XMemo.command, 'npx'); - assert.deepEqual(config.mcpServers.XMemo.args, [ - '-y', - 'mcp-remote', - 'https://mcp.example.test/mcp', - '--header', - 'Authorization:Bearer ${XMEMO_KEY}', - '--header', - 'X-Memory-OS-Agent-ID:kiro', - '--header', - 'X-Memory-OS-Agent-Instance-ID:${XMEMO_AGENT_INSTANCE_ID}' - ]); - assert.equal(config.mcpServers.XMemo.env.XMEMO_KEY, '${env:XMEMO_KEY}'); - assert.match(config.mcpServers.XMemo.env.XMEMO_AGENT_INSTANCE_ID, /^xmemo-/); + assert.equal(config.mcpServers.XMemo.url, 'https://mcp.example.test/mcp'); + assert.equal(config.mcpServers.XMemo.command, undefined); + assert.equal(config.mcpServers.XMemo.headers.Authorization, undefined); + assert.deepEqual(config.mcpServers.XMemo.oauth.oauthScopes, ['memory:read', 'knowledge:read']); + assert.match(config.mcpServers.XMemo.headers['X-Memory-OS-Agent-Instance-ID'], /^xmemo-/); + assert.doesNotMatch(JSON.stringify(config), /secret-token-that-must-not-leak/); + + const profile = await fs.readFile(path.join(tempDir, '.kiro', 'steering', 'AGENTS.md'), 'utf8'); + assert.match(profile, /XMemo Agent profile/); + assert.match(profile, /recall\/search/); + assert.doesNotMatch(profile, /secret-token-that-must-not-leak/); +}); + +test('setup kiro Key mode writes native headers and reports Key authentication', async () => { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'memory-os-setup-kiro-')); + const result = await invoke(['setup', 'kiro', '--auth', 'key', '--url', 'https://api.example.test', '--json'], { + env: { + HOME: tempDir, + USERPROFILE: tempDir, + XMEMO_KEY: 'secret-token-that-must-not-leak' + }, + fetch: discoveryFetch() + }); + + assert.equal(result.code, 0); + assert.doesNotMatch(result.stdout, /secret-token-that-must-not-leak/); + const plan = JSON.parse(result.stdout); + assert.equal(plan.selectedClient.id, 'kiro'); + assert.equal(plan.selectedClient.written, true); + assert.equal(plan.selectedClient.behaviorProfile.targetPath, path.join(tempDir, '.kiro', 'steering', 'AGENTS.md')); + assert.equal(plan.selectedClient.behaviorProfile.written, true); + assert.equal(plan.selectedClient.behaviorProfile.writesTokenValue, false); + + const config = JSON.parse(await fs.readFile(path.join(tempDir, '.kiro', 'settings', 'mcp.json'), 'utf8')); + assert.equal(config.mcpServers.XMemo.url, 'https://mcp.example.test/mcp'); + assert.equal(config.mcpServers.XMemo.command, undefined); + assert.equal(config.mcpServers.XMemo.headers.Authorization, 'Bearer ${XMEMO_KEY}'); + assert.equal(config.mcpServers.XMemo.oauth, undefined); + assert.equal(plan.selectedClient.authentication, 'env-bearer'); + assert.equal(plan.selectedClient.tokenEnvVar, 'XMEMO_KEY'); + assert.match(config.mcpServers.XMemo.headers['X-Memory-OS-Agent-Instance-ID'], /^xmemo-/); assert.doesNotMatch(JSON.stringify(config), /secret-token-that-must-not-leak/); const profile = await fs.readFile(path.join(tempDir, '.kiro', 'steering', 'AGENTS.md'), 'utf8'); diff --git a/test/kiro-doctor.test.js b/test/kiro-doctor.test.js new file mode 100644 index 0000000..443c187 --- /dev/null +++ b/test/kiro-doctor.test.js @@ -0,0 +1,105 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import { test } from 'node:test'; +import { run } from '../src/cli.js'; +import { jsonClientServerConfig } from '../src/mcp/formats/json.js'; +const identity = { agentId: 'kiro', agentInstanceId: 'xmemo-test-instance' }; +async function invoke(args, env = {}) { + let stdout = '', stderr = ''; + const code = await run(args, { stdout: { write: s => { stdout += s; } }, stderr: { write: s => { stderr += s; } }, env, fetch: async () => { throw Error('Offline test must not fetch'); } }); + return { code, stdout, stderr }; +} +async function fixture(t, entry) { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'xmemo-kiro-doctor-')); + t.after(() => fs.rm(dir, { recursive: true, force: true })); + const file = path.join(dir, 'mcp.json'); + const config = { mcpServers: { XMemo: entry, Other: { command: 'other', env: { SECRET: 'unrelated-secret' } } }, powers: { mcpServers: {} }, custom: 7 }; + await fs.writeFile(file, JSON.stringify(config)); + return { dir, file, config }; +} +const legacy = { command: 'npx', args: ['-y', 'mcp-remote', 'https://xmemo.dev/mcp', '--header', 'Authorization:Bearer ${XMEMO_KEY}'], env: { XMEMO_KEY: '${env:XMEMO_KEY}', XMEMO_AGENT_INSTANCE_ID: 'stable-instance' }, autoApprove: ['recall'], disabled: true, timeout: 12345 }; + +test('Kiro native OAuth and Key templates are separate and never embed credentials', async () => { + for (const auth of ['oauth', 'key']) { + const r = await invoke(['mcp', 'config', '--client', 'kiro', '--auth', auth, '--json'], { XMEMO_KEY: 'secret-sentinel' }); + assert.equal(r.code, 0, r.stderr); + const template = JSON.parse(r.stdout), s = template.snippet.mcpServers.XMemo; + assert.equal(s.url, 'https://xmemo.dev/mcp'); + assert.equal(s.command, undefined); assert.equal(s.env, undefined); + assert.doesNotMatch(r.stdout, /secret-sentinel|mcp-remote|env:XMEMO_KEY/); + if (auth === 'oauth') { assert.equal(s.headers.Authorization, undefined); assert.deepEqual(template.requiresEnv, []); } + else { assert.equal(s.headers.Authorization, 'Bearer ${XMEMO_KEY}'); assert.equal(s.oauth, undefined); assert.deepEqual(template.requiresEnv, ['XMEMO_KEY']); } + } +}); +test('mcp add kiro writes native Key config with matching guidance', async t => { + const { dir, file } = await fixture(t, legacy); + const r = await invoke(['mcp', 'add', 'kiro', '--auth', 'key', '--write', '--force', '--config', file], { HOME: dir, USERPROFILE: dir, XMEMO_KEY: 'secret-sentinel' }); + assert.equal(r.code, 0, r.stderr); + const s = JSON.parse(await fs.readFile(file)).mcpServers.XMemo; + assert.equal(s.headers.Authorization, 'Bearer ${XMEMO_KEY}'); assert.equal(s.command, undefined); + assert.match(r.stdout, /read XMEMO_KEY/); assert.doesNotMatch(r.stdout, /secret-sentinel/); +}); +test('doctor detects legacy auth loop offline, backs up and repairs only XMemo, then is idempotent', async t => { + const { file, config } = await fixture(t, legacy); + const before = await fs.readFile(file, 'utf8'); + let r = await invoke(['doctor', '--client', 'kiro', '--config', file, '--json']); + assert.equal(r.code, 1); assert.match(r.stdout, /legacy_mcp_remote|unsupported_key_interpolation/); + assert.equal(await fs.readFile(file, 'utf8'), before); + r = await invoke(['doctor', '--client', 'kiro', '--config', file, '--fix', '--json']); + assert.equal(r.code, 0, r.stderr); + const report = JSON.parse(r.stdout); assert.equal(report.fixed, true); assert.equal(report.authenticationVerified, false); + assert.equal(await fs.readFile(report.backupPath, 'utf8'), before); + const after = JSON.parse(await fs.readFile(file, 'utf8')); + assert.deepEqual(after.mcpServers.Other, config.mcpServers.Other); assert.deepEqual(after.powers, config.powers); assert.equal(after.custom, 7); + assert.equal(after.mcpServers.XMemo.headers['X-Memory-OS-Agent-Instance-ID'], 'stable-instance'); + for (const field of ['autoApprove', 'disabled', 'timeout']) assert.deepEqual(after.mcpServers.XMemo[field], legacy[field]); + assert.equal(after.mcpServers.XMemo.command, undefined); assert.equal(after.mcpServers.XMemo.headers.Authorization, undefined); + assert.doesNotMatch(r.stdout, /unrelated-secret/); + r = await invoke(['doctor', '--client', 'kiro', '--config', file, '--fix', '--json']); + assert.equal(r.code, 0); assert.equal(JSON.parse(r.stdout).fixed, false); +}); +test('doctor explicit Key repair removes OAuth and unsupported interpolation', async t => { + const { file } = await fixture(t, { url: 'https://xmemo.dev/mcp', oauth: { oauthScopes: ['memory:read'] }, headers: { authorization: 'Bearer ${env:XMEMO_KEY}' } }); + const r = await invoke(['doctor', '--client', 'kiro', '--config', file, '--fix', '--auth', 'key', '--json']); + assert.equal(r.code, 0, r.stderr); + const s = JSON.parse(await fs.readFile(file)).mcpServers.XMemo; + assert.equal(s.oauth, undefined); assert.equal(s.headers.Authorization, 'Bearer ${XMEMO_KEY}'); assert.equal(s.headers.authorization, undefined); +}); +test('doctor refuses malformed, custom-command and unsafe endpoint repairs without mutation', async t => { + for (const entry of [{ command: 'custom' }, { ...legacy, args: ['mcp-remote', 'https://user:secret@xmemo.dev/mcp'] }, { url: 'http://xmemo.dev/mcp' }]) { + const { file } = await fixture(t, entry), before = await fs.readFile(file, 'utf8'); + const r = await invoke(['doctor', '--client', 'kiro', '--config', file, '--fix', '--json']); + assert.notEqual(r.code, 0); assert.equal(await fs.readFile(file, 'utf8'), before); assert.doesNotMatch(r.stdout + r.stderr, /user:secret/); + } + const { file } = await fixture(t, legacy); await fs.writeFile(file, '{"secret":"hidden",'); + const r = await invoke(['doctor', '--client', 'kiro', '--config', file, '--fix']); + assert.notEqual(r.code, 0); assert.doesNotMatch(r.stderr, /hidden/); +}); +test('doctor native OAuth passes without requesting a Key or network', async t => { + const { file } = await fixture(t, jsonClientServerConfig('kiro', 'https://xmemo.dev/mcp', identity)); + const r = await invoke(['doctor', '--client', 'kiro', '--config', file, '--json']); + assert.equal(r.code, 0); assert.equal(JSON.parse(r.stdout).networkUsed, false); +}); + +test('doctor rejects unknown options and malformed auth objects without touching files', async t => { + const { file } = await fixture(t, legacy); + const before = await fs.readFile(file, 'utf8'); + const r = await invoke(['doctor', '--client', 'kiro', '--config', file, '--fix', '--dry-run']); + assert.notEqual(r.code, 0); assert.equal(await fs.readFile(file, 'utf8'), before); + const bad = { mcpServers: { XMemo: { url: 'https://xmemo.dev/mcp', headers: 'secret-invalid-header' } } }; + await fs.writeFile(file, JSON.stringify(bad)); + const r2 = await invoke(['doctor', '--client', 'kiro', '--config', file, '--fix']); + assert.notEqual(r2.code, 0); assert.doesNotMatch(r2.stderr, /secret-invalid-header/); + assert.deepEqual(JSON.parse(await fs.readFile(file)), bad); +}); +test('doctor preserves existing OAuth write scopes while removing conflicting auth header', async t => { + const scopes = ['memory:read', 'memory:write', 'knowledge:read', 'knowledge:write']; + const { file } = await fixture(t, { url: 'https://xmemo.dev/mcp', oauth: { oauthScopes: scopes }, headers: { Authorization: 'Bearer secret-sentinel', 'X-Custom': 'keep' } }); + const r = await invoke(['doctor', '--client', 'kiro', '--config', file, '--fix', '--json']); + assert.equal(r.code, 0, r.stderr); + const s = JSON.parse(await fs.readFile(file)).mcpServers.XMemo; + assert.deepEqual(s.oauth.oauthScopes, scopes); assert.equal(s.headers.Authorization, undefined); assert.equal(s.headers['X-Custom'], 'keep'); + assert.doesNotMatch(r.stdout + r.stderr, /secret-sentinel/); +});