deskbuddy 0.2.0-beta.13 → 0.2.0-beta.14
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 +2241 -359
- package/dist/gate.cjs +21 -13
- package/dist/hook.cjs +287 -9
- package/dist/ocTranslate.mjs +106 -0
- package/dist/opencode.mjs +100 -69
- package/hook/gate.cjs +21 -13
- package/hook/hook-debug.log +5 -0
- package/hook/hook.cjs +287 -9
- package/hook/ocTranslate.d.mts +7 -0
- package/hook/ocTranslate.mjs +106 -0
- package/hook/opencode.d.mts +4 -1
- package/hook/opencode.mjs +100 -69
- package/package.json +3 -2
- package/vendor/node-pty/LICENSE +69 -0
- package/vendor/node-pty/lib/conpty_console_list_agent.js +16 -0
- package/vendor/node-pty/lib/eventEmitter2.js +47 -0
- package/vendor/node-pty/lib/index.js +52 -0
- package/vendor/node-pty/lib/interfaces.js +7 -0
- package/vendor/node-pty/lib/shared/conout.js +11 -0
- package/vendor/node-pty/lib/terminal.js +190 -0
- package/vendor/node-pty/lib/types.js +7 -0
- package/vendor/node-pty/lib/unixTerminal.js +346 -0
- package/vendor/node-pty/lib/utils.js +39 -0
- package/vendor/node-pty/lib/windowsConoutConnection.js +125 -0
- package/vendor/node-pty/lib/windowsPtyAgent.js +320 -0
- package/vendor/node-pty/lib/windowsTerminal.js +199 -0
- package/vendor/node-pty/lib/worker/conoutSocketWorker.js +22 -0
- package/vendor/node-pty/package.json +7 -0
- package/vendor/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
- package/vendor/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
- package/vendor/node-pty/prebuilds/darwin-x64/pty.node +0 -0
- package/vendor/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
- package/vendor/node-pty/prebuilds/win32-arm64/conpty/OpenConsole.exe +0 -0
- package/vendor/node-pty/prebuilds/win32-arm64/conpty/conpty.dll +0 -0
- package/vendor/node-pty/prebuilds/win32-arm64/conpty.node +0 -0
- package/vendor/node-pty/prebuilds/win32-arm64/conpty_console_list.node +0 -0
- package/vendor/node-pty/prebuilds/win32-arm64/pty.node +0 -0
- package/vendor/node-pty/prebuilds/win32-arm64/winpty-agent.exe +0 -0
- package/vendor/node-pty/prebuilds/win32-arm64/winpty.dll +0 -0
- package/vendor/node-pty/prebuilds/win32-x64/conpty/OpenConsole.exe +0 -0
- package/vendor/node-pty/prebuilds/win32-x64/conpty/conpty.dll +0 -0
- package/vendor/node-pty/prebuilds/win32-x64/conpty.node +0 -0
- package/vendor/node-pty/prebuilds/win32-x64/conpty_console_list.node +0 -0
- package/vendor/node-pty/prebuilds/win32-x64/pty.node +0 -0
- package/vendor/node-pty/prebuilds/win32-x64/winpty-agent.exe +0 -0
- package/vendor/node-pty/prebuilds/win32-x64/winpty.dll +0 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// opencode 系事件 → cc-hook 同构输入的翻译(spec 2026-10-03 §3.2「共用翻译」)。
|
|
2
|
+
// opencode 插件(opencode.mjs,跑在 opencode 自带的 bun 里)与 daemon 的 DumateWatch(百度搭子 = 改名的 opencode)共用这一份:
|
|
3
|
+
// 插件 import 同目录的本文件(随 hook.cjs 一起同步到 ~/.deskbuddy/),daemon 经 src/core/ocTranslate.ts 打包进去。
|
|
4
|
+
// 🔴 零依赖纯函数:不能 import 任何东西(插件那边同目录只有我们自己的文件)
|
|
5
|
+
|
|
6
|
+
/** opencode 工具 → cc-hook 同构的 tool_name / tool_input(daemon 的 classify 按 Claude Code 的工具名判断) */
|
|
7
|
+
export function toCcTool(tool, args) {
|
|
8
|
+
const a = args && typeof args === 'object' ? args : {}
|
|
9
|
+
const s = (v) => (typeof v === 'string' ? v : '')
|
|
10
|
+
switch (tool) {
|
|
11
|
+
case 'bash':
|
|
12
|
+
return { tool_name: 'Bash', tool_input: { command: s(a.command) } }
|
|
13
|
+
case 'read':
|
|
14
|
+
return { tool_name: 'Read', tool_input: { file_path: s(a.filePath) } }
|
|
15
|
+
case 'edit':
|
|
16
|
+
return { tool_name: 'Edit', tool_input: { file_path: s(a.filePath) } }
|
|
17
|
+
case 'write':
|
|
18
|
+
return { tool_name: 'Write', tool_input: { file_path: s(a.filePath) } }
|
|
19
|
+
case 'glob':
|
|
20
|
+
return { tool_name: 'Glob', tool_input: { pattern: s(a.pattern), path: s(a.path) } }
|
|
21
|
+
case 'grep':
|
|
22
|
+
return { tool_name: 'Grep', tool_input: { pattern: s(a.pattern), path: s(a.path) } }
|
|
23
|
+
case 'list':
|
|
24
|
+
return { tool_name: 'LS', tool_input: { path: s(a.path) } }
|
|
25
|
+
case 'webfetch':
|
|
26
|
+
return { tool_name: 'WebFetch', tool_input: { url: s(a.url) } }
|
|
27
|
+
case 'task':
|
|
28
|
+
return { tool_name: 'Task', tool_input: {} }
|
|
29
|
+
case 'todowrite':
|
|
30
|
+
case 'todoread':
|
|
31
|
+
return { tool_name: 'TodoWrite', tool_input: {} }
|
|
32
|
+
case 'patch':
|
|
33
|
+
case 'apply_patch':
|
|
34
|
+
return { tool_name: 'apply_patch', tool_input: { patch: s(a.patchText) || s(a.patch) || s(a.input) } }
|
|
35
|
+
default:
|
|
36
|
+
// 不认识的工具(MCP 等)原名原参数交出去:classify 对不认识的一律推设备
|
|
37
|
+
return { tool_name: String(tool || ''), tool_input: a }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** permission.asked → PermissionRequest 的 tool_name / tool_input,给设备卡片生成摘要 */
|
|
42
|
+
export function askToCc(props) {
|
|
43
|
+
const md = (props && props.metadata) || {}
|
|
44
|
+
if (typeof md.command === 'string' && md.command) return { tool_name: 'Bash', tool_input: { command: md.command } }
|
|
45
|
+
const file = md.filepath || md.filePath
|
|
46
|
+
if (typeof file === 'string' && file) return { tool_name: 'Edit', tool_input: { file_path: file } }
|
|
47
|
+
const patterns = Array.isArray(props && props.patterns) ? props.patterns.join(' ') : ''
|
|
48
|
+
return { tool_name: String((props && props.permission) || 'permission'), tool_input: { path: patterns } }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 授权请求里 opencode 自己给的「以后都允许」模式(permission.asked 的 always,如 bash 的 `npm *`、edit 的 `*`)
|
|
53
|
+
* → 一条 session 级 addRules 建议(与 Claude Code 的 permission_suggestions 同形),daemon 据此在设备上给「允许并记住」。
|
|
54
|
+
* 设备选了记住,gate 输出带 updatedPermissions,插件回 always(opencode 在本次运行期间记住这些模式)。没有 always 返回 undefined
|
|
55
|
+
*/
|
|
56
|
+
export function alwaysSuggestions(props) {
|
|
57
|
+
const perm = String((props && props.permission) || '').trim()
|
|
58
|
+
const always = props && Array.isArray(props.always) ? props.always.filter((x) => typeof x === 'string' && x.trim() !== '') : []
|
|
59
|
+
if (!perm || always.length === 0) return undefined
|
|
60
|
+
return [{ type: 'addRules', rules: always.map((x) => ({ toolName: perm, ruleContent: `${perm} ${x.trim()}` })), behavior: 'allow', destination: 'session' }]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 这次提问来自哪个工具:question.asked 只带 tool.callID,工具名要从 tool.execute.before 记下的调用里查。
|
|
65
|
+
* 🔴 只推 question 工具本身的提问:plan_exit 也走 question.asked(「计划完成,是否切到 build」Yes/No),
|
|
66
|
+
* 推到设备 = 盲批计划,违背 ExitPlanMode 不推的原则(设备答选择题 spec §3.7)。认不出来源的一律不推
|
|
67
|
+
*/
|
|
68
|
+
export function isPlainQuestion(props, callTool) {
|
|
69
|
+
if (callTool !== 'question') return false
|
|
70
|
+
const qs = props && Array.isArray(props.questions) ? props.questions : []
|
|
71
|
+
// 兜底:plan_exit 的固定题面 / header
|
|
72
|
+
return !qs.some((q) => q && (q.header === 'Build Agent' || (typeof q.question === 'string' && /^Plan at .* is complete\./.test(q.question))))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* question.asked(opencode 的 question 工具 / plan_exit 等向用户提问)→ AskUserQuestion 形状的 tool_input,
|
|
77
|
+
* 统一走 daemon 的选择题链路(设备答选择题 spec §3.4)。`multiple` → multiSelect;`custom:false` 原样带上(不许自己填)
|
|
78
|
+
*/
|
|
79
|
+
export function questionToCc(props) {
|
|
80
|
+
const qs = props && Array.isArray(props.questions) ? props.questions : null
|
|
81
|
+
if (!qs || qs.length === 0) return null
|
|
82
|
+
return {
|
|
83
|
+
tool_name: 'AskUserQuestion',
|
|
84
|
+
tool_input: {
|
|
85
|
+
questions: qs.map((q) => ({
|
|
86
|
+
question: q && q.question,
|
|
87
|
+
header: (q && q.header) || '',
|
|
88
|
+
multiSelect: !!(q && q.multiple),
|
|
89
|
+
options: q && Array.isArray(q.options) ? q.options.map((o) => ({ label: o && o.label, description: (o && o.description) || '' })) : [],
|
|
90
|
+
...(q && q.custom === false ? { custom: false } : {}),
|
|
91
|
+
})),
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** opencode 起标题前的占位名(1.18.32 isDefaultTitle:`New session - ` / `Child session - ` + ISO 时间),不当标题 */
|
|
97
|
+
const OC_DEFAULT_TITLE_RE = /^(New session - |Child session - )\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
|
|
98
|
+
|
|
99
|
+
/** session.updated 的 info → 记下自动标题。子会话(subagent,带 parentID)、占位名不记 */
|
|
100
|
+
export function rememberTitle(titles, info) {
|
|
101
|
+
if (!info || typeof info.id !== 'string' || !info.id || info.parentID) return
|
|
102
|
+
const t = typeof info.title === 'string' ? info.title.trim() : ''
|
|
103
|
+
if (!t || OC_DEFAULT_TITLE_RE.test(t)) return
|
|
104
|
+
titles.set(info.id, t.slice(0, 100))
|
|
105
|
+
}
|
|
106
|
+
|
package/hook/opencode.d.mts
CHANGED
|
@@ -6,6 +6,9 @@ export function openPermissions(cfg: unknown): void
|
|
|
6
6
|
export const OC_GATED_KEYS: string[]
|
|
7
7
|
export function userText(output: unknown): string
|
|
8
8
|
export function rememberTitle(titles: Map<string, string>, info: unknown): void
|
|
9
|
-
export function make(opts?: { node?: string }): (ctx: unknown) => Promise<Record<string, unknown>>
|
|
9
|
+
export function make(opts?: { node?: string; env?: Record<string, string | undefined>; execPath?: string }): (ctx: unknown) => Promise<Record<string, unknown>>
|
|
10
|
+
export function isDumateHost(env?: Record<string, string | undefined>, execPath?: string): boolean
|
|
10
11
|
export function injectAppended(output: unknown, raw: string): string
|
|
11
12
|
export function alwaysSuggestions(props: unknown): Array<Record<string, unknown>> | undefined
|
|
13
|
+
export function questionToCc(props: unknown): { tool_name: string; tool_input: { questions: Array<Record<string, unknown>> } } | null
|
|
14
|
+
export function isPlainQuestion(props: unknown, callTool: unknown): boolean
|
package/hook/opencode.mjs
CHANGED
|
@@ -21,8 +21,13 @@
|
|
|
21
21
|
// 🔴 窗口模式反过来:任何失败都不表态,授权框照常由人处理。
|
|
22
22
|
|
|
23
23
|
import { spawn } from 'node:child_process'
|
|
24
|
+
import { appendFileSync } from 'node:fs'
|
|
24
25
|
import { dirname, join } from 'node:path'
|
|
25
26
|
import { fileURLToPath } from 'node:url'
|
|
27
|
+
import { alwaysSuggestions, askToCc, isPlainQuestion, questionToCc, rememberTitle, toCcTool } from './ocTranslate.mjs'
|
|
28
|
+
|
|
29
|
+
// 翻译函数搬到了 ocTranslate.mjs(与 DumateWatch 共用),这里转出去保持原来的导出
|
|
30
|
+
export { alwaysSuggestions, askToCc, isPlainQuestion, questionToCc, rememberTitle, toCcTool }
|
|
26
31
|
|
|
27
32
|
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
28
33
|
const HOOK = join(HERE, 'hook.cjs')
|
|
@@ -62,63 +67,6 @@ export function isHeadless(argv = process.argv) {
|
|
|
62
67
|
return (end >= 0 ? args.slice(0, end) : args).includes('run')
|
|
63
68
|
}
|
|
64
69
|
|
|
65
|
-
/** opencode 工具 → cc-hook 同构的 tool_name / tool_input(daemon 的 classify 按 Claude Code 的工具名判断) */
|
|
66
|
-
export function toCcTool(tool, args) {
|
|
67
|
-
const a = args && typeof args === 'object' ? args : {}
|
|
68
|
-
const s = (v) => (typeof v === 'string' ? v : '')
|
|
69
|
-
switch (tool) {
|
|
70
|
-
case 'bash':
|
|
71
|
-
return { tool_name: 'Bash', tool_input: { command: s(a.command) } }
|
|
72
|
-
case 'read':
|
|
73
|
-
return { tool_name: 'Read', tool_input: { file_path: s(a.filePath) } }
|
|
74
|
-
case 'edit':
|
|
75
|
-
return { tool_name: 'Edit', tool_input: { file_path: s(a.filePath) } }
|
|
76
|
-
case 'write':
|
|
77
|
-
return { tool_name: 'Write', tool_input: { file_path: s(a.filePath) } }
|
|
78
|
-
case 'glob':
|
|
79
|
-
return { tool_name: 'Glob', tool_input: { pattern: s(a.pattern), path: s(a.path) } }
|
|
80
|
-
case 'grep':
|
|
81
|
-
return { tool_name: 'Grep', tool_input: { pattern: s(a.pattern), path: s(a.path) } }
|
|
82
|
-
case 'list':
|
|
83
|
-
return { tool_name: 'LS', tool_input: { path: s(a.path) } }
|
|
84
|
-
case 'webfetch':
|
|
85
|
-
return { tool_name: 'WebFetch', tool_input: { url: s(a.url) } }
|
|
86
|
-
case 'task':
|
|
87
|
-
return { tool_name: 'Task', tool_input: {} }
|
|
88
|
-
case 'todowrite':
|
|
89
|
-
case 'todoread':
|
|
90
|
-
return { tool_name: 'TodoWrite', tool_input: {} }
|
|
91
|
-
case 'patch':
|
|
92
|
-
case 'apply_patch':
|
|
93
|
-
return { tool_name: 'apply_patch', tool_input: { patch: s(a.patchText) || s(a.patch) || s(a.input) } }
|
|
94
|
-
default:
|
|
95
|
-
// 不认识的工具(MCP 等)原名原参数交出去:classify 对不认识的一律推设备
|
|
96
|
-
return { tool_name: String(tool || ''), tool_input: a }
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** permission.asked → PermissionRequest 的 tool_name / tool_input,给设备卡片生成摘要 */
|
|
101
|
-
export function askToCc(props) {
|
|
102
|
-
const md = (props && props.metadata) || {}
|
|
103
|
-
if (typeof md.command === 'string' && md.command) return { tool_name: 'Bash', tool_input: { command: md.command } }
|
|
104
|
-
const file = md.filepath || md.filePath
|
|
105
|
-
if (typeof file === 'string' && file) return { tool_name: 'Edit', tool_input: { file_path: file } }
|
|
106
|
-
const patterns = Array.isArray(props && props.patterns) ? props.patterns.join(' ') : ''
|
|
107
|
-
return { tool_name: String((props && props.permission) || 'permission'), tool_input: { path: patterns } }
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* 授权请求里 opencode 自己给的「以后都允许」模式(permission.asked 的 always,如 bash 的 `npm *`、edit 的 `*`)
|
|
112
|
-
* → 一条 session 级 addRules 建议(与 Claude Code 的 permission_suggestions 同形),daemon 据此在设备上给「允许并记住」。
|
|
113
|
-
* 设备选了记住,gate 输出带 updatedPermissions,插件回 always(opencode 在本次运行期间记住这些模式)。没有 always 返回 undefined
|
|
114
|
-
*/
|
|
115
|
-
export function alwaysSuggestions(props) {
|
|
116
|
-
const perm = String((props && props.permission) || '').trim()
|
|
117
|
-
const always = props && Array.isArray(props.always) ? props.always.filter((x) => typeof x === 'string' && x.trim() !== '') : []
|
|
118
|
-
if (!perm || always.length === 0) return undefined
|
|
119
|
-
return [{ type: 'addRules', rules: always.map((x) => ({ toolName: perm, ruleContent: `${perm} ${x.trim()}` })), behavior: 'allow', destination: 'session' }]
|
|
120
|
-
}
|
|
121
|
-
|
|
122
70
|
/**
|
|
123
71
|
* 远程模式:关卡改由 tool.execute.before 把,放开权限。🔴 必须原地改(整对象替换不生效,O8)。
|
|
124
72
|
* - OC_GATED_KEYS 是 daemon 压成 deny 的,放成 allow(用户原来在这些键上写的值已被覆盖,找不回来 —— 每一步都要过设备)
|
|
@@ -184,17 +132,6 @@ export function injectAppended(output, raw) {
|
|
|
184
132
|
return o && typeof o.systemMessage === 'string' ? o.systemMessage : ''
|
|
185
133
|
}
|
|
186
134
|
|
|
187
|
-
/** opencode 起标题前的占位名(1.18.32 isDefaultTitle:`New session - ` / `Child session - ` + ISO 时间),不当标题 */
|
|
188
|
-
const OC_DEFAULT_TITLE_RE = /^(New session - |Child session - )\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
|
|
189
|
-
|
|
190
|
-
/** session.updated 的 info → 记下自动标题。子会话(subagent,带 parentID)、占位名不记 */
|
|
191
|
-
export function rememberTitle(titles, info) {
|
|
192
|
-
if (!info || typeof info.id !== 'string' || !info.id || info.parentID) return
|
|
193
|
-
const t = typeof info.title === 'string' ? info.title.trim() : ''
|
|
194
|
-
if (!t || OC_DEFAULT_TITLE_RE.test(t)) return
|
|
195
|
-
titles.set(info.id, t.slice(0, 100))
|
|
196
|
-
}
|
|
197
|
-
|
|
198
135
|
/** 活着的子进程;opencode 退出时一并结束,不留孤儿 gate 挂着设备卡片(评审) */
|
|
199
136
|
const LIVE = new Set()
|
|
200
137
|
process.on('exit', () => {
|
|
@@ -238,7 +175,18 @@ function startChild(node, script, args, input, killMs = 0) {
|
|
|
238
175
|
|
|
239
176
|
const runChild = (node, script, args, input, killMs) => startChild(node, script, args, input, killMs).done
|
|
240
177
|
|
|
178
|
+
/**
|
|
179
|
+
* 跑在百度搭子(DuMate,内核是改名的 opencode:dumate-opencode)里。那边的会话由 daemon 的 DumateWatch 直连 HTTP 上报和推卡,
|
|
180
|
+
* 插件再报一遍就是 tool=opencode 的第二份会话 + 双卡(spec 2026-10-03 §3.2「防双卡」)
|
|
181
|
+
*/
|
|
182
|
+
export function isDumateHost(env = process.env, execPath = process.execPath) {
|
|
183
|
+
return !!(env && env.DUMATE_INAPP_KEY) || /dumate-opencode/i.test(String(execPath || ''))
|
|
184
|
+
}
|
|
185
|
+
|
|
241
186
|
export function make(opts = {}) {
|
|
187
|
+
if (isDumateHost(opts.env, opts.execPath)) return async function DeskBuddyOff() {
|
|
188
|
+
return {}
|
|
189
|
+
}
|
|
242
190
|
return async function DeskBuddy(ctx) {
|
|
243
191
|
const node = process.env.DESKBUDDY_NODE || opts.node || 'node'
|
|
244
192
|
const client = ctx && ctx.client
|
|
@@ -325,6 +273,82 @@ export function make(opts = {}) {
|
|
|
325
273
|
if (typeof out.systemMessage === 'string' && out.systemMessage) await toast(out.systemMessage)
|
|
326
274
|
}
|
|
327
275
|
|
|
276
|
+
// ---- 选择题(question.asked):推设备作答,答案经 /question/{id}/reply 回填;设备跳过 → reject ----
|
|
277
|
+
const pendingQ = new Map() // requestID → gate 子进程
|
|
278
|
+
const debugLog = (line) => {
|
|
279
|
+
try {
|
|
280
|
+
appendFileSync(join(HERE, 'hook-debug.log'), `${new Date().toISOString()} [opencode] ${line}\n`, 'utf8')
|
|
281
|
+
} catch (_) {
|
|
282
|
+
/* 记不下不影响 */
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/** 返回是否成功(hey-api 客户端不抛错,失败在返回值的 error / response.status 里) */
|
|
286
|
+
const postRaw = async (url, path, body) => {
|
|
287
|
+
// 插件拿到的是 v1 SDK 客户端,没有 question 的方法:用它底下的 HTTP 客户端直接打(路径参数由客户端填)
|
|
288
|
+
const http = client && (client._client || client.client)
|
|
289
|
+
if (!http || typeof http.post !== 'function') {
|
|
290
|
+
debugLog(`回填选择题失败:客户端没有 post(${url})`)
|
|
291
|
+
return false
|
|
292
|
+
}
|
|
293
|
+
try {
|
|
294
|
+
const r = await http.post({ url, path, ...(body ? { body, headers: { 'Content-Type': 'application/json' } } : {}) })
|
|
295
|
+
const status = r && r.response && typeof r.response.status === 'number' ? r.response.status : 0
|
|
296
|
+
if ((r && r.error) || status >= 400) {
|
|
297
|
+
debugLog(`回填选择题失败:${url} status=${status || '?'}`)
|
|
298
|
+
return false
|
|
299
|
+
}
|
|
300
|
+
return true
|
|
301
|
+
} catch (e) {
|
|
302
|
+
debugLog(`回填选择题出错:${url} ${(e && e.message) || e}`)
|
|
303
|
+
return false
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const onQuestion = async (p) => {
|
|
307
|
+
const { id, sessionID } = p || {}
|
|
308
|
+
if (!id || !sessionID || runId || headless || !client) return
|
|
309
|
+
const call = p.tool && p.tool.callID ? calls.get(p.tool.callID) : null
|
|
310
|
+
if (!isPlainQuestion(p, call && call.tool)) return
|
|
311
|
+
const cc = questionToCc(p)
|
|
312
|
+
if (!cc) return
|
|
313
|
+
const input = { hook_event_name: 'PermissionRequest', session_id: sessionID, cwd, ...cc }
|
|
314
|
+
const { child, done } = startChild(node, GATE, ['--window', '--tool', TOOL], input)
|
|
315
|
+
if (child) pendingQ.set(id, child)
|
|
316
|
+
const r = await done
|
|
317
|
+
pendingQ.delete(id)
|
|
318
|
+
if (r.code !== 0 || !r.out) return
|
|
319
|
+
let out
|
|
320
|
+
try {
|
|
321
|
+
out = JSON.parse(r.out)
|
|
322
|
+
} catch (_) {
|
|
323
|
+
return
|
|
324
|
+
}
|
|
325
|
+
const d = out && out.hookSpecificOutput && out.hookSpecificOutput.decision
|
|
326
|
+
if (!d) return
|
|
327
|
+
let ok
|
|
328
|
+
if (d.behavior === 'allow' && Array.isArray(out.deskbuddyAnswers)) {
|
|
329
|
+
ok = await postRaw('/question/{requestID}/reply', { requestID: id }, { answers: out.deskbuddyAnswers })
|
|
330
|
+
} else if (d.behavior === 'deny') {
|
|
331
|
+
ok = await postRaw('/question/{requestID}/reject', { requestID: id })
|
|
332
|
+
} else return
|
|
333
|
+
if (!ok) {
|
|
334
|
+
await toast('设备答案未能回填,请在窗口作答')
|
|
335
|
+
return
|
|
336
|
+
}
|
|
337
|
+
if (typeof out.systemMessage === 'string' && out.systemMessage) await toast(out.systemMessage)
|
|
338
|
+
}
|
|
339
|
+
const onQuestionDone = (p) => {
|
|
340
|
+
// 人先在窗口里答了 / 拒了:结束 gate 子进程,daemon 随之撤掉设备上的卡片
|
|
341
|
+
const child = p && pendingQ.get(p.requestID)
|
|
342
|
+
if (child) {
|
|
343
|
+
pendingQ.delete(p.requestID)
|
|
344
|
+
try {
|
|
345
|
+
child.kill('SIGTERM')
|
|
346
|
+
} catch (_) {
|
|
347
|
+
/* 已经退了 */
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
328
352
|
const onReplied = (p) => {
|
|
329
353
|
// 人先在窗口里答了:结束 gate 子进程,daemon 随之撤掉设备上的卡片
|
|
330
354
|
const child = p && pending.get(p.requestID)
|
|
@@ -362,7 +386,7 @@ export function make(opts = {}) {
|
|
|
362
386
|
|
|
363
387
|
'tool.execute.before': async (input, output) => {
|
|
364
388
|
const cc = toCcTool(input && input.tool, output && output.args)
|
|
365
|
-
if (input && input.callID) calls.set(input.callID, { cc, at: Date.now() })
|
|
389
|
+
if (input && input.callID) calls.set(input.callID, { cc, tool: input && input.tool, at: Date.now() })
|
|
366
390
|
report('PreToolUse', input && input.sessionID, cc)
|
|
367
391
|
if (!runId) return
|
|
368
392
|
// 远程关卡:等设备。任何失败都拒(见顶部)
|
|
@@ -398,6 +422,13 @@ export function make(opts = {}) {
|
|
|
398
422
|
case 'permission.replied':
|
|
399
423
|
onReplied(p)
|
|
400
424
|
break
|
|
425
|
+
case 'question.asked':
|
|
426
|
+
onQuestion(p).catch(() => {})
|
|
427
|
+
break
|
|
428
|
+
case 'question.replied':
|
|
429
|
+
case 'question.rejected':
|
|
430
|
+
onQuestionDone(p)
|
|
431
|
+
break
|
|
401
432
|
case 'session.idle':
|
|
402
433
|
report('Stop', p.sessionID)
|
|
403
434
|
break
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "deskbuddy",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.14",
|
|
4
4
|
"description": "把 Claude Code、Codex 等 AI 编码智能体每个会话窗口的状态,实时同步到桌面硬件屏幕",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -19,11 +19,12 @@
|
|
|
19
19
|
"files": [
|
|
20
20
|
"dist",
|
|
21
21
|
"hook",
|
|
22
|
+
"vendor",
|
|
22
23
|
"docs/INSTALL.md",
|
|
23
24
|
"README.md"
|
|
24
25
|
],
|
|
25
26
|
"scripts": {
|
|
26
|
-
"build": "tsup && cp hook/hook.cjs dist/hook.cjs && cp hook/gate.cjs dist/gate.cjs && cp hook/wake.cjs dist/wake.cjs && cp hook/opencode.mjs dist/opencode.mjs && cp hook/launcher.cjs dist/launcher.cjs",
|
|
27
|
+
"build": "tsup && cp hook/hook.cjs dist/hook.cjs && cp hook/gate.cjs dist/gate.cjs && cp hook/wake.cjs dist/wake.cjs && cp hook/opencode.mjs dist/opencode.mjs && cp hook/ocTranslate.mjs dist/ocTranslate.mjs && cp hook/launcher.cjs dist/launcher.cjs",
|
|
27
28
|
"prepublishOnly": "npm run typecheck && npm test && npm run build",
|
|
28
29
|
"test": "vitest run",
|
|
29
30
|
"typecheck": "tsc --noEmit"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
Copyright (c) 2012-2015, Christopher Jeffrey (https://github.com/chjj/)
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
|
11
|
+
all copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
19
|
+
THE SOFTWARE.
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
The MIT License (MIT)
|
|
24
|
+
|
|
25
|
+
Copyright (c) 2016, Daniel Imms (http://www.growingwiththeweb.com)
|
|
26
|
+
|
|
27
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
28
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
29
|
+
in the Software without restriction, including without limitation the rights
|
|
30
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
31
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
32
|
+
furnished to do so, subject to the following conditions:
|
|
33
|
+
|
|
34
|
+
The above copyright notice and this permission notice shall be included in all
|
|
35
|
+
copies or substantial portions of the Software.
|
|
36
|
+
|
|
37
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
38
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
39
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
40
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
41
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
42
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
43
|
+
SOFTWARE.
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
MIT License
|
|
48
|
+
|
|
49
|
+
Copyright (c) 2018 - present Microsoft Corporation
|
|
50
|
+
|
|
51
|
+
All rights reserved.
|
|
52
|
+
|
|
53
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
54
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
55
|
+
in the Software without restriction, including without limitation the rights
|
|
56
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
57
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
58
|
+
furnished to do so, subject to the following conditions:
|
|
59
|
+
|
|
60
|
+
The above copyright notice and this permission notice shall be included in all
|
|
61
|
+
copies or substantial portions of the Software.
|
|
62
|
+
|
|
63
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
64
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
65
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
66
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
67
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
68
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
69
|
+
SOFTWARE.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2019, Microsoft Corporation (MIT License).
|
|
4
|
+
*
|
|
5
|
+
* This module fetches the console process list for a particular PID. It must be
|
|
6
|
+
* called from a different process (child_process.fork) as there can only be a
|
|
7
|
+
* single console attached to a process.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
var utils_1 = require("./utils");
|
|
11
|
+
var getConsoleProcessList = utils_1.loadNativeModule('conpty_console_list').module.getConsoleProcessList;
|
|
12
|
+
var shellPid = parseInt(process.argv[2], 10);
|
|
13
|
+
var consoleProcessList = getConsoleProcessList(shellPid);
|
|
14
|
+
process.send({ consoleProcessList: consoleProcessList });
|
|
15
|
+
process.exit(0);
|
|
16
|
+
//# sourceMappingURL=conpty_console_list_agent.js.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2019, Microsoft Corporation (MIT License).
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.EventEmitter2 = void 0;
|
|
7
|
+
var EventEmitter2 = /** @class */ (function () {
|
|
8
|
+
function EventEmitter2() {
|
|
9
|
+
this._listeners = [];
|
|
10
|
+
}
|
|
11
|
+
Object.defineProperty(EventEmitter2.prototype, "event", {
|
|
12
|
+
get: function () {
|
|
13
|
+
var _this = this;
|
|
14
|
+
if (!this._event) {
|
|
15
|
+
this._event = function (listener) {
|
|
16
|
+
_this._listeners.push(listener);
|
|
17
|
+
var disposable = {
|
|
18
|
+
dispose: function () {
|
|
19
|
+
for (var i = 0; i < _this._listeners.length; i++) {
|
|
20
|
+
if (_this._listeners[i] === listener) {
|
|
21
|
+
_this._listeners.splice(i, 1);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
return disposable;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
return this._event;
|
|
31
|
+
},
|
|
32
|
+
enumerable: false,
|
|
33
|
+
configurable: true
|
|
34
|
+
});
|
|
35
|
+
EventEmitter2.prototype.fire = function (data) {
|
|
36
|
+
var queue = [];
|
|
37
|
+
for (var i = 0; i < this._listeners.length; i++) {
|
|
38
|
+
queue.push(this._listeners[i]);
|
|
39
|
+
}
|
|
40
|
+
for (var i = 0; i < queue.length; i++) {
|
|
41
|
+
queue[i].call(undefined, data);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
return EventEmitter2;
|
|
45
|
+
}());
|
|
46
|
+
exports.EventEmitter2 = EventEmitter2;
|
|
47
|
+
//# sourceMappingURL=eventEmitter2.js.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2012-2015, Christopher Jeffrey, Peter Sunde (MIT License)
|
|
4
|
+
* Copyright (c) 2016, Daniel Imms (MIT License).
|
|
5
|
+
* Copyright (c) 2018, Microsoft Corporation (MIT License).
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.native = exports.open = exports.createTerminal = exports.fork = exports.spawn = void 0;
|
|
9
|
+
var utils_1 = require("./utils");
|
|
10
|
+
var terminalCtor;
|
|
11
|
+
if (process.platform === 'win32') {
|
|
12
|
+
terminalCtor = require('./windowsTerminal').WindowsTerminal;
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
terminalCtor = require('./unixTerminal').UnixTerminal;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Forks a process as a pseudoterminal.
|
|
19
|
+
* @param file The file to launch.
|
|
20
|
+
* @param args The file's arguments as argv (string[]) or in a pre-escaped
|
|
21
|
+
* CommandLine format (string). Note that the CommandLine option is only
|
|
22
|
+
* available on Windows and is expected to be escaped properly.
|
|
23
|
+
* @param options The options of the terminal.
|
|
24
|
+
* @throws When the file passed to spawn with does not exists.
|
|
25
|
+
* @see CommandLineToArgvW https://msdn.microsoft.com/en-us/library/windows/desktop/bb776391(v=vs.85).aspx
|
|
26
|
+
* @see Parsing C++ Comamnd-Line Arguments https://msdn.microsoft.com/en-us/library/17w5ykft.aspx
|
|
27
|
+
* @see GetCommandLine https://msdn.microsoft.com/en-us/library/windows/desktop/ms683156.aspx
|
|
28
|
+
*/
|
|
29
|
+
function spawn(file, args, opt) {
|
|
30
|
+
return new terminalCtor(file, args, opt);
|
|
31
|
+
}
|
|
32
|
+
exports.spawn = spawn;
|
|
33
|
+
/** @deprecated */
|
|
34
|
+
function fork(file, args, opt) {
|
|
35
|
+
return new terminalCtor(file, args, opt);
|
|
36
|
+
}
|
|
37
|
+
exports.fork = fork;
|
|
38
|
+
/** @deprecated */
|
|
39
|
+
function createTerminal(file, args, opt) {
|
|
40
|
+
return new terminalCtor(file, args, opt);
|
|
41
|
+
}
|
|
42
|
+
exports.createTerminal = createTerminal;
|
|
43
|
+
function open(options) {
|
|
44
|
+
return terminalCtor.open(options);
|
|
45
|
+
}
|
|
46
|
+
exports.open = open;
|
|
47
|
+
/**
|
|
48
|
+
* Expose the native API when not Windows, note that this is not public API and
|
|
49
|
+
* could be removed at any time.
|
|
50
|
+
*/
|
|
51
|
+
exports.native = (process.platform !== 'win32' ? utils_1.loadNativeModule('pty').module : null);
|
|
52
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2020, Microsoft Corporation (MIT License).
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getWorkerPipeName = void 0;
|
|
7
|
+
function getWorkerPipeName(conoutPipeName) {
|
|
8
|
+
return conoutPipeName + "-worker";
|
|
9
|
+
}
|
|
10
|
+
exports.getWorkerPipeName = getWorkerPipeName;
|
|
11
|
+
//# sourceMappingURL=conout.js.map
|