From ccd54864caebd3b67c0ec8917577c55ac762f867 Mon Sep 17 00:00:00 2001 From: pc-gemini Date: Thu, 10 Sep 2026 11:33:52 +0900 Subject: [PATCH 1/3] chore: remove retired Kiro package assets --- .gitignore | 2 + .npmignore | 2 - README.md | 2 +- .../MOS_01_CLOUD_SKILL_SAFE_WRITE_HANDOFF.md | 60 ----- ..._CLI_SERVICE_CLIENT_BASELINE_2026-09-03.md | 167 ------------ ...CLIENT_IMPLEMENTATION_STATUS_2026-09-04.md | 99 ------- package.json | 1 - plugins/kiro/.kiro-plugin/power.json | 35 --- plugins/kiro/CHANGELOG.md | 9 - plugins/kiro/LICENSE | 7 - plugins/kiro/POWER.md | 148 ---------- plugins/kiro/README.md | 45 ---- plugins/kiro/SETUP.md | 254 ------------------ plugins/kiro/assets/logo.png | Bin 38217 -> 0 bytes plugins/kiro/mcp.json | 7 - plugins/kiro/steering/AGENTS.md | 32 --- test/branding.test.js | 6 +- 17 files changed, 4 insertions(+), 872 deletions(-) delete mode 100644 docs/design/MOS_01_CLOUD_SKILL_SAFE_WRITE_HANDOFF.md delete mode 100644 docs/design/XMEMO_CLI_SERVICE_CLIENT_BASELINE_2026-09-03.md delete mode 100644 docs/design/XMEMO_CLI_SERVICE_CLIENT_IMPLEMENTATION_STATUS_2026-09-04.md delete mode 100644 plugins/kiro/.kiro-plugin/power.json delete mode 100644 plugins/kiro/CHANGELOG.md delete mode 100644 plugins/kiro/LICENSE delete mode 100644 plugins/kiro/POWER.md delete mode 100644 plugins/kiro/README.md delete mode 100644 plugins/kiro/SETUP.md delete mode 100644 plugins/kiro/assets/logo.png delete mode 100644 plugins/kiro/mcp.json delete mode 100644 plugins/kiro/steering/AGENTS.md 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/README.md b/README.md index c7b93d6..1ced3b0 100644 --- a/README.md +++ b/README.md @@ -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 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.json b/package.json index 88c53a0..61a5911 100644 --- a/package.json +++ b/package.json @@ -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 645821f13ac1c00fb579051863cde79d4b495a05..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 38217 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 Rf` { 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) { From 535c86b8fb6fcefa96b9389c037d7ec4a8db32ac Mon Sep 17 00:00:00 2001 From: pc-gemini Date: Fri, 11 Sep 2026 16:16:27 +0900 Subject: [PATCH 2/3] fix(kiro): generate native auth configs and repair legacy MCP setup --- MCP-README.md | 2 +- MCP-SETUP-GUIDE.md | 19 ++++++- README.md | 11 +++- src/commands/diagnostics.js | 7 +++ src/commands/kiro-doctor.js | 72 +++++++++++++++++++++++++ src/commands/mcp.js | 18 ++++--- src/commands/setup.js | 6 ++- src/mcp/clients/registry.js | 4 +- src/mcp/core/templates.js | 6 +-- src/mcp/formats/json.js | 28 ++++++---- src/ui/help.js | 2 + src/ui/setup.js | 7 +-- test/cli.test.js | 55 ++++++++++++++----- test/kiro-doctor.test.js | 105 ++++++++++++++++++++++++++++++++++++ 14 files changed, 298 insertions(+), 44 deletions(-) create mode 100644 src/commands/kiro-doctor.js create mode 100644 test/kiro-doctor.test.js 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 1ced3b0..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 | @@ -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/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/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/); +}); From 09f52b61ed741984c40a96f3b9e0c5c820b10245 Mon Sep 17 00:00:00 2001 From: pc-gemini Date: Fri, 11 Sep 2026 16:19:07 +0900 Subject: [PATCH 3/3] chore: prepare CLI 0.4.183 release --- package-lock.json | 4 ++-- package.json | 2 +- server.json | 12 +++++++++--- 3 files changed, 12 insertions(+), 6 deletions(-) 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 61a5911..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", 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": [ {