@clawos-dev/clawd 0.2.277 → 0.2.279

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3227,8 +3227,8 @@ var require_utils = __commonJS({
3227
3227
  }
3228
3228
  return ind;
3229
3229
  }
3230
- function removeDotSegments(path4) {
3231
- let input = path4;
3230
+ function removeDotSegments(path2) {
3231
+ let input = path2;
3232
3232
  const output = [];
3233
3233
  let nextSlash = -1;
3234
3234
  let len = 0;
@@ -3480,8 +3480,8 @@ var require_schemes = __commonJS({
3480
3480
  wsComponent.secure = void 0;
3481
3481
  }
3482
3482
  if (wsComponent.resourceName) {
3483
- const [path4, query] = wsComponent.resourceName.split("?");
3484
- wsComponent.path = path4 && path4 !== "/" ? path4 : void 0;
3483
+ const [path2, query] = wsComponent.resourceName.split("?");
3484
+ wsComponent.path = path2 && path2 !== "/" ? path2 : void 0;
3485
3485
  wsComponent.query = query;
3486
3486
  wsComponent.resourceName = void 0;
3487
3487
  }
@@ -6874,12 +6874,12 @@ var require_dist = __commonJS({
6874
6874
  throw new Error(`Unknown format "${name}"`);
6875
6875
  return f;
6876
6876
  };
6877
- function addFormats(ajv, list, fs6, exportName) {
6877
+ function addFormats(ajv, list, fs4, exportName) {
6878
6878
  var _a;
6879
6879
  var _b;
6880
6880
  (_a = (_b = ajv.opts.code).formats) !== null && _a !== void 0 ? _a : _b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`;
6881
6881
  for (const f of list)
6882
- ajv.addFormat(f, fs6[f]);
6882
+ ajv.addFormat(f, fs4[f]);
6883
6883
  }
6884
6884
  module2.exports = exports2 = formatsPlugin;
6885
6885
  Object.defineProperty(exports2, "__esModule", { value: true });
@@ -6895,7 +6895,7 @@ __export(mcp_server_exports, {
6895
6895
  handlePersonaDispatchToolCall: () => handlePersonaDispatchToolCall
6896
6896
  });
6897
6897
  module.exports = __toCommonJS(mcp_server_exports);
6898
- var import_node_fs5 = __toESM(require("fs"), 1);
6898
+ var import_node_fs3 = __toESM(require("fs"), 1);
6899
6899
 
6900
6900
  // ../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
6901
6901
  var external_exports = {};
@@ -7375,8 +7375,8 @@ function getErrorMap() {
7375
7375
 
7376
7376
  // ../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js
7377
7377
  var makeIssue = (params) => {
7378
- const { data, path: path4, errorMaps, issueData } = params;
7379
- const fullPath = [...path4, ...issueData.path || []];
7378
+ const { data, path: path2, errorMaps, issueData } = params;
7379
+ const fullPath = [...path2, ...issueData.path || []];
7380
7380
  const fullIssue = {
7381
7381
  ...issueData,
7382
7382
  path: fullPath
@@ -7492,11 +7492,11 @@ var errorUtil;
7492
7492
 
7493
7493
  // ../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.js
7494
7494
  var ParseInputLazyPath = class {
7495
- constructor(parent, value, path4, key) {
7495
+ constructor(parent, value, path2, key) {
7496
7496
  this._cachedPath = [];
7497
7497
  this.parent = parent;
7498
7498
  this.data = value;
7499
- this._path = path4;
7499
+ this._path = path2;
7500
7500
  this._key = key;
7501
7501
  }
7502
7502
  get path() {
@@ -11133,10 +11133,10 @@ function assignProp(target, prop, value) {
11133
11133
  configurable: true
11134
11134
  });
11135
11135
  }
11136
- function getElementAtPath(obj, path4) {
11137
- if (!path4)
11136
+ function getElementAtPath(obj, path2) {
11137
+ if (!path2)
11138
11138
  return obj;
11139
- return path4.reduce((acc, key) => acc?.[key], obj);
11139
+ return path2.reduce((acc, key) => acc?.[key], obj);
11140
11140
  }
11141
11141
  function promiseAllObject(promisesObj) {
11142
11142
  const keys = Object.keys(promisesObj);
@@ -11456,11 +11456,11 @@ function aborted(x, startIndex = 0) {
11456
11456
  }
11457
11457
  return false;
11458
11458
  }
11459
- function prefixIssues(path4, issues) {
11459
+ function prefixIssues(path2, issues) {
11460
11460
  return issues.map((iss) => {
11461
11461
  var _a;
11462
11462
  (_a = iss).path ?? (_a.path = []);
11463
- iss.path.unshift(path4);
11463
+ iss.path.unshift(path2);
11464
11464
  return iss;
11465
11465
  });
11466
11466
  }
@@ -21959,12 +21959,22 @@ var sessionMetaShape = {
21959
21959
  contextWindowSize: external_exports.number().int().positive().optional()
21960
21960
  };
21961
21961
  var SessionMetaSchema = external_exports.object(sessionMetaShape);
21962
+ var SelectOptionSchema = external_exports.object({
21963
+ value: external_exports.string(),
21964
+ label: external_exports.string(),
21965
+ description: external_exports.string().optional()
21966
+ });
21962
21967
  var ModelInfoSchema = external_exports.object({
21963
21968
  id: external_exports.string().min(1),
21964
21969
  label: external_exports.string().min(1),
21965
21970
  description: external_exports.string().optional(),
21966
21971
  contextWindowSize: external_exports.number().int().positive(),
21967
- default: external_exports.boolean().optional()
21972
+ default: external_exports.boolean().optional(),
21973
+ // 该模型支持的 reasoning effort 档位(codex:per-model,gpt-5.6 系有 max/ultra、5.5 只有四档)。
21974
+ // 缺省 = 该 tool 的 effort 与模型无关(claude 即此情形),消费方回落到 configSchema 里
21975
+ // name==='effort' 那条字段的 options。
21976
+ efforts: external_exports.array(SelectOptionSchema).optional(),
21977
+ defaultEffort: external_exports.string().optional()
21968
21978
  });
21969
21979
  var ModeInfoSchema = external_exports.object({
21970
21980
  id: external_exports.string().min(1),
@@ -21976,13 +21986,7 @@ var ConfigFieldSchemaSchema = external_exports.object({
21976
21986
  type: external_exports.enum(["string", "select", "toggle"]),
21977
21987
  label: external_exports.string().min(1),
21978
21988
  description: external_exports.string().optional(),
21979
- options: external_exports.array(
21980
- external_exports.object({
21981
- value: external_exports.string(),
21982
- label: external_exports.string(),
21983
- description: external_exports.string().optional()
21984
- })
21985
- ).optional(),
21989
+ options: external_exports.array(SelectOptionSchema).optional(),
21986
21990
  default: external_exports.unknown().optional(),
21987
21991
  scope: external_exports.enum(["core", "tool-specific"])
21988
21992
  });
@@ -22376,6 +22380,16 @@ var SessionCreateArgs = external_exports.object({
22376
22380
  }).refine((args) => args.cwd != null || args.ownerPersonaId != null, {
22377
22381
  message: "cwd \u4E0E ownerPersonaId \u81F3\u5C11\u4F20\u4E00\u4E2A"
22378
22382
  });
22383
+ var SessionImportExternalArgs = external_exports.object({
22384
+ cwd: external_exports.string().min(1),
22385
+ tool: external_exports.string().min(1),
22386
+ toolSessionId: external_exports.string().min(1),
22387
+ label: external_exports.string().optional(),
22388
+ iconKey: external_exports.string().optional(),
22389
+ createdAt: external_exports.string().datetime(),
22390
+ updatedAt: external_exports.string().datetime(),
22391
+ ownerPersonaId: external_exports.string().min(1).optional()
22392
+ });
22379
22393
  var SessionListArgs = external_exports.object({
22380
22394
  /** true = 只返回 mirror(originOwnerPrincipalId 非空,即「我在朋友 daemon 上创建」的本机存证)。 */
22381
22395
  originOnly: external_exports.boolean().optional(),
@@ -22915,6 +22929,10 @@ var METHOD_DOCS = {
22915
22929
  anyOf: [{ required: ["cwd"] }, { required: ["ownerPersonaId"] }]
22916
22930
  }
22917
22931
  },
22932
+ "session:importExternal": {
22933
+ summary: "\u628A Claude Code / Codex \u65E2\u6709\u5386\u53F2\u4F1A\u8BDD\u5BFC\u5165\u4E3A clawd \u5F15\u7528\uFF1A\u53EA\u5199 SessionFile\uFF0C\u4E0D\u62C9\u8D77 runner\uFF0C\u4E0D\u6539\u539F\u59CB transcript\uFF1B\u6309 toolSessionId \u8DE8 owned scope \u53BB\u91CD\uFF0C\u65B0\u5BFC\u5165\u4F1A\u5E7F\u64AD session:info",
22934
+ args: SessionImportExternalArgs
22935
+ },
22918
22936
  "session:list": {
22919
22937
  summary: "\u5217\u51FA\u5F53\u524D\u8EAB\u4EFD\u53EF\u89C1\u7684\u4F1A\u8BDD\uFF08owner \u5168\u91CF\uFF1Bguest \u9650\u88AB\u6388\u6743 persona\uFF09",
22920
22938
  args: SessionListArgs
@@ -23514,11 +23532,11 @@ Diff.prototype = {
23514
23532
  }
23515
23533
  }
23516
23534
  },
23517
- addToPath: function addToPath(path4, added, removed, oldPosInc, options) {
23518
- var last = path4.lastComponent;
23535
+ addToPath: function addToPath(path2, added, removed, oldPosInc, options) {
23536
+ var last = path2.lastComponent;
23519
23537
  if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
23520
23538
  return {
23521
- oldPos: path4.oldPos + oldPosInc,
23539
+ oldPos: path2.oldPos + oldPosInc,
23522
23540
  lastComponent: {
23523
23541
  count: last.count + 1,
23524
23542
  added,
@@ -23528,7 +23546,7 @@ Diff.prototype = {
23528
23546
  };
23529
23547
  } else {
23530
23548
  return {
23531
- oldPos: path4.oldPos + oldPosInc,
23549
+ oldPos: path2.oldPos + oldPosInc,
23532
23550
  lastComponent: {
23533
23551
  count: 1,
23534
23552
  added,
@@ -24023,80 +24041,6 @@ var CLAUDE_CAPABILITIES = {
24023
24041
  ]
24024
24042
  };
24025
24043
 
24026
- // src/tools/codex.ts
24027
- var import_node_child_process5 = require("child_process");
24028
- var import_node_fs4 = __toESM(require("fs"), 1);
24029
-
24030
- // src/tools/codex-session.ts
24031
- var import_node_child_process4 = require("child_process");
24032
- var import_node_path3 = __toESM(require("path"), 1);
24033
-
24034
- // src/tools/codex-app-server-client.ts
24035
- var import_node_readline = require("readline");
24036
-
24037
- // src/tools/codex-mcp-config.ts
24038
- var import_node_fs3 = __toESM(require("fs"), 1);
24039
- var import_node_path2 = __toESM(require("path"), 1);
24040
-
24041
- // src/tools/codex-features.ts
24042
- var import_node_child_process3 = require("child_process");
24043
-
24044
- // src/tools/codex.ts
24045
- var CODEX_MODEL_IDS = ["gpt-5.5", "gpt-5.4", "gpt-5.4-mini"];
24046
- var CODEX_MODELS = [
24047
- { id: "", label: "Default", description: "codex config.toml \u9ED8\u8BA4", contextWindowSize: 258400, default: true },
24048
- { id: "gpt-5.5", label: "GPT-5.5", description: "Frontier model for complex coding", contextWindowSize: 258400 },
24049
- { id: "gpt-5.4", label: "GPT-5.4", contextWindowSize: 258400 },
24050
- { id: "gpt-5.4-mini", label: "GPT-5.4-Mini", contextWindowSize: 258400 }
24051
- ];
24052
- var CODEX_EFFORTS = [
24053
- { value: "low", label: "Low", description: "Fast responses with lighter reasoning" },
24054
- { value: "medium", label: "Medium", description: "Balances speed and reasoning depth for everyday tasks" },
24055
- { value: "high", label: "High", description: "Greater reasoning depth for complex problems" },
24056
- { value: "xhigh", label: "Extra high", description: "Extra high reasoning depth for complex problems" }
24057
- ];
24058
- var CODEX_APPROVAL_PRESETS = [
24059
- { id: "untrusted", label: "Ask for approval", description: "\u51E0\u4E4E\u6BCF\u6761\u547D\u4EE4\u90FD\u95EE\u4F60\uFF08\u6700\u8C28\u614E\uFF09" },
24060
- { id: "on-request", label: "Approve for me", description: "\u6A21\u578B\u81EA\u5DF1\u5224\u65AD\uFF0C\u53EA\u5BF9\u9AD8\u98CE\u9669\u64CD\u4F5C\u95EE\u4F60\uFF08\u9ED8\u8BA4\uFF09" },
24061
- { id: "never", label: "Full Access", description: "\u4ECE\u4E0D\u8BE2\u95EE\uFF0C\u81EA\u4E3B\u6267\u884C" }
24062
- ];
24063
- var CODEX_CAPABILITIES = {
24064
- tool: "codex",
24065
- toolSessionIdLabel: "Codex Thread ID",
24066
- models: CODEX_MODELS,
24067
- permissionModes: CODEX_APPROVAL_PRESETS,
24068
- // codex 不支持这些 clawd 功能(rewind 无 file-snapshot / 无子 agent / 无 TUI pty /
24069
- // 无 observe 推送 / 无文件分享 / fork 是 claude jsonl 专用);UI 据此 gate 入口。
24070
- features: { rewind: false, subagents: false, tui: false, observe: false, fileSharing: false, fork: false },
24071
- configSchema: [
24072
- {
24073
- name: "model",
24074
- type: "select",
24075
- label: "Model",
24076
- scope: "core",
24077
- options: CODEX_MODELS.map((m) => ({ value: m.id, label: m.label, description: m.description })),
24078
- default: ""
24079
- },
24080
- {
24081
- name: "permissionMode",
24082
- type: "select",
24083
- label: "Approval",
24084
- scope: "core",
24085
- options: CODEX_APPROVAL_PRESETS.map((m) => ({ value: m.id, label: m.label, description: m.description })),
24086
- default: "on-request"
24087
- },
24088
- {
24089
- name: "effort",
24090
- type: "select",
24091
- label: "Reasoning effort",
24092
- scope: "tool-specific",
24093
- // 渲染在 Advanced 区(与 claude effort 一致)
24094
- options: CODEX_EFFORTS,
24095
- default: "medium"
24096
- }
24097
- ]
24098
- };
24099
-
24100
24044
  // src/dispatch/mcp-server.ts
24101
24045
  var DISPATCH_LOG = process.env.CLAWD_DISPATCH_LOG;
24102
24046
  function log(msg, info) {
@@ -24104,7 +24048,7 @@ function log(msg, info) {
24104
24048
  `;
24105
24049
  if (DISPATCH_LOG) {
24106
24050
  try {
24107
- import_node_fs5.default.appendFileSync(DISPATCH_LOG, line);
24051
+ import_node_fs3.default.appendFileSync(DISPATCH_LOG, line);
24108
24052
  return;
24109
24053
  } catch {
24110
24054
  }
@@ -24341,11 +24285,13 @@ async function main() {
24341
24285
  targetDeviceId: external_exports.string().optional().describe(
24342
24286
  "contact deviceId of the remote device when delegating cross-device (omit for a local persona)"
24343
24287
  ),
24344
- // T-20: dispatch model override. Enum 走 MCP tool JSON schema 硬约束,
24345
- // caller LLM 只能从可选值挑,避免瞎填历史模型名。claude / codex 两表 union——
24346
- // 精确校验(按 target persona tool 选表)在 daemon handler 层做(fail-fast)。
24347
- model: external_exports.enum([...CLAUDE_MODEL_IDS, ...CODEX_MODEL_IDS]).optional().describe(
24348
- "override the target persona's default model. Omit to use the persona default. Claude personas: " + CLAUDE_MODEL_IDS.join(" / ") + ". Codex personas: " + CODEX_MODEL_IDS.join(" / ") + "."
24288
+ // T-20: dispatch model override
24289
+ // 2026-08-03 起不再用 enum 硬约束:codex 的模型表已运行时化(随安装的 codex 版本变),
24290
+ // 没有编译期常量可枚举——硬 enum 的前提正是一张静态表,而静态表会过期并拒掉 UI
24291
+ // 明明能选到的新模型。精确校验下沉到 daemon handler(按 target persona tool 查运行时
24292
+ // capabilities,fail-fast 且错误消息列出 allowed 值,caller 据此自我纠正)。
24293
+ model: external_exports.string().optional().describe(
24294
+ "override the target persona's default model. Omit to use the persona default. Claude personas: " + CLAUDE_MODEL_IDS.join(" / ") + ". Codex personas: model ids vary by installed codex version \u2014 omit unless you know the exact id (invalid values are rejected with the allowed list)."
24349
24295
  ),
24350
24296
  // butler 顶部入口 (spec 2026-07-27):butler 给新 worker session 起 label 用。
24351
24297
  // Sidebar 直接显示这个 label(否则兜底 cwd-basename 都是 persona 目录名,同一
@@ -39,6 +39,7 @@ node .claude/skills/clawd-session-import/scripts/link-sessions.mjs --days 7 --ap
39
39
  | `--min-turns N` | 少于 N 轮用户发言的会话跳过,默认 2(滤掉「hello」这种试水会话) |
40
40
  | `--include-clawd` | 连 `~/.clawd` 目录下的会话也关联。默认跳过——那些本来就是 clawd 自己开的 |
41
41
  | `--icon KEY` | 侧边栏图标:`research/code/loop/qa/reading/debug/idea/doc/assist`,默认 `reading` |
42
+ | `--owner-persona-id ID` | 显式挂到某个 persona。默认不传,导入普通 session,进入 Recent |
42
43
  | `--json` | 输出 JSON,含被跳过的条目和原因,便于你自己筛 |
43
44
 
44
45
  撤销(删掉本工具建过的全部引用,原会话文件不受影响):
@@ -48,19 +49,27 @@ node .claude/skills/clawd-session-import/scripts/link-sessions.mjs --undo
48
49
  node .claude/skills/clawd-session-import/scripts/link-sessions.mjs --undo --apply # 真删
49
50
  ```
50
51
 
51
- ## 原理(已对 clawd 源码核实)
52
+ ## 原理
52
53
 
53
54
  一条 clawd 会话的元数据里有 `cwd` + `toolSessionId` 两个字段,指向底层工具那条真实
54
- 对话。关联就是把这两个字段填对:
55
+ 对话。关联就是调用 daemon 的 `session:importExternal` RPC 建一条引用:
55
56
 
56
- 1. `session:create` —— 只写一份元数据文件,**不拉起任何进程**,所以批量关联很便宜
57
- 2. `session:resume` —— 该会话还没进程时,只是把 `toolSessionId` 写进元数据
58
- 3. 脚本再把元数据里的创建/更新时间改回原会话的真实时间,侧边栏才按真实先后排序
59
- (clawd 每次列会话都重读磁盘,改完立刻生效,不用重启)
57
+ - daemon 只创建 clawd 引用,**不拉起任何进程**
58
+ - daemon 不复制、不移动、不修改 Claude Code / Codex 的原始 transcript
59
+ - daemon 按 `toolSessionId` 跨 default/persona scope 去重,已导入过就返回既有 session
60
+ - daemon 用原会话 `createdAt` / `updatedAt` 写入 metadata,并广播 `session:info`,已打开的
61
+ desktop/mobile sidepanel 会收到通知刷新列表
60
62
 
61
63
  用户点开时,clawd 按 `(cwd, toolSessionId)` 去读原始 transcript 渲染;发消息才真正
62
64
  把 Claude Code / Codex 进程拉起来 resume 那条对话。
63
65
 
66
+ 归属规则:
67
+
68
+ - 默认不传 `ownerPersonaId`:导入普通 session,不挂任何 persona,出现在 Recent
69
+ - 显式传 `--owner-persona-id <personaId>`:导入该 persona 的 owner session,出现在对应
70
+ People/persona 分组
71
+ - 外部 cwd(例如 `~/Desktop` 项目)不会因为执行导入的是 butler 就自动挂到 butler
72
+
64
73
  两边的会话数据在哪:
65
74
 
66
75
  - Claude Code:`~/.claude/projects/<编码后的 cwd>/<会话 id>.jsonl`,文件名就是会话 id
@@ -99,7 +108,7 @@ node .claude/skills/clawd-session-import/scripts/link-sessions.mjs --undo --appl
99
108
  ## 注意
100
109
 
101
110
  - clawd 桌面端得在跑,否则脚本连不上(会明确报错)
102
- - 关联完让用户刷新一下 clawd 界面才看得到
111
+ - 如果脚本提示 daemon 没有 `session:importExternal`,说明当前 clawd 还没升级/重启到支持后台导入的版本;脚本不会退回旧的直接改 JSON 路径
103
112
  - Codex 会话在 clawd 里不支持回滚 / fork / 实时接管(这是 codex 侧的能力边界,
104
113
  不是关联的问题);能看能续聊
105
114
  - 会话很多时一次全灌会把侧边栏冲爆,建议配 `--limit` 分批,或先用 `--days 3` 试水
@@ -1,10 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  // 把本机 Claude Code / Codex 的历史会话关联成 clawd session。
3
3
  //
4
- // 原理(对 clawd 源码核实过,非猜测):
5
- // - session:create 只写一份 SessionFile 元数据,不拉起任何进程 → 批量关联很便宜
6
- // - session:resume 在该 session 尚无 runner 时,只是把 toolSessionId 写进元数据
7
- // - SessionStore.list() 每次都从磁盘重读 → 事后回填 createdAt/updatedAt 即时生效
4
+ // 原理:
5
+ // - session:importExternal 只写一份 clawd SessionFile 引用,不拉起任何进程
6
+ // - daemon 负责去重、写入正确 scope,并广播 session:info 通知已打开的侧边栏刷新
8
7
  // 关联完,会话出现在 clawd sidebar;点开时由 history reader 按 (cwd, toolSessionId)
9
8
  // 渲染原始 transcript;用户发消息才真正 resume 起 CC / codex 进程接着聊。
10
9
  //
@@ -427,62 +426,33 @@ function isDir(p) {
427
426
  }
428
427
  }
429
428
 
430
- // SessionFile 只落这两处:<clawd>/sessions/** <clawd>/personas/<id>/.clawd/sessions/**。
431
- // 别退化成扫整个 <clawd>/personas——那底下有用户的 projects/ 和 node_modules,而本函数
432
- // 每关联一条会话调一次,--limit 50 就是 50 次全树遍历。
433
- function sessionRoots() {
434
- const roots = [path.join(CLAWD_DIR, 'sessions')]
435
- let personas = []
429
+ async function link(cand, icon, ownerPersonaId = '') {
430
+ let imported
436
431
  try {
437
- personas = fs.readdirSync(path.join(CLAWD_DIR, 'personas'), { withFileTypes: true })
438
- } catch {
439
- /* 没有 personas 目录 */
440
- }
441
- for (const d of personas) {
442
- if (d.isDirectory()) roots.push(path.join(CLAWD_DIR, 'personas', d.name, '.clawd', 'sessions'))
443
- }
444
- return roots
445
- }
446
-
447
- function findSessionFile(sessionId) {
448
- const want = `${sessionId}.json`
449
- for (const root of sessionRoots()) {
450
- const hit = listFiles(root).find((f) => path.basename(f) === want)
451
- if (hit) return hit
452
- }
453
- return null
454
- }
455
-
456
- // 把 SessionFile 的时间戳改回原始会话的,sidebar 才按真实先后排
457
- function backfillTimestamps(sessionId, cand) {
458
- const file = findSessionFile(sessionId)
459
- if (!file) return '会话文件没找到,时间戳未回填'
460
- try {
461
- const data = JSON.parse(fs.readFileSync(file, 'utf8'))
462
- data.createdAt = isoOf(cand.createdMs)
463
- data.updatedAt = isoOf(cand.updatedMs)
464
- const tmp = `${file}.tmp-import`
465
- fs.writeFileSync(tmp, JSON.stringify(data, null, 2), { encoding: 'utf8', mode: 0o600 })
466
- fs.renameSync(tmp, file)
467
- return ''
432
+ imported = await rpc('session:importExternal', {
433
+ cwd: cand.cwd,
434
+ tool: cand.tool,
435
+ toolSessionId: cand.toolSessionId,
436
+ label: cand.label || `${cand.tool} 会话`,
437
+ iconKey: icon,
438
+ createdAt: isoOf(cand.createdMs),
439
+ updatedAt: isoOf(cand.updatedMs),
440
+ ...(ownerPersonaId ? { ownerPersonaId } : {}),
441
+ })
468
442
  } catch (err) {
469
- return `时间戳回填失败:${err.message}`
443
+ if (err instanceof RpcError && /session:importExternal|METHOD_NOT_ALLOWED|METHOD_NOT_IMPLEMENTED|unknown method|not implemented/.test(err.message)) {
444
+ throw new RpcError(
445
+ '当前 clawd daemon 还没有 session:importExternal RPC。请升级/重启 clawd 后再导入;脚本不会退回旧的直接改 JSON 路径。',
446
+ )
447
+ }
448
+ throw err
470
449
  }
471
- }
472
-
473
- async function link(cand, icon) {
474
- const created = await rpc('session:create', {
475
- cwd: cand.cwd,
476
- tool: cand.tool,
477
- label: cand.label || `${cand.tool} 会话`,
478
- iconKey: icon,
479
- })
480
- const sessionId = created.sessionId
481
- if (!sessionId) throw new RpcError(`session:create 没返回 sessionId:${clip(JSON.stringify(created), 150)}`)
482
- await rpc('session:resume', { sessionId, toolSessionId: cand.toolSessionId })
483
- const warn = backfillTimestamps(sessionId, cand)
484
- cand.result = sessionId + (warn ? `(${warn})` : '')
485
- return sessionId
450
+ const sessionId = imported.sessionId
451
+ if (!sessionId) {
452
+ throw new RpcError(`session:importExternal 没返回 sessionId:${clip(JSON.stringify(imported), 150)}`)
453
+ }
454
+ cand.result = sessionId + (imported.alreadyLinked ? '(已存在)' : '')
455
+ return { sessionId, linked: !imported.alreadyLinked }
486
456
  }
487
457
 
488
458
  // ---------------------------------------------------------------- 台账 / 撤销
@@ -542,7 +512,7 @@ const ICON_KEYS = ['research', 'code', 'loop', 'qa', 'reading', 'debug', 'idea',
542
512
  function parseArgs(argv) {
543
513
  const a = {
544
514
  days: 7, tool: 'both', apply: false, limit: 0, minTurns: 2,
545
- includeClawd: false, icon: 'reading', undo: false, json: false,
515
+ includeClawd: false, icon: 'reading', ownerPersonaId: '', undo: false, json: false,
546
516
  }
547
517
  for (let i = 0; i < argv.length; i++) {
548
518
  const k = argv[i]
@@ -557,6 +527,7 @@ function parseArgs(argv) {
557
527
  case '--limit': a.limit = Number(next()); break
558
528
  case '--min-turns': a.minTurns = Number(next()); break
559
529
  case '--icon': a.icon = next(); break
530
+ case '--owner-persona-id': a.ownerPersonaId = next(); break
560
531
  case '--apply': a.apply = true; break
561
532
  case '--include-clawd': a.includeClawd = true; break
562
533
  case '--undo': a.undo = true; break
@@ -585,6 +556,8 @@ const HELP = `把本机 Claude Code / Codex 历史会话关联进 clawd
585
556
  --min-turns N 少于这么多轮用户发言的会话跳过(默认 2)
586
557
  --include-clawd 连 ~/.clawd 目录下的会话也关联(默认跳过)
587
558
  --icon KEY research/code/loop/qa/reading/debug/idea/doc/assist(默认 reading)
559
+ --owner-persona-id ID
560
+ 显式挂到某个 persona;不传则导入为普通 session,进入 Recent
588
561
  --undo 撤销本工具关联过的会话(配 --apply 才真删)
589
562
  --json 输出 JSON(含被跳过的和原因)`
590
563
 
@@ -615,13 +588,21 @@ async function main() {
615
588
  if (args.apply) {
616
589
  for (const c of todo) {
617
590
  try {
618
- const sid = await link(c, args.icon)
591
+ const linked = await link(c, args.icon, args.ownerPersonaId)
619
592
  // 每成功一条就落台账:攒到循环结束再写的话,中途崩了 --undo 就清不掉已建的会话
620
- ledgerWrite(
621
- ledgerRead().concat([
622
- { sessionId: sid, tool: c.tool, label: c.label, toolSessionId: c.toolSessionId },
623
- ]),
624
- )
593
+ if (linked.linked) {
594
+ ledgerWrite(
595
+ ledgerRead().concat([
596
+ {
597
+ sessionId: linked.sessionId,
598
+ tool: c.tool,
599
+ label: c.label,
600
+ toolSessionId: c.toolSessionId,
601
+ ...(args.ownerPersonaId ? { ownerPersonaId: args.ownerPersonaId } : {}),
602
+ },
603
+ ]),
604
+ )
605
+ }
625
606
  } catch (err) {
626
607
  c.result = `失败:${err.message}`
627
608
  }