dsh-dev-workbench 0.2.1 → 0.4.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,365 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/harness-linky.ts
4
+ import { spawn } from "node:child_process";
5
+ import { createInterface } from "node:readline";
6
+ import { existsSync as existsSync2, readFileSync as readFileSync2, realpathSync } from "node:fs";
7
+ import { relative, isAbsolute as isAbsolute3, join as join3 } from "node:path";
8
+ import { randomUUID as randomUUID2 } from "node:crypto";
9
+
10
+ // src/host/storage/development-store.ts
11
+ import { mkdirSync, readFileSync, writeFileSync, renameSync, existsSync, readdirSync as readdirSync2 } from "node:fs";
12
+ import { join as join2, resolve, isAbsolute as isAbsolute2 } from "node:path";
13
+ import { homedir as homedir2 } from "node:os";
14
+ import { randomUUID } from "node:crypto";
15
+
16
+ // src/shared/development.ts
17
+ function checkedArgv(value) {
18
+ if (!Array.isArray(value) || !value.length || value.length > 80 || value.some((item) => typeof item !== "string" || !item || item.includes("\0") || item.length > 4096)) throw new Error("命令必须是非空的 JSON 参数数组");
19
+ return value.slice();
20
+ }
21
+
22
+ // src/host/storage/development-environment.ts
23
+ import { accessSync, constants, readdirSync, statSync } from "node:fs";
24
+ import { homedir } from "node:os";
25
+ import { join, isAbsolute, delimiter } from "node:path";
26
+ function discoverLinky(options = {}) {
27
+ const home = options.home ?? homedir(), platform = options.platform ?? process.platform, arch = options.arch ?? process.arch;
28
+ const name = platform === "win32" ? "linky.exe" : "linky", apps = options.applications ?? ["/Applications", join(home, "Applications")];
29
+ const candidates = [
30
+ join(home, ".local", "bin", name),
31
+ join(home, ".linky", "bin", name),
32
+ ...(options.path ?? process.env.PATH ?? "").split(delimiter).filter(isAbsolute).map((dir) => join(dir, name)),
33
+ ...platform === "darwin" ? apps.map((dir) => join(dir, "Linky.app", "Contents", "Resources", "bin", "darwin", arch === "arm64" ? "arm64" : "amd64", "linky")) : [],
34
+ ...platform === "win32" ? [join(home, "AppData", "Local", "Linky", "cli", "bin", name)] : []
35
+ ];
36
+ const cliPaths = [...new Set(candidates)].filter(executable);
37
+ let profiles = [];
38
+ try {
39
+ profiles = readdirSync(join(home, ".linky", "profiles"), { withFileTypes: true }).filter((d) => d.isDirectory() && /^[\w.-]{1,120}$/.test(d.name)).map((d) => d.name);
40
+ } catch {
41
+ }
42
+ const profile = profiles.includes("desktop-linky.kingdee.com") ? "desktop-linky.kingdee.com" : profiles.length === 1 ? profiles[0] : "default";
43
+ return { cliPaths, profiles, profile };
44
+ }
45
+ function executable(path) {
46
+ try {
47
+ accessSync(path, constants.X_OK);
48
+ return statSync(path).isFile();
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
53
+
54
+ // src/host/storage/development-store.ts
55
+ function developmentDir() {
56
+ return join2(process.env.DSH_HOME || join2(homedir2(), ".dsh"), "storages", "dev-workbench", "development");
57
+ }
58
+ function developmentConfigPath() {
59
+ return join2(developmentDir(), "config.json");
60
+ }
61
+ function installedHarnessCommand() {
62
+ const base = join2(homedir2(), ".nvm", "versions", "node");
63
+ if (existsSync(base)) for (const version of readdirSync2(base).sort((a, b) => b.localeCompare(a, void 0, { numeric: true })).slice(0, 20)) {
64
+ const node = join2(base, version, "bin", "node"), cli = join2(base, version, "lib", "node_modules", "@deepseek-ai", "dsh", "lib", "bin.js");
65
+ if (existsSync(node) && existsSync(cli)) return [node, "--expose-internals", cli, "--profile", "acp"];
66
+ }
67
+ return ["dsh", "--profile", "acp"];
68
+ }
69
+ function writeDevelopmentFile(path, value) {
70
+ mkdirSync(resolve(path, ".."), { recursive: true, mode: 448 });
71
+ const tmp = path + "." + randomUUID() + ".tmp";
72
+ writeFileSync(tmp, JSON.stringify(value, null, 2), { mode: 384 });
73
+ renameSync(tmp, path);
74
+ }
75
+ function readDevelopmentConfig() {
76
+ const found = discoverLinky();
77
+ const defaults = {
78
+ cliPath: found.cliPaths[0] || join2(homedir2(), ".local", "bin", "linky"),
79
+ profile: found.profile,
80
+ workspaceId: "",
81
+ worktreeRoot: join2(homedir2(), "HarnessWorkspaces"),
82
+ harnessCommand: installedHarnessCommand(),
83
+ model: "deepseek-v4-flash",
84
+ adapterRoots: [],
85
+ checks: {},
86
+ adapterPermissionMode: "read-only",
87
+ agentId: ""
88
+ };
89
+ if (!existsSync(developmentConfigPath())) return defaults;
90
+ return { ...defaults, ...JSON.parse(readFileSync(developmentConfigPath(), "utf8")) };
91
+ }
92
+
93
+ // src/cli/harness-linky.ts
94
+ var config = process.env.HARNESS_LINKY_CONFIG ? JSON.parse(readFileSync2(process.env.HARNESS_LINKY_CONFIG, "utf8")) : readDevelopmentConfig();
95
+ var model = String(config.model || "deepseek-v4-flash");
96
+ var modelRow = { id: model, slug: model, model, name: "Harness · " + model, display_name: "Harness · " + model, displayName: "Harness · " + model, is_default: true, isDefault: true, hide: false, hidden: false, supportedReasoningEfforts: [{ reasoningEffort: "high", description: "High" }], defaultReasoningEffort: "high" };
97
+ var send = (value) => process.stdout.write(JSON.stringify(value) + "\n");
98
+ var event = (method, params) => send({ method, params });
99
+ var threads = /* @__PURE__ */ new Map();
100
+ var turns = /* @__PURE__ */ new Map();
101
+ var acp;
102
+ var initializing;
103
+ var counter = 0;
104
+ var closing = false;
105
+ var pending = /* @__PURE__ */ new Map();
106
+ var runId = /^[\w-]{1,120}$/.test(process.env.LINKY_TASK_ID || "") ? process.env.LINKY_TASK_ID : randomUUID2();
107
+ var record = { id: runId, issueId: process.env.LINKY_ISSUE_ID || "", issueCode: process.env.LINKY_ISSUE_CODE || "", workspaceId: process.env.LINKY_WORKSPACE_ID || "", profile: config.profile || "", taskId: process.env.LINKY_TASK_ID || "", engine: "Harness", status: "starting", createdAt: (/* @__PURE__ */ new Date()).toISOString() };
108
+ function persist() {
109
+ record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
110
+ writeDevelopmentFile(join3(developmentDir(), "native-runs", runId + ".json"), record);
111
+ }
112
+ function write(value) {
113
+ if (!acp?.stdin.writable) throw new Error("Harness ACP disconnected");
114
+ acp.stdin.write(JSON.stringify(value) + "\n");
115
+ }
116
+ function call(method, params, timeout = 3e4) {
117
+ const id = ++counter;
118
+ return new Promise((resolve2, reject) => {
119
+ const timer = setTimeout(() => {
120
+ pending.delete(id);
121
+ reject(new Error("Harness ACP timeout: " + method));
122
+ }, timeout);
123
+ pending.set(id, { resolve: resolve2, reject, timer });
124
+ try {
125
+ write({ jsonrpc: "2.0", id, method, params });
126
+ } catch (error) {
127
+ clearTimeout(timer);
128
+ pending.delete(id);
129
+ reject(error);
130
+ }
131
+ });
132
+ }
133
+ function rejectAll() {
134
+ for (const waiter of pending.values()) {
135
+ clearTimeout(waiter.timer);
136
+ waiter.reject(new Error("Harness ACP disconnected"));
137
+ }
138
+ pending.clear();
139
+ }
140
+ function update(params) {
141
+ const sid = params.sessionId, row = turns.get(sid);
142
+ if (!row) return;
143
+ const u = params.update || {}, kind = u.sessionUpdate;
144
+ if (kind === "agent_message_chunk" && u.content?.type === "text") {
145
+ row.text += u.content.text || "";
146
+ if (row.text.length > 512 * 1024) {
147
+ row.text = row.text.slice(0, 512 * 1024);
148
+ void shutdown("输出超出512KB上限");
149
+ }
150
+ }
151
+ if (kind === "tool_call") row.tools.set(u.toolCallId, u);
152
+ if (kind === "tool_call" || kind === "tool_call_update") {
153
+ const original = row.tools.get(u.toolCallId), bash = original?.title === "bash";
154
+ if (bash && (kind === "tool_call" || ["completed", "failed"].includes(u.status))) {
155
+ const item = { id: u.toolCallId, type: "commandExecution", command: original.rawInput?.command || "", status: kind === "tool_call" ? "inProgress" : u.status, aggregatedOutput: (u.content || []).filter((c) => c.type === "content").map((c) => c.content?.text || "").join("\n").slice(-64e3) };
156
+ event(kind === "tool_call" ? "item/started" : "item/completed", { threadId: sid, turnId: row.id, item });
157
+ }
158
+ }
159
+ }
160
+ async function ensureACP() {
161
+ if (initializing) return initializing;
162
+ initializing = (async () => {
163
+ const argv = checkedArgv(config.harnessCommand), env = { ...process.env, DSH_PERMISSION_MODE: config.adapterPermissionMode === "workspace-write" ? "workspace-write" : "read-only" };
164
+ acp = spawn(argv[0], argv.slice(1), { stdio: ["pipe", "pipe", "pipe"], env, detached: process.platform !== "win32" });
165
+ acp.on("error", rejectAll);
166
+ acp.on("exit", rejectAll);
167
+ acp.stderr.resume();
168
+ const reader = createInterface({ input: acp.stdout });
169
+ reader.on("line", (line) => {
170
+ let packet;
171
+ try {
172
+ packet = JSON.parse(line);
173
+ } catch {
174
+ return;
175
+ }
176
+ if (packet.id != null && !packet.method) {
177
+ const waiter = pending.get(packet.id);
178
+ if (waiter) {
179
+ clearTimeout(waiter.timer);
180
+ pending.delete(packet.id);
181
+ packet.error ? waiter.reject(new Error("Harness ACP request failed")) : waiter.resolve(packet.result);
182
+ }
183
+ } else if (packet.method === "session/update") update(packet.params || {});
184
+ else if (packet.id != null) {
185
+ const reject = packet.params?.options?.find((o) => o.kind === "reject_once");
186
+ if (packet.method === "session/request_permission" && reject) write({ jsonrpc: "2.0", id: packet.id, result: { outcome: { outcome: "selected", optionId: reject.optionId } } });
187
+ else write({ jsonrpc: "2.0", id: packet.id, error: { code: -32601, message: "Unsupported ACP client action" } });
188
+ }
189
+ });
190
+ await call("initialize", { protocolVersion: 1, clientCapabilities: {}, clientInfo: { name: "harness-linky", version: "0.1.0" } });
191
+ })();
192
+ return initializing;
193
+ }
194
+ function allowedCwd(value) {
195
+ if (typeof value !== "string" || !isAbsolute3(value) || !existsSync2(value)) throw new Error("An existing absolute cwd is required");
196
+ const cwd = realpathSync(value), roots = [config.worktreeRoot, ...config.adapterRoots || []].filter((p) => typeof p === "string" && existsSync2(p)).map((p) => realpathSync(p));
197
+ if (!roots.some((root) => {
198
+ const rel = relative(root, cwd);
199
+ return root !== "/" && !rel.startsWith("..") && !isAbsolute3(rel);
200
+ })) throw new Error("cwd is outside configured Harness workspace roots");
201
+ return cwd;
202
+ }
203
+ async function selectModel(sid, options) {
204
+ const option = options?.find((o) => o.category === "model" || o.id === "model");
205
+ if (!option) throw new Error("Harness did not expose verifiable model configuration; update Harness ACP");
206
+ const route = (value) => {
207
+ try {
208
+ const r = JSON.parse(value);
209
+ return Array.isArray(r) && r.length === 2 ? r : null;
210
+ } catch {
211
+ return null;
212
+ }
213
+ };
214
+ const current = route(option.currentValue);
215
+ if (current?.[1] === model) {
216
+ record.model = current[1];
217
+ record.provider = current[0];
218
+ return;
219
+ }
220
+ const choices = (option.options || []).flatMap((o) => o.options || [o]).filter((o) => route(o.value)?.[1] === model);
221
+ const target = choices.find((o) => route(o.value)?.[0] === current?.[0]) || (choices.length === 1 ? choices[0] : null);
222
+ if (!target) throw new Error("Configured Harness model is unavailable or ambiguous: " + model);
223
+ const result = await call("session/set_config_option", { sessionId: sid, configId: option.id, value: target.value });
224
+ const applied = result.configOptions?.find((o) => o.id === option.id);
225
+ if (applied?.currentValue !== target.value) throw new Error("Harness did not confirm the requested model");
226
+ const selected = route(target.value);
227
+ record.model = selected[1];
228
+ record.provider = selected[0];
229
+ }
230
+ async function turn(sid, id, text) {
231
+ let status = "completed", failure = null;
232
+ try {
233
+ const thread = threads.get(sid), resources = process.env.CODEX_HOME ? join3(process.env.CODEX_HOME, "skills") : "";
234
+ const body = [thread.instructions, resources ? "Linky 已准备的技能目录(需要时读取 SKILL.md 与引用文件):" + resources : "", text].filter(Boolean).join("\n\n");
235
+ const result = await call("session/prompt", { sessionId: sid, prompt: [{ type: "text", text: body }] }, 30 * 60 * 1e3);
236
+ if (result.stopReason === "cancelled") status = "interrupted";
237
+ else if (result.stopReason !== "end_turn") throw new Error("Harness stopped without end_turn");
238
+ const answer = turns.get(sid)?.text || "";
239
+ if (status === "completed" && !answer.trim()) throw new Error("Harness returned an empty answer");
240
+ record.status = status;
241
+ record.output = answer;
242
+ record.endedAt = (/* @__PURE__ */ new Date()).toISOString();
243
+ persist();
244
+ if (answer) event("item/completed", { threadId: sid, turnId: id, item: { id: randomUUID2(), type: "agentMessage", text: answer, phase: "final_answer" } });
245
+ } catch (error) {
246
+ status = "failed";
247
+ failure = { message: String(error.message || error) };
248
+ record.status = status;
249
+ record.error = failure.message;
250
+ persist();
251
+ if (acp) void shutdown(failure.message);
252
+ } finally {
253
+ event("turn/completed", { threadId: sid, turn: { id, status, error: failure, items: [] } });
254
+ turns.delete(sid);
255
+ }
256
+ }
257
+ async function handle(packet) {
258
+ if (packet.id == null) return;
259
+ const p = packet.params || {}, method = packet.method;
260
+ try {
261
+ let result;
262
+ if (method === "initialize") result = { userAgent: "harness-linky/0.1.0", platformFamily: process.platform === "win32" ? "windows" : "unix", platformOs: process.platform };
263
+ else if (method === "model/list") result = { data: [modelRow], nextCursor: null };
264
+ else if (method === "thread/start" || method === "thread/resume") {
265
+ if (p.model && p.model !== model) throw new Error("Unsupported model; select " + model);
266
+ const cwd = allowedCwd(p.cwd);
267
+ await ensureACP();
268
+ let sid, session;
269
+ if (method === "thread/resume") {
270
+ sid = String(p.threadId || "");
271
+ if (!sid) throw new Error("threadId required");
272
+ session = await call("session/resume", { sessionId: sid, cwd, mcpServers: [] });
273
+ } else {
274
+ session = await call("session/new", { cwd, mcpServers: [] });
275
+ sid = session.sessionId;
276
+ }
277
+ if (!sid) throw new Error("Harness returned no session id");
278
+ await selectModel(sid, session.configOptions);
279
+ threads.set(sid, { cwd, instructions: String(p.developerInstructions || "") });
280
+ Object.assign(record, { sessionId: sid, cwd, status: "ready" });
281
+ persist();
282
+ result = { thread: { id: sid, cwd, turns: [] }, model, modelProvider: "harness", cwd };
283
+ } else if (method === "turn/start") {
284
+ const sid = String(p.threadId || "");
285
+ if (!threads.has(sid) || turns.size) throw new Error("Unknown thread or adapter busy");
286
+ if (!Array.isArray(p.input) || !p.input.length || p.input.some((b) => b.type !== "text" || typeof b.text !== "string")) throw new Error("Only text input is supported");
287
+ const text = p.input.map((b) => b.text).join("\n");
288
+ if (text.length > 512 * 1024) throw new Error("Input too large");
289
+ const id = randomUUID2();
290
+ turns.set(sid, { id, text: "", tools: /* @__PURE__ */ new Map() });
291
+ record.status = "running";
292
+ record.turnId = id;
293
+ persist();
294
+ result = { turn: { id, status: "inProgress", items: [], error: null } };
295
+ send({ id: packet.id, result });
296
+ event("turn/started", { threadId: sid, turn: result.turn });
297
+ void turn(sid, id, text);
298
+ return;
299
+ } else if (method === "turn/interrupt") {
300
+ const sid = String(p.threadId || ""), current = turns.get(sid);
301
+ if (!current || p.turnId && p.turnId !== current.id) throw new Error("Unknown turn");
302
+ write({ jsonrpc: "2.0", method: "session/cancel", params: { sessionId: sid } });
303
+ result = {};
304
+ } else {
305
+ send({ id: packet.id, error: { code: -32601, message: "Unsupported method: " + method } });
306
+ return;
307
+ }
308
+ send({ id: packet.id, result });
309
+ } catch (error) {
310
+ send({ id: packet.id, error: { code: -32e3, message: String(error.message || error) } });
311
+ }
312
+ }
313
+ async function shutdown(reason = "") {
314
+ if (closing) return;
315
+ closing = true;
316
+ if (turns.size && record.status === "running") {
317
+ record.status = "interrupted";
318
+ record.error = reason || "Linky connection closed";
319
+ persist();
320
+ }
321
+ if (acp) {
322
+ acp.stdin.end();
323
+ const child = acp;
324
+ const kill = (signal) => {
325
+ try {
326
+ if (child.pid && process.platform !== "win32") process.kill(-child.pid, signal);
327
+ else child.kill(signal);
328
+ } catch {
329
+ }
330
+ };
331
+ kill("SIGTERM");
332
+ const timer = setTimeout(() => kill("SIGKILL"), 1500);
333
+ timer.unref();
334
+ child.once("exit", () => clearTimeout(timer));
335
+ rejectAll();
336
+ }
337
+ }
338
+ var args = process.argv.slice(2);
339
+ if (args[0] === "--version") console.log("codex-cli 0.151.0-compatible (harness-linky 0.1.0; backend=Harness ACP)");
340
+ else if (args[0] === "debug" && args[1] === "models") console.log(JSON.stringify({ models: [modelRow] }));
341
+ else if (args[0] === "app-server" && args.slice(1).join(" ") === "--listen stdio://") {
342
+ let queue = Promise.resolve();
343
+ const input = createInterface({ input: process.stdin });
344
+ input.on("line", (line) => {
345
+ if (line.length > 1024 * 1024) {
346
+ send({ id: null, error: { code: -32700, message: "Input too large" } });
347
+ return;
348
+ }
349
+ queue = queue.then(async () => {
350
+ try {
351
+ await handle(JSON.parse(line));
352
+ } catch {
353
+ send({ id: null, error: { code: -32700, message: "Invalid JSON" } });
354
+ }
355
+ });
356
+ });
357
+ input.once("close", () => void queue.finally(() => shutdown()));
358
+ for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => {
359
+ input.close();
360
+ void shutdown();
361
+ });
362
+ } else {
363
+ console.error("Use app-server --listen stdio://; this is a Harness adapter, no Codex installation required.");
364
+ process.exitCode = 2;
365
+ }