@clawos-dev/clawd 0.2.71 → 0.2.72-beta.140.1753cf8

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.
Files changed (2) hide show
  1. package/dist/cli.cjs +142 -105
  2. package/package.json +1 -1
package/dist/cli.cjs CHANGED
@@ -175,7 +175,11 @@ var init_errors = __esm({
175
175
  PERMISSION_REQUEST_STALE: "PERMISSION_REQUEST_STALE",
176
176
  FILE_TOO_LARGE: "FILE_TOO_LARGE",
177
177
  INTERNAL: "INTERNAL",
178
- UNAUTHORIZED: "UNAUTHORIZED"
178
+ UNAUTHORIZED: "UNAUTHORIZED",
179
+ // Ready gate:用户在 pendingSend 已存在时再次 send,daemon 拒绝(不覆盖暂存)。
180
+ // UI ChatInput 应在 popup 态 disable,daemon 兜底;error 帧 broadcast 通知所有 tab。
181
+ // Spec: clawd/superpowers/specs/2026-05-22-cc-ready-gate-design.md §错误处理
182
+ SEND_PENDING: "SEND_PENDING"
179
183
  };
180
184
  ClawdError = class extends Error {
181
185
  constructor(code, message) {
@@ -20872,7 +20876,9 @@ function cloneState(s) {
20872
20876
  freshSpawn: s.freshSpawn,
20873
20877
  procAlive: s.procAlive,
20874
20878
  seenUuids: s.seenUuids,
20875
- subSessionMeta: s.subSessionMeta
20879
+ subSessionMeta: s.subSessionMeta,
20880
+ readyForSend: s.readyForSend,
20881
+ pendingSend: s.pendingSend
20876
20882
  };
20877
20883
  }
20878
20884
  var IDLE_KILL_DELAY_MS = 5e3;
@@ -21104,6 +21110,23 @@ function applyCommand(state, command, deps) {
21104
21110
  effects.push({ kind: "cancel-idle-kill", sessionId });
21105
21111
  next.status = next.procAlive ? "running" : "idle";
21106
21112
  }
21113
+ if (next.pendingSend) {
21114
+ return {
21115
+ state: next,
21116
+ effects: [
21117
+ ...effects,
21118
+ {
21119
+ kind: "emit-frame",
21120
+ frame: {
21121
+ type: "error",
21122
+ sessionId,
21123
+ code: ERROR_CODES.SEND_PENDING,
21124
+ message: "a previous message is still pending; cc not yet ready"
21125
+ }
21126
+ }
21127
+ ]
21128
+ };
21129
+ }
21107
21130
  if (!next.procAlive) {
21108
21131
  next.procAlive = true;
21109
21132
  next.freshSpawn = true;
@@ -21112,6 +21135,9 @@ function applyCommand(state, command, deps) {
21112
21135
  next.nextSeq = 0;
21113
21136
  next.turnOpen = false;
21114
21137
  next.status = "running";
21138
+ if (deps.mode === "tui") {
21139
+ next.readyForSend = false;
21140
+ }
21115
21141
  effects.push({ kind: "spawn", ctx: buildSpawnContext(next, deps) });
21116
21142
  effects.push({
21117
21143
  kind: "emit-frame",
@@ -21128,10 +21154,14 @@ function applyCommand(state, command, deps) {
21128
21154
  uuid: deps.genUuid?.()
21129
21155
  };
21130
21156
  const pushed = pushEventToBuffer(next, userEvent, deps);
21131
- const nextState = pushed.state;
21157
+ let nextState = pushed.state;
21132
21158
  effects.push(...pushed.effects);
21133
- const payload = deps.encodeStdin(command.text, { sessionId });
21134
- effects.push({ kind: "write-stdin", payload });
21159
+ if (nextState.readyForSend) {
21160
+ const payload = deps.encodeStdin(command.text, { sessionId });
21161
+ effects.push({ kind: "write-stdin", payload });
21162
+ } else {
21163
+ nextState = { ...nextState, pendingSend: { text: command.text } };
21164
+ }
21135
21165
  return { state: nextState, effects };
21136
21166
  }
21137
21167
  case "stop": {
@@ -21147,6 +21177,7 @@ function applyCommand(state, command, deps) {
21147
21177
  }
21148
21178
  next.pendingPermissions = {};
21149
21179
  next.pendingQuestions = {};
21180
+ next.pendingSend = void 0;
21150
21181
  if (next.procAlive) {
21151
21182
  next.status = "stopping";
21152
21183
  effects.push({ kind: "kill", signal: "SIGKILL" });
@@ -21184,6 +21215,8 @@ function applyCommand(state, command, deps) {
21184
21215
  }
21185
21216
  next.pendingPermissions = {};
21186
21217
  next.pendingQuestions = {};
21218
+ next.pendingSend = void 0;
21219
+ next.readyForSend = false;
21187
21220
  if (next.procAlive) {
21188
21221
  effects.push({ kind: "kill", signal: "SIGKILL" });
21189
21222
  }
@@ -21227,6 +21260,7 @@ function applyCommand(state, command, deps) {
21227
21260
  next.pendingPermissions = {};
21228
21261
  next.freshSpawn = false;
21229
21262
  next.seenUuids = [];
21263
+ next.pendingSend = void 0;
21230
21264
  effects.push({
21231
21265
  kind: "emit-frame",
21232
21266
  frame: { type: "session:cleared", sessionId }
@@ -21264,6 +21298,7 @@ function applyCommand(state, command, deps) {
21264
21298
  next.pendingPermissions = {};
21265
21299
  next.freshSpawn = false;
21266
21300
  next.seenUuids = [];
21301
+ next.pendingSend = void 0;
21267
21302
  effects.push({ kind: "persist-file", file: nextFile });
21268
21303
  effects.push(sessionInfoFrame(nextFile));
21269
21304
  effects.push({
@@ -21298,6 +21333,8 @@ function reduceSession(state, input, deps) {
21298
21333
  const next = cloneState(state);
21299
21334
  next.procAlive = false;
21300
21335
  next.status = "stopped";
21336
+ next.pendingSend = void 0;
21337
+ next.readyForSend = false;
21301
21338
  const sessionId = next.file.sessionId;
21302
21339
  const clearedEffects = Object.keys(state.pendingQuestions ?? {}).map(
21303
21340
  (toolUseId) => ({
@@ -21329,6 +21366,8 @@ function reduceSession(state, input, deps) {
21329
21366
  case "proc-error": {
21330
21367
  const next = cloneState(state);
21331
21368
  next.status = "error";
21369
+ next.pendingSend = void 0;
21370
+ next.readyForSend = false;
21332
21371
  return {
21333
21372
  state: next,
21334
21373
  effects: [
@@ -21455,6 +21494,30 @@ function reduceSession(state, input, deps) {
21455
21494
  effects: [{ kind: "kill", signal: "SIGKILL" }]
21456
21495
  };
21457
21496
  }
21497
+ case "ready-detected": {
21498
+ if (!state.procAlive) {
21499
+ return { state, effects: [] };
21500
+ }
21501
+ const next = cloneState(state);
21502
+ next.readyForSend = true;
21503
+ const flushEffects = [];
21504
+ if (state.pendingSend) {
21505
+ const payload = deps.encodeStdin(state.pendingSend.text, {
21506
+ sessionId: next.file.sessionId
21507
+ });
21508
+ flushEffects.push({ kind: "write-stdin", payload });
21509
+ next.pendingSend = void 0;
21510
+ }
21511
+ return { state: next, effects: flushEffects };
21512
+ }
21513
+ case "popup-detected": {
21514
+ if (!state.procAlive) {
21515
+ return { state, effects: [] };
21516
+ }
21517
+ const next = cloneState(state);
21518
+ next.readyForSend = false;
21519
+ return { state: next, effects: [] };
21520
+ }
21458
21521
  case "inject-owner-text": {
21459
21522
  const ownerEvent = {
21460
21523
  kind: "meta-text",
@@ -21642,7 +21705,8 @@ var SessionRunner = class {
21642
21705
  now: this.hooks.now ?? Date.now,
21643
21706
  resolveContextWindow: this.hooks.resolveContextWindow,
21644
21707
  genUuid: this.hooks.genUuid ?? v4_default,
21645
- ownerDisplayName: this.hooks.ownerDisplayName
21708
+ ownerDisplayName: this.hooks.ownerDisplayName,
21709
+ mode: this.hooks.mode ?? "sdk"
21646
21710
  };
21647
21711
  const { state, effects } = reduceSession(this.state, inputMsg, deps);
21648
21712
  this.state = state;
@@ -22016,7 +22080,14 @@ function makeInitialState(file, subSessionMeta) {
22016
22080
  freshSpawn: false,
22017
22081
  procAlive: false,
22018
22082
  seenUuids: [],
22019
- subSessionMeta
22083
+ subSessionMeta,
22084
+ // Ready gate:保守起手 true,让 SDK 模式与 TUI 接线未完成时 send 仍走旧的"立即写 stdin"
22085
+ // 路径不破坏 SDK 用户体验。TUI 模式 manager 集成(Task 6)会在 spawn 时显式 dispatch
22086
+ // 'popup-detected' 把 flag 翻 false → 等 ReadyGate emit ready 再 dispatch 'ready-detected'
22087
+ // 翻回 true 并 flush pendingSend。
22088
+ // daemon-only 字段,不上 wire / 不写 SessionFile(compressFrameForWire 只处理
22089
+ // session:status,SessionState 不直接序列化)。
22090
+ readyForSend: true
22020
22091
  };
22021
22092
  }
22022
22093
  var SessionManager = class {
@@ -22192,6 +22263,9 @@ var SessionManager = class {
22192
22263
  dataDir: this.deps.dataDir,
22193
22264
  personaStore: this.deps.personaStore,
22194
22265
  ownerDisplayName: this.deps.ownerDisplayName,
22266
+ // 透传给 ReducerDeps.mode:reducer send !procAlive spawn 路径据此判断要不要把
22267
+ // readyForSend 翻 false(TUI 翻 false 让 user_text 走 pendingSend 暂存;SDK 不动保持直写)
22268
+ mode: this.deps.mode,
22195
22269
  // file-sharing (spec §6 PR 3):闭包 scope + sessionId,runner 只暴露 tool/relPath/cwd
22196
22270
  onFileEdit: attachmentGroup ? (input) => attachmentGroup.onFileEdit({
22197
22271
  scope,
@@ -23192,6 +23266,9 @@ var SessionManager = class {
23192
23266
  },
23193
23267
  "broadcast"
23194
23268
  );
23269
+ const runner = this.runners.get(sid);
23270
+ if (!runner) return;
23271
+ runner.input({ kind: visible ? "popup-detected" : "ready-detected" });
23195
23272
  }
23196
23273
  /** toolSessionId → sessionId 反查(遍历 runners);session 数典型 < 10,O(n) 可接受 */
23197
23274
  sessionIdByToolSid(toolSessionId) {
@@ -24510,10 +24587,22 @@ function spawnPty(opts) {
24510
24587
 
24511
24588
  // src/tools/claude-tui.ts
24512
24589
  var { Terminal } = import_headless.default;
24513
- var POPUP_FOOTER_PATTERNS = [
24514
- { kind: "permission", regex: /Tab to amend|Do you want to proceed/i },
24515
- { kind: "question", regex: /Enter to select|↑\/↓ to navigate/i }
24590
+ function hasInputFrame(bufferText) {
24591
+ const lines = bufferText.split("\n");
24592
+ for (let i = 1; i < lines.length; i++) {
24593
+ const cur = lines[i];
24594
+ if (!/^\s*❯\s/.test(cur)) continue;
24595
+ const prev = lines[i - 1];
24596
+ if (/─{20,}/.test(prev)) return true;
24597
+ }
24598
+ return false;
24599
+ }
24600
+ var READY_SIGNATURE_PATTERNS = [
24601
+ { name: "input-frame", match: hasInputFrame }
24516
24602
  ];
24603
+ function isReadySignature(bufferText) {
24604
+ return READY_SIGNATURE_PATTERNS.some((p2) => p2.match(bufferText));
24605
+ }
24517
24606
  function createPopupDetector(opts) {
24518
24607
  const term = new Terminal({
24519
24608
  cols: opts.cols ?? 120,
@@ -24525,39 +24614,46 @@ function createPopupDetector(opts) {
24525
24614
  term.loadAddon(serializeAddon);
24526
24615
  let visible = null;
24527
24616
  let pendingClear = null;
24528
- const scanFooter = () => {
24617
+ let quietTimer = null;
24618
+ const QUIET_MS = opts.quietMs ?? 300;
24619
+ const scanReady = () => {
24529
24620
  const buf = term.buffer.active;
24621
+ const lines = [];
24530
24622
  for (let i = 0; i < buf.length; i++) {
24531
24623
  const line = buf.getLine(i);
24532
24624
  if (!line) continue;
24533
- const text = line.translateToString(true);
24534
- if (!text) continue;
24535
- for (const p2 of POPUP_FOOTER_PATTERNS) {
24536
- if (p2.regex.test(text)) return p2.kind;
24537
- }
24625
+ lines.push(line.translateToString(true));
24538
24626
  }
24539
- return null;
24627
+ return isReadySignature(lines.join("\n"));
24540
24628
  };
24541
24629
  const tick = () => {
24542
- const matched = scanFooter();
24543
- if (matched && visible === null) {
24544
- visible = matched;
24545
- opts.onTransition(matched, true);
24546
- } else if (matched && visible !== null) {
24547
- if (pendingClear) {
24548
- clearTimeout(pendingClear);
24549
- pendingClear = null;
24550
- }
24551
- } else if (!matched && visible !== null && !pendingClear) {
24552
- const toClear = visible;
24553
- pendingClear = setTimeout(() => {
24554
- pendingClear = null;
24555
- if (visible === toClear) {
24556
- visible = null;
24557
- opts.onTransition(toClear, false);
24558
- }
24559
- }, opts.clearQuietMs);
24560
- }
24630
+ if (quietTimer) clearTimeout(quietTimer);
24631
+ quietTimer = setTimeout(() => {
24632
+ quietTimer = null;
24633
+ const ready = scanReady();
24634
+ if (!ready && visible === null) {
24635
+ visible = "unknown";
24636
+ opts.onTransition("unknown", true);
24637
+ } else if (!ready && visible !== null) {
24638
+ if (pendingClear) {
24639
+ clearTimeout(pendingClear);
24640
+ pendingClear = null;
24641
+ }
24642
+ } else if (ready && visible !== null) {
24643
+ if (pendingClear) {
24644
+ clearTimeout(pendingClear);
24645
+ pendingClear = null;
24646
+ }
24647
+ const toClear = visible;
24648
+ pendingClear = setTimeout(() => {
24649
+ pendingClear = null;
24650
+ if (visible === toClear) {
24651
+ visible = null;
24652
+ opts.onTransition(toClear, false);
24653
+ }
24654
+ }, opts.clearQuietMs);
24655
+ }
24656
+ }, QUIET_MS);
24561
24657
  };
24562
24658
  return {
24563
24659
  get visibleKind() {
@@ -24572,6 +24668,10 @@ function createPopupDetector(opts) {
24572
24668
  });
24573
24669
  },
24574
24670
  dispose() {
24671
+ if (quietTimer) {
24672
+ clearTimeout(quietTimer);
24673
+ quietTimer = null;
24674
+ }
24575
24675
  if (pendingClear) {
24576
24676
  clearTimeout(pendingClear);
24577
24677
  pendingClear = null;
@@ -24584,64 +24684,6 @@ function createPopupDetector(opts) {
24584
24684
  }
24585
24685
  };
24586
24686
  }
24587
- function createBootGate(pty, ptyChild, logger, toolSessionId) {
24588
- const FALLBACK_MS = 4e3;
24589
- const POST_POPUP_QUIET_MS = 800;
24590
- const stripAnsi = (s) => s.replace(/\x1b\[[0-9;?]*C/g, " ").replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, "").replace(/\x1b\][^\x07]*\x07/g, "").replace(/\x1b[NOPM=>78]/g, "").replace(/[\x00-\x1f\x7f]/g, " ");
24591
- const popups = {
24592
- trust: { handled: false, regex: /trust this folder/i, accept: "\r", label: "trust-folder" },
24593
- bypass: {
24594
- handled: false,
24595
- regex: /Bypass Permissions mode|By proceeding/i,
24596
- accept: "2\r",
24597
- label: "bypass-warning"
24598
- }
24599
- };
24600
- let scanBuf = "";
24601
- let opened = false;
24602
- let postPopupQuiet = null;
24603
- const openGate = (reason) => {
24604
- if (opened) return;
24605
- opened = true;
24606
- logger?.debug("boot gate setReady", { toolSessionId, reason });
24607
- clearTimeout(fallbackTimer);
24608
- if (postPopupQuiet) clearTimeout(postPopupQuiet);
24609
- scanBuf = "";
24610
- ptyChild.setReady();
24611
- };
24612
- const fallbackTimer = setTimeout(() => openGate("timeout-4s"), FALLBACK_MS);
24613
- return {
24614
- get opened() {
24615
- return opened;
24616
- },
24617
- feedBytes(data) {
24618
- scanBuf += data.toString("utf8");
24619
- const cleanBuf = stripAnsi(scanBuf).replace(/\s+/g, " ");
24620
- for (const popup of Object.values(popups)) {
24621
- if (popup.handled) continue;
24622
- if (popup.regex.test(cleanBuf)) {
24623
- popup.handled = true;
24624
- logger?.debug(`${popup.label} popup auto-yes`, { toolSessionId });
24625
- try {
24626
- pty.write(popup.accept);
24627
- } catch (err) {
24628
- logger?.warn(`${popup.label} auto-yes write failed`, {
24629
- error: err.message
24630
- });
24631
- }
24632
- scanBuf = "";
24633
- if (postPopupQuiet) clearTimeout(postPopupQuiet);
24634
- postPopupQuiet = setTimeout(() => openGate("post-popup-quiet"), POST_POPUP_QUIET_MS);
24635
- }
24636
- }
24637
- if (scanBuf.length > 8192) scanBuf = scanBuf.slice(-4096);
24638
- },
24639
- dispose() {
24640
- clearTimeout(fallbackTimer);
24641
- if (postPopupQuiet) clearTimeout(postPopupQuiet);
24642
- }
24643
- };
24644
- }
24645
24687
  var ClaudeTuiAdapter = class extends ClaudeAdapter {
24646
24688
  // id 仍是 'claude'(继承)— UI / SessionFile.tool / capabilities:get RPC 字面量不变,
24647
24689
  // 完全兼容 SDK 模式 wiring。TUI 行为通过 daemon mode 流转,不通过 tool id 区分
@@ -24676,14 +24718,14 @@ var ClaudeTuiAdapter = class extends ClaudeAdapter {
24676
24718
  const ptyChild = new PtyChildProcess(pty, {
24677
24719
  logger: this.tuiLogger,
24678
24720
  tag: ctx.toolSessionId ?? "no-tsid",
24679
- // boot gate:CC TUI 启动 + popup 处理前 stdin.write 一律 buffer,防止用户 prompt 在
24680
- // CC 加载 node runtime 时被吞。bootGate scanner popup 处理完成 / 4s 兜底任一条件
24681
- // 触发后调 setReady() 开闸
24682
- bootGate: true
24721
+ // MVP 阶段:boot gate 自动应答 + 4s 硬超时已废弃(由 ReadyGate 反向判定接管),
24722
+ // stdin buffer 关闸 —— 用户 query 直发 CC。
24723
+ // boot gate 废弃;Task 5/6 在 reducer 层接 ReadyGate ready 信号做 pending-send 调度
24724
+ bootGate: false
24683
24725
  });
24684
- const bootGate = createBootGate(pty, ptyChild, this.tuiLogger, ctx.toolSessionId);
24685
24726
  const detector = createPopupDetector({
24686
24727
  clearQuietMs: 200,
24728
+ // quietMs 走默认 300ms — 屏幕静止判定时长,对 cc TUI 启动期重绘节奏足够
24687
24729
  onTransition: (kind, visible) => {
24688
24730
  if (!ctx.toolSessionId || !this.tuiOpts.onPopupTransition) return;
24689
24731
  this.tuiLogger?.debug("popup transition", {
@@ -24704,14 +24746,9 @@ var ClaudeTuiAdapter = class extends ClaudeAdapter {
24704
24746
  if (ctx.toolSessionId && this.tuiOpts.onPtyData) {
24705
24747
  this.tuiOpts.onPtyData(ctx.toolSessionId, data.toString("base64"));
24706
24748
  }
24707
- if (bootGate.opened) {
24708
- void detector.writeBytes(data.toString("utf8"));
24709
- } else {
24710
- bootGate.feedBytes(data);
24711
- }
24749
+ void detector.writeBytes(data.toString("utf8"));
24712
24750
  });
24713
24751
  ptyChild.on("exit", () => {
24714
- bootGate.dispose();
24715
24752
  if (ctx.toolSessionId && this.tuiOpts.onPtyUnregister) {
24716
24753
  this.tuiOpts.onPtyUnregister(ctx.toolSessionId);
24717
24754
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawos-dev/clawd",
3
- "version": "0.2.71",
3
+ "version": "0.2.72-beta.140.1753cf8",
4
4
  "description": "Standalone clawd daemon — Claude Code (and future Codex) session server over WebSocket",
5
5
  "type": "module",
6
6
  "license": "MIT",