@clawos-dev/clawd 0.2.277 → 0.2.278

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.
package/dist/cli.cjs CHANGED
@@ -6779,7 +6779,7 @@ var init_principal = __esm({
6779
6779
  });
6780
6780
 
6781
6781
  // ../protocol/src/schemas.ts
6782
- var SessionStatusSchema, UsageSchema, ContextUsageSchema, sessionMetaShape, SessionMetaSchema, ModelInfoSchema, ModeInfoSchema, ConfigFieldSchemaSchema, CapabilitiesGetArgs, ToolFeaturesSchema, CapabilitiesResponseSchema, AllowRuleSchema, SessionFileSchema, ParsedEventBase, HistoryUserMetaSchema, SubagentToolStatsSchema, StructuredPatchHunkSchema, ToolResultExtraSchema, MemoryEntrySchema, AskQuestionOptionSchema, AskQuestionItemSchema, ParsedEventSchema, SessionCreateArgs, SessionListArgs, SessionIdArgs, SessionUpdateArgs, SessionSendArgs, SessionInterruptArgs, SessionRewindArgs, SessionRewindResponseSchema, SessionRewindDiffArgs, RewindDiffHunkSchema, RewindDiffFileSchema, SessionRewindDiffResponseSchema, SessionRewindableMessageIdsArgs, SessionRewindableMessageIdsResponseSchema, SessionResumeArgs, SessionForkArgs, SessionForkResponseSchema, SessionObserveArgs, SessionEventsArgs, PermissionRespondArgs, HistoryListArgs, HistoryReadArgs, HistorySubagentsArgs, HistorySubagentReadArgs, WorkspaceListArgs, WorkspaceReadArgs, SkillsListArgs, SkillEntrySchema, AgentEntrySchema, AgentsListArgs, AgentsListResponseSchema, SessionCurrentTurnSenderArgs, SessionSubscribeArgs, SessionPinArgs, PeerSessionUpsertArgs, PeerSessionUpsertResponseSchema, PeerSessionRemoveArgs, PeerSessionRemoveResponseSchema, SessionReorderPinsArgs, GitRootArgs, GitRootResponseSchema, GitBranchArgs, GitBranchResponseSchema, GitBranchesArgs, GitBranchesResponseSchema, HistoryRecentDirsArgs, RecentDirEntrySchema, HistoryRecentDirsResponseSchema, SessionQuestionFrameSchema, SessionQuestionClearedFrameSchema, AnswerQuestionArgs, AnswerQuestionResponseSchema, CancelQuestionArgs, CancelQuestionResponseSchema, ClientInfoSchema, AuthRequestFrameSchema, AuthOkFrameSchema, DeviceConnectionEntrySchema, DeviceConnectionsResponseSchema, TunnelReadyEventSchema, TunnelExitedEventSchema, TunnelUnavailableEventSchema, InfoRunningSessionSchema, InfoResponseSchema, PROJECT_PORT_MIN, PROJECT_PORT_MAX, projectNameRegex, DEFAULT_DEV_COMMAND, ProjectMetadataSchema, PROJECT_STAGE_VALUES, ProjectStageSchema, ProjectWithStatusSchema, ListProjectsArgsSchema, ListProjectsResultSchema, GetProjectArgsSchema, GetProjectResultSchema, CreateProjectArgsSchema, CreateProjectResultSchema, DeleteProjectArgsSchema, DeleteProjectResultSchema, UpdateProjectPortArgsSchema, UpdateProjectPortResultSchema, SetProdUrlArgsSchema, SetProdUrlResultSchema, StartDevServerArgsSchema, StartDevServerResultSchema, ASSISTANT_REPORTABLE_STAGES, ReportStageArgsSchema, ReportStageResultSchema, StopDevServerArgsSchema, StopDevServerResultSchema, AppBuilderProjectUpdatedEventSchema, PublishArgsSchema, PublishResultSchema, DismissPublishJobArgsSchema, DismissPublishJobResultSchema, AppBuilderPublishProgressEventSchema, AppBuilderPublishFailedEventSchema;
6782
+ var SessionStatusSchema, UsageSchema, ContextUsageSchema, sessionMetaShape, SessionMetaSchema, ModelInfoSchema, ModeInfoSchema, ConfigFieldSchemaSchema, CapabilitiesGetArgs, ToolFeaturesSchema, CapabilitiesResponseSchema, AllowRuleSchema, SessionFileSchema, ParsedEventBase, HistoryUserMetaSchema, SubagentToolStatsSchema, StructuredPatchHunkSchema, ToolResultExtraSchema, MemoryEntrySchema, AskQuestionOptionSchema, AskQuestionItemSchema, ParsedEventSchema, SessionCreateArgs, SessionImportExternalArgs, SessionListArgs, SessionIdArgs, SessionUpdateArgs, SessionSendArgs, SessionInterruptArgs, SessionRewindArgs, SessionRewindResponseSchema, SessionRewindDiffArgs, RewindDiffHunkSchema, RewindDiffFileSchema, SessionRewindDiffResponseSchema, SessionRewindableMessageIdsArgs, SessionRewindableMessageIdsResponseSchema, SessionResumeArgs, SessionForkArgs, SessionForkResponseSchema, SessionObserveArgs, SessionEventsArgs, PermissionRespondArgs, HistoryListArgs, HistoryReadArgs, HistorySubagentsArgs, HistorySubagentReadArgs, WorkspaceListArgs, WorkspaceReadArgs, SkillsListArgs, SkillEntrySchema, AgentEntrySchema, AgentsListArgs, AgentsListResponseSchema, SessionCurrentTurnSenderArgs, SessionSubscribeArgs, SessionPinArgs, PeerSessionUpsertArgs, PeerSessionUpsertResponseSchema, PeerSessionRemoveArgs, PeerSessionRemoveResponseSchema, SessionReorderPinsArgs, GitRootArgs, GitRootResponseSchema, GitBranchArgs, GitBranchResponseSchema, GitBranchesArgs, GitBranchesResponseSchema, HistoryRecentDirsArgs, RecentDirEntrySchema, HistoryRecentDirsResponseSchema, SessionQuestionFrameSchema, SessionQuestionClearedFrameSchema, AnswerQuestionArgs, AnswerQuestionResponseSchema, CancelQuestionArgs, CancelQuestionResponseSchema, ClientInfoSchema, AuthRequestFrameSchema, AuthOkFrameSchema, DeviceConnectionEntrySchema, DeviceConnectionsResponseSchema, TunnelReadyEventSchema, TunnelExitedEventSchema, TunnelUnavailableEventSchema, InfoRunningSessionSchema, InfoResponseSchema, PROJECT_PORT_MIN, PROJECT_PORT_MAX, projectNameRegex, DEFAULT_DEV_COMMAND, ProjectMetadataSchema, PROJECT_STAGE_VALUES, ProjectStageSchema, ProjectWithStatusSchema, ListProjectsArgsSchema, ListProjectsResultSchema, GetProjectArgsSchema, GetProjectResultSchema, CreateProjectArgsSchema, CreateProjectResultSchema, DeleteProjectArgsSchema, DeleteProjectResultSchema, UpdateProjectPortArgsSchema, UpdateProjectPortResultSchema, SetProdUrlArgsSchema, SetProdUrlResultSchema, StartDevServerArgsSchema, StartDevServerResultSchema, ASSISTANT_REPORTABLE_STAGES, ReportStageArgsSchema, ReportStageResultSchema, StopDevServerArgsSchema, StopDevServerResultSchema, AppBuilderProjectUpdatedEventSchema, PublishArgsSchema, PublishResultSchema, DismissPublishJobArgsSchema, DismissPublishJobResultSchema, AppBuilderPublishProgressEventSchema, AppBuilderPublishFailedEventSchema;
6783
6783
  var init_schemas = __esm({
6784
6784
  "../protocol/src/schemas.ts"() {
6785
6785
  "use strict";
@@ -7225,6 +7225,16 @@ var init_schemas = __esm({
7225
7225
  }).refine((args) => args.cwd != null || args.ownerPersonaId != null, {
7226
7226
  message: "cwd \u4E0E ownerPersonaId \u81F3\u5C11\u4F20\u4E00\u4E2A"
7227
7227
  });
7228
+ SessionImportExternalArgs = external_exports.object({
7229
+ cwd: external_exports.string().min(1),
7230
+ tool: external_exports.string().min(1),
7231
+ toolSessionId: external_exports.string().min(1),
7232
+ label: external_exports.string().optional(),
7233
+ iconKey: external_exports.string().optional(),
7234
+ createdAt: external_exports.string().datetime(),
7235
+ updatedAt: external_exports.string().datetime(),
7236
+ ownerPersonaId: external_exports.string().min(1).optional()
7237
+ });
7228
7238
  SessionListArgs = external_exports.object({
7229
7239
  /** true = 只返回 mirror(originOwnerPrincipalId 非空,即「我在朋友 daemon 上创建」的本机存证)。 */
7230
7240
  originOnly: external_exports.boolean().optional(),
@@ -7801,6 +7811,10 @@ var init_methods = __esm({
7801
7811
  anyOf: [{ required: ["cwd"] }, { required: ["ownerPersonaId"] }]
7802
7812
  }
7803
7813
  },
7814
+ "session:importExternal": {
7815
+ 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",
7816
+ args: SessionImportExternalArgs
7817
+ },
7804
7818
  "session:list": {
7805
7819
  summary: "\u5217\u51FA\u5F53\u524D\u8EAB\u4EFD\u53EF\u89C1\u7684\u4F1A\u8BDD\uFF08owner \u5168\u91CF\uFF1Bguest \u9650\u88AB\u6388\u6743 persona\uFF09",
7806
7820
  args: SessionListArgs
@@ -45963,6 +45977,41 @@ var SessionManager = class {
45963
45977
  const written = this.storeFor(scope).write(file);
45964
45978
  return { response: written, broadcast: [] };
45965
45979
  }
45980
+ importExternal(args, creatorPrincipalId, creatorDisplayName, creatorFeishuUnionId) {
45981
+ const existing = this.listAllOwned().find((file2) => file2.toolSessionId === args.toolSessionId);
45982
+ if (existing) {
45983
+ return {
45984
+ response: { ...existing, alreadyLinked: true },
45985
+ broadcast: []
45986
+ };
45987
+ }
45988
+ try {
45989
+ const stat = import_node_fs9.default.statSync(args.cwd);
45990
+ if (!stat.isDirectory()) throw new Error("not dir");
45991
+ } catch {
45992
+ throw new ClawdError(ERROR_CODES.INVALID_CWD, `cwd not a directory: ${args.cwd}`);
45993
+ }
45994
+ this.deps.getAdapter(args.tool);
45995
+ const file = {
45996
+ sessionId: newSessionId(),
45997
+ cwd: args.cwd,
45998
+ tool: args.tool,
45999
+ toolSessionId: args.toolSessionId,
46000
+ label: args.label,
46001
+ iconKey: args.iconKey,
46002
+ ownerPersonaId: args.ownerPersonaId,
46003
+ creatorPrincipalId,
46004
+ creatorDisplayName,
46005
+ ...creatorFeishuUnionId ? { creatorFeishuUnionId } : {},
46006
+ createdAt: args.createdAt,
46007
+ updatedAt: args.updatedAt
46008
+ };
46009
+ const written = this.writeOwned(file);
46010
+ return {
46011
+ response: written,
46012
+ broadcast: [{ frame: { type: "session:info", ...written }, target: "all" }]
46013
+ };
46014
+ }
45966
46015
  pin(args) {
45967
46016
  const existing = this.getFile(args.sessionId);
45968
46017
  const pinnedAt = args.pinned ? (this.deps.now ?? Date.now)() : null;
@@ -56332,6 +56381,31 @@ function buildSessionHandlers(deps) {
56332
56381
  );
56333
56382
  return { response: { type: "session:info", ...response }, broadcast };
56334
56383
  };
56384
+ const importExternal = async (frame, _client, ctx) => {
56385
+ if (ctx && ctx.principal.kind !== "owner") {
56386
+ throw new ClawdError(
56387
+ ERROR_CODES.UNAUTHORIZED,
56388
+ `principal ${ctx.principal.kind}:${ctx.principal.id} cannot session:importExternal`
56389
+ );
56390
+ }
56391
+ const args = SessionImportExternalArgs.parse(frame);
56392
+ if (args.ownerPersonaId) {
56393
+ const persona = deps.personaRegistry.get(args.ownerPersonaId);
56394
+ if (!persona) {
56395
+ throw new Error(`persona not found: ${args.ownerPersonaId}`);
56396
+ }
56397
+ }
56398
+ const creatorPrincipalId = ctx?.principal.id ?? ownerPrincipalId;
56399
+ const creatorDisplayName = ctx?.principal.displayName ?? "Owner";
56400
+ const creatorFeishuUnionId = ctx?.principal.feishuUnionId;
56401
+ const { response, broadcast } = manager.importExternal(
56402
+ args,
56403
+ creatorPrincipalId,
56404
+ creatorDisplayName,
56405
+ creatorFeishuUnionId
56406
+ );
56407
+ return { response: { type: "session:info", ...response }, broadcast };
56408
+ };
56335
56409
  const list = async (frame, _client, ctx) => {
56336
56410
  const args = SessionListArgs.parse(frame ?? {});
56337
56411
  const narrow = (sessions) => {
@@ -56650,6 +56724,7 @@ function buildSessionHandlers(deps) {
56650
56724
  };
56651
56725
  return {
56652
56726
  "session:create": create,
56727
+ "session:importExternal": importExternal,
56653
56728
  "session:list": list,
56654
56729
  "session:get": get,
56655
56730
  "session:update": update,
@@ -57738,6 +57813,7 @@ var METHOD_GRANT_MAP = {
57738
57813
  // dispatcher 不验资源,handler 内按 ctx + frame.args 反查 ownerPersonaId 调 assertGrant。
57739
57814
  // owner 自动通过(ctx 自带 '*':'admin' grant 一切 match);guest 在被授权 persona 内可调。
57740
57815
  "session:create": CAPABILITY_SCOPED,
57816
+ "session:importExternal": CAPABILITY_SCOPED,
57741
57817
  "session:list": CAPABILITY_SCOPED,
57742
57818
  "session:get": CAPABILITY_SCOPED,
57743
57819
  "session:update": CAPABILITY_SCOPED,
@@ -57929,7 +58005,7 @@ function computeMethodAccess(args) {
57929
58005
  }
57930
58006
 
57931
58007
  // src/version.ts
57932
- var version = "0.2.277".length > 0 ? "0.2.277" : "dev";
58008
+ var version = "0.2.278".length > 0 ? "0.2.278" : "dev";
57933
58009
 
57934
58010
  // src/cli-probe/probe.ts
57935
58011
  var fs54 = __toESM(require("fs"), 1);
@@ -22376,6 +22376,16 @@ var SessionCreateArgs = external_exports.object({
22376
22376
  }).refine((args) => args.cwd != null || args.ownerPersonaId != null, {
22377
22377
  message: "cwd \u4E0E ownerPersonaId \u81F3\u5C11\u4F20\u4E00\u4E2A"
22378
22378
  });
22379
+ var SessionImportExternalArgs = external_exports.object({
22380
+ cwd: external_exports.string().min(1),
22381
+ tool: external_exports.string().min(1),
22382
+ toolSessionId: external_exports.string().min(1),
22383
+ label: external_exports.string().optional(),
22384
+ iconKey: external_exports.string().optional(),
22385
+ createdAt: external_exports.string().datetime(),
22386
+ updatedAt: external_exports.string().datetime(),
22387
+ ownerPersonaId: external_exports.string().min(1).optional()
22388
+ });
22379
22389
  var SessionListArgs = external_exports.object({
22380
22390
  /** true = 只返回 mirror(originOwnerPrincipalId 非空,即「我在朋友 daemon 上创建」的本机存证)。 */
22381
22391
  originOnly: external_exports.boolean().optional(),
@@ -22915,6 +22925,10 @@ var METHOD_DOCS = {
22915
22925
  anyOf: [{ required: ["cwd"] }, { required: ["ownerPersonaId"] }]
22916
22926
  }
22917
22927
  },
22928
+ "session:importExternal": {
22929
+ 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",
22930
+ args: SessionImportExternalArgs
22931
+ },
22918
22932
  "session:list": {
22919
22933
  summary: "\u5217\u51FA\u5F53\u524D\u8EAB\u4EFD\u53EF\u89C1\u7684\u4F1A\u8BDD\uFF08owner \u5168\u91CF\uFF1Bguest \u9650\u88AB\u6388\u6743 persona\uFF09",
22920
22934
  args: SessionListArgs
@@ -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
  }