deepseek-computer-use 0.0.0-stage → 0.1.1

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.
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Build the Windows helper with the system C# compiler.
3
+ *
4
+ * Why compile instead of shipping a binary: Windows 10/11 always carry
5
+ * csc.exe (part of .NET Framework 4.x), so this needs no SDK, no npm native
6
+ * module, and no build server. The exe is cached and rebuilt only when a
7
+ * source file is newer.
8
+ *
9
+ * @module deepseek-computer-use/build-helper
10
+ */
11
+ import { execFileSync } from 'node:child_process';
12
+ import fs from 'node:fs';
13
+ import path from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+
16
+ const here = path.dirname(fileURLToPath(import.meta.url));
17
+ export const PLUGIN_ROOT = path.resolve(here, '..');
18
+ export const NATIVE_DIR = path.join(PLUGIN_ROOT, 'native');
19
+ export const BIN_DIR = path.join(NATIVE_DIR, 'bin');
20
+ export const HELPER_EXE = path.join(BIN_DIR, 'dsh-computer-use-helper.exe');
21
+ export const OVERLAY_EXE = path.join(BIN_DIR, 'dsh-cu-overlay.exe');
22
+
23
+ const SOURCES = ['Json.cs', 'Driver.cs', 'Program.cs'].map(name => path.join(NATIVE_DIR, name));
24
+ const OVERLAY_SOURCES = ['Overlay.cs'].map(name => path.join(NATIVE_DIR, name));
25
+
26
+ /** GAC assembly paths the compiler needs (UIAutomation + WindowsBase are not default refs). */
27
+ const GAC_ROOT = 'C:\\Windows\\Microsoft.NET\\assembly\\GAC_MSIL';
28
+ const GAC_REFS = ['WindowsBase', 'UIAutomationClient', 'UIAutomationTypes'];
29
+
30
+ function cscCandidates() {
31
+ return [
32
+ 'C:\\Windows\\Microsoft.NET\\Framework64\\v4.0.30319\\csc.exe',
33
+ 'C:\\Windows\\Microsoft.NET\\Framework\\v4.0.30319\\csc.exe',
34
+ ];
35
+ }
36
+
37
+ function findGacAssembly(name) {
38
+ const dir = path.join(GAC_ROOT, name);
39
+ if (!fs.existsSync(dir)) return null;
40
+ for (const version of fs.readdirSync(dir)) {
41
+ const candidate = path.join(dir, version, name + '.dll');
42
+ if (fs.existsSync(candidate)) return candidate;
43
+ }
44
+ return null;
45
+ }
46
+
47
+ export function findCompiler() {
48
+ for (const candidate of cscCandidates()) {
49
+ if (fs.existsSync(candidate)) return candidate;
50
+ }
51
+ return null;
52
+ }
53
+
54
+ export function isHelperFresh(exe = HELPER_EXE) {
55
+ if (!fs.existsSync(exe)) return false;
56
+ const exeTime = fs.statSync(exe).mtimeMs;
57
+ return SOURCES.every(source => fs.existsSync(source) && fs.statSync(source).mtimeMs <= exeTime);
58
+ }
59
+
60
+ /**
61
+ * Compile the helper if it is missing or stale.
62
+ * @returns {{ exe: string, built: boolean, reason?: string }}
63
+ */
64
+ export function ensureHelper({ force = false, exe = HELPER_EXE, log } = {}) {
65
+ if (!force && isHelperFresh(exe)) return { exe, built: false };
66
+ if (process.platform !== 'win32') return { exe, built: false, reason: 'not-windows' };
67
+
68
+ const compiler = findCompiler();
69
+ if (compiler === null) throw new Error('cannot build the Computer Use helper: csc.exe (.NET Framework 4.x) was not found');
70
+
71
+ const refs = [];
72
+ for (const name of GAC_REFS) {
73
+ const found = findGacAssembly(name);
74
+ if (found === null) throw new Error('cannot build the Computer Use helper: reference assembly ' + name + ' is missing from the GAC');
75
+ refs.push('/r:' + found);
76
+ }
77
+
78
+ fs.mkdirSync(path.dirname(exe), { recursive: true });
79
+ const args = ['/nologo', '/target:exe', '/platform:anycpu', '/out:' + exe, ...SOURCES, ...refs];
80
+ log?.('[dsh-computer-use] compiling helper with ' + compiler);
81
+ const output = execFileSync(compiler, args, { encoding: 'utf8', windowsHide: true });
82
+ if (!fs.existsSync(exe)) throw new Error('helper compilation produced no executable: ' + output);
83
+ return { exe, built: true };
84
+ }
85
+
86
+ /** The in-use indicator (breathing frame + virtual cursor) is a separate win-exe. */
87
+ export function isOverlayFresh(exe = OVERLAY_EXE) {
88
+ if (!fs.existsSync(exe)) return false;
89
+ const exeTime = fs.statSync(exe).mtimeMs;
90
+ return OVERLAY_SOURCES.every(source => fs.existsSync(source) && fs.statSync(source).mtimeMs <= exeTime);
91
+ }
92
+
93
+ export function ensureOverlay({ force = false, exe = OVERLAY_EXE, log } = {}) {
94
+ if (!force && isOverlayFresh(exe)) return { exe, built: false };
95
+ if (process.platform !== 'win32') return { exe, built: false, reason: 'not-windows' };
96
+ const compiler = findCompiler();
97
+ if (compiler === null) throw new Error('cannot build the Computer Use overlay: csc.exe (.NET Framework 4.x) was not found');
98
+
99
+ fs.mkdirSync(path.dirname(exe), { recursive: true });
100
+ const args = ['/nologo', '/target:winexe', '/platform:anycpu', '/out:' + exe, ...OVERLAY_SOURCES,
101
+ '/r:System.Windows.Forms.dll', '/r:System.Drawing.dll'];
102
+ log?.('[dsh-computer-use] compiling overlay with ' + compiler);
103
+ const output = execFileSync(compiler, args, { encoding: 'utf8', windowsHide: true });
104
+ if (!fs.existsSync(exe)) throw new Error('overlay compilation produced no executable: ' + output);
105
+ return { exe, built: true };
106
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Build the optional native helpers:
3
+ * capture/ Windows.Graphics.Capture tool (net48 + SharpDX)
4
+ * audio/ WASAPI loopback recorder (net48 + NAudio)
5
+ *
6
+ * Both target .NET Framework 4.8 - no runtime for the user to install - but they
7
+ * do need the .NET SDK to compile (NuGet restore). When dotnet is missing the
8
+ * plugin still works: capture falls back to PrintWindow and audio reports
9
+ * `audio_missing`.
10
+ *
11
+ * @module deepseek-computer-use/build-native
12
+ */
13
+ import { execFileSync } from 'node:child_process';
14
+ import fs from 'node:fs';
15
+ import path from 'node:path';
16
+ import { BIN_DIR, PLUGIN_ROOT } from './build-helper.mjs';
17
+
18
+ const PROJECTS = [
19
+ { name: 'capture', csproj: path.join(PLUGIN_ROOT, 'native', 'wgcfw', 'Capture.csproj'), output: path.join(BIN_DIR, 'capture'), exe: 'dsh-cu-capture.exe' },
20
+ { name: 'audio', csproj: path.join(PLUGIN_ROOT, 'native', 'audio', 'Audio.csproj'), output: path.join(BIN_DIR, 'audio'), exe: 'dsh-cu-audio.exe' },
21
+ ];
22
+
23
+ function hasDotnet() {
24
+ try { execFileSync('dotnet', ['--version'], { stdio: 'ignore', windowsHide: true }); return true; } catch { return false; }
25
+ }
26
+
27
+ export function buildNative({ log } = {}) {
28
+ if (!hasDotnet()) {
29
+ log?.('[dsh-computer-use] .NET SDK not found: skipping capture/audio (fallbacks stay active)');
30
+ return { built: [], skipped: PROJECTS.map(p => p.name) };
31
+ }
32
+ const built = [];
33
+ const skipped = [];
34
+ for (const project of PROJECTS) {
35
+ if (!fs.existsSync(project.csproj)) { skipped.push(project.name); continue; }
36
+ try {
37
+ log?.('[dsh-computer-use] building native/' + project.name);
38
+ execFileSync('dotnet', ['build', project.csproj, '-c', 'Release', '-v', 'q', '--nologo'], { stdio: 'inherit', windowsHide: true });
39
+ const produced = path.join(path.dirname(project.csproj), 'bin', 'Release', 'net48');
40
+ fs.mkdirSync(project.output, { recursive: true });
41
+ for (const file of fs.readdirSync(produced)) {
42
+ if (!file.endsWith('.pdb')) fs.copyFileSync(path.join(produced, file), path.join(project.output, file));
43
+ }
44
+ if (fs.existsSync(path.join(project.output, project.exe))) built.push(project.name);
45
+ else skipped.push(project.name);
46
+ } catch (error) {
47
+ log?.('[dsh-computer-use] ' + project.name + ' build failed: ' + error.message);
48
+ skipped.push(project.name);
49
+ }
50
+ }
51
+ return { built, skipped };
52
+ }
package/lib/build.mjs ADDED
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Build every native piece this plugin uses:
4
+ * helper UIA / SendInput / capture dispatch (csc, always available)
5
+ * overlay in-use indicator (csc, always available)
6
+ * capture Windows.Graphics.Capture (needs .NET SDK)
7
+ * audio WASAPI loopback recorder (needs .NET SDK)
8
+ */
9
+ import { ensureHelper, ensureOverlay, HELPER_EXE, OVERLAY_EXE } from './build-helper.mjs';
10
+ import { buildNative } from './build-native.mjs';
11
+
12
+ const log = message => console.log(message);
13
+
14
+ const helper = ensureHelper({ force: process.argv.includes('--force'), log });
15
+ log('[dsh-computer-use] helper ' + (helper.built ? 'built' : 'up to date') + ' -> ' + HELPER_EXE);
16
+
17
+ const overlay = ensureOverlay({ force: process.argv.includes('--force'), log });
18
+ log('[dsh-computer-use] overlay ' + (overlay.built ? 'built' : 'up to date') + ' -> ' + OVERLAY_EXE);
19
+
20
+ const native = buildNative({ log });
21
+ log('[dsh-computer-use] capture/audio built: ' + (native.built.join(', ') || 'none') +
22
+ (native.skipped.length > 0 ? ' | skipped: ' + native.skipped.join(', ') : ''));
package/lib/config.js ADDED
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Configuration for deepseek-computer-use.
3
+ *
4
+ * Every field has a safe default: an empty config block must boot without
5
+ * touching a mouse, a keyboard, or the network.
6
+ *
7
+ * @module deepseek-computer-use/config
8
+ */
9
+
10
+ /** Accessible names that almost always carry an irreversible side effect. */
11
+ export const DEFAULT_RISKY_PATTERN =
12
+ '(\\bdelete\\b|\\bremove\\b|\\buninstall\\b|\\bformat\\b|\\berase\\b|\\bwipe\\b|\\breset\\b|' +
13
+ '\\bpay\\b|\\bpurchase\\b|\\bbuy\\b|\\btransfer\\b|\\bsubmit\\b|\\bsend\\b|\\bpublish\\b|\\bpost\\b|' +
14
+ '\\binstall\\b|删除|移除|卸载|格式化|清空|重置|支付|付款|转账|购买|下单|提交|发送|发布|安装|运行)';
15
+
16
+ const DEFAULTS = {
17
+ /** 'never' | 'sensitive' | 'always' — when to ask the DSH approval seam. */
18
+ approval: 'sensitive',
19
+ /** Empty array means "every app is reachable". Substrings of exe path or process name. */
20
+ allowApps: [],
21
+ denyApps: [],
22
+ /** Codex parity: get_window_state captures a screenshot unless told otherwise. */
23
+ defaultIncludeScreenshot: true,
24
+ maxElements: 260,
25
+ snapshotTtlMs: 120000,
26
+ /** Codex waits for the UI to settle before the next capture; same idea here. */
27
+ settleMs: 1000,
28
+ /** Ask once per app before Computer Use touches it ('session') or never ('never'). */
29
+ appConsent: 'session',
30
+ /** Apps whose screenshots must never be captured (substring match on path/name). */
31
+ redactApps: [],
32
+ /** WASAPI loopback recording tools (Codex gates the same feature behind SKY_ENABLE_AUDIO). */
33
+ audio: false,
34
+ /** Record actions in the result instead of injecting them (integration rehearsals). */
35
+ dryRun: false,
36
+ /** Prefix for every tool name, e.g. 'cu_' when another plugin already owns click/type_text. */
37
+ toolPrefix: '',
38
+ /** Request budget. Codex uses 10s for ordinary calls and 15s for launch_app. */
39
+ timeoutMs: 10000,
40
+ launchTimeoutMs: 15000,
41
+ helperPath: null,
42
+ riskyPattern: DEFAULT_RISKY_PATTERN,
43
+ };
44
+
45
+ const APPROVAL_MODES = new Set(['never', 'sensitive', 'always']);
46
+
47
+ function clampInt(value, min, max, fallback) {
48
+ const n = Number(value);
49
+ if (!Number.isFinite(n)) return fallback;
50
+ return Math.min(max, Math.max(min, Math.trunc(n)));
51
+ }
52
+
53
+ function stringList(value, fallback) {
54
+ if (!Array.isArray(value)) return fallback;
55
+ return value.map(item => String(item).trim()).filter(item => item.length > 0);
56
+ }
57
+
58
+ /**
59
+ * Normalize an untrusted config block into the resolved shape the plugin uses.
60
+ * Unknown keys are dropped; invalid values fall back instead of throwing so a
61
+ * bad config can never keep the host from starting.
62
+ */
63
+ export function resolveConfig(raw = {}) {
64
+ const input = raw && typeof raw === 'object' ? raw : {};
65
+ const approval = APPROVAL_MODES.has(input.approval) ? input.approval : DEFAULTS.approval;
66
+ let riskyPattern = DEFAULTS.riskyPattern;
67
+ if (typeof input.riskyPattern === 'string' && input.riskyPattern.length > 0) {
68
+ try {
69
+ new RegExp(input.riskyPattern);
70
+ riskyPattern = input.riskyPattern;
71
+ } catch {
72
+ riskyPattern = DEFAULTS.riskyPattern;
73
+ }
74
+ }
75
+ return {
76
+ approval,
77
+ allowApps: stringList(input.allowApps, DEFAULTS.allowApps),
78
+ denyApps: stringList(input.denyApps, DEFAULTS.denyApps),
79
+ defaultIncludeScreenshot: input.defaultIncludeScreenshot !== false,
80
+ maxElements: clampInt(input.maxElements, 20, 2000, DEFAULTS.maxElements),
81
+ snapshotTtlMs: clampInt(input.snapshotTtlMs, 1000, 3600000, DEFAULTS.snapshotTtlMs),
82
+ settleMs: clampInt(input.settleMs, 0, 10000, DEFAULTS.settleMs),
83
+ appConsent: input.appConsent === 'never' ? 'never' : DEFAULTS.appConsent,
84
+ redactApps: stringList(input.redactApps, DEFAULTS.redactApps),
85
+ audio: input.audio === true,
86
+ dryRun: input.dryRun === true,
87
+ toolPrefix: typeof input.toolPrefix === 'string' ? input.toolPrefix : DEFAULTS.toolPrefix,
88
+ timeoutMs: clampInt(input.timeoutMs, 1000, 120000, DEFAULTS.timeoutMs),
89
+ launchTimeoutMs: clampInt(input.launchTimeoutMs, 1000, 120000, DEFAULTS.launchTimeoutMs),
90
+ helperPath: typeof input.helperPath === 'string' && input.helperPath.length > 0 ? input.helperPath : null,
91
+ riskyPattern,
92
+ };
93
+ }
94
+
95
+ export { DEFAULTS };
package/lib/index.js ADDED
@@ -0,0 +1,159 @@
1
+ /**
2
+ * deepseek-computer-use - Windows Computer Use for DeepSeek Harness.
3
+ *
4
+ * Registers the Codex window2 tool surface on top of a native Windows driver
5
+ * (UI Automation + SendInput + PrintWindow). The heavy lifting lives in a
6
+ * long-lived C# helper so element indexes stay valid across calls; this module
7
+ * owns the DSH seams: tool registration, image attachment, approval gating and
8
+ * config.
9
+ *
10
+ * @module deepseek-computer-use
11
+ */
12
+ import fs from 'node:fs';
13
+ import path from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+ import { HelperBridge } from './bridge.js';
16
+ import { resolveConfig } from './config.js';
17
+ import { ensureHelper, ensureOverlay, findCompiler, HELPER_EXE, isHelperFresh, isOverlayFresh } from './build-helper.mjs';
18
+ import { buildTools } from './tools.js';
19
+
20
+ export const name = 'deepseek-computer-use';
21
+ /** Only the tool registry is mandatory; skills/attachments/approval are optional. */
22
+ export const inject = ['tools'];
23
+
24
+ const here = path.dirname(fileURLToPath(import.meta.url));
25
+ const SKILL_DIR = path.resolve(here, '..', 'skills', 'computer-use');
26
+
27
+ function readSkill() {
28
+ try {
29
+ const text = fs.readFileSync(path.join(SKILL_DIR, 'SKILL.md'), 'utf8').replace(/^\uFEFF/, '').replace(/\r\n?/g, '\n');
30
+ const match = /^---[ \t]*\n([\s\S]*?)\n---[ \t]*\n?/.exec(text);
31
+ if (match === null) return null;
32
+ let skillName = '';
33
+ let description = '';
34
+ for (const rawLine of match[1].split('\n')) {
35
+ const keyMatch = /^([A-Za-z][A-Za-z0-9_-]*):[ \t]*(.*)$/.exec(rawLine.trim());
36
+ if (keyMatch === null) continue;
37
+ if (keyMatch[1] === 'name') skillName = keyMatch[2].replace(/^["']|["']$/g, '').trim();
38
+ if (keyMatch[1] === 'description') description = keyMatch[2].replace(/^["']|["']$/g, '').trim();
39
+ }
40
+ const content = text.slice(match[0].length).trimStart();
41
+ if (skillName === '' || description === '' || content.length === 0) return null;
42
+ return { name: skillName, description, content };
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ export function apply(ctx, rawConfig = {}) {
49
+ const config = resolveConfig(rawConfig);
50
+ const warn = message => {
51
+ try { ctx.logger?.warn?.(message); } catch { /* logger is optional */ }
52
+ };
53
+
54
+ const states = new Map();
55
+ let bridge = null;
56
+ let starting = null;
57
+ const exePath = config.helperPath ?? HELPER_EXE;
58
+
59
+ async function startBridge() {
60
+ const built = ensureHelper({ exe: exePath, log: warn });
61
+ // The in-use indicator is a second exe; build it eagerly so the very first
62
+ // action already shows the breathing frame instead of a silent no-op.
63
+ try { ensureOverlay({ log: warn }); } catch (error) { warn('overlay build skipped: ' + error.message); }
64
+ return new HelperBridge({ exe: built.exe, timeoutMs: config.timeoutMs, log: warn });
65
+ }
66
+
67
+ async function getBridge() {
68
+ if (bridge !== null) return bridge;
69
+ if (starting === null) starting = startBridge();
70
+ try {
71
+ bridge = await starting;
72
+ return bridge;
73
+ } catch (error) {
74
+ starting = null;
75
+ throw error;
76
+ }
77
+ }
78
+
79
+ let configured = false;
80
+ let lastToken = null;
81
+
82
+ async function request(method, params = {}, opts = {}) {
83
+ const active = await getBridge();
84
+
85
+ if (!configured) {
86
+ // The helper refuses windows whose app was never approved; keep that switch in
87
+ // sync with the plugin policy.
88
+ try {
89
+ await active.request('configure', { requireAppApproval: config.appConsent !== 'never' }, { timeoutMs: config.timeoutMs });
90
+ } catch { /* older helper */ }
91
+ configured = true;
92
+ }
93
+
94
+ const token = opts.turnToken;
95
+ if (token !== undefined && token !== lastToken) {
96
+ if (lastToken !== null) {
97
+ // Turn boundary: drop element snapshots, screenshot regions and the Esc block.
98
+ try { await active.request('end_turn', {}, { timeoutMs: 5000 }); } catch { /* best effort */ }
99
+ }
100
+ lastToken = token;
101
+ }
102
+
103
+ const timeoutMs = opts.timeoutMs ?? (method === 'launch_app' ? config.launchTimeoutMs : config.timeoutMs);
104
+ return active.request(method, params, { ...opts, timeoutMs });
105
+ }
106
+
107
+ const describeHelper = () => {
108
+ try {
109
+ return {
110
+ path: exePath,
111
+ exists: fs.existsSync(exePath),
112
+ fresh: isHelperFresh(exePath),
113
+ compiler: findCompiler(),
114
+ overlay: isOverlayFresh(),
115
+ platform: process.platform,
116
+ };
117
+ } catch (error) {
118
+ return { path: exePath, error: error.message };
119
+ }
120
+ };
121
+
122
+ const disposers = [];
123
+ for (const definition of buildTools(config, { request, ctx, log: warn, states, describeHelper })) {
124
+ disposers.push(ctx.tools.register(definition));
125
+ }
126
+
127
+ // Optional: publish the bundled operator guide as a DSH skill.
128
+ try {
129
+ const skills = ctx.get?.('skills') ?? ctx.skills;
130
+ const skill = readSkill();
131
+ if (skill !== null && skills !== undefined && typeof skills.register === 'function') {
132
+ disposers.push(skills.register({
133
+ name: skill.name,
134
+ description: skill.description,
135
+ content: skill.content,
136
+ resourceBase: { kind: 'directory', path: SKILL_DIR },
137
+ source: 'runtime',
138
+ }));
139
+ }
140
+ } catch (error) {
141
+ warn('[dsh-computer-use] skill registration skipped: ' + error.message);
142
+ }
143
+
144
+ if (typeof ctx.on === 'function') {
145
+ ctx.on('dispose', () => {
146
+ // The overlay hides the system cursor while it is up; make sure teardown puts it
147
+ // back even if the overlay process was killed instead of exiting cleanly.
148
+ try { bridge?.request('restore_cursor', {}, { timeoutMs: 2000, skipTurnTransition: false }); } catch { /* best effort */ }
149
+ try { bridge?.dispose(); } catch { /* already gone */ }
150
+ bridge = null;
151
+ for (const dispose of disposers) {
152
+ try { dispose(); } catch { /* registry may be tearing down */ }
153
+ }
154
+ });
155
+ }
156
+ }
157
+
158
+ export { resolveConfig, buildTools, HelperBridge };
159
+ export default { name, inject, apply };