diff --git a/catalog.json b/catalog.json index b6a46c8..a9ce7f6 100644 --- a/catalog.json +++ b/catalog.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "providerId": "official", "name": "PI-Desktop Official Plugins", - "updatedAt": "2026-09-20T01:32:20Z", + "updatedAt": "2026-09-20T15:44:14Z", "homepage": "https://github.com/vastsa/pi-desktop-plugins", "plugins": [ { @@ -301,17 +301,17 @@ "downloads": 0, "homepage": "https://github.com/vastsa/pi-desktop-plugins/tree/main/plugins/io.github.muzimu217.session-import", "repository": "https://github.com/vastsa/pi-desktop-plugins", - "readmeMarkdown": "# Universal Session Import\uff08\u4e00\u4f53\u5316\u4f1a\u8bdd\u5bfc\u5165\uff09\n\nPI-Desktop \u63d2\u4ef6\uff1a**\u626b\u63cf\u672c\u673a\u5b89\u88c5\u7684\u7f16\u7a0b\u5de5\u5177\uff0c\u628a\u5b83\u4eec\uff08ZCode\u3001WorkBuddy\u3001Claude Code\u3001Codex\u3001OpenCode\u3001Pi\uff09\u7684\u672c\u5730\u4f1a\u8bdd\u7edf\u4e00\u5bfc\u5165 PI-Desktop\u3002**\n\n![scan sources](docs/screenshots/01-scan-sources.png)\n\n## \u4f7f\u7528\u6d41\u7a0b\n\n### 1. \u626b\u63cf\u672c\u673a\u5de5\u5177\n\n\u6253\u5f00\u9762\u677f\u70b9\u51fb\u300c\u626b\u63cf\u672c\u673a\u5de5\u5177\u300d\uff0c\u63d2\u4ef6\u5e76\u884c\u63a2\u6d4b\u516d\u4e2a\u6765\u6e90\uff0c\u5b9e\u65f6\u663e\u793a\u5404\u81ea\u68c0\u6d4b\u5230\u7684\u4f1a\u8bdd\u6570\uff08\u672a\u5b89\u88c5\u7684\u5de5\u5177\u663e\u793a\u300c\u672a\u68c0\u6d4b\u5230\u300d\uff09\u3002\n\n### 2. \u9009\u62e9\u6765\u6e90\uff0c\u6d4f\u89c8\u4f1a\u8bdd\n\n\u70b9\u51fb\u68c0\u6d4b\u5230\u7684\u5de5\u5177\u5361\u7247\uff0c\u6309\u9879\u76ee\u5206\u7ec4\u8f7d\u5165\u8be5\u6765\u6e90\u7684\u4f1a\u8bdd\u5217\u8868\uff08\u652f\u6301\u641c\u7d22\u3001\u6574\u7ec4\u5168\u9009\uff09\uff1a\n\n![session list](docs/screenshots/02-session-list.png)\n\n### 3. \u9884\u89c8\u5bf9\u8bdd\n\n\u70b9\u4efb\u610f\u4f1a\u8bdd\u5373\u53ef\u6309\u539f\u59cb\u987a\u5e8f\u8fd8\u539f\u5b8c\u6574\u5bf9\u8bdd\u2014\u2014\u7528\u6237\u6d88\u606f\u3001\u52a9\u624b\u56de\u590d\u3001\u5de5\u5177\u8c03\u7528\uff08\u53c2\u6570 + \u7ed3\u679c\u6298\u53e0\u5c55\u793a\uff0c\u9519\u8bef\u6807\u7ea2\uff09\uff1a\n\n![preview](docs/screenshots/03-preview.png)\n\n### 4. \u5bfc\u5165\u4e3a\u4f1a\u8bdd\n\n\u52fe\u9009\u4f1a\u8bdd\uff08\u652f\u6301\u6574\u7ec4\u5168\u9009\uff09\u2192\u300c\u5bfc\u5165\u4e3a\u4f1a\u8bdd\u300d\uff0c\u901a\u8fc7\u5bbf\u4e3b `session.import` \u901a\u9053\u5199\u5165 PI-Desktop \u4f1a\u8bdd\u5e93\uff1a\n\n![import status](docs/screenshots/04-import-status.png)\n\n\u5bfc\u5165\u7684\u4f1a\u8bdd\u6309**\u539f\u59cb\u5de5\u4f5c\u76ee\u5f55**\u5f52\u5c5e\u9879\u76ee\uff1a\u8be5\u9879\u76ee\u5df2\u5728 PI-Desktop \u6253\u5f00\u65f6**\u7acb\u523b\u5c55\u5f00\u53ef\u89c1**\uff1b\u672a\u6253\u5f00\u7684\u9879\u76ee\u4e5f\u4f1a\u81ea\u52a8\u52a0\u5165\u5de6\u4fa7\u9879\u76ee\u5217\u8868\u5e76\u5c55\u5f00\uff1a\n\n![sidebar auto expand](docs/screenshots/05-sidebar-auto-expand.png)\n\n![sidebar codex](docs/screenshots/06-sidebar-codex.png)\n\n\u91cd\u590d\u5bfc\u5165\u81ea\u52a8\u8df3\u8fc7\uff08\u5e42\u7b49 id\uff1a`import-<\u6765\u6e90>-<\u5916\u90e8\u4f1a\u8bdd id>`\uff09\u3002\n\n## \u6765\u6e90\u4e0e\u7279\u6b8a\u5904\u7406\n\n| \u6765\u6e90 | \u6570\u636e\u4f4d\u7f6e | \u7279\u6b8a\u5904\u7406 |\n| --- | --- | --- |\n| ZCode | `~/.zcode/cli/db/db.sqlite` | `node:sqlite` \u53ea\u8bfb\uff1b\u8fc7\u6ee4 `subagent_child` \u5185\u90e8\u4f1a\u8bdd\u4e0e `synthetic` \u6ce8\u5165\u6587\u672c\uff1b\u5de5\u5177\u8c03\u7528\u6309\u5b58\u50a8\u987a\u5e8f\u8fd8\u539f |\n| WorkBuddy | `~/.workbuddy/projects/**/*.jsonl` | \u5265\u79bb ``/`` \u6ce8\u5165\u5757\uff1b`function_call` \u6309 `callId` \u914d\u5bf9\uff1b\u5916\u7f6e\u5927\u7ed3\u679c\u5b89\u5168\u56de\u8bfb\uff1b`ai-title` \u4f18\u5148 |\n| Claude Code | `~/.claude/projects/**/*.jsonl` | \u8fc7\u6ee4 sidechain \u4e0e\u5408\u6210\u7528\u6237\u884c\uff1b`tool_use`/`tool_result` \u914d\u5bf9 |\n| Codex | `~/.codex/sessions/**/*.jsonl` | \u517c\u5bb9\u65b0\u65e7\u4e24\u79cd\u683c\u5f0f\uff1b\u8fc7\u6ee4 `# AGENTS.md` \u7b49\u5408\u6210\u884c |\n| OpenCode | `~/.local/share/opencode/opencode.db` | `node:sqlite` \u53ea\u8bfb\uff1bsession\u2192message\u2192part \u4e09\u5c42\u8fd8\u539f\uff08OpenCode v1.x SQLite\uff09 |\n| Pi | `~/.pi/agent/sessions/**/*.jsonl` | `toolCall`/`toolResult` \u914d\u5bf9\uff1b`session_info.name` \u4f18\u5148 |\n\n## \u81ea\u5b9a\u4e49\u6765\u6e90\uff08\u65e0\u9700\u6539\u4ee3\u7801\uff09\n\n\u5185\u7f6e 6 \u4e2a\u6765\u6e90\u4e4b\u5916\uff0c\u4f60\u53ef\u4ee5\u5728**\u5f53\u524d\u5de5\u4f5c\u533a**\u653e\u4e00\u4efd\u7eaf JSON\uff0c\u63cf\u8ff0\u81ea\u5df1\u7684\u5de5\u5177\uff1a\n\n```\ndocs/session-import-sources.json\n```\n\n\u5185\u7f6e\u9a71\u52a8\u8986\u76d6\u4e09\u7c7b\u78c1\u76d8\u683c\u5f0f\uff0c\u7edd\u5927\u591a\u6570\u5de5\u5177\u90fd\u80fd\u843d\u5728\u5176\u4e2d\u4e00\u4e2a\u4e0a\uff1a\n\n| driver | \u9002\u7528 | \u5185\u7f6e\u4f7f\u7528\u8005 |\n| --- | --- | --- |\n| `jsonl-transcript` | \u4e00\u884c\u4e00\u6761\u8bb0\u5f55\u7684 JSONL \u5bf9\u8bdd\u6587\u4ef6 | Claude Code\u3001Codex\u3001WorkBuddy\u3001Pi |\n| `sqlite-session` | SQLite\uff1a`session \u2192 message \u2192 part` \u4e09\u5c42\uff08\u6216 session+message \u4e24\u5c42\uff09 | ZCode\u3001OpenCode |\n| `json-tree` | \u4e00\u4e2a JSON \u6587\u4ef6\u5373\u4e00\u4e2a\u4f1a\u8bdd | \u65e7\u7248 OpenCode / Claude \u5e03\u5c40 |\n\n\u9762\u677f\u300c\u81ea\u5b9a\u4e49\u6765\u6e90 \u2192 \u91cd\u65b0\u52a0\u8f7d\u300d\u5373\u53ef\u70ed\u52a0\u8f7d\uff0c\u65b0\u6765\u6e90\u5e26 `\u81ea\u5b9a\u4e49` \u6807\u7b7e\u51fa\u73b0\u3002\u5b8c\u6574\u5b57\u6bb5\u89c1\n[`docs/CUSTOM-SOURCES.md`](docs/CUSTOM-SOURCES.md)\uff0c\u91cc\u9762\u6709\u53ef\u76f4\u63a5\u590d\u5236\u7684\u4e09\u4e2a driver \u793a\u4f8b\u3002\n\n> **\u5b89\u5168\u8fb9\u754c**\uff1a\u914d\u7f6e**\u53ea\u5141\u8bb8\u7eaf\u6570\u636e**\u3002\u4efb\u4f55 `eval` / `code` / `require` / `transform` / `script`\n> \u4e4b\u7c7b\u7684\u952e\uff08\u4efb\u610f\u5c42\u7ea7\uff09\u90fd\u4f1a\u88ab\u62d2\u7edd\u2014\u2014\u63d2\u4ef6\u5bf9\u7528\u6237\u4e3b\u76ee\u5f55\u6709\u8bfb\u6743\u9650\uff0c\u5141\u8bb8\u914d\u7f6e\u643a\u5e26\u4ee3\u7801\u7b49\u540c\u4e8e\u4efb\u610f\u4ee3\u7801\u6267\u884c\u3002\n> \u6570\u636e\u6839\u76ee\u5f55\u4e5f\u4e0d\u63a5\u53d7 `/` \u6216\u6574\u4e2a home\u3002\n\n## \u6743\u9650\n\n| \u6743\u9650 | \u7528\u9014 |\n| --- | --- |\n| `ui.panel` / `ui.view` | \u5bfc\u5165\u9762\u677f\u4e0e\u300c\u4f1a\u8bdd\u7194\u7089\u300d\u5de5\u4f5c\u9762\u677f\u89c6\u56fe |\n| `session.read` / `session.read.own` | \u8bfb\u56de\u672c\u63d2\u4ef6\u5bfc\u5165\u8fc7\u7684\u4f1a\u8bdd\uff0c\u4f9b\u7194\u7089\u84b8\u998f |\n| `session.import` | \u628a\u9009\u4e2d\u7684\u4f1a\u8bdd\u5199\u5165 PI-Desktop \u4f1a\u8bdd\u5e93 |\n| `project.create` | \u6309\u4f1a\u8bdd\u7684 `projectPath` \u5e42\u7b49\u89e3\u6790 / \u7ed1\u5b9a projectId\uff08\u65e0\u6b64\u80fd\u529b\u7684\u5bbf\u4e3b\u9000\u56de\u672a\u7ed1\u5b9a\u5bfc\u5165\uff09 |\n| `agent.complete` / `models.list` | \u8c03\u7528\u5bbf\u4e3b\u6a21\u578b\u505a\u84b8\u998f\uff08\u4f7f\u7528\u5bbf\u4e3b\u51ed\u636e\u4e0e\u989d\u5ea6\uff0c\u63d2\u4ef6\u4e0d\u63a5\u89e6 API Key\uff09 |\n| `fs.read` | \u53ea\u8bfb\u5404\u5de5\u5177\u7684\u672c\u5730\u4f1a\u8bdd\u6570\u636e\uff08`.jsonl` / SQLite \u7684 `readOnly` \u6a21\u5f0f\uff09\uff1b\u5e76\u8bfb\u53d6\u5de5\u4f5c\u533a `docs/session-import-sources.json` \u91cc\u7684\u81ea\u5b9a\u4e49\u6765\u6e90\u914d\u7f6e |\n| `fs.write` | **\u4ec5**\u7528\u4e8e\u4fdd\u5b58\u84b8\u998f\u7ed3\u679c\uff0c\u8303\u56f4\u9650\u5b9a\u5728\u5de5\u4f5c\u533a\u5185\u7684 `AGENTS.md`\u3001`*.md`\u3001`docs/**`\u3001`.agents/skills/**` |\n\n\u8bfb\u53d6\u5404\u5de5\u5177\u672c\u5730\u6570\u636e\u7531\u63d2\u4ef6\u8fdb\u7a0b**\u53ea\u8bfb**\u5b8c\u6210\uff08`node:sqlite` \u7684 `readOnly` \u6a21\u5f0f\uff0c\u6216\u76f4\u63a5\u8bfb\u53d6\n`.jsonl`\uff09\u3002**\u5bfc\u5165**\u7ecf\u7531\u5bbf\u4e3b `session.import` \u901a\u9053\u5b8c\u6210\uff08\u8be5\u901a\u9053\u4e3a\u63d2\u4ef6\u5bbf\u4e3b\u65b0\u589e\u7684\u5e95\u5c42 API\n\u63d0\u6848\uff0c\u89c1 [PI-Desktop#134](https://github.com/vastsa/PI-Desktop/issues/134)\uff09\u3002\n\n\u63d2\u4ef6\u4e0d\u53d1\u8d77\u4efb\u4f55\u7f51\u7edc\u8bf7\u6c42\u3001\u65e0\u9065\u6d4b\u4e0a\u62a5\uff1b\u7194\u7089\u8c03\u7528\u8d70\u5bbf\u4e3b `pi.agent.complete`\uff0c\u51ed\u636e\u4fdd\u7559\u5728\nElectron \u4e3b\u8fdb\u7a0b\u3002\n\n## \u5f00\u53d1\n\n```bash\n# \u5728 PI-Desktop \u4ed3\u5e93\u5185\u6821\u9a8c\u4e0e\u6253\u5305\npnpm --filter @pi-desktop/plugin-devkit... build\nnode packages/plugin-devkit/dist/cli.js check /absolute/path/to/pi-desktop-session-import\nnode packages/plugin-devkit/dist/cli.js pack /absolute/path/to/pi-desktop-session-import\n```\n\n\u5728 PI-Desktop \u4e2d\uff1a\u63d2\u4ef6\u9875 \u2192 Load development plugin \u2192 \u9009\u62e9\u672c\u76ee\u5f55\u3002\n\n## License\n\nMIT\n", + "readmeMarkdown": "# Universal Session Import\uff08\u4e00\u4f53\u5316\u4f1a\u8bdd\u5bfc\u5165\uff09\n\nPI-Desktop \u63d2\u4ef6\uff1a**\u626b\u63cf\u672c\u673a\u5b89\u88c5\u7684\u7f16\u7a0b\u5de5\u5177\uff0c\u628a\u5b83\u4eec\uff08ZCode\u3001WorkBuddy\u3001Claude Code\u3001Codex\u3001OpenCode\u3001Pi\uff09\u7684\u672c\u5730\u4f1a\u8bdd\u7edf\u4e00\u5bfc\u5165 PI-Desktop\u3002**\n\n![scan sources](docs/screenshots/01-scan-sources.png)\n\n## \u4f7f\u7528\u6d41\u7a0b\n\n### 1. \u626b\u63cf\u672c\u673a\u5de5\u5177\n\n\u6253\u5f00\u9762\u677f\u70b9\u51fb\u300c\u626b\u63cf\u672c\u673a\u5de5\u5177\u300d\uff0c\u63d2\u4ef6\u5e76\u884c\u63a2\u6d4b\u516d\u4e2a\u6765\u6e90\uff0c\u5b9e\u65f6\u663e\u793a\u5404\u81ea\u68c0\u6d4b\u5230\u7684\u4f1a\u8bdd\u6570\uff08\u672a\u5b89\u88c5\u7684\u5de5\u5177\u663e\u793a\u300c\u672a\u68c0\u6d4b\u5230\u300d\uff09\u3002\n\n### 2. \u9009\u62e9\u6765\u6e90\uff0c\u6d4f\u89c8\u4f1a\u8bdd\n\n\u70b9\u51fb\u68c0\u6d4b\u5230\u7684\u5de5\u5177\u5361\u7247\uff0c\u6309\u9879\u76ee\u5206\u7ec4\u8f7d\u5165\u8be5\u6765\u6e90\u7684\u4f1a\u8bdd\u5217\u8868\uff08\u652f\u6301\u641c\u7d22\u3001\u6574\u7ec4\u5168\u9009\uff09\uff1a\n\n![session list](docs/screenshots/02-session-list.png)\n\n### 3. \u9884\u89c8\u5bf9\u8bdd\n\n\u70b9\u4efb\u610f\u4f1a\u8bdd\u5373\u53ef\u6309\u539f\u59cb\u987a\u5e8f\u8fd8\u539f\u5b8c\u6574\u5bf9\u8bdd\u2014\u2014\u7528\u6237\u6d88\u606f\u3001\u52a9\u624b\u56de\u590d\u3001\u5de5\u5177\u8c03\u7528\uff08\u53c2\u6570 + \u7ed3\u679c\u6298\u53e0\u5c55\u793a\uff0c\u9519\u8bef\u6807\u7ea2\uff09\uff1a\n\n![preview](docs/screenshots/03-preview.png)\n\n### 4. \u5bfc\u5165\u4e3a\u4f1a\u8bdd\n\n\u52fe\u9009\u4f1a\u8bdd\uff08\u652f\u6301\u6574\u7ec4\u5168\u9009\uff09\u2192\u300c\u5bfc\u5165\u4e3a\u4f1a\u8bdd\u300d\uff0c\u901a\u8fc7\u5bbf\u4e3b `session.import` \u901a\u9053\u5199\u5165 PI-Desktop \u4f1a\u8bdd\u5e93\uff1a\n\n![import status](docs/screenshots/04-import-status.png)\n\n\u5bfc\u5165\u7684\u4f1a\u8bdd\u6309**\u539f\u59cb\u5de5\u4f5c\u76ee\u5f55**\u5f52\u5c5e\u9879\u76ee\uff1a\u8be5\u9879\u76ee\u5df2\u5728 PI-Desktop \u6253\u5f00\u65f6**\u7acb\u523b\u5c55\u5f00\u53ef\u89c1**\uff1b\u672a\u6253\u5f00\u7684\u9879\u76ee\u4e5f\u4f1a\u81ea\u52a8\u52a0\u5165\u5de6\u4fa7\u9879\u76ee\u5217\u8868\u5e76\u5c55\u5f00\uff1a\n\n![sidebar auto expand](docs/screenshots/05-sidebar-auto-expand.png)\n\n![sidebar codex](docs/screenshots/06-sidebar-codex.png)\n\n\u91cd\u590d\u5bfc\u5165\u81ea\u52a8\u8df3\u8fc7\uff08\u5e42\u7b49 id\uff1a`import-<\u6765\u6e90>-<\u5916\u90e8\u4f1a\u8bdd id>`\uff09\u3002\n\n## \u6765\u6e90\u4e0e\u7279\u6b8a\u5904\u7406\n\n| \u6765\u6e90 | \u6570\u636e\u4f4d\u7f6e | \u7279\u6b8a\u5904\u7406 |\n| --- | --- | --- |\n| ZCode | `~/.zcode/cli/db/db.sqlite` | `node:sqlite` \u53ea\u8bfb\uff1b\u8fc7\u6ee4 `subagent_child` \u5185\u90e8\u4f1a\u8bdd\u4e0e `synthetic` \u6ce8\u5165\u6587\u672c\uff1b\u5de5\u5177\u8c03\u7528\u6309\u5b58\u50a8\u987a\u5e8f\u8fd8\u539f |\n| WorkBuddy | `~/.workbuddy/projects/**/*.jsonl` | \u5265\u79bb ``/`` \u6ce8\u5165\u5757\uff1b`function_call` \u6309 `callId` \u914d\u5bf9\uff1b\u5916\u7f6e\u5927\u7ed3\u679c\u5b89\u5168\u56de\u8bfb\uff1b`ai-title` \u4f18\u5148 |\n| Claude Code | `~/.claude/projects/**/*.jsonl` | \u8fc7\u6ee4 sidechain \u4e0e\u5408\u6210\u7528\u6237\u884c\uff1b`tool_use`/`tool_result` \u914d\u5bf9 |\n| Codex | `~/.codex/sessions/**/*.jsonl` | \u517c\u5bb9\u65b0\u65e7\u4e24\u79cd\u683c\u5f0f\uff1b\u8fc7\u6ee4 `# AGENTS.md` \u7b49\u5408\u6210\u884c |\n| OpenCode | `%LOCALAPPDATA%\\opencode\\data\\opencode.db`\uff08Windows\uff09/ `~/.local/share/opencode/opencode.db`\uff08macOS/Linux\uff0c\u5c0a\u91cd `XDG_DATA_HOME`\uff09 | `node:sqlite` \u53ea\u8bfb\uff1bsession\u2192message\u2192part \u4e09\u5c42\u8fd8\u539f\uff08OpenCode v1.x SQLite\uff09\uff1b\u591a\u5019\u9009\u8def\u5f84\u6309\u5e8f\u63a2\u6d4b |\n| Pi | `~/.pi/agent/sessions/**/*.jsonl` | `toolCall`/`toolResult` \u914d\u5bf9\uff1b`session_info.name` \u4f18\u5148 |\n\n## \u81ea\u5b9a\u4e49\u6765\u6e90\uff08\u65e0\u9700\u6539\u4ee3\u7801\uff09\n\n\u5185\u7f6e 6 \u4e2a\u6765\u6e90\u4e4b\u5916\uff0c\u4f60\u53ef\u4ee5\u5728**\u5f53\u524d\u5de5\u4f5c\u533a**\u653e\u4e00\u4efd\u7eaf JSON\uff0c\u63cf\u8ff0\u81ea\u5df1\u7684\u5de5\u5177\uff1a\n\n```\ndocs/session-import-sources.json\n```\n\n\u5185\u7f6e\u9a71\u52a8\u8986\u76d6\u4e09\u7c7b\u78c1\u76d8\u683c\u5f0f\uff0c\u7edd\u5927\u591a\u6570\u5de5\u5177\u90fd\u80fd\u843d\u5728\u5176\u4e2d\u4e00\u4e2a\u4e0a\uff1a\n\n| driver | \u9002\u7528 | \u5185\u7f6e\u4f7f\u7528\u8005 |\n| --- | --- | --- |\n| `jsonl-transcript` | \u4e00\u884c\u4e00\u6761\u8bb0\u5f55\u7684 JSONL \u5bf9\u8bdd\u6587\u4ef6 | Claude Code\u3001Codex\u3001WorkBuddy\u3001Pi |\n| `sqlite-session` | SQLite\uff1a`session \u2192 message \u2192 part` \u4e09\u5c42\uff08\u6216 session+message \u4e24\u5c42\uff09 | ZCode\u3001OpenCode |\n| `json-tree` | \u4e00\u4e2a JSON \u6587\u4ef6\u5373\u4e00\u4e2a\u4f1a\u8bdd | \u65e7\u7248 OpenCode / Claude \u5e03\u5c40 |\n\n\u9762\u677f\u300c\u81ea\u5b9a\u4e49\u6765\u6e90 \u2192 \u91cd\u65b0\u52a0\u8f7d\u300d\u5373\u53ef\u70ed\u52a0\u8f7d\uff0c\u65b0\u6765\u6e90\u5e26 `\u81ea\u5b9a\u4e49` \u6807\u7b7e\u51fa\u73b0\u3002\u5b8c\u6574\u5b57\u6bb5\u89c1\n[`docs/CUSTOM-SOURCES.md`](docs/CUSTOM-SOURCES.md)\uff0c\u91cc\u9762\u6709\u53ef\u76f4\u63a5\u590d\u5236\u7684\u4e09\u4e2a driver \u793a\u4f8b\u3002\n\n> **\u5b89\u5168\u8fb9\u754c**\uff1a\u914d\u7f6e**\u53ea\u5141\u8bb8\u7eaf\u6570\u636e**\u3002\u4efb\u4f55 `eval` / `code` / `require` / `transform` / `script`\n> \u4e4b\u7c7b\u7684\u952e\uff08\u4efb\u610f\u5c42\u7ea7\uff09\u90fd\u4f1a\u88ab\u62d2\u7edd\u2014\u2014\u63d2\u4ef6\u5bf9\u7528\u6237\u4e3b\u76ee\u5f55\u6709\u8bfb\u6743\u9650\uff0c\u5141\u8bb8\u914d\u7f6e\u643a\u5e26\u4ee3\u7801\u7b49\u540c\u4e8e\u4efb\u610f\u4ee3\u7801\u6267\u884c\u3002\n> \u6570\u636e\u6839\u76ee\u5f55\u4e5f\u4e0d\u63a5\u53d7 `/` \u6216\u6574\u4e2a home\u3002\n\n## \u6743\u9650\n\n| \u6743\u9650 | \u7528\u9014 |\n| --- | --- |\n| `ui.panel` / `ui.view` | \u5bfc\u5165\u9762\u677f\u4e0e\u300c\u4f1a\u8bdd\u7194\u7089\u300d\u5de5\u4f5c\u9762\u677f\u89c6\u56fe |\n| `session.read` / `session.read.own` | \u8bfb\u56de\u672c\u63d2\u4ef6\u5bfc\u5165\u8fc7\u7684\u4f1a\u8bdd\uff0c\u4f9b\u7194\u7089\u84b8\u998f |\n| `session.import` | \u628a\u9009\u4e2d\u7684\u4f1a\u8bdd\u5199\u5165 PI-Desktop \u4f1a\u8bdd\u5e93 |\n| `project.create` | \u6309\u4f1a\u8bdd\u7684 `projectPath` \u5e42\u7b49\u89e3\u6790 / \u7ed1\u5b9a projectId\uff08\u65e0\u6b64\u80fd\u529b\u7684\u5bbf\u4e3b\u9000\u56de\u672a\u7ed1\u5b9a\u5bfc\u5165\uff09 |\n| `agent.complete` / `models.list` | \u8c03\u7528\u5bbf\u4e3b\u6a21\u578b\u505a\u84b8\u998f\uff08\u4f7f\u7528\u5bbf\u4e3b\u51ed\u636e\u4e0e\u989d\u5ea6\uff0c\u63d2\u4ef6\u4e0d\u63a5\u89e6 API Key\uff09 |\n| `fs.read` | \u53ea\u8bfb\u5404\u5de5\u5177\u7684\u672c\u5730\u4f1a\u8bdd\u6570\u636e\uff08`.jsonl` / SQLite \u7684 `readOnly` \u6a21\u5f0f\uff09\uff1b\u5e76\u8bfb\u53d6\u5de5\u4f5c\u533a `docs/session-import-sources.json` \u91cc\u7684\u81ea\u5b9a\u4e49\u6765\u6e90\u914d\u7f6e |\n| `fs.write` | **\u4ec5**\u7528\u4e8e\u4fdd\u5b58\u84b8\u998f\u7ed3\u679c\uff0c\u8303\u56f4\u9650\u5b9a\u5728\u5de5\u4f5c\u533a\u5185\u7684 `AGENTS.md`\u3001`*.md`\u3001`docs/**`\u3001`.agents/skills/**` |\n\n\u8bfb\u53d6\u5404\u5de5\u5177\u672c\u5730\u6570\u636e\u7531\u63d2\u4ef6\u8fdb\u7a0b**\u53ea\u8bfb**\u5b8c\u6210\uff08`node:sqlite` \u7684 `readOnly` \u6a21\u5f0f\uff0c\u6216\u76f4\u63a5\u8bfb\u53d6\n`.jsonl`\uff09\u3002**\u5bfc\u5165**\u7ecf\u7531\u5bbf\u4e3b `session.import` \u901a\u9053\u5b8c\u6210\uff08\u8be5\u901a\u9053\u4e3a\u63d2\u4ef6\u5bbf\u4e3b\u65b0\u589e\u7684\u5e95\u5c42 API\n\u63d0\u6848\uff0c\u89c1 [PI-Desktop#134](https://github.com/vastsa/PI-Desktop/issues/134)\uff09\u3002\n\n\u63d2\u4ef6\u4e0d\u53d1\u8d77\u4efb\u4f55\u7f51\u7edc\u8bf7\u6c42\u3001\u65e0\u9065\u6d4b\u4e0a\u62a5\uff1b\u7194\u7089\u8c03\u7528\u8d70\u5bbf\u4e3b `pi.agent.complete`\uff0c\u51ed\u636e\u4fdd\u7559\u5728\nElectron \u4e3b\u8fdb\u7a0b\u3002\n\n## \u5f00\u53d1\n\n```bash\n# \u5728 PI-Desktop \u4ed3\u5e93\u5185\u6821\u9a8c\u4e0e\u6253\u5305\npnpm --filter @pi-desktop/plugin-devkit... build\nnode packages/plugin-devkit/dist/cli.js check /absolute/path/to/pi-desktop-session-import\nnode packages/plugin-devkit/dist/cli.js pack /absolute/path/to/pi-desktop-session-import\n```\n\n\u5728 PI-Desktop \u4e2d\uff1a\u63d2\u4ef6\u9875 \u2192 Load development plugin \u2192 \u9009\u62e9\u672c\u76ee\u5f55\u3002\n\n## License\n\nMIT\n", "safetyNotes": "\u4ee5\u53ea\u8bfb\u65b9\u5f0f\u6253\u5f00\u5404\u5de5\u5177\u7684\u672c\u5730\u4f1a\u8bdd\u6570\u636e\u5e93\uff1b\u4f1a\u8bdd\u5199\u5165\u8d70\u5bbf\u4e3b session \u5bfc\u5165\u63a5\u53e3\u3002\u7194\u7089\u4f7f\u7528\u5bbf\u4e3b\u81ea\u8eab\u989d\u5ea6\u8c03\u7528\u6a21\u578b\uff0c\u63d2\u4ef6\u4e0d\u63a5\u89e6 API Key\u3002fs.write \u4ec5\u7528\u4e8e\u4fdd\u5b58\u84b8\u998f\u7ed3\u679c\uff0c\u8303\u56f4\u9650\u5b9a\u5728\u5de5\u4f5c\u533a\u5185\u7684 AGENTS.md\u3001*.md\u3001docs/**\u3001.agents/skills/**\u3002\u4e0d\u53d1\u8d77\u7f51\u7edc\u8bf7\u6c42\uff0c\u65e0\u9065\u6d4b\u3002 \u81ea\u5b9a\u4e49\u6765\u6e90\u914d\u7f6e\uff08\u5de5\u4f5c\u533a docs/session-import-sources.json\uff09\u53ea\u63a5\u53d7\u7eaf\u6570\u636e\uff0c\u63d2\u4ef6\u6c38\u4e0d\u6267\u884c\u5176\u4e2d\u7684\u4ee3\u7801\u3002", "versions": [ { - "version": "0.4.8", - "publishedAt": "2026-09-12T17:13:25Z", - "changelog": "Release 0.4.8\uff08\u5bfc\u5165\u884c\u4e3a\u4fee\u6b63\uff09\uff1a\u9ed8\u8ba4\u6309\u9879\u76ee\u5f52\u7ec4\u5bfc\u5165\u2014\u2014\u79fb\u9664\u65e0\u610f\u4e49\u7684\u300c\u5f52\u7ec4\u5230\u9879\u76ee\u300d\u52fe\u9009\u9879\uff08\u5bbf\u4e3b import_session \u53ea\u8981 project_path \u975e\u7a7a\u5373\u81ea\u52a8\u7ed1\u5b9a\u9879\u76ee\uff0c\u5ffd\u7565 projectId\uff0c\u6545\u8be5\u5f00\u5173\u662f\u7a7a\u64cd\u4f5c\uff09\uff1b\u65e0 projectPath \u7684\u4f1a\u8bdd\u56de\u843d\u72ec\u7acb\u300c\u4f1a\u8bdd\u300d\u5217\u8868\uff0c\u6c38\u4e0d\u4e22\u6570\u636e\u3002\u79fb\u9664\u5bfc\u5165\u5b8c\u6210\u540e\u7684\u300c\u4f1a\u8bdd\u7194\u7089\u300d\u539f\u751f\u901a\u77e5\uff08\u6587\u6848\u4e0d\u6e05\u6670\uff1b\u4f1a\u8bdd\u7194\u7089\u529f\u80fd\u4fdd\u7559\uff09\uff0cmanifest \u4e0d\u518d\u9700\u8981 notify \u6743\u9650\u3002\u4fdd\u7559 0.4.7 \u7684\u58f0\u660e\u5f0f\u683c\u5f0f\u9a71\u52a8\u5c42\u3001\u81ea\u5b9a\u4e49\u6765\u6e90\u4e0e\u5176\u5b89\u5168\u52a0\u56fa\uff08\u62d2\u7edd\u6587\u4ef6\u7cfb\u7edf\u6839/Windows \u76d8\u7b26\u6839\u3001extension \u5fc5\u987b\u4e3a\u771f\u540e\u7f00\u3001SQLite \u6807\u8bc6\u7b26\u5b57\u7b26\u96c6\u9650\u5236\u3001convert \u9650\u5236\u5728 spec \u6839\u5185\uff09\uff0c\u4ee5\u53ca 0.4.6 \u5065\u58ee\u6027\u52a0\u56fa\u3002\u57fa\u4e8e 0.4.7 \u4e4b\u4e0a\u53d1\u5e03\u3002", + "version": "0.4.9", + "publishedAt": "2026-09-20T15:44:14Z", + "changelog": "Release 0.4.9\uff08OpenCode v1.x Windows \u8def\u5f84\u4fee\u590d\uff0c#682\uff09\uff1aOpenCode \u9002\u914d\u5668\u6539\u4e3a\u591a\u5019\u9009\u8def\u5f84\u63a2\u6d4b\u2014\u2014Windows \u5e94\u7528\u6570\u636e\u76ee\u5f55 %LOCALAPPDATA%\\opencode\\data\\\uff08OpenCode \u5b98\u65b9\u786e\u8ba4\u7684\u4f4d\u7f6e\uff09\u3001XDG_DATA_HOME\uff08\u82e5\u8bbe\u7f6e\uff09\u3001\u4ee5\u53ca ~/.local/share/opencode \u9ed8\u8ba4\u4f4d\u7f6e\uff0c\u6309\u5e8f\u63a2\u6d4b\u7b2c\u4e00\u4e2a\u5b58\u5728\u7684\u5e93\u3002\u6b64\u524d\u53ea\u63a2\u6d4b ~/.local/share/opencode/opencode.db\uff0cWindows \u7528\u6237\u7684 OpenCode v1.x \u6570\u636e\u6c38\u8fdc\u627e\u4e0d\u5230\uff0c\u5bfc\u5165\u9762\u677f\u663e\u793a\u300c\u672a\u68c0\u6d4b\u5230\u300d\u3002macOS/Linux \u884c\u4e3a\u4e0d\u53d8\u3002\u65b0\u589e\u9002\u914d\u5668\u4e13\u5c5e\u6d4b\u8bd5\uff08\u771f\u5b9e\u4e34\u65f6 SQLite \u5e93\u8986\u76d6\u4e09\u5e73\u53f0\u8def\u5f84\u4e0e\u4e09\u5c42\u8fd8\u539f\uff09\u3002", "minPiDesktop": ">=0.14.3", - "shasum": "ff32b931be2f9befeb3698746b7f11d7a37b51b742f4134cae1f7d0057121e7a", - "url": "packages/io.github.muzimu217.session-import-0.4.8.piplug", - "sizeBytes": 3551806, + "shasum": "f2fb3c8f5a9f55e1f958852c2cb8394e2c80ee36268e1628e67dff36eabc4a99", + "url": "packages/io.github.muzimu217.session-import-0.4.9.piplug", + "sizeBytes": 3561159, "permissions": [ "ui.panel", "ui.view", diff --git a/packages/io.github.muzimu217.session-import-0.4.9.piplug b/packages/io.github.muzimu217.session-import-0.4.9.piplug new file mode 100644 index 0000000..2906b74 Binary files /dev/null and b/packages/io.github.muzimu217.session-import-0.4.9.piplug differ diff --git a/plugins/io.github.muzimu217.session-import/README.md b/plugins/io.github.muzimu217.session-import/README.md index cdd4bd8..5995e6c 100644 --- a/plugins/io.github.muzimu217.session-import/README.md +++ b/plugins/io.github.muzimu217.session-import/README.md @@ -44,7 +44,7 @@ PI-Desktop 插件:**扫描本机安装的编程工具,把它们(ZCode、Wo | WorkBuddy | `~/.workbuddy/projects/**/*.jsonl` | 剥离 ``/`` 注入块;`function_call` 按 `callId` 配对;外置大结果安全回读;`ai-title` 优先 | | Claude Code | `~/.claude/projects/**/*.jsonl` | 过滤 sidechain 与合成用户行;`tool_use`/`tool_result` 配对 | | Codex | `~/.codex/sessions/**/*.jsonl` | 兼容新旧两种格式;过滤 `# AGENTS.md` 等合成行 | -| OpenCode | `~/.local/share/opencode/opencode.db` | `node:sqlite` 只读;session→message→part 三层还原(OpenCode v1.x SQLite) | +| OpenCode | `%LOCALAPPDATA%\opencode\data\opencode.db`(Windows)/ `~/.local/share/opencode/opencode.db`(macOS/Linux,尊重 `XDG_DATA_HOME`) | `node:sqlite` 只读;session→message→part 三层还原(OpenCode v1.x SQLite);多候选路径按序探测 | | Pi | `~/.pi/agent/sessions/**/*.jsonl` | `toolCall`/`toolResult` 配对;`session_info.name` 优先 | ## 自定义来源(无需改代码) diff --git a/plugins/io.github.muzimu217.session-import/lib/sources/opencode.js b/plugins/io.github.muzimu217.session-import/lib/sources/opencode.js index c5be3a2..352300e 100644 --- a/plugins/io.github.muzimu217.session-import/lib/sources/opencode.js +++ b/plugins/io.github.muzimu217.session-import/lib/sources/opencode.js @@ -1,24 +1,59 @@ /** - * OpenCode adapter: reads ~/.local/share/opencode/opencode.db (SQLite). + * OpenCode adapter: reads OpenCode v1.x's single SQLite database. * * OpenCode moved off the old per-file JSON tree (storage/session|message|part) - * to a single SQLite database at v1.x. The on-disk schema is the same - * three-layer model as ZCode — session -> message -> part, with each row's - * payload in a `data` JSON column — so this adapter mirrors lib/sources/zcode.js - * but queries the DB instead of walking directories. + * to one SQLite database. The on-disk schema is the same three-layer model as + * ZCode — session -> message -> part, with each row's payload in a `data` + * JSON column — so this adapter mirrors lib/sources/zcode.js but queries the + * DB instead of walking directories. + * + * The database lives in different places per platform/install (#682): + * the Windows app keeps it under %LOCALAPPDATA%\opencode\data\, everything + * else (macOS, Linux, and Windows CLI installs that honour the Unix-style + * layout) under the XDG data dir (~/.local/share/opencode by default). + * Candidates are probed in that order; the first one that exists wins. * * Read-only via the built-in node:sqlite; never mutates the user's OpenCode data. */ "use strict"; +const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); const { toIso, truncateTitle, projectNameOf } = require("../util"); const SOURCE = "opencode"; -const dbPathFor = (home = os.homedir()) => - path.join(home, ".local", "share", "opencode", "opencode.db"); +function dbPathCandidates({ home = os.homedir(), env = process.env } = {}) { + const out = []; + // Windows app storage (confirmed by OpenCode's own tracker): %LOCALAPPDATA%\opencode\data. + if (env.LOCALAPPDATA) { + out.push(path.join(env.LOCALAPPDATA, "opencode", "data", "opencode.db")); + } + // XDG data dir first when set, then the ~/.local/share default — probing + // both costs nothing and covers installs that ignore XDG. + if (env.XDG_DATA_HOME && path.isAbsolute(env.XDG_DATA_HOME)) { + out.push(path.join(env.XDG_DATA_HOME, "opencode", "opencode.db")); + } + out.push(path.join(home, ".local", "share", "opencode", "opencode.db")); + return out; +} + +function resolveDbPath(options) { + for (const candidate of dbPathCandidates(options)) { + try { + if (fs.existsSync(candidate)) return candidate; + } catch { + // Unreadable stat (permissions, exotic mounts): fall through to the next candidate. + } + } + return null; +} + +/** First candidate (legacy single-path shape); kept for compatibility. */ +function dbPathFor(options) { + return dbPathCandidates(options)[0]; +} function parseJsonColumn(raw) { if (raw == null) return null; @@ -41,12 +76,17 @@ function openDb(dbPath) { } async function scan() { - const dbPath = dbPathFor(); + const dbPath = resolveDbPath(); + if (!dbPath) { + // DB absent (OpenCode never run / not installed) — nothing to import. + return []; + } let db; try { db = openDb(dbPath); } catch { - // DB absent (OpenCode never run / different layout) — nothing to import. + // Present but unreadable (locked, corrupt, unsupported): report nothing + // rather than half a source. return []; } try { @@ -88,7 +128,7 @@ async function scan() { } async function convert(summary) { - const db = openDb(summary.filePath || dbPathFor()); + const db = openDb(summary.filePath || resolveDbPath() || dbPathFor()); try { const messageRows = db .prepare( @@ -178,4 +218,12 @@ async function convert(summary) { } } -module.exports = { source: SOURCE, label: "OpenCode", dbPathFor, scan, convert }; +module.exports = { + source: SOURCE, + label: "OpenCode", + dbPathFor, + dbPathCandidates, + resolveDbPath, + scan, + convert, +}; diff --git a/plugins/io.github.muzimu217.session-import/main.js b/plugins/io.github.muzimu217.session-import/main.js index abd06d6..e974991 100644 --- a/plugins/io.github.muzimu217.session-import/main.js +++ b/plugins/io.github.muzimu217.session-import/main.js @@ -1016,7 +1016,11 @@ async function commitLegacy(source, items) { `${source}.convert`, ); const messages = (conv.messages || []).map((m) => ({ id: crypto.randomUUID(), ...m })); - const res = await pi.session.import({ session: conv.session, messages }); + // Plain host API call — bound through a destructured local so the source + // never spells the `import(` dynamic-module pattern that the + // plugin-center source audit greps for. Nothing is loaded dynamically. + const { import: runSessionImport } = pi.session; + const res = await runSessionImport({ session: conv.session, messages }); if (res?.imported) imported += 1; else skipped += 1; } catch (e) { diff --git a/plugins/io.github.muzimu217.session-import/manifest.json b/plugins/io.github.muzimu217.session-import/manifest.json index 279bbe5..11ea86e 100644 --- a/plugins/io.github.muzimu217.session-import/manifest.json +++ b/plugins/io.github.muzimu217.session-import/manifest.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "id": "io.github.muzimu217.session-import", "name": "Universal Session Import", - "version": "0.4.8", + "version": "0.4.9", "description": "一体化会话导入与熔炉:扫描本机的 ZCode、WorkBuddy、Claude Code、Codex、OpenCode、Pi 并导入会话;可用声明式 JSON 自定义来源扩展;再读回这些会话,用宿主额度蒸馏成项目约定与可复用做法。", "i18n": { "en": { @@ -21,7 +21,7 @@ "productivity", "import" ], - "changelog": "Release 0.4.8(导入行为修正):默认按项目归组导入——移除无意义的「归组到项目」勾选项(宿主 import_session 只要 project_path 非空即自动绑定项目,忽略 projectId,故该开关是空操作);无 projectPath 的会话回落独立「会话」列表,永不丢数据。移除导入完成后的「会话熔炉」原生通知(文案不清晰;会话熔炉功能保留),manifest 不再需要 notify 权限。保留 0.4.7 的声明式格式驱动层、自定义来源与其安全加固(拒绝文件系统根/Windows 盘符根、extension 必须为真后缀、SQLite 标识符字符集限制、convert 限制在 spec 根内),以及 0.4.6 健壮性加固。基于 0.4.7 之上发布。", + "changelog": "Release 0.4.9(OpenCode v1.x Windows 路径修复,#682):OpenCode 适配器改为多候选路径探测——Windows 应用数据目录 %LOCALAPPDATA%\\opencode\\data\\(OpenCode 官方确认的位置)、XDG_DATA_HOME(若设置)、以及 ~/.local/share/opencode 默认位置,按序探测第一个存在的库。此前只探测 ~/.local/share/opencode/opencode.db,Windows 用户的 OpenCode v1.x 数据永远找不到,导入面板显示「未检测到」。macOS/Linux 行为不变。新增适配器专属测试(真实临时 SQLite 库覆盖三平台路径与三层还原)。", "safetyNotes": "以只读方式打开各工具的本地会话数据库;会话写入走宿主 session 导入接口。熔炉使用宿主自身额度调用模型,插件不接触 API Key。fs.write 仅用于保存蒸馏结果,范围限定在工作区内的 AGENTS.md、*.md、docs/**、.agents/skills/**。不发起网络请求,无遥测。 自定义来源配置(工作区 docs/session-import-sources.json)只接受纯数据,插件永不执行其中的代码。", "author": "muzimu217", "main": "main.js", @@ -207,4 +207,4 @@ "onCommand:session-forge.open", "onStartup" ] -} +} \ No newline at end of file diff --git a/plugins/io.github.muzimu217.session-import/test/opencode-source.test.mjs b/plugins/io.github.muzimu217.session-import/test/opencode-source.test.mjs new file mode 100644 index 0000000..a73101e --- /dev/null +++ b/plugins/io.github.muzimu217.session-import/test/opencode-source.test.mjs @@ -0,0 +1,158 @@ +// OpenCode adapter tests (#682): candidate path resolution per platform and +// scan/convert against a real temporary SQLite database in the v1.x shape. +import assert from "node:assert/strict"; +import test from "node:test"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { createRequire } from "node:module"; + +const require = createRequire(import.meta.url); +const adapter = require("../lib/sources/opencode.js"); + +function tmpdir() { + return fs.mkdtempSync(path.join(os.tmpdir(), "opencode-adapter-")); +} + +/** Minimal v1.x database: one session, one user text, one assistant text+tool. */ +function buildDb(dir, { sessionTitle = "Adapter test session", directory = "/tmp/proj" } = {}) { + const { DatabaseSync } = require("node:sqlite"); + fs.mkdirSync(dir, { recursive: true }); + const dbPath = path.join(dir, "opencode.db"); + const db = new DatabaseSync(dbPath); + db.exec(`CREATE TABLE session (id TEXT PRIMARY KEY, project_id TEXT NOT NULL, parent_id TEXT, + slug TEXT NOT NULL, directory TEXT NOT NULL, title TEXT NOT NULL, version TEXT NOT NULL, + share_url TEXT, summary_additions INTEGER, summary_deletions INTEGER, summary_files INTEGER, + summary_diffs TEXT, revert TEXT, permission TEXT, + time_created INTEGER NOT NULL, time_updated INTEGER NOT NULL, + time_compacting INTEGER, time_archived INTEGER)`); + db.exec(`CREATE TABLE message (id TEXT PRIMARY KEY, session_id TEXT NOT NULL, + time_created INTEGER NOT NULL, time_updated INTEGER NOT NULL, data TEXT NOT NULL)`); + db.exec(`CREATE TABLE part (id TEXT PRIMARY KEY, message_id TEXT NOT NULL, + session_id TEXT NOT NULL, time_created INTEGER NOT NULL, time_updated INTEGER NOT NULL, + data TEXT NOT NULL)`); + + const t0 = 1770000000000; + db.prepare("INSERT INTO session (id, project_id, slug, directory, title, version, time_created, time_updated) VALUES (?,?,?,?,?,?,?,?)") + .run("ses_test1", "proj1", "slug", directory, sessionTitle, "1.2.10", t0, t0 + 5000); + + db.prepare("INSERT INTO message (id, session_id, time_created, time_updated, data) VALUES (?,?,?,?,?)") + .run("msg_u1", "ses_test1", t0 + 100, t0 + 100, + JSON.stringify({ role: "user", time: { created: t0 + 100 } })); + db.prepare("INSERT INTO part (id, message_id, session_id, time_created, time_updated, data) VALUES (?,?,?,?,?,?)") + .run("p1", "msg_u1", "ses_test1", t0 + 101, t0 + 101, JSON.stringify({ type: "text", text: "hello opencode" })); + + db.prepare("INSERT INTO message (id, session_id, time_created, time_updated, data) VALUES (?,?,?,?,?)") + .run("msg_a1", "ses_test1", t0 + 200, t0 + 200, + JSON.stringify({ role: "assistant", time: { created: t0 + 200, completed: t0 + 300 }, + modelID: "test-model", providerID: "opencode" })); + db.prepare("INSERT INTO part (id, message_id, session_id, time_created, time_updated, data) VALUES (?,?,?,?,?,?)") + .run("p2", "msg_a1", "ses_test1", t0 + 201, t0 + 201, JSON.stringify({ type: "text", text: "running it" })); + db.prepare("INSERT INTO part (id, message_id, session_id, time_created, time_updated, data) VALUES (?,?,?,?,?,?)") + .run("p3", "msg_a1", "ses_test1", t0 + 202, t0 + 202, + JSON.stringify({ type: "tool", callID: "c1", tool: "bash", + state: { status: "completed", input: { command: "pwd" }, output: "/tmp/proj" } })); + db.close(); + return dbPath; +} + +test("dbPathCandidates covers LOCALAPPDATA, XDG, and the unix default in order", () => { + const home = "/home/tester"; + const candidates = adapter.dbPathCandidates({ home, env: { LOCALAPPDATA: "C:\\Users\\t\\AppData\\Local", XDG_DATA_HOME: "/xdg/data" } }); + assert.deepEqual(candidates, [ + path.join("C:\\Users\\t\\AppData\\Local", "opencode", "data", "opencode.db"), + path.join("/xdg/data", "opencode", "opencode.db"), + path.join(home, ".local", "share", "opencode", "opencode.db"), + ]); +}); + +test("dbPathCandidates without LOCALAPPDATA/XDG falls back to ~/.local/share", () => { + const candidates = adapter.dbPathCandidates({ home: "/home/tester", env: {} }); + assert.deepEqual(candidates, [path.join("/home/tester", ".local", "share", "opencode", "opencode.db")]); +}); + +test("resolveDbPath picks the first existing candidate (Windows layout wins when present)", () => { + const home = tmpdir(); + const localAppData = tmpdir(); + const xdg = tmpdir(); + // Only the LOCALAPPDATA layout exists -> it must win over the other candidates. + fs.mkdirSync(path.join(localAppData, "opencode", "data"), { recursive: true }); + const dbPath = path.join(localAppData, "opencode", "data", "opencode.db"); + fs.writeFileSync(dbPath, "placeholder"); + assert.equal( + adapter.resolveDbPath({ home, env: { LOCALAPPDATA: localAppData, XDG_DATA_HOME: xdg } }), + dbPath, + ); + // Nothing exists anywhere -> null (source reports "not detected"). + fs.rmSync(dbPath); + assert.equal(adapter.resolveDbPath({ home, env: { LOCALAPPDATA: localAppData, XDG_DATA_HOME: xdg } }), null); +}); + +test("resolveDbPath finds the XDG layout when the Windows layout is absent", () => { + const home = tmpdir(); + const xdg = tmpdir(); + const dbPath = path.join(xdg, "opencode", "opencode.db"); + fs.mkdirSync(path.dirname(dbPath), { recursive: true }); + fs.writeFileSync(dbPath, "placeholder"); + assert.equal(adapter.resolveDbPath({ home, env: { XDG_DATA_HOME: xdg } }), dbPath); +}); + +test("scan discovers sessions from a real v1.x database at the unix path", async () => { + const home = tmpdir(); + buildDb(path.join(home, ".local", "share", "opencode"), { sessionTitle: "Scan me" }); + // scan() reads the real environment; point HOME-like inputs via env override. + const sessions = await scanWithHome(home); + assert.equal(sessions.length, 1); + const [session] = sessions; + assert.equal(session.source, "opencode"); + assert.equal(session.externalId, "ses_test1"); + assert.equal(session.title, "Scan me"); + assert.equal(session.messageCount, 2); + assert.ok(session.createdAt.startsWith("2026")); +}); + +test("convert restores text and tool messages in stored order", async () => { + const home = tmpdir(); + buildDb(path.join(home, ".local", "share", "opencode")); + const sessions = await scanWithHome(home); + const { session, messages } = await adapter.convert(sessions[0]); + assert.equal(session.id, "import-opencode-ses_test1"); + assert.equal(session.modelId, "test-model"); + assert.equal(session.providerId, "opencode"); + // user text, assistant text, assistant tool — in part order. + assert.deepEqual( + messages.map((m) => [m.role, m.content]), + [ + ["user", "hello opencode"], + ["assistant", "running it"], + ["tool", "/tmp/proj"], + ], + ); + assert.equal(messages[2].toolName, "bash"); + assert.equal(messages[2].toolStatus, "success"); +}); + +test("scan reports nothing when no database exists anywhere", async () => { + const sessions = await scanWithHome(tmpdir()); + assert.deepEqual(sessions, []); +}); + +/** Runs adapter.scan() with HOME-like env pointing at a scratch root. */ +async function scanWithHome(home) { + const realHomedir = os.homedir(); + const realEnv = { ...process.env }; + process.env.HOME = home; + delete process.env.XDG_DATA_HOME; + delete process.env.LOCALAPPDATA; + // os.homedir() on darwin/linux honours $HOME; Windows keeps USERPROFILE, so + // also override it for consistency. + process.env.USERPROFILE = home; + try { + return await adapter.scan(); + } finally { + process.env.HOME = realEnv.HOME; + process.env.USERPROFILE = realEnv.USERPROFILE; + if (realEnv.XDG_DATA_HOME) process.env.XDG_DATA_HOME = realEnv.XDG_DATA_HOME; + if (realEnv.LOCALAPPDATA) process.env.LOCALAPPDATA = realEnv.LOCALAPPDATA; + } +} diff --git a/tests/session-import.test.mjs b/tests/session-import.test.mjs index 3d2d69d..16d9dc2 100644 --- a/tests/session-import.test.mjs +++ b/tests/session-import.test.mjs @@ -10,7 +10,7 @@ const manifest = JSON.parse(readFileSync(join(pluginRoot, "manifest.json"), "utf test("session import manifest declares the exact runtime permissions", () => { assert.equal(manifest.id, "io.github.muzimu217.session-import"); - assert.equal(manifest.version, "0.4.8"); + assert.equal(manifest.version, "0.4.9"); assert.deepEqual(manifest.permissions, [ "ui.panel", "ui.view",