dsh-codex-style-computer-use 1.2.0-rc.3

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 (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +144 -0
  3. package/README.zh.md +132 -0
  4. package/cordis.patch.yml +17 -0
  5. package/lib/artifacts.js +35 -0
  6. package/lib/attention-native.js +93 -0
  7. package/lib/attention-worker.cs +194 -0
  8. package/lib/attention.js +140 -0
  9. package/lib/audit.js +30 -0
  10. package/lib/batch.js +52 -0
  11. package/lib/bin/attention-worker.exe +0 -0
  12. package/lib/bin/attention-worker.exe.json +7 -0
  13. package/lib/core/bin/dsh-computer-use-worker.exe +0 -0
  14. package/lib/core/bin/dsh-computer-use-worker.exe.json +7 -0
  15. package/lib/core/compile-worker.ps1 +58 -0
  16. package/lib/core/index.js +844 -0
  17. package/lib/core/native-runtime.js +97 -0
  18. package/lib/core/worker.cs +6375 -0
  19. package/lib/cycle.js +314 -0
  20. package/lib/exposure.js +91 -0
  21. package/lib/feedback.js +35 -0
  22. package/lib/image-output.js +92 -0
  23. package/lib/index.js +1715 -0
  24. package/lib/observations.js +78 -0
  25. package/lib/policy.js +109 -0
  26. package/lib/recovery.js +8 -0
  27. package/lib/skill-gate.js +68 -0
  28. package/lib/tools.js +717 -0
  29. package/mcp/package.json +12 -0
  30. package/mcp/src/index.js +335 -0
  31. package/package.json +88 -0
  32. package/scripts/build-native.mjs +7 -0
  33. package/scripts/smoke-native.mjs +38 -0
  34. package/scripts/verify-package.mjs +28 -0
  35. package/skills/computer-use/SKILL.md +74 -0
  36. package/skills/computer-use/references/lifecycle.md +180 -0
  37. package/skills/computer-use/references/maintenance.md +56 -0
  38. package/skills/computer-use/references/precision.md +297 -0
  39. package/skills/computer-use/references/progressive-control.md +30 -0
  40. package/skills/computer-use/references/shared-input.md +27 -0
@@ -0,0 +1,140 @@
1
+ import { AttentionNative } from './attention-native.js'
2
+
3
+ // Pure event wiring + a separately testable native boundary. Never handles answers or approvals.
4
+ export function installHumanAttention (ctx, log, options = {}) {
5
+ let raise = null
6
+ const native = options.native || new AttentionNative()
7
+ // Official 0.2.x launches dsh-desktop-host as Electron's Node child and does NOT
8
+ // register the legacy desktopRuntime Cordis service. Waiting for it disables ALL listeners.
9
+ const nativeHost = options.nativeHost ?? (process.platform === 'win32' &&
10
+ process.argv.some(arg => String(arg).replaceAll('\\', '/').includes('/dsh-desktop-host/')))
11
+ native.log({ stage: 'installer-enter', nativeHost, pid: process.pid, argvEntry: process.argv[1],
12
+ electronNodeMode: process.env.ELECTRON_RUN_AS_NODE === '1' })
13
+ const install = desktopCtx => {
14
+ if (raise) return
15
+ let runtime
16
+ // Cordis' property proxy may throw for an unregistered service; optional chaining
17
+ // on that property is insufficient. get() is the optional service lookup API.
18
+ try { runtime = typeof desktopCtx.get === 'function' ? desktopCtx.get('desktopRuntime') : desktopCtx.desktopRuntime } catch {}
19
+ if (typeof runtime?.show !== 'function' && !nativeHost) { log.warn('human attention: desktopRuntime.show unavailable'); return }
20
+ const now = options.now || Date.now
21
+ const pending = new Map(), inFlight = new Map(), recent = new Map(), questionKeys = new WeakMap()
22
+ let anonymous = 0, disposed = false
23
+ const record = data => native.log(data)
24
+ const trim = map => { while (map.size > 256) map.delete(map.keys().next().value) }
25
+ const bounded = (promise, ms) => new Promise((resolve, reject) => {
26
+ const timer = setTimeout(() => reject(new Error(`show timed out (${ms}ms)`)), ms)
27
+ Promise.resolve(promise).then(v => { clearTimeout(timer); resolve(v) }, e => { clearTimeout(timer); reject(e) })
28
+ })
29
+ native.start().catch(error => record({ stage: 'prewarm-failed', error: error.stack || String(error) }))
30
+ raise = (key, what, stillPending = () => true) => {
31
+ if (disposed) return Promise.resolve(false)
32
+ key ||= `anon:${++anonymous}`
33
+ if (inFlight.has(key)) return inFlight.get(key)
34
+ if (recent.has(key) && now() - recent.get(key) < 1500) return Promise.resolve(true)
35
+ const started = performance.now()
36
+ const work = (async () => {
37
+ record({ stage: 'raise-start', key, what })
38
+ let before, probeError
39
+ try { before = await native.request('probe') } catch (e) { probeError = e.message }
40
+ if (disposed || !stillPending()) {
41
+ record({ stage: 'raise-cancelled', key, what })
42
+ return false
43
+ }
44
+ if (before?.focused && before?.sampledVisible) {
45
+ record({ stage: 'raise-result', key, what, status: 'already-visible', visible: true, focused: true, elapsedMs: Math.round(performance.now() - started) })
46
+ log.info(`Desktop already in front for ${what}`)
47
+ return true
48
+ }
49
+ // The host request and the independent visibility fallback run together. A rejected/hung
50
+ // host RPC must not suppress the fallback or delay the card waterfall.
51
+ let show
52
+ try { show = typeof runtime?.show === 'function'
53
+ ? bounded(runtime.show(), 150).then(() => 'requested', e => `error: ${e.message}`)
54
+ : Promise.resolve('unavailable-native-fallback') }
55
+ catch (e) { show = Promise.resolve(`error: ${e.message}`) }
56
+ let result
57
+ try { result = await native.request('raise') }
58
+ catch (error) {
59
+ record({ stage: 'raise-result', key, what, status: 'error', visible: false, probeError, show: await show, error: error.stack || String(error), elapsedMs: Math.round(performance.now() - started) })
60
+ log.warn(`could not reveal Desktop for ${what}: ${error.message}`)
61
+ return false
62
+ }
63
+ const elapsedMs = Math.round(performance.now() - started)
64
+ record({ stage: 'raise-result', key, what, ...result, show: await show, probeError, elapsedMs, within1s: result.visible === true && elapsedMs <= 1000 })
65
+ if (result.visible === true) log.info(`Desktop ${result.focused ? 'foreground' : 'visible without focus'} CONFIRMED for ${what} (${elapsedMs}ms)`)
66
+ else log.warn(`could not reveal Desktop for ${what}: ${result.status}; flash=${result.flash || 'unknown'}`)
67
+ return result.visible === true
68
+ })().catch(error => { record({ stage: 'raise-error', key, error: error.stack || String(error) }); return false })
69
+ inFlight.set(key, work)
70
+ work.then(ok => { inFlight.delete(key); if (ok) { recent.set(key, now()); trim(recent) } })
71
+ return work
72
+ }
73
+ const schedule = (key, what) => {
74
+ const previous = pending.get(key)
75
+ if (previous && (!previous.exhausted || inFlight.has(key))) return
76
+ const item = { timer: null, attempts: 0 }
77
+ pending.set(key, item)
78
+ const attempt = async () => {
79
+ if (disposed || pending.get(key) !== item) return
80
+ item.attempts++
81
+ const ok = await raise(key, what, () => pending.get(key) === item)
82
+ if (!ok && item.attempts < 2 && pending.get(key) === item && !disposed) {
83
+ item.timer = setTimeout(attempt, options.retryMs ?? 700)
84
+ item.timer.unref?.()
85
+ record({ stage: 'retry-scheduled', key })
86
+ }
87
+ if (!ok && item.attempts >= 2) item.exhausted = true
88
+ }
89
+ void attempt()
90
+ }
91
+ const finish = key => {
92
+ const item = pending.get(key)
93
+ if (item) clearTimeout(item.timer)
94
+ pending.delete(key)
95
+ }
96
+ const question = (...args) => {
97
+ const next = args[args.length - 1]
98
+ const request = args.find(a => a && typeof a === 'object' && Array.isArray(a.questions)) || {}
99
+ let key = questionKeys.get(request)
100
+ if (!key) {
101
+ const ids = (request.questions || []).map(q => q?.id).filter(Boolean).join(',')
102
+ key = `q:${ids}#${++anonymous}`
103
+ questionKeys.set(request, key)
104
+ }
105
+ schedule(key, `pending question (${(request.questions || []).length} question(s))`)
106
+ // Preserve the original waterfall result, including rejecting promises.
107
+ let answer
108
+ try { answer = typeof next === 'function' ? next() : undefined }
109
+ catch (e) { finish(key); throw e }
110
+ if (answer?.then) answer.then(() => finish(key), () => finish(key))
111
+ else finish(key)
112
+ return answer
113
+ }
114
+ const approval = (session, event) => {
115
+ const id = event?.data?.id
116
+ if (typeof id !== 'string' || !id) return
117
+ const key = `a:${typeof session === 'string' ? session : session?.id || ''}:${id}`
118
+ if (event.type === 'approval/decided') { finish(key); return }
119
+ if (event.type === 'approval/asked') schedule(key, `pending approval ${id} (${event.data.toolName || 'tool'})`)
120
+ }
121
+ for (const target of new Set([ctx, desktopCtx])) {
122
+ target.on('user-questions/request', question, { prepend: true })
123
+ target.on('session/event', approval, { prepend: true })
124
+ }
125
+ desktopCtx.on('dispose', () => {
126
+ disposed = true
127
+ for (const key of pending.keys()) finish(key)
128
+ void native.close()
129
+ })
130
+ record({ stage: 'installed', hostMode: nativeHost ? 'official-desktop-native' : 'legacy-desktop-runtime', pid: process.pid, execPath: process.execPath })
131
+ log.info('human attention enabled: foreground, measured visibility, flash request, persistent diagnostics')
132
+ }
133
+ if (nativeHost) install(ctx)
134
+ else ctx.inject(['desktopRuntime'], install)
135
+ return (key, what) => {
136
+ if (raise) return raise(key, what)
137
+ log.warn(`human attention: desktopRuntime not ready for ${what}`)
138
+ return Promise.resolve(false)
139
+ }
140
+ }
package/lib/audit.js ADDED
@@ -0,0 +1,30 @@
1
+ // audit.js — snapshot paths + JSONL audit trail for dsh-computer-use.
2
+ // Every actuation the core emits lands here: {ts, op, args(redacted), result?|error?}.
3
+ // Screenshots taken before risky ops (v1) will join the same stream.
4
+
5
+ import { appendFileSync, mkdirSync, existsSync } from 'node:fs'
6
+ import { join } from 'node:path'
7
+ import { homedir } from 'node:os'
8
+ import { randomUUID } from 'node:crypto'
9
+
10
+ export function mkDataDir (configured) {
11
+ const dshHome = process.env.DSH_HOME || join(homedir(), '.dsh')
12
+ const dir = configured && configured.length > 0
13
+ ? configured
14
+ : join(dshHome, 'data', 'computer-use')
15
+ const shots = join(dir, 'shots')
16
+ if (!existsSync(shots)) mkdirSync(shots, { recursive: true })
17
+ return { root: dir, shots }
18
+ }
19
+
20
+ export function snapshotPath (dataDir) {
21
+ const ts = new Date().toISOString().replace(/[:.]/g, '-').replace('T', '_').slice(0, 19)
22
+ const ms = Date.now() % 1000
23
+ return join(dataDir.shots, `shot-${ts}-${ms}-${randomUUID()}.jpg`)
24
+ }
25
+
26
+ export function appendAudit (dataDir, rec) {
27
+ try {
28
+ appendFileSync(join(dataDir.root, 'audit.jsonl'), JSON.stringify(rec) + '\n')
29
+ } catch { /* audit must never break the action */ }
30
+ }
package/lib/batch.js ADDED
@@ -0,0 +1,52 @@
1
+ // Serial execution and observation selection; no desktop or lifecycle side effects here.
2
+ const reads = new Set(['computer_state', 'computer_uia', 'computer_marks', 'computer_shot', 'computer_wait'])
3
+ const isRead = (step) => reads.has(step?.tool) || (step?.tool === 'computer_clip' && step.args?.text === undefined)
4
+ const isReadback = (step) => ['computer_state', 'computer_uia', 'computer_marks', 'computer_shot'].includes(step.tool) ||
5
+ (step.tool === 'computer_clip' && step.args?.text === undefined)
6
+
7
+ export function actionRefusal (step, result) {
8
+ if (result?.ok === false || result?.success === false) return result.error || 'action reported failure'
9
+ if (step.tool === 'computer_window' && (step.args?.op || 'activate') === 'activate' && result?.activated !== true) {
10
+ return 'target window activation was not confirmed; subsequent actions were not dispatched'
11
+ }
12
+ if (step.tool === 'computer_ctrl' && step.args?.action === 'recover' && result?.recovered !== true) return result.reason || 'temporary pause recovery refused'
13
+ return null
14
+ }
15
+
16
+ export async function runBatch ({ steps, handlers, signal, stopped, screenshot, shot = 'auto', exec }) {
17
+ if (!Array.isArray(steps) || !['auto', 'always', 'never'].includes(shot)) throw new Error('invalid batch actions or shot mode')
18
+ const actions = []
19
+ let failed = false
20
+ for (const step of steps) {
21
+ const name = step?.tool
22
+ if (failed || signal?.aborted) {
23
+ actions.push({ tool: name || '', ok: false, outcome: 'not-dispatched', error: signal?.aborted ? 'cancelled' : 'earlier action failed' })
24
+ failed = true
25
+ continue
26
+ }
27
+ const fn = Object.hasOwn(handlers, name) && handlers[name]
28
+ if (!fn) { actions.push({ tool: name || '', ok: false, outcome: 'not-dispatched', error: 'unknown tool' }); failed = true; continue }
29
+ try {
30
+ const stoppedBefore = stopped()
31
+ const result = await fn(step.args || {}, exec)
32
+ const refusal = actionRefusal(step, result)
33
+ actions.push({ tool: name, ok: !refusal, result, ...(refusal ? { error: refusal } : {}) })
34
+ failed = !!refusal || (!stoppedBefore && stopped()) ||
35
+ (name === 'computer_ctrl' && ['stop', 'exit'].includes(step.args?.action))
36
+ } catch (error) {
37
+ actions.push({ tool: name, ok: false, error: String(error.message || error),
38
+ code: error.code || 'action-error', outcome: error.outcome || 'unknown' })
39
+ failed = true
40
+ }
41
+ }
42
+ const needsShot = shot === 'always' || (shot === 'auto' && steps.some(s => !isRead(s)) && !isReadback(steps.at(-1) || {}))
43
+ const result = { actions, attachedShot: null }
44
+ if (!failed && !signal?.aborted && !stopped() && needsShot) {
45
+ try { result.attachedShot = await screenshot() }
46
+ catch (error) { result.observationError = String(error.message || error) }
47
+ }
48
+ // A transport/provider return does not prove that a form was saved or a message was sent.
49
+ if (steps.some(s => s && !isRead(s))) result.verification = 'Inspect returned readback or image before claiming the task effect; ok only means the action returned.'
50
+ if (result.attachedShot) result.view = 'read_image the attachedShot if visual verification is needed'
51
+ return result
52
+ }
Binary file
@@ -0,0 +1,7 @@
1
+ {
2
+ "format": 1,
3
+ "platform": "win32",
4
+ "architecture": "x64",
5
+ "sourceSha256": "6e17308fc1cb0de1ece6f3164866b6de03ecdf74a42a6e78c0a8e6d02912c3bf",
6
+ "binarySha256": "c15feb47d7ec975ccab12fea2ee4300ea0a4699069be21d0fb9d1fedebe26158"
7
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "format": 1,
3
+ "platform": "win32",
4
+ "architecture": "x64",
5
+ "sourceSha256": "7e24c3fa73fed54eb8209efc253753d44664037954d936573e7c80150caf5856",
6
+ "binarySha256": "ee890a28d1aec984f98b48caf30acab536477ea4d9261892761ea6e8bd485c46"
7
+ }
@@ -0,0 +1,58 @@
1
+ param(
2
+ # RESOLVED FROM THIS SCRIPT'S OWN LOCATION, NOT FROM A MACHINE-SPECIFIC PATH (2026-09-16).
3
+ # It used to default to `D:\BuyKey\dsh-computer-use\packages\computer-use-core\src\worker.cs`, which is
4
+ # correct on exactly one computer — and `compileWorkerSync()` in index.js calls this script WITHOUT
5
+ # `-Path`, so on any other machine the plugin's compile-on-demand path pointed at a file that is not
6
+ # there. That is a silent portability failure: the plugin only reaches it when no cached worker exists,
7
+ # i.e. on a fresh install, i.e. on the machine that has never worked. `$PSScriptRoot` is the directory
8
+ # this file lives in, and `worker.cs` ships beside it.
9
+ [string]$Path = '',
10
+ [switch]$Force,
11
+ # OPTIONAL NAMESPACE FOR THE CACHE DIRECTORY. The cache key is the source's sha16, which is right
12
+ # for the deployed worker but wrong for a TEST SANDBOX: two runs of the same test compile identical
13
+ # sources, so they land on the SAME exe path, and a worker left running by an earlier run holds
14
+ # that file open — `csc` then fails with CS0016 ("cannot write output file") and a test that cannot
15
+ # rebuild its fault-injected worker silently has nothing to measure. Measured exactly that:
16
+ # build/test-ask-release.mjs alternated pass/fail depending on whether a stale sandbox worker from
17
+ # its own previous run was still alive. A caller that needs isolation passes a unique token (e.g.
18
+ # its own pid); the default is '' so the deployed worker's cache path is unchanged.
19
+ [string]$CacheToken = ''
20
+ )
21
+ if (-not $Path) { $Path = Join-Path $PSScriptRoot 'worker.cs' }
22
+ # compile-worker.ps1 — turn worker.cs into a cached native worker.exe.
23
+ # Uses ONLY the C# 5 compiler shipping with Windows (Framework64 csc.exe):
24
+ # no SDK, no node-gyp, no network. C# source stays C#-5-compatible on purpose.
25
+ # Cache: %LOCALAPPDATA%\dsh-computer-use\worker\<sha16>[<CacheToken>]\worker.exe
26
+ $ErrorActionPreference = 'Stop'
27
+
28
+ $cacheRoot = Join-Path $env:LOCALAPPDATA 'dsh-computer-use\worker'
29
+ $sha = (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash
30
+ $outDir = Join-Path $cacheRoot ($sha.Substring(0, 16) + $CacheToken)
31
+ $outExe = Join-Path $outDir 'dsh-computer-use-worker.exe'
32
+ Write-Host "worker cache dir: $outDir"
33
+
34
+ if ((Test-Path -LiteralPath $outExe) -and -not $Force) {
35
+ Write-Host 'cached worker is current; use -Force to recompile'
36
+ Write-Output $outExe
37
+ return
38
+ }
39
+
40
+ $csc = "$env:windir\Microsoft.NET\Framework64\v4.0.30319\csc.exe"
41
+ if (-not (Test-Path -LiteralPath $csc)) { throw 'csc.exe not found (.NET Framework 4.x missing?)' }
42
+
43
+ # csc does not resolve GAC simple names — pass full assembly paths
44
+ $gac = "$env:windir\Microsoft.NET\assembly\GAC_MSIL"
45
+ $refs = @(
46
+ '/r:System.dll', '/r:System.Core.dll', '/r:System.Drawing.dll',
47
+ '/r:System.Web.Extensions.dll', '/r:System.Windows.Forms.dll',
48
+ "/r:$gac\UIAutomationClient\v4.0_4.0.0.0__31bf3856ad364e35\UIAutomationClient.dll",
49
+ "/r:$gac\UIAutomationTypes\v4.0_4.0.0.0__31bf3856ad364e35\UIAutomationTypes.dll",
50
+ "/r:$gac\WindowsBase\v4.0_4.0.0.0__31bf3856ad364e35\WindowsBase.dll"
51
+ )
52
+
53
+ New-Item -ItemType Directory -Path $outDir -Force | Out-Null
54
+ & $csc /nologo /target:exe /platform:x64 /optimize+ /warn:1 /out:$outExe $refs $Path
55
+ if ($LASTEXITCODE -ne 0) { throw "csc failed with exit code $LASTEXITCODE" }
56
+ if (-not (Test-Path -LiteralPath $outExe)) { throw 'compile produced no exe' }
57
+ Write-Host "compiled OK -> $outExe"
58
+ Write-Output $outExe