deskbuddy 0.2.0-beta.10 → 0.2.0-beta.11

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.js CHANGED
@@ -6323,7 +6323,7 @@ var Daemon = class {
6323
6323
  if (cmd.sessionId) {
6324
6324
  const sid = resolveAlias(cmd.sessionId);
6325
6325
  const st = readState(sid);
6326
- if (st && st.cwd && st.interactive === true && st.remoteWindow !== true && !!st.pid && isAlive(st.pid, st.pidName)) {
6326
+ if (st && st.cwd && (st.interactive === true || st.embedded === true) && st.remoteWindow !== true && !!st.pid && isAlive(st.pid, st.pidName)) {
6327
6327
  void this.#track(this.#idleReply(cmd, sid, st.cwd));
6328
6328
  return;
6329
6329
  }
@@ -6392,7 +6392,7 @@ var Daemon = class {
6392
6392
  try {
6393
6393
  await this.#execute(cmd, sid, runCwd, cmd.tool, true, (r) => {
6394
6394
  const now = readState(sid);
6395
- if (!now || now.interactive !== true || !now.pid || !isAlive(now.pid, now.pidName)) return;
6395
+ if (!now || now.interactive !== true && now.embedded !== true || !now.pid || !isAlive(now.pid, now.pidName)) return;
6396
6396
  writeAppended(sid, { text: cmd.text, result: resultSummary(cmd.tool, r.stdout), ok: r.ok, at: this.#deps.now() });
6397
6397
  });
6398
6398
  } finally {
package/dist/hook.cjs CHANGED
@@ -1027,6 +1027,22 @@ function isInteractiveArgs(args) {
1027
1027
  )
1028
1028
  }
1029
1029
 
1030
+ /**
1031
+ * 嵌入宿主(插件 / 桌面版 / SDK 长连):`--input-format stream-json`(含 `=` 写法)且不带 -p / --print。
1032
+ * 这类会话 isInteractiveArgs 判 false(不是终端窗口),但用户就在宿主里一轮轮发话:没有唤醒等待者(wake.cjs 铁律③),
1033
+ * daemon 收到设备回复要按「用户自己的窗口」处理(正在跑就排队等 Stop、空闲才后台续跑并写 appended),不能直接并发 --resume。
1034
+ * 🔴 只落本机状态,不改 interactive(服务端按 interactive 挡 8348),也不上报
1035
+ */
1036
+ function isEmbeddedArgs(args) {
1037
+ const tokens = String(args || '').trim().split(/\s+/).slice(1)
1038
+ let streamIn = false
1039
+ for (let i = 0; i < tokens.length; i++) {
1040
+ const t = tokens[i]
1041
+ if (t === '--input-format=stream-json' || (t === '--input-format' && tokens[i + 1] === 'stream-json')) streamIn = true
1042
+ }
1043
+ return streamIn && !tokens.some((t) => t === '--print' || /^-[a-zA-Z]*p[a-zA-Z]*$/.test(t))
1044
+ }
1045
+
1030
1046
  /** 只看进程在不在(kill 0),不查进程名:每次事件都要调,ps 太贵 */
1031
1047
  function pidExists(pid) {
1032
1048
  try {
@@ -1037,23 +1053,34 @@ function pidExists(pid) {
1037
1053
  }
1038
1054
  }
1039
1055
 
1040
- function detectInteractive(pid) {
1041
- if (!pid) return false
1056
+ /** 智能体进程的命令行(Windows 见 winproc 的规范化参数串)。取不到返回 '' */
1057
+ function agentArgs(pid) {
1058
+ if (!pid) return ''
1042
1059
  if (IS_WIN) {
1043
1060
  const p = winProc(pid)
1044
- return !!p && p.args !== '' && isInteractiveArgs(p.args)
1061
+ return p ? p.args : ''
1045
1062
  }
1046
1063
  try {
1047
- const args = execFileSync('ps', ['-ww', '-o', 'args=', '-p', String(pid)], {
1064
+ return execFileSync('ps', ['-ww', '-o', 'args=', '-p', String(pid)], {
1048
1065
  encoding: 'utf8',
1049
1066
  stdio: ['ignore', 'pipe', 'ignore'],
1050
- })
1051
- return args.trim() !== '' && isInteractiveArgs(args)
1067
+ }).trim()
1052
1068
  } catch (e) {
1053
- return false
1069
+ return ''
1054
1070
  }
1055
1071
  }
1056
1072
 
1073
+ /** 交互窗口 / 嵌入宿主两个判定共用一次命令行查询。🔴 取不到命令行两个都按 false */
1074
+ function detectHost(pid) {
1075
+ const args = agentArgs(pid)
1076
+ if (!args) return { interactive: false, embedded: false }
1077
+ return { interactive: isInteractiveArgs(args), embedded: isEmbeddedArgs(args) }
1078
+ }
1079
+
1080
+ function detectInteractive(pid) {
1081
+ return detectHost(pid).interactive
1082
+ }
1083
+
1057
1084
  // ---------- /clear 改绑(二期 P2) ----------
1058
1085
 
1059
1086
  /** 与 src/core/state.ts 的 ALIAS_TTL_MS 一致 */
@@ -1465,6 +1492,13 @@ async function main(argv) {
1465
1492
  // 不能拿死掉的 -p 进程的 interactive=false 挡不住窗口
1466
1493
  const prevAlive = !!(prev && prev.pid && pidExists(prev.pid))
1467
1494
  const pid = (prevAlive && prev.pid) || resolveAgentPid(process.pid, agentNames(registry, tool)) || undefined
1495
+ // 两个判定沿用规则相同:进程还活着且上次判过就沿用,否则查一次命令行(只查一次)
1496
+ let host = null
1497
+ const hostFlag = (k) => {
1498
+ if (prevAlive && typeof prev[k] === 'boolean') return prev[k]
1499
+ if (!host) host = detectHost(pid)
1500
+ return host[k]
1501
+ }
1468
1502
  const next = {
1469
1503
  sessionId: sessionId,
1470
1504
  tool: tool,
@@ -1485,7 +1519,9 @@ async function main(argv) {
1485
1519
  pid: pid,
1486
1520
  pidName: pid ? (prevAlive && prev.pidName) || processName(pid) || undefined : undefined,
1487
1521
  // 二期 P1:窗口开着的会话,设备不能再往里下指令(两个进程会互相吞掉对话)。每个会话只判一次
1488
- interactive: windowId ? true : prevAlive && typeof prev.interactive === 'boolean' ? prev.interactive : detectInteractive(pid),
1522
+ interactive: windowId ? true : hostFlag('interactive'),
1523
+ // 嵌入宿主(插件 / 桌面版,见 isEmbeddedArgs):daemon 据此按用户窗口处理设备回复。只落本机、不上报
1524
+ embedded: hostFlag('embedded'),
1489
1525
  // 会话开在 DeskBuddy 窗口里(spec 2026-09-30 §3.2):服务端据此放行 8348,指令由包装层写进窗口
1490
1526
  // Codex(切片 B):会话加载在 daemon 连着的 app-server 里 = 窗口开着且能往里发
1491
1527
  remoteWindow: windowId || (tool === 'codex' && codexRemoteHas(sessionId)) ? true : undefined,
@@ -1602,6 +1638,8 @@ module.exports = {
1602
1638
  APPENDED_SYNC_TOOLS,
1603
1639
  RUNNING_MESSAGE,
1604
1640
  isInteractiveArgs,
1641
+ isEmbeddedArgs,
1642
+ detectHost,
1605
1643
  recordDir,
1606
1644
  markClear,
1607
1645
  pairClear,
package/dist/wake.cjs CHANGED
@@ -11,8 +11,10 @@
11
11
  // ② 除了「认证过的 daemon 发来、且逐条 HMAC 校验通过的指令」这一种情况,一律 exit 0、绝不写 stderr:
12
12
  // exit 2 会唤醒窗口,stderr 的内容会被模型当成用户输入执行。通道(Windows 命名管道 / tmp 下的 socket)
13
13
  // 别的进程也能连、能抢注,所以先双向认证(密钥 ~/.deskbuddy/wake/secret,daemon 每次启动重新生成)。
14
- // ③ 非交互的 `claude -p` 里 asyncRewake 会被当成**同步** hook 执行(源码 `!isInteractive || hasStreamingInput`),
15
- // 等在那儿就把人家的 -p 卡住。只对交互 CLI / VS Code 插件 / 桌面版放行,认不出宿主一律立刻退出。
14
+ // ③ 非交互的 `claude -p` 和 `--input-format stream-json` 宿主(IDE 插件 / 桌面版)里 asyncRewake 会被当成**同步** hook
15
+ // 执行(源码 `!isInteractive || hasStreamingInput`),等在那儿就把人家卡住:插件里 SessionStart 挂着不退,
16
+ // 60 秒后报「Subprocess initialization did not complete」会话被杀(2026-10-02 Trae CN 内置 Claude Code 插件 2.1.281 必现)。
17
+ // 只对交互 CLI 窗口放行,认不出宿主一律立刻退出。插件 / 桌面版的会话靠 daemon 后台续跑接设备回复。
16
18
  // ④ 窗口关了(智能体进程没了)就退出:Claude Code 被强杀时不会替我们收尸。
17
19
  //
18
20
  // ⚠️ 通道地址 / 密钥 / HMAC / 标记 / transcript 分析与 src/core/wake.ts、src/daemon/wake.ts 同构,
@@ -193,8 +195,9 @@ function entrypointAllows(ep) {
193
195
  }
194
196
 
195
197
  /**
196
- * 宿主命令行判定(环境变量里没有入口时才用):交互窗口可以;`--input-format stream-json` 是插件/桌面版那种异步宿主;
197
- * 带 -p / --print 的是一次性调用,asyncRewake 在那里是同步的,绝不能等。🔴 拿不到命令行 → false
198
+ * 宿主命令行判定(环境变量里没有入口时才用):只有交互窗口可以。
199
+ * 带 -p / --print 的一次性调用、`--input-format stream-json` 的插件 / 桌面版宿主里 asyncRewake 都是同步的,绝不能等(见铁律③)。
200
+ * 🔴 拿不到命令行 → false
198
201
  */
199
202
  function canWake(args) {
200
203
  const all = String(args || '').trim()
@@ -202,7 +205,7 @@ function canWake(args) {
202
205
  const tokens = all.split(/\s+/).slice(1)
203
206
  for (let i = 0; i < tokens.length; i++) {
204
207
  const t = tokens[i]
205
- if (t === '--input-format=stream-json' || (t === '--input-format' && tokens[i + 1] === 'stream-json')) return true
208
+ if (t === '--input-format=stream-json' || (t === '--input-format' && tokens[i + 1] === 'stream-json')) return false
206
209
  }
207
210
  return !tokens.some((t) => t === '--print' || /^-[a-zA-Z]*p[a-zA-Z]*$/.test(t))
208
211
  }
package/hook/hook.cjs CHANGED
@@ -1027,6 +1027,22 @@ function isInteractiveArgs(args) {
1027
1027
  )
1028
1028
  }
1029
1029
 
1030
+ /**
1031
+ * 嵌入宿主(插件 / 桌面版 / SDK 长连):`--input-format stream-json`(含 `=` 写法)且不带 -p / --print。
1032
+ * 这类会话 isInteractiveArgs 判 false(不是终端窗口),但用户就在宿主里一轮轮发话:没有唤醒等待者(wake.cjs 铁律③),
1033
+ * daemon 收到设备回复要按「用户自己的窗口」处理(正在跑就排队等 Stop、空闲才后台续跑并写 appended),不能直接并发 --resume。
1034
+ * 🔴 只落本机状态,不改 interactive(服务端按 interactive 挡 8348),也不上报
1035
+ */
1036
+ function isEmbeddedArgs(args) {
1037
+ const tokens = String(args || '').trim().split(/\s+/).slice(1)
1038
+ let streamIn = false
1039
+ for (let i = 0; i < tokens.length; i++) {
1040
+ const t = tokens[i]
1041
+ if (t === '--input-format=stream-json' || (t === '--input-format' && tokens[i + 1] === 'stream-json')) streamIn = true
1042
+ }
1043
+ return streamIn && !tokens.some((t) => t === '--print' || /^-[a-zA-Z]*p[a-zA-Z]*$/.test(t))
1044
+ }
1045
+
1030
1046
  /** 只看进程在不在(kill 0),不查进程名:每次事件都要调,ps 太贵 */
1031
1047
  function pidExists(pid) {
1032
1048
  try {
@@ -1037,23 +1053,34 @@ function pidExists(pid) {
1037
1053
  }
1038
1054
  }
1039
1055
 
1040
- function detectInteractive(pid) {
1041
- if (!pid) return false
1056
+ /** 智能体进程的命令行(Windows 见 winproc 的规范化参数串)。取不到返回 '' */
1057
+ function agentArgs(pid) {
1058
+ if (!pid) return ''
1042
1059
  if (IS_WIN) {
1043
1060
  const p = winProc(pid)
1044
- return !!p && p.args !== '' && isInteractiveArgs(p.args)
1061
+ return p ? p.args : ''
1045
1062
  }
1046
1063
  try {
1047
- const args = execFileSync('ps', ['-ww', '-o', 'args=', '-p', String(pid)], {
1064
+ return execFileSync('ps', ['-ww', '-o', 'args=', '-p', String(pid)], {
1048
1065
  encoding: 'utf8',
1049
1066
  stdio: ['ignore', 'pipe', 'ignore'],
1050
- })
1051
- return args.trim() !== '' && isInteractiveArgs(args)
1067
+ }).trim()
1052
1068
  } catch (e) {
1053
- return false
1069
+ return ''
1054
1070
  }
1055
1071
  }
1056
1072
 
1073
+ /** 交互窗口 / 嵌入宿主两个判定共用一次命令行查询。🔴 取不到命令行两个都按 false */
1074
+ function detectHost(pid) {
1075
+ const args = agentArgs(pid)
1076
+ if (!args) return { interactive: false, embedded: false }
1077
+ return { interactive: isInteractiveArgs(args), embedded: isEmbeddedArgs(args) }
1078
+ }
1079
+
1080
+ function detectInteractive(pid) {
1081
+ return detectHost(pid).interactive
1082
+ }
1083
+
1057
1084
  // ---------- /clear 改绑(二期 P2) ----------
1058
1085
 
1059
1086
  /** 与 src/core/state.ts 的 ALIAS_TTL_MS 一致 */
@@ -1465,6 +1492,13 @@ async function main(argv) {
1465
1492
  // 不能拿死掉的 -p 进程的 interactive=false 挡不住窗口
1466
1493
  const prevAlive = !!(prev && prev.pid && pidExists(prev.pid))
1467
1494
  const pid = (prevAlive && prev.pid) || resolveAgentPid(process.pid, agentNames(registry, tool)) || undefined
1495
+ // 两个判定沿用规则相同:进程还活着且上次判过就沿用,否则查一次命令行(只查一次)
1496
+ let host = null
1497
+ const hostFlag = (k) => {
1498
+ if (prevAlive && typeof prev[k] === 'boolean') return prev[k]
1499
+ if (!host) host = detectHost(pid)
1500
+ return host[k]
1501
+ }
1468
1502
  const next = {
1469
1503
  sessionId: sessionId,
1470
1504
  tool: tool,
@@ -1485,7 +1519,9 @@ async function main(argv) {
1485
1519
  pid: pid,
1486
1520
  pidName: pid ? (prevAlive && prev.pidName) || processName(pid) || undefined : undefined,
1487
1521
  // 二期 P1:窗口开着的会话,设备不能再往里下指令(两个进程会互相吞掉对话)。每个会话只判一次
1488
- interactive: windowId ? true : prevAlive && typeof prev.interactive === 'boolean' ? prev.interactive : detectInteractive(pid),
1522
+ interactive: windowId ? true : hostFlag('interactive'),
1523
+ // 嵌入宿主(插件 / 桌面版,见 isEmbeddedArgs):daemon 据此按用户窗口处理设备回复。只落本机、不上报
1524
+ embedded: hostFlag('embedded'),
1489
1525
  // 会话开在 DeskBuddy 窗口里(spec 2026-09-30 §3.2):服务端据此放行 8348,指令由包装层写进窗口
1490
1526
  // Codex(切片 B):会话加载在 daemon 连着的 app-server 里 = 窗口开着且能往里发
1491
1527
  remoteWindow: windowId || (tool === 'codex' && codexRemoteHas(sessionId)) ? true : undefined,
@@ -1602,6 +1638,8 @@ module.exports = {
1602
1638
  APPENDED_SYNC_TOOLS,
1603
1639
  RUNNING_MESSAGE,
1604
1640
  isInteractiveArgs,
1641
+ isEmbeddedArgs,
1642
+ detectHost,
1605
1643
  recordDir,
1606
1644
  markClear,
1607
1645
  pairClear,
package/hook/wake.cjs CHANGED
@@ -11,8 +11,10 @@
11
11
  // ② 除了「认证过的 daemon 发来、且逐条 HMAC 校验通过的指令」这一种情况,一律 exit 0、绝不写 stderr:
12
12
  // exit 2 会唤醒窗口,stderr 的内容会被模型当成用户输入执行。通道(Windows 命名管道 / tmp 下的 socket)
13
13
  // 别的进程也能连、能抢注,所以先双向认证(密钥 ~/.deskbuddy/wake/secret,daemon 每次启动重新生成)。
14
- // ③ 非交互的 `claude -p` 里 asyncRewake 会被当成**同步** hook 执行(源码 `!isInteractive || hasStreamingInput`),
15
- // 等在那儿就把人家的 -p 卡住。只对交互 CLI / VS Code 插件 / 桌面版放行,认不出宿主一律立刻退出。
14
+ // ③ 非交互的 `claude -p` 和 `--input-format stream-json` 宿主(IDE 插件 / 桌面版)里 asyncRewake 会被当成**同步** hook
15
+ // 执行(源码 `!isInteractive || hasStreamingInput`),等在那儿就把人家卡住:插件里 SessionStart 挂着不退,
16
+ // 60 秒后报「Subprocess initialization did not complete」会话被杀(2026-10-02 Trae CN 内置 Claude Code 插件 2.1.281 必现)。
17
+ // 只对交互 CLI 窗口放行,认不出宿主一律立刻退出。插件 / 桌面版的会话靠 daemon 后台续跑接设备回复。
16
18
  // ④ 窗口关了(智能体进程没了)就退出:Claude Code 被强杀时不会替我们收尸。
17
19
  //
18
20
  // ⚠️ 通道地址 / 密钥 / HMAC / 标记 / transcript 分析与 src/core/wake.ts、src/daemon/wake.ts 同构,
@@ -193,8 +195,9 @@ function entrypointAllows(ep) {
193
195
  }
194
196
 
195
197
  /**
196
- * 宿主命令行判定(环境变量里没有入口时才用):交互窗口可以;`--input-format stream-json` 是插件/桌面版那种异步宿主;
197
- * 带 -p / --print 的是一次性调用,asyncRewake 在那里是同步的,绝不能等。🔴 拿不到命令行 → false
198
+ * 宿主命令行判定(环境变量里没有入口时才用):只有交互窗口可以。
199
+ * 带 -p / --print 的一次性调用、`--input-format stream-json` 的插件 / 桌面版宿主里 asyncRewake 都是同步的,绝不能等(见铁律③)。
200
+ * 🔴 拿不到命令行 → false
198
201
  */
199
202
  function canWake(args) {
200
203
  const all = String(args || '').trim()
@@ -202,7 +205,7 @@ function canWake(args) {
202
205
  const tokens = all.split(/\s+/).slice(1)
203
206
  for (let i = 0; i < tokens.length; i++) {
204
207
  const t = tokens[i]
205
- if (t === '--input-format=stream-json' || (t === '--input-format' && tokens[i + 1] === 'stream-json')) return true
208
+ if (t === '--input-format=stream-json' || (t === '--input-format' && tokens[i + 1] === 'stream-json')) return false
206
209
  }
207
210
  return !tokens.some((t) => t === '--print' || /^-[a-zA-Z]*p[a-zA-Z]*$/.test(t))
208
211
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deskbuddy",
3
- "version": "0.2.0-beta.10",
3
+ "version": "0.2.0-beta.11",
4
4
  "description": "把 Claude Code、Codex 等 AI 编码智能体每个会话窗口的状态,实时同步到桌面硬件屏幕",
5
5
  "keywords": [
6
6
  "ai",