@clawos-dev/clawd 0.2.224 → 0.2.226

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
@@ -11379,6 +11379,45 @@ function hashDirToCwd(hash) {
11379
11379
  const body = hash.startsWith("-") ? hash.slice(1) : hash;
11380
11380
  return "/" + body.replace(/-/g, "/");
11381
11381
  }
11382
+ function findTranscriptByToolSessionId(projectsRoot, toolSessionId) {
11383
+ let entries;
11384
+ try {
11385
+ entries = import_node_fs5.default.readdirSync(projectsRoot, { withFileTypes: true });
11386
+ } catch {
11387
+ return null;
11388
+ }
11389
+ let best = null;
11390
+ for (const ent of entries) {
11391
+ if (!ent.isDirectory()) continue;
11392
+ const candidate = import_node_path5.default.join(projectsRoot, ent.name, `${toolSessionId}.jsonl`);
11393
+ try {
11394
+ const st = import_node_fs5.default.statSync(candidate);
11395
+ if (!best || st.mtimeMs > best.mtime) best = { p: candidate, mtime: st.mtimeMs };
11396
+ } catch {
11397
+ }
11398
+ }
11399
+ return best?.p ?? null;
11400
+ }
11401
+ function deriveCwdFromTranscript(filePath) {
11402
+ const dirName = import_node_path5.default.basename(import_node_path5.default.dirname(filePath));
11403
+ let raw;
11404
+ try {
11405
+ raw = import_node_fs5.default.readFileSync(filePath, "utf8");
11406
+ } catch {
11407
+ return null;
11408
+ }
11409
+ let hit = null;
11410
+ for (const line of raw.split("\n")) {
11411
+ const t = line.trim();
11412
+ if (!t) continue;
11413
+ try {
11414
+ const o = JSON.parse(t);
11415
+ if (typeof o.cwd === "string" && cwdToHashDir(o.cwd) === dirName) hit = o.cwd;
11416
+ } catch {
11417
+ }
11418
+ }
11419
+ return hit;
11420
+ }
11382
11421
  function safeStatMtime(p2) {
11383
11422
  try {
11384
11423
  return import_node_fs5.default.statSync(p2).mtimeMs;
@@ -41880,7 +41919,7 @@ var CONTACT_SSH_SYSTEM_PROMPT_HINT = `## \u8DE8\u8BBE\u5907\u6587\u4EF6\u8BBF\u9
41880
41919
 
41881
41920
  **\u6CE8\u610F**\uFF1A
41882
41921
  - \u6388\u6743\u76EE\u5F55\u6570\u636E\u6E90\u5B9E\u65F6\uFF08jail \u6BCF\u6B21\u8BFB A \u4FA7\u6700\u65B0\u914D\u7F6E\uFF09\uFF0CA \u6539\u4E86\u6388\u6743\u540E\u4F60\u4E0B\u4E00\u6B21\u62E8\u53F7\u7ACB\u5373\u611F\u77E5
41883
- - \u6392\u969C\uFF1A\u672C\u673A debug \u65E5\u5FD7\u5728 \`~/.clawd/contact-ssh.log\`\uFF08\u7528\u6237\u53EF \`tail -f\` \u770B\u6BCF\u4E00\u6B65\uFF09\uFF0C\u4F60\u51FA\u9519\u65F6\u544A\u8BC9\u7528\u6237\u67E5\u8FD9\u4E2A\u6587\u4EF6
41922
+ - \u6392\u969C\uFF1A\u672C\u673A debug \u65E5\u5FD7\u5728 \`~/.clawd/log/contact-ssh.log\`\uFF08\u7528\u6237\u53EF \`tail -f\` \u770B\u6BCF\u4E00\u6B65\uFF09\uFF0C\u4F60\u51FA\u9519\u65F6\u544A\u8BC9\u7528\u6237\u67E5\u8FD9\u4E2A\u6587\u4EF6
41884
41923
  - \u5982\u679C \`ls ~/.clawd/contact-ssh-keys/\` \u4E3A\u7A7A\uFF0C\u8BF4\u660E\u8FD8\u6CA1\u6709\u5BF9\u7AEF\u6388\u6743\u4F60\u6216 key \u8FD8\u6CA1\u62C9\u5230\uFF08B \u4FA7 daemon \u6BCF 60s \u4E00\u6B21\u81EA\u52A8\u62C9\uFF09\uFF0C\u5982\u5B9E\u544A\u8BC9\u7528\u6237
41885
41924
  `;
41886
41925
 
@@ -42357,6 +42396,13 @@ function applyCommand(state, command, deps) {
42357
42396
  }
42358
42397
  return { state: next, effects };
42359
42398
  }
42399
+ case "sync-cwd": {
42400
+ if (next.file.cwd === command.cwd) return { state: next, effects };
42401
+ next.file = { ...next.file, cwd: command.cwd, updatedAt: nowIso(deps) };
42402
+ effects.push({ kind: "persist-file", file: next.file });
42403
+ effects.push(sessionInfoFrame(next.file));
42404
+ return { state: next, effects };
42405
+ }
42360
42406
  case "resume": {
42361
42407
  const sameToolSession = next.file.toolSessionId === command.toolSessionId;
42362
42408
  const nextFile = {
@@ -43955,6 +44001,23 @@ var SessionManager = class {
43955
44001
  this.writeOwned(updated);
43956
44002
  return { response: updated, broadcast: [] };
43957
44003
  }
44004
+ /**
44005
+ * [T-17] observer 检测到 CC 挪走 transcript(cwd 中途变化)后的 cwd 校正入口。
44006
+ * 不进 withCollector:observer 回调没有 in-flight RPC,session:info 帧走
44007
+ * routeFromRunner 的异步 broadcast 路径。有 runner 走 reducer sync-cwd(无 kill,
44008
+ * 区别于 update 的 RUNTIME_PATCH_KEYS 重启语义);无 runner 直写 store。
44009
+ * 幂等:reducer 同值 noop;session 不存在静默返回(observer 生命周期与 store 解耦)。
44010
+ */
44011
+ syncSessionCwd(sessionId, cwd) {
44012
+ const runner = this.runners.get(sessionId);
44013
+ if (runner) {
44014
+ runner.input({ kind: "command", command: { kind: "sync-cwd", cwd } });
44015
+ return;
44016
+ }
44017
+ const existing = this.findOwnedSession(sessionId);
44018
+ if (!existing || existing.cwd === cwd) return;
44019
+ this.writeOwned({ ...existing, cwd, updatedAt: nowIso2(this.deps) });
44020
+ }
43958
44021
  /**
43959
44022
  * 启动清扫:删除所有磁盘残留的 ephemeral(快速问答临时)session。
43960
44023
  * 客户端崩溃 / 没关抽屉会留下永久不可见的孤儿(ephemeral 被 sidebar 过滤,用户自己也删不掉)。
@@ -49640,14 +49703,19 @@ var SessionObserver = class {
49640
49703
  lastSize: size,
49641
49704
  buf: "",
49642
49705
  adapter: args.adapter,
49643
- prevIsRejectSentinel: false
49706
+ prevIsRejectSentinel: false,
49707
+ toolSessionId: args.toolSessionId,
49708
+ sawFile: import_node_fs25.default.existsSync(filePath),
49709
+ lastRelocateScanMs: 0,
49710
+ pendingCwdDerive: false,
49711
+ lastDeriveMs: 0
49644
49712
  };
49645
49713
  try {
49646
49714
  import_node_fs25.default.mkdirSync(import_node_path24.default.dirname(filePath), { recursive: true });
49647
49715
  } catch {
49648
49716
  }
49649
49717
  w2.watcher = import_node_fs25.default.watch(import_node_path24.default.dirname(filePath), { persistent: false }, (_event, changedName) => {
49650
- if (!changedName || !filePath.endsWith(changedName)) return;
49718
+ if (!changedName || !w2.filePath.endsWith(changedName)) return;
49651
49719
  this.poll(w2);
49652
49720
  });
49653
49721
  w2.pollTimer = setInterval(() => this.poll(w2), 200);
@@ -49695,8 +49763,10 @@ var SessionObserver = class {
49695
49763
  try {
49696
49764
  size = import_node_fs25.default.statSync(w2.filePath).size;
49697
49765
  } catch {
49766
+ if (w2.sawFile) this.maybeRelocate(w2);
49698
49767
  return;
49699
49768
  }
49769
+ w2.sawFile = true;
49700
49770
  if (size < w2.lastSize) {
49701
49771
  w2.lastSize = 0;
49702
49772
  w2.buf = "";
@@ -49731,6 +49801,57 @@ var SessionObserver = class {
49731
49801
  } finally {
49732
49802
  import_node_fs25.default.closeSync(fd);
49733
49803
  }
49804
+ if (w2.pendingCwdDerive) this.maybeDeriveCwd(w2);
49805
+ }
49806
+ // [T-17] 文件消失后的兜底重定位:按 tsid 全局扫描 → 切 watcher → lastSize/buf 保留接续读
49807
+ // (CC 是 move,内容前缀不变,不重复喂;异常变小走 poll 的截断重建分支,上游 uuid dedup 兜底)。
49808
+ // 检测点唯一(poll ENOENT),fs.watch rename 事件也 funnel 到 poll,天然幂等;scan 加 1s
49809
+ // rate-limit 防 ENOENT 持续期(如 cleanupPeriodDays 真删除)反复 readdir 空转。
49810
+ maybeRelocate(w2) {
49811
+ const now = Date.now();
49812
+ if (now - w2.lastRelocateScanMs < 1e3) return;
49813
+ w2.lastRelocateScanMs = now;
49814
+ const projectsRoot = import_node_path24.default.join(this.home, ".claude", "projects");
49815
+ const found = findTranscriptByToolSessionId(projectsRoot, w2.toolSessionId);
49816
+ if (!found || found === w2.filePath) return;
49817
+ try {
49818
+ w2.watcher?.close();
49819
+ } catch {
49820
+ }
49821
+ w2.filePath = found;
49822
+ try {
49823
+ w2.watcher = import_node_fs25.default.watch(import_node_path24.default.dirname(found), { persistent: false }, (_event, changedName) => {
49824
+ if (!changedName || !w2.filePath.endsWith(changedName)) return;
49825
+ this.poll(w2);
49826
+ });
49827
+ } catch {
49828
+ w2.watcher = null;
49829
+ }
49830
+ const cwd = deriveCwdFromTranscript(found);
49831
+ if (cwd) {
49832
+ this.applyDerivedCwd(w2, cwd);
49833
+ } else {
49834
+ w2.pendingCwdDerive = true;
49835
+ }
49836
+ this.opts.onJsonlRelocated?.(w2.sessionId, { filePath: found, cwd });
49837
+ this.poll(w2);
49838
+ }
49839
+ maybeDeriveCwd(w2) {
49840
+ const now = Date.now();
49841
+ if (now - w2.lastDeriveMs < 500) return;
49842
+ w2.lastDeriveMs = now;
49843
+ const cwd = deriveCwdFromTranscript(w2.filePath);
49844
+ if (!cwd) return;
49845
+ w2.pendingCwdDerive = false;
49846
+ this.applyDerivedCwd(w2, cwd);
49847
+ this.opts.onJsonlRelocated?.(w2.sessionId, { filePath: w2.filePath, cwd });
49848
+ }
49849
+ applyDerivedCwd(w2, cwd) {
49850
+ this.metaObserver?.start({
49851
+ sessionId: w2.sessionId,
49852
+ cwd,
49853
+ toolSessionId: w2.toolSessionId
49854
+ });
49734
49855
  }
49735
49856
  // 解析 JSONL 单行:仅当是主链 user 文本行(非 sidechain / 非 sub-agent / message.role='user'
49736
49857
  // 且 content 含 text part)时,抽出 real uuid + 第一段 text + timestamp 上报。
@@ -53069,7 +53190,9 @@ var TunnelManager = class {
53069
53190
  const proc = (this.deps.spawnImpl ?? import_node_child_process9.spawn)(frpcBin, ["-c", tomlPath], {
53070
53191
  stdio: ["ignore", "pipe", "pipe"]
53071
53192
  });
53072
- const logFilePath = import_node_path35.default.join(this.deps.dataDir, "frpc.log");
53193
+ const logDir = import_node_path35.default.join(this.deps.dataDir, "log");
53194
+ import_node_fs34.default.mkdirSync(logDir, { recursive: true });
53195
+ const logFilePath = import_node_path35.default.join(logDir, "frpc.log");
53073
53196
  const logStream = import_node_fs34.default.createWriteStream(logFilePath, { flags: "a", mode: 384 });
53074
53197
  logStream.on("error", () => {
53075
53198
  });
@@ -53310,8 +53433,11 @@ var CLAWD_SSH_JAIL_SCRIPT = String.raw`#!/usr/bin/env bash
53310
53433
 
53311
53434
  set -euo pipefail
53312
53435
 
53313
- # 用户可读审计日志:追加到 ~/.clawd/contact-ssh.log(跟 daemon TS 侧 contact-ssh-log.ts 共写)
53314
- CONTACT_SSH_LOG="${"${HOME}"}/.clawd/contact-ssh.log"
53436
+ # 用户可读审计日志:追加到 ~/.clawd/log/contact-ssh.log(跟 daemon TS 侧 contact-ssh-log.ts 共写)
53437
+ # bash 侧写死 ~/.clawd/log/,因为 jail 脚本在 authorized_keys command= 里被 sshd 起,
53438
+ # 拿不到 daemon --data-dir 覆盖。dev daemon 用 --data-dir=/tmp/x 时 jail 仍写 ~/.clawd/log/,
53439
+ # 跟改动前 (~/.clawd/contact-ssh.log) 一样的分歧行为,不做统一。
53440
+ CONTACT_SSH_LOG="${"${HOME}"}/.clawd/log/contact-ssh.log"
53315
53441
  log_line() {
53316
53442
  # 参数: level tag msg
53317
53443
  local ts
@@ -53508,7 +53634,9 @@ var SshdManager = class {
53508
53634
  const proc = (this.deps.spawnImpl ?? import_node_child_process11.spawn)(sshdBin, ["-D", "-e", "-f", configPath], {
53509
53635
  stdio: ["ignore", "pipe", "pipe"]
53510
53636
  });
53511
- const logStream = import_node_fs38.default.createWriteStream(import_node_path39.default.join(this.sshdDir, "sshd.log"), {
53637
+ const sshdLogDir = import_node_path39.default.join(this.deps.dataDir, "log");
53638
+ import_node_fs38.default.mkdirSync(sshdLogDir, { recursive: true });
53639
+ const logStream = import_node_fs38.default.createWriteStream(import_node_path39.default.join(sshdLogDir, "sshd.log"), {
53512
53640
  flags: "a",
53513
53641
  mode: 384
53514
53642
  });
@@ -53689,7 +53817,7 @@ var import_node_path42 = __toESM(require("path"), 1);
53689
53817
  var import_node_fs40 = __toESM(require("fs"), 1);
53690
53818
  var import_node_path41 = __toESM(require("path"), 1);
53691
53819
  function createContactSshLog(dataDir) {
53692
- const file = import_node_path41.default.join(dataDir, "contact-ssh.log");
53820
+ const file = import_node_path41.default.join(dataDir, "log", "contact-ssh.log");
53693
53821
  function append(level, tag, message, meta) {
53694
53822
  const time = (/* @__PURE__ */ new Date()).toISOString();
53695
53823
  let line = `[${time}] [${level}] [${tag}] ${message}`;
@@ -54681,10 +54809,18 @@ function readJsonlEntries(file) {
54681
54809
  }
54682
54810
  function forkSession(input) {
54683
54811
  const baseDir = input.baseDir ?? import_node_path47.default.join(import_node_os16.default.homedir(), ".claude");
54684
- const projectDir = import_node_path47.default.join(baseDir, "projects", cwdToHashDir(input.cwd));
54685
- const sourceFile = import_node_path47.default.join(projectDir, `${input.toolSessionId}.jsonl`);
54812
+ let projectDir = import_node_path47.default.join(baseDir, "projects", cwdToHashDir(input.cwd));
54813
+ let sourceFile = import_node_path47.default.join(projectDir, `${input.toolSessionId}.jsonl`);
54686
54814
  if (!import_node_fs45.default.existsSync(sourceFile)) {
54687
- throw new Error(`fork: source transcript not found: ${sourceFile}`);
54815
+ const found = findTranscriptByToolSessionId(
54816
+ import_node_path47.default.join(baseDir, "projects"),
54817
+ input.toolSessionId
54818
+ );
54819
+ if (!found) {
54820
+ throw new Error(`fork: source transcript not found: ${sourceFile}`);
54821
+ }
54822
+ sourceFile = found;
54823
+ projectDir = import_node_path47.default.dirname(found);
54688
54824
  }
54689
54825
  const entries = readJsonlEntries(sourceFile);
54690
54826
  const mainEntries = entries.filter((e) => e.isSidechain !== true);
@@ -59292,18 +59428,20 @@ async function startDaemon(config) {
59292
59428
  sampling: logShippingCfg.sampling,
59293
59429
  homeDir: import_node_os21.default.homedir()
59294
59430
  });
59431
+ const logDir = import_node_path64.default.join(config.dataDir, "log");
59432
+ import_node_fs51.default.mkdirSync(logDir, { recursive: true });
59295
59433
  const logger = createLogger({
59296
59434
  level: config.logLevel,
59297
- file: import_node_path64.default.join(config.dataDir, "clawd.log"),
59435
+ file: import_node_path64.default.join(logDir, "clawd.log"),
59298
59436
  logClient
59299
59437
  });
59300
59438
  logger.info("starting clawd", { version, config: { port: config.port, host: config.host, dataDir: config.dataDir } });
59301
59439
  const screenIdleProbeLogger = createFileOnlyLogger({
59302
- file: import_node_path64.default.join(config.dataDir, "screen-idle-probe.log"),
59440
+ file: import_node_path64.default.join(logDir, "screen-idle-probe.log"),
59303
59441
  level: "debug"
59304
59442
  });
59305
59443
  logger.info("screen-idle probe logger enabled", {
59306
- file: import_node_path64.default.join(config.dataDir, "screen-idle-probe.log")
59444
+ file: import_node_path64.default.join(logDir, "screen-idle-probe.log")
59307
59445
  });
59308
59446
  const stateMgr = new StateFileManager({ dataDir: config.dataDir });
59309
59447
  const pre = stateMgr.preflight();
@@ -59474,7 +59612,7 @@ async function startDaemon(config) {
59474
59612
  const dispatchServerScriptPath = dispatchServerCandidates.find((p2) => import_node_fs51.default.existsSync(p2));
59475
59613
  let dispatchMcpConfigPath2;
59476
59614
  if (dispatchServerScriptPath) {
59477
- const dispatchLogPath = import_node_path64.default.join(config.dataDir, "dispatch-mcp-server.log");
59615
+ const dispatchLogPath = import_node_path64.default.join(logDir, "dispatch-mcp-server.log");
59478
59616
  dispatchMcpConfigPath2 = writeDispatchMcpConfig({
59479
59617
  dataDir: config.dataDir,
59480
59618
  serverScriptPath: dispatchServerScriptPath,
@@ -59497,7 +59635,7 @@ async function startDaemon(config) {
59497
59635
  const ticketServerScriptPath = ticketServerCandidates.find((p2) => import_node_fs51.default.existsSync(p2));
59498
59636
  let ticketMcpConfigPath2;
59499
59637
  if (ticketServerScriptPath) {
59500
- const ticketLogPath = import_node_path64.default.join(config.dataDir, "ticket-mcp-server.log");
59638
+ const ticketLogPath = import_node_path64.default.join(logDir, "ticket-mcp-server.log");
59501
59639
  ticketMcpConfigPath2 = writeTicketMcpConfig({
59502
59640
  dataDir: config.dataDir,
59503
59641
  serverScriptPath: ticketServerScriptPath,
@@ -59522,7 +59660,7 @@ async function startDaemon(config) {
59522
59660
  const shiftServerScriptPath = shiftServerCandidates.find((p2) => import_node_fs51.default.existsSync(p2));
59523
59661
  let shiftMcpConfigPath2;
59524
59662
  if (shiftServerScriptPath) {
59525
- const shiftLogPath = import_node_path64.default.join(config.dataDir, "shift-mcp-server.log");
59663
+ const shiftLogPath = import_node_path64.default.join(logDir, "shift-mcp-server.log");
59526
59664
  shiftMcpConfigPath2 = await writeShiftMcpConfig({
59527
59665
  dataDir: config.dataDir,
59528
59666
  serverScriptPath: shiftServerScriptPath,
@@ -59546,7 +59684,7 @@ async function startDaemon(config) {
59546
59684
  const inboxServerScriptPath = inboxServerCandidates.find((p2) => import_node_fs51.default.existsSync(p2));
59547
59685
  let inboxMcpConfigPath2;
59548
59686
  if (inboxServerScriptPath) {
59549
- const inboxLogPath = import_node_path64.default.join(config.dataDir, "inbox-mcp-server.log");
59687
+ const inboxLogPath = import_node_path64.default.join(logDir, "inbox-mcp-server.log");
59550
59688
  inboxMcpConfigPath2 = await writeInboxMcpConfig({
59551
59689
  dataDir: config.dataDir,
59552
59690
  serverScriptPath: inboxServerScriptPath,
@@ -59686,6 +59824,13 @@ async function startDaemon(config) {
59686
59824
  // daemon 内部转译,UI 看到的始终是 events 流里的 synth uuid(详见 manager 注释)
59687
59825
  onUserMessageObserved: (sessionId, realUuid, text) => {
59688
59826
  manager.recordRealUserUuid({ sessionId, realUuid, text });
59827
+ },
59828
+ // [T-17] CC 中途改 cwd 挪走 transcript:observer 重定位后校正 SessionFile.cwd
59829
+ // (persist + session:info,不 kill),fork / respawn / UI 缓存随之对齐。
59830
+ // cwd 可能两段式到达(挪走瞬间新 cwd 行未落盘 → 先 null 后补),回调幂等
59831
+ onJsonlRelocated: (sessionId, { filePath, cwd }) => {
59832
+ logger.info("observer jsonl relocated", { sessionId, filePath, cwd });
59833
+ if (cwd) manager.syncSessionCwd(sessionId, cwd);
59689
59834
  }
59690
59835
  });
59691
59836
  manager.attachObserver(observer);
@@ -60351,7 +60496,7 @@ async function startDaemon(config) {
60351
60496
  `Tunnel: ${r.url}`,
60352
60497
  ...resolvedAuthToken ? [`Connect: ${connectUrl}`] : [],
60353
60498
  `Frpc config: ${import_node_path64.default.join(config.dataDir, "frpc.toml")}`,
60354
- `Frpc log: ${import_node_path64.default.join(config.dataDir, "frpc.log")}`
60499
+ `Frpc log: ${import_node_path64.default.join(logDir, "frpc.log")}`
60355
60500
  ];
60356
60501
  const width = Math.max(...lines.map((l) => l.length));
60357
60502
  const bar = "\u2550".repeat(width + 4);