@sparkelf/dsh-plus 0.2.0-rc.14 → 0.2.0-rc.16
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/cordis.patch.yml +7 -4
- package/lib/bin.js +335 -0
- package/lib/types/standalone-capabilities.d.ts +109 -0
- package/lib/types/standalone-capabilities.js +324 -0
- package/lib/types/standalone-cli.js +59 -0
- package/package.json +24 -21
package/cordis.patch.yml
CHANGED
|
@@ -16,10 +16,6 @@
|
|
|
16
16
|
# The model-facing tools over that index: search, trace, and event read. The
|
|
17
17
|
# backend above answers only once something mounts the tools that call it, so a
|
|
18
18
|
# deployment carrying the backend alone has an index nothing asks for.
|
|
19
|
-
- id: session-query-tool
|
|
20
|
-
config:
|
|
21
|
-
searchTimeoutMs: 900000
|
|
22
|
-
|
|
23
19
|
- insert:
|
|
24
20
|
- id: plus-session-query-tool
|
|
25
21
|
name: '@deepseek-ai/dsh-tool-session-query'
|
|
@@ -68,3 +64,10 @@
|
|
|
68
64
|
provider: fork
|
|
69
65
|
enabled: false
|
|
70
66
|
maxDepth: 0
|
|
67
|
+
|
|
68
|
+
# The first content search builds the whole index in one transaction, so it needs far
|
|
69
|
+
# more than a steady-state query. This row follows the insert above because a patch
|
|
70
|
+
# only reaches rows an earlier patch created.
|
|
71
|
+
- id: plus-session-query-tool
|
|
72
|
+
config:
|
|
73
|
+
searchTimeoutMs: 900000
|
package/lib/bin.js
CHANGED
|
@@ -75,6 +75,296 @@ function newerVersion(name, installed, registry = "https://registry.npmjs.org")
|
|
|
75
75
|
return publishedVersions(name, registry).filter((entry) => semver.gt(entry.version, installed)).at(-1);
|
|
76
76
|
}
|
|
77
77
|
//#endregion
|
|
78
|
+
//#region lib/types/standalone-capabilities.js
|
|
79
|
+
/** Capabilities offered at install time, in interview order. */
|
|
80
|
+
const CAPABILITIES = [
|
|
81
|
+
{
|
|
82
|
+
id: "exa",
|
|
83
|
+
title: "Web search (Exa)",
|
|
84
|
+
detail: "Search the web through Exa instead of the built-in provider; needs an API key",
|
|
85
|
+
needsEndpoint: false
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: "mineru",
|
|
89
|
+
title: "PDF parsing (MinerU)",
|
|
90
|
+
detail: "Parse uploaded PDFs locally; the service is installed and started here",
|
|
91
|
+
needsEndpoint: true
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: "officecli",
|
|
95
|
+
title: "Office documents (OfficeCLI)",
|
|
96
|
+
detail: "Create and open DOCX, XLSX, and PPTX deliverables",
|
|
97
|
+
needsEndpoint: false
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: "computer-use",
|
|
101
|
+
title: "Windows desktop control (computer-use)",
|
|
102
|
+
detail: "Drive the Windows desktop from WSL through the cua driver",
|
|
103
|
+
needsEndpoint: false
|
|
104
|
+
}
|
|
105
|
+
];
|
|
106
|
+
/** Where this command starts MinerU when the capability is enabled. */
|
|
107
|
+
const DEFAULT_MINERU_ENDPOINT = "http://127.0.0.1:8000/file_parse";
|
|
108
|
+
/** File under the deployment home recording which capabilities were enabled. */
|
|
109
|
+
const CAPABILITY_RECORD = "capabilities.json";
|
|
110
|
+
/** Deployment env file the launcher reads as its `user-env` layer. */
|
|
111
|
+
const CAPABILITY_ENV_FILE = ".env";
|
|
112
|
+
/** One prompt on the terminal, resolving the trimmed line the user typed. */
|
|
113
|
+
function ask(question) {
|
|
114
|
+
return new Promise((resolveAnswer) => {
|
|
115
|
+
process.stdout.write(question);
|
|
116
|
+
process.stdin.once("data", (chunk) => {
|
|
117
|
+
resolveAnswer(String(chunk).trim());
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/** Ask one yes/no question, defaulting to yes. */
|
|
122
|
+
async function confirmDefaultYes(question) {
|
|
123
|
+
const answer = (await ask(question + " [Y/n] ")).toLowerCase();
|
|
124
|
+
return answer === "" || answer === "y" || answer === "yes";
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Interview the user about the optional capabilities.
|
|
128
|
+
*
|
|
129
|
+
* Each is offered on its own line and defaults to enabled, so pressing Enter
|
|
130
|
+
* through the interview produces a deployment whose features work. The answers are
|
|
131
|
+
* returned rather than applied, so a caller can report what it will do before it
|
|
132
|
+
* touches the profile.
|
|
133
|
+
*
|
|
134
|
+
* @param interactive - when false, every capability is enabled without asking.
|
|
135
|
+
* @returns the ids to enable and the values the enabled ones need.
|
|
136
|
+
*/
|
|
137
|
+
async function interviewCapabilities(interactive) {
|
|
138
|
+
if (!interactive) return { enabled: CAPABILITIES.map((capability) => capability.id) };
|
|
139
|
+
console.log("");
|
|
140
|
+
console.log("Optional capabilities. Press Enter to accept the default (all enabled),");
|
|
141
|
+
console.log("or answer n for any you do not want.");
|
|
142
|
+
console.log("");
|
|
143
|
+
const enabled = [];
|
|
144
|
+
for (const capability of CAPABILITIES) if (await confirmDefaultYes(" Enable " + capability.title + "? (" + capability.detail + ")")) enabled.push(capability.id);
|
|
145
|
+
let exaApiKey;
|
|
146
|
+
if (enabled.includes("exa")) {
|
|
147
|
+
console.log("");
|
|
148
|
+
console.log(" Exa needs an API key. Create one at https://dashboard.exa.ai/api-keys");
|
|
149
|
+
const typed = await ask(" Exa API key (leave empty to add it later): ");
|
|
150
|
+
if (typed !== "") exaApiKey = typed;
|
|
151
|
+
}
|
|
152
|
+
let mineruEndpoint;
|
|
153
|
+
if (enabled.includes("mineru")) {
|
|
154
|
+
const typed = await ask(" MinerU endpoint [http://127.0.0.1:8000/file_parse]: ");
|
|
155
|
+
mineruEndpoint = typed === "" ? DEFAULT_MINERU_ENDPOINT : typed;
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
enabled,
|
|
159
|
+
...exaApiKey === void 0 ? {} : { exaApiKey },
|
|
160
|
+
...mineruEndpoint === void 0 ? {} : { mineruEndpoint }
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/** Names this module owns in the deployment env file. */
|
|
164
|
+
const CAPABILITY_ENV_NAMES = ["DSH_MINERU_ENDPOINT", "EXA_API_KEY"];
|
|
165
|
+
/**
|
|
166
|
+
* The deployment env file, with one assignment per enabled capability that reaches
|
|
167
|
+
* its service through the launch environment.
|
|
168
|
+
*
|
|
169
|
+
* MinerU is switched on by the presence of its endpoint and Exa reads its key from
|
|
170
|
+
* the launch environment, so a capability the user selected is enabled here as well
|
|
171
|
+
* as in the profile layer. Only the names this interview owns are rewritten: a
|
|
172
|
+
* deployment that keeps its own assignments in the same file keeps them, and
|
|
173
|
+
* deselecting a capability removes the assignment that turned it on.
|
|
174
|
+
*
|
|
175
|
+
* @param answers - the interview's answers.
|
|
176
|
+
* @param existing - the file's current text, when it already exists.
|
|
177
|
+
* @returns the environment file's text.
|
|
178
|
+
*/
|
|
179
|
+
function capabilityEnvironment(answers, existing = "") {
|
|
180
|
+
const enabled = new Set(answers.enabled);
|
|
181
|
+
const owned = new Set(CAPABILITY_ENV_NAMES);
|
|
182
|
+
const kept = existing.split("\n").filter((line) => {
|
|
183
|
+
const name = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=/.exec(line)?.[1];
|
|
184
|
+
return line.trim() !== "" && !line.trimStart().startsWith("#") && (name === void 0 || !owned.has(name));
|
|
185
|
+
});
|
|
186
|
+
const written = ["# Written by dsh-plus start from the capability interview. Enabling or", "# disabling a capability rewrites the lines below; other lines are kept."];
|
|
187
|
+
if (enabled.has("mineru")) written.push("DSH_MINERU_ENDPOINT=" + (answers.mineruEndpoint ?? "http://127.0.0.1:8000/file_parse"));
|
|
188
|
+
if (enabled.has("exa") && answers.exaApiKey !== void 0) written.push("EXA_API_KEY=" + answers.exaApiKey);
|
|
189
|
+
return [...written, ...kept].join("\n") + "\n";
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* The profile patch layer that turns the selected capabilities on.
|
|
193
|
+
*
|
|
194
|
+
* The file is data the profile's loader merges, so enabling a capability writes a
|
|
195
|
+
* row rather than editing the deployment. An empty selection still writes the file,
|
|
196
|
+
* because a layer that exists and mounts nothing is how a previously enabled
|
|
197
|
+
* capability is turned back off.
|
|
198
|
+
*
|
|
199
|
+
* The profile's own layer is a separate file with its own name: a deployment that
|
|
200
|
+
* configured something by hand keeps it, and the capability rows are rewritten
|
|
201
|
+
* whole on every start.
|
|
202
|
+
*
|
|
203
|
+
* @param answers - the interview's answers.
|
|
204
|
+
* @returns the patch layer's YAML text.
|
|
205
|
+
*/
|
|
206
|
+
function capabilityPatchLayer(answers) {
|
|
207
|
+
const enabled = new Set(answers.enabled);
|
|
208
|
+
const rows = [];
|
|
209
|
+
if (enabled.has("exa")) rows.push(" - id: web-search-exa", " name: '@deepseek-ai/dsh-web-search-exa'", " config:", " searchType: auto", " numResults: 8");
|
|
210
|
+
if (enabled.has("computer-use")) rows.push(" - id: computer-use", " name: '@deepseek-ai/dsh-computer-use'", " - id: computer-use-cua-driver-mcp", " name: '@deepseek-ai/dsh-experimental-computer-use-cua-driver-mcp'");
|
|
211
|
+
const lines = ["# Written by dsh-plus start from the capability interview. Enabling or disabling a capability", "# rewrites this file; edits here are replaced."];
|
|
212
|
+
if (rows.length > 0) lines.push("- insert:", ...rows);
|
|
213
|
+
if (enabled.has("exa")) lines.push("", "# Route web_search through Exa rather than the built-in provider.", "- id: web", " config:", " searchProvider: exa");
|
|
214
|
+
return lines.join("\n") + "\n";
|
|
215
|
+
}
|
|
216
|
+
/** Run one command, reporting a failure rather than throwing. */
|
|
217
|
+
function runStep(command, args) {
|
|
218
|
+
return spawnSync(command, [...args], { stdio: "inherit" }).status === 0;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Install and start the backing services the selected capabilities need.
|
|
222
|
+
*
|
|
223
|
+
* Enabling a capability means the deployment expects its service to answer, so the
|
|
224
|
+
* command that offers the choice is also the command that provides it. Each step
|
|
225
|
+
* reports what it is doing and, on failure, the command a user can run by hand —
|
|
226
|
+
* a missing runtime is a fact about the host, not a reason to abandon the install.
|
|
227
|
+
*
|
|
228
|
+
* @param answers - the interview's answers.
|
|
229
|
+
* @param home - the deployment home, where generated service units are recorded.
|
|
230
|
+
* @returns the ids whose service is ready.
|
|
231
|
+
*/
|
|
232
|
+
async function installCapabilityServices(answers, home) {
|
|
233
|
+
const enabled = new Set(answers.enabled);
|
|
234
|
+
const ready = [];
|
|
235
|
+
mkdirSync(home, { recursive: true });
|
|
236
|
+
writeFileSync(join(home, CAPABILITY_RECORD), JSON.stringify({
|
|
237
|
+
enabled: [...answers.enabled],
|
|
238
|
+
...answers.mineruEndpoint === void 0 ? {} : { mineruEndpoint: answers.mineruEndpoint }
|
|
239
|
+
}, null, 2) + "\n");
|
|
240
|
+
if (enabled.has("mineru")) {
|
|
241
|
+
console.log("");
|
|
242
|
+
console.log(" MinerU: installing the parser and starting its service...");
|
|
243
|
+
if (installMineru(home) && startMineru(home)) {
|
|
244
|
+
ready.push("mineru");
|
|
245
|
+
console.log(" MinerU: ready at " + (answers.mineruEndpoint ?? "http://127.0.0.1:8000/file_parse"));
|
|
246
|
+
} else {
|
|
247
|
+
console.log(" MinerU: not installed. Install it later with:");
|
|
248
|
+
console.log(" pip install -U \"mineru[core]\"");
|
|
249
|
+
console.log(" mineru-api --host 127.0.0.1 --port 8000");
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (enabled.has("officecli")) ready.push("officecli");
|
|
253
|
+
if (enabled.has("computer-use")) if (desktopDriverAvailable()) ready.push("computer-use");
|
|
254
|
+
else {
|
|
255
|
+
console.log("");
|
|
256
|
+
console.log(" computer-use: the cua driver was not found on PATH. The plugin is");
|
|
257
|
+
console.log(" still mounted, so it starts once the driver is installed:");
|
|
258
|
+
console.log(" install the cua-driver release for this architecture, then restart");
|
|
259
|
+
}
|
|
260
|
+
return ready;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Install MinerU when it is absent and start its API service.
|
|
264
|
+
*
|
|
265
|
+
* MinerU is a Python package whose API server answers on a local port; the profile
|
|
266
|
+
* points at that port. The parser is installed into a virtual environment under the
|
|
267
|
+
* deployment home and an existing one is upgraded rather than skipped, so enabling
|
|
268
|
+
* the capability keeps the parser current without touching the system interpreter.
|
|
269
|
+
*
|
|
270
|
+
* @param home - the deployment home that owns the venv.
|
|
271
|
+
* @returns whether the package is installed; not whether it answered.
|
|
272
|
+
*/
|
|
273
|
+
function installMineru(home) {
|
|
274
|
+
const python = pythonInterpreter();
|
|
275
|
+
if (python === void 0) {
|
|
276
|
+
console.log(" MinerU: no Python interpreter found on PATH.");
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
const venv = join(home, ".mineru-venv");
|
|
280
|
+
if (!runStep(python, [
|
|
281
|
+
"-m",
|
|
282
|
+
"venv",
|
|
283
|
+
venv
|
|
284
|
+
])) return false;
|
|
285
|
+
return runStep(join(venv, "bin", "python"), [
|
|
286
|
+
"-m",
|
|
287
|
+
"pip",
|
|
288
|
+
"install",
|
|
289
|
+
"-U",
|
|
290
|
+
"mineru[all]"
|
|
291
|
+
]);
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Start the MinerU API server as a background service when none answers yet.
|
|
295
|
+
*
|
|
296
|
+
* The service has to outlive the install command, so it is registered with the
|
|
297
|
+
* host's service manager when one is available and otherwise started detached.
|
|
298
|
+
*
|
|
299
|
+
* @returns whether the server is running after this call.
|
|
300
|
+
*/
|
|
301
|
+
function startMineru(home) {
|
|
302
|
+
const api = mineruApiBinary(home);
|
|
303
|
+
if (api === void 0) return false;
|
|
304
|
+
if (mineruAnswers()) return true;
|
|
305
|
+
if (spawnSync("systemctl", ["--version"], { stdio: "ignore" }).status === 0) {
|
|
306
|
+
writeFileSync("/etc/systemd/system/mineru-api.service", [
|
|
307
|
+
"[Unit]",
|
|
308
|
+
"Description=MinerU document parsing API for DeepSeek Harness Plus",
|
|
309
|
+
"After=network-online.target",
|
|
310
|
+
"Wants=network-online.target",
|
|
311
|
+
"",
|
|
312
|
+
"[Service]",
|
|
313
|
+
"Type=simple",
|
|
314
|
+
"User=root",
|
|
315
|
+
"ExecStart=" + api + " --host 127.0.0.1 --port 8000",
|
|
316
|
+
"Restart=on-failure",
|
|
317
|
+
"RestartSec=2",
|
|
318
|
+
"TimeoutStopSec=15",
|
|
319
|
+
"",
|
|
320
|
+
"[Install]",
|
|
321
|
+
"WantedBy=multi-user.target",
|
|
322
|
+
""
|
|
323
|
+
].join("\n"));
|
|
324
|
+
spawnSync("systemctl", ["daemon-reload"], { stdio: "ignore" });
|
|
325
|
+
return spawnSync("systemctl", [
|
|
326
|
+
"enable",
|
|
327
|
+
"--now",
|
|
328
|
+
"mineru-api.service"
|
|
329
|
+
], { stdio: "ignore" }).status === 0;
|
|
330
|
+
}
|
|
331
|
+
console.log(" MinerU: start the API server with:");
|
|
332
|
+
console.log(" " + api + " --host 127.0.0.1 --port 8000");
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
/** The Python interpreter to install MinerU with, preferring python3. */
|
|
336
|
+
function pythonInterpreter() {
|
|
337
|
+
return ["python3", "python"].find((candidate) => spawnSync(candidate, ["--version"], { stdio: "ignore" }).status === 0);
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* The MinerU API entry point, wherever the install placed it.
|
|
341
|
+
*
|
|
342
|
+
* The venv this command creates comes first: it is the interpreter the parser was
|
|
343
|
+
* installed into, so its `mineru-api` is the one that can import MinerU. A binary on
|
|
344
|
+
* PATH is a fallback for a deployment that installed MinerU some other way.
|
|
345
|
+
*
|
|
346
|
+
* @param home - the deployment home that owns the venv.
|
|
347
|
+
* @returns the command to run, or `undefined` when none answers.
|
|
348
|
+
*/
|
|
349
|
+
function mineruApiBinary(home) {
|
|
350
|
+
return [join(home, ".mineru-venv", "bin", "mineru-api"), "mineru-api"].find((candidate) => spawnSync(candidate, ["--help"], { stdio: "ignore" }).status === 0);
|
|
351
|
+
}
|
|
352
|
+
/** Whether a MinerU API server already answers on the default port. */
|
|
353
|
+
function mineruAnswers() {
|
|
354
|
+
return spawnSync("curl", [
|
|
355
|
+
"-sf",
|
|
356
|
+
"-o",
|
|
357
|
+
"/dev/null",
|
|
358
|
+
"--max-time",
|
|
359
|
+
"3",
|
|
360
|
+
"http://127.0.0.1:8000/docs"
|
|
361
|
+
], { stdio: "ignore" }).status === 0;
|
|
362
|
+
}
|
|
363
|
+
/** Whether the Windows-side desktop driver answers from this shell. */
|
|
364
|
+
function desktopDriverAvailable() {
|
|
365
|
+
return spawnSync("cua-driver", ["--version"], { stdio: "ignore" }).status === 0;
|
|
366
|
+
}
|
|
367
|
+
//#endregion
|
|
78
368
|
//#region lib/types/standalone-server.js
|
|
79
369
|
/**
|
|
80
370
|
* Process lifecycle for one standalone Plus server.
|
|
@@ -681,6 +971,8 @@ function resolveInstalledPackage(from, packageName) {
|
|
|
681
971
|
*/
|
|
682
972
|
/** Milliseconds a start waits for the server to answer before reporting failure. */
|
|
683
973
|
const READY_TIMEOUT_MILLISECONDS = 9e4;
|
|
974
|
+
/** Profile patch file the capability interview rewrites (the profile's user layer). */
|
|
975
|
+
const CAPABILITY_PATCH_FILE = "cordis.patch.yml";
|
|
684
976
|
function parseStartOptions(argv) {
|
|
685
977
|
let port = DEFAULT_PORT;
|
|
686
978
|
let host = "127.0.0.1";
|
|
@@ -801,6 +1093,15 @@ async function start(argv) {
|
|
|
801
1093
|
const created = ensureProfile(paths, installationRoot());
|
|
802
1094
|
console.log(created ? "Created the plus profile at " + paths.profileDirectory : "Using the existing plus profile");
|
|
803
1095
|
for (const label of applyProfileNpmPatches(paths.distributionDirectory, paths.profileDirectory)) console.log("Applied the reviewed patch " + label);
|
|
1096
|
+
const answers = created || !existsSync(join(paths.home, "capabilities.json")) ? await interviewCapabilities(true) : void 0;
|
|
1097
|
+
if (answers !== void 0) {
|
|
1098
|
+
const ready = await installCapabilityServices(answers, paths.home);
|
|
1099
|
+
writeCapabilityPatch(paths.profileDirectory, answers);
|
|
1100
|
+
writeCapabilityEnvironment(paths.home, answers);
|
|
1101
|
+
console.log(" Enabled: " + (answers.enabled.length === 0 ? "(none)" : answers.enabled.join(", ")));
|
|
1102
|
+
if (answers.enabled.length > 0) console.log(" Services ready: " + (ready.length === 0 ? "(none)" : ready.join(", ")));
|
|
1103
|
+
if (answers.enabled.includes("exa") && answers.exaApiKey === void 0) console.log(" Exa: add EXA_API_KEY to " + join(paths.home, CAPABILITY_ENV_FILE) + " when you have a key.");
|
|
1104
|
+
}
|
|
804
1105
|
const entry = launcherEntry(anchor);
|
|
805
1106
|
if (options.foreground) return runForeground(entry, options.port, options.host, options.open);
|
|
806
1107
|
const existing = readState(paths.home);
|
|
@@ -942,6 +1243,40 @@ async function update(argv) {
|
|
|
942
1243
|
if (readState(paths.home) !== void 0) console.log("The running server still serves " + installed + "; run dsh-plus restart to load the new release.");
|
|
943
1244
|
return 0;
|
|
944
1245
|
}
|
|
1246
|
+
/**
|
|
1247
|
+
* Write the profile's capability patch layer.
|
|
1248
|
+
*
|
|
1249
|
+
* The profile's loader merges this file, so enabling a capability is a data change
|
|
1250
|
+
* rather than an edit to the deployment. Writing it on every configured run also
|
|
1251
|
+
* turns a capability back off when the interview no longer selects it.
|
|
1252
|
+
*
|
|
1253
|
+
* @param profileDirectory - the profile whose layer is replaced.
|
|
1254
|
+
* @param answers - the interview's answers.
|
|
1255
|
+
*/
|
|
1256
|
+
function writeCapabilityPatch(profileDirectory, answers) {
|
|
1257
|
+
const path = join(profileDirectory, CAPABILITY_PATCH_FILE);
|
|
1258
|
+
const existing = existsSync(path) ? readFileSync(path, "utf8") : void 0;
|
|
1259
|
+
if (existing !== void 0 && !existing.includes("Written by dsh-plus start from the capability interview.")) {
|
|
1260
|
+
writeFileSync(path + ".before-capabilities", existing);
|
|
1261
|
+
console.log(" Kept the existing profile layer at cordis.patch.yml.before-capabilities");
|
|
1262
|
+
}
|
|
1263
|
+
writeFileSync(path, capabilityPatchLayer(answers));
|
|
1264
|
+
}
|
|
1265
|
+
/**
|
|
1266
|
+
* Write the deployment's capability environment file.
|
|
1267
|
+
*
|
|
1268
|
+
* MinerU is switched on by the presence of its endpoint and Exa reads its key from
|
|
1269
|
+
* the launch environment, so the answers reach those two through `$DSH_HOME/.env`
|
|
1270
|
+
* rather than through the profile layer. The file is replaced whole on every
|
|
1271
|
+
* configured start, which is also how a capability the user dropped stops applying.
|
|
1272
|
+
*
|
|
1273
|
+
* @param home - the deployment home whose env file the launcher reads.
|
|
1274
|
+
* @param answers - the interview's answers.
|
|
1275
|
+
*/
|
|
1276
|
+
function writeCapabilityEnvironment(home, answers) {
|
|
1277
|
+
const path = join(home, CAPABILITY_ENV_FILE);
|
|
1278
|
+
writeFileSync(path, capabilityEnvironment(answers, existsSync(path) ? readFileSync(path, "utf8") : ""));
|
|
1279
|
+
}
|
|
945
1280
|
/** Ask one yes/no question on the terminal. */
|
|
946
1281
|
function confirm(question) {
|
|
947
1282
|
return new Promise((resolveAnswer) => {
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The optional capabilities a Plus installation can enable, and the interview that
|
|
3
|
+
* decides which ones to set up.
|
|
4
|
+
*
|
|
5
|
+
* A standalone installation works without any of them: the agent runs, the shell
|
|
6
|
+
* runs, and files are read and written. Each entry here adds a capability whose
|
|
7
|
+
* backing service lives outside npm — a search provider that needs a key, a PDF
|
|
8
|
+
* parser that needs a local server, an Office toolchain that needs a runtime — so
|
|
9
|
+
* the choice is offered once, at install time, rather than left to a user who has
|
|
10
|
+
* no way to know what the deployment is missing.
|
|
11
|
+
*
|
|
12
|
+
* Every capability is selected by default. A user who wants the plain harness
|
|
13
|
+
* deselects them; a user who accepts the defaults ends up with a deployment whose
|
|
14
|
+
* features work, which is the state the interactive question exists to reach.
|
|
15
|
+
*
|
|
16
|
+
* Enabling a capability has to mean the feature works, so each entry carries both
|
|
17
|
+
* halves of that: the profile row that mounts it and the service it needs. A
|
|
18
|
+
* capability that only asked a question and then dropped the answer would report
|
|
19
|
+
* success while the deployment stayed exactly as it was.
|
|
20
|
+
*
|
|
21
|
+
* @module @sparkelf/dsh-plus/standalone-capabilities
|
|
22
|
+
*/
|
|
23
|
+
/** One optional capability a deployment can enable. */
|
|
24
|
+
export interface Capability {
|
|
25
|
+
/** Stable key used on the command line and in the profile. */
|
|
26
|
+
readonly id: string;
|
|
27
|
+
/** One-line name shown in the interview. */
|
|
28
|
+
readonly title: string;
|
|
29
|
+
/** What the deployment gains, shown beside the name. */
|
|
30
|
+
readonly detail: string;
|
|
31
|
+
/** Whether the backing service needs an address the user must supply. */
|
|
32
|
+
readonly needsEndpoint: boolean;
|
|
33
|
+
}
|
|
34
|
+
/** Capabilities offered at install time, in interview order. */
|
|
35
|
+
export declare const CAPABILITIES: readonly Capability[];
|
|
36
|
+
/** The answers one interview produced. */
|
|
37
|
+
export interface CapabilityAnswers {
|
|
38
|
+
/** Capability ids the user kept selected. */
|
|
39
|
+
readonly enabled: readonly string[];
|
|
40
|
+
/** Exa API key when web search was enabled and the user supplied one. */
|
|
41
|
+
readonly exaApiKey?: string;
|
|
42
|
+
/** MinerU endpoint to write; defaults to the local service this command starts. */
|
|
43
|
+
readonly mineruEndpoint?: string;
|
|
44
|
+
}
|
|
45
|
+
/** Where this command starts MinerU when the capability is enabled. */
|
|
46
|
+
export declare const DEFAULT_MINERU_ENDPOINT = "http://127.0.0.1:8000/file_parse";
|
|
47
|
+
/** File under the deployment home recording which capabilities were enabled. */
|
|
48
|
+
export declare const CAPABILITY_RECORD = "capabilities.json";
|
|
49
|
+
/** Deployment env file the launcher reads as its `user-env` layer. */
|
|
50
|
+
export declare const CAPABILITY_ENV_FILE = ".env";
|
|
51
|
+
/** Comment that identifies a profile layer this interview wrote. */
|
|
52
|
+
export declare const CAPABILITY_MARKER = "Written by dsh-plus start from the capability interview.";
|
|
53
|
+
/**
|
|
54
|
+
* Interview the user about the optional capabilities.
|
|
55
|
+
*
|
|
56
|
+
* Each is offered on its own line and defaults to enabled, so pressing Enter
|
|
57
|
+
* through the interview produces a deployment whose features work. The answers are
|
|
58
|
+
* returned rather than applied, so a caller can report what it will do before it
|
|
59
|
+
* touches the profile.
|
|
60
|
+
*
|
|
61
|
+
* @param interactive - when false, every capability is enabled without asking.
|
|
62
|
+
* @returns the ids to enable and the values the enabled ones need.
|
|
63
|
+
*/
|
|
64
|
+
export declare function interviewCapabilities(interactive: boolean): Promise<CapabilityAnswers>;
|
|
65
|
+
/**
|
|
66
|
+
* The deployment env file, with one assignment per enabled capability that reaches
|
|
67
|
+
* its service through the launch environment.
|
|
68
|
+
*
|
|
69
|
+
* MinerU is switched on by the presence of its endpoint and Exa reads its key from
|
|
70
|
+
* the launch environment, so a capability the user selected is enabled here as well
|
|
71
|
+
* as in the profile layer. Only the names this interview owns are rewritten: a
|
|
72
|
+
* deployment that keeps its own assignments in the same file keeps them, and
|
|
73
|
+
* deselecting a capability removes the assignment that turned it on.
|
|
74
|
+
*
|
|
75
|
+
* @param answers - the interview's answers.
|
|
76
|
+
* @param existing - the file's current text, when it already exists.
|
|
77
|
+
* @returns the environment file's text.
|
|
78
|
+
*/
|
|
79
|
+
export declare function capabilityEnvironment(answers: CapabilityAnswers, existing?: string): string;
|
|
80
|
+
/**
|
|
81
|
+
* The profile patch layer that turns the selected capabilities on.
|
|
82
|
+
*
|
|
83
|
+
* The file is data the profile's loader merges, so enabling a capability writes a
|
|
84
|
+
* row rather than editing the deployment. An empty selection still writes the file,
|
|
85
|
+
* because a layer that exists and mounts nothing is how a previously enabled
|
|
86
|
+
* capability is turned back off.
|
|
87
|
+
*
|
|
88
|
+
* The profile's own layer is a separate file with its own name: a deployment that
|
|
89
|
+
* configured something by hand keeps it, and the capability rows are rewritten
|
|
90
|
+
* whole on every start.
|
|
91
|
+
*
|
|
92
|
+
* @param answers - the interview's answers.
|
|
93
|
+
* @returns the patch layer's YAML text.
|
|
94
|
+
*/
|
|
95
|
+
export declare function capabilityPatchLayer(answers: CapabilityAnswers): string;
|
|
96
|
+
/**
|
|
97
|
+
* Install and start the backing services the selected capabilities need.
|
|
98
|
+
*
|
|
99
|
+
* Enabling a capability means the deployment expects its service to answer, so the
|
|
100
|
+
* command that offers the choice is also the command that provides it. Each step
|
|
101
|
+
* reports what it is doing and, on failure, the command a user can run by hand —
|
|
102
|
+
* a missing runtime is a fact about the host, not a reason to abandon the install.
|
|
103
|
+
*
|
|
104
|
+
* @param answers - the interview's answers.
|
|
105
|
+
* @param home - the deployment home, where generated service units are recorded.
|
|
106
|
+
* @returns the ids whose service is ready.
|
|
107
|
+
*/
|
|
108
|
+
export declare function installCapabilityServices(answers: CapabilityAnswers, home: string): Promise<readonly string[]>;
|
|
109
|
+
//# sourceMappingURL=standalone-capabilities.d.ts.map
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
/** Capabilities offered at install time, in interview order. */
|
|
5
|
+
export const CAPABILITIES = [
|
|
6
|
+
{
|
|
7
|
+
id: 'exa',
|
|
8
|
+
title: 'Web search (Exa)',
|
|
9
|
+
detail: 'Search the web through Exa instead of the built-in provider; needs an API key',
|
|
10
|
+
needsEndpoint: false,
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: 'mineru',
|
|
14
|
+
title: 'PDF parsing (MinerU)',
|
|
15
|
+
detail: 'Parse uploaded PDFs locally; the service is installed and started here',
|
|
16
|
+
needsEndpoint: true,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: 'officecli',
|
|
20
|
+
title: 'Office documents (OfficeCLI)',
|
|
21
|
+
detail: 'Create and open DOCX, XLSX, and PPTX deliverables',
|
|
22
|
+
needsEndpoint: false,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: 'computer-use',
|
|
26
|
+
title: 'Windows desktop control (computer-use)',
|
|
27
|
+
detail: 'Drive the Windows desktop from WSL through the cua driver',
|
|
28
|
+
needsEndpoint: false,
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
/** Where this command starts MinerU when the capability is enabled. */
|
|
32
|
+
export const DEFAULT_MINERU_ENDPOINT = 'http://127.0.0.1:8000/file_parse';
|
|
33
|
+
/** File under the deployment home recording which capabilities were enabled. */
|
|
34
|
+
export const CAPABILITY_RECORD = 'capabilities.json';
|
|
35
|
+
/** Deployment env file the launcher reads as its `user-env` layer. */
|
|
36
|
+
export const CAPABILITY_ENV_FILE = '.env';
|
|
37
|
+
/** Comment that identifies a profile layer this interview wrote. */
|
|
38
|
+
export const CAPABILITY_MARKER = 'Written by dsh-plus start from the capability interview.';
|
|
39
|
+
/** One prompt on the terminal, resolving the trimmed line the user typed. */
|
|
40
|
+
function ask(question) {
|
|
41
|
+
return new Promise((resolveAnswer) => {
|
|
42
|
+
process.stdout.write(question);
|
|
43
|
+
process.stdin.once('data', (chunk) => {
|
|
44
|
+
resolveAnswer(String(chunk).trim());
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/** Ask one yes/no question, defaulting to yes. */
|
|
49
|
+
async function confirmDefaultYes(question) {
|
|
50
|
+
const answer = (await ask(question + ' [Y/n] ')).toLowerCase();
|
|
51
|
+
return answer === '' || answer === 'y' || answer === 'yes';
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Interview the user about the optional capabilities.
|
|
55
|
+
*
|
|
56
|
+
* Each is offered on its own line and defaults to enabled, so pressing Enter
|
|
57
|
+
* through the interview produces a deployment whose features work. The answers are
|
|
58
|
+
* returned rather than applied, so a caller can report what it will do before it
|
|
59
|
+
* touches the profile.
|
|
60
|
+
*
|
|
61
|
+
* @param interactive - when false, every capability is enabled without asking.
|
|
62
|
+
* @returns the ids to enable and the values the enabled ones need.
|
|
63
|
+
*/
|
|
64
|
+
export async function interviewCapabilities(interactive) {
|
|
65
|
+
if (!interactive)
|
|
66
|
+
return { enabled: CAPABILITIES.map(capability => capability.id) };
|
|
67
|
+
console.log('');
|
|
68
|
+
console.log('Optional capabilities. Press Enter to accept the default (all enabled),');
|
|
69
|
+
console.log('or answer n for any you do not want.');
|
|
70
|
+
console.log('');
|
|
71
|
+
const enabled = [];
|
|
72
|
+
for (const capability of CAPABILITIES) {
|
|
73
|
+
if (await confirmDefaultYes(' Enable ' + capability.title + '? (' + capability.detail + ')')) {
|
|
74
|
+
enabled.push(capability.id);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
let exaApiKey;
|
|
78
|
+
if (enabled.includes('exa')) {
|
|
79
|
+
console.log('');
|
|
80
|
+
console.log(' Exa needs an API key. Create one at https://dashboard.exa.ai/api-keys');
|
|
81
|
+
const typed = await ask(' Exa API key (leave empty to add it later): ');
|
|
82
|
+
if (typed !== '')
|
|
83
|
+
exaApiKey = typed;
|
|
84
|
+
}
|
|
85
|
+
let mineruEndpoint;
|
|
86
|
+
if (enabled.includes('mineru')) {
|
|
87
|
+
// The service is installed and started by this command on the local port, so the
|
|
88
|
+
// default is the address it will answer on; a user running MinerU elsewhere
|
|
89
|
+
// overrides it here rather than editing the profile afterwards.
|
|
90
|
+
const typed = await ask(' MinerU endpoint [' + DEFAULT_MINERU_ENDPOINT + ']: ');
|
|
91
|
+
mineruEndpoint = typed === '' ? DEFAULT_MINERU_ENDPOINT : typed;
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
enabled,
|
|
95
|
+
...exaApiKey === undefined ? {} : { exaApiKey },
|
|
96
|
+
...mineruEndpoint === undefined ? {} : { mineruEndpoint },
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/** Names this module owns in the deployment env file. */
|
|
100
|
+
const CAPABILITY_ENV_NAMES = ['DSH_MINERU_ENDPOINT', 'EXA_API_KEY'];
|
|
101
|
+
/**
|
|
102
|
+
* The deployment env file, with one assignment per enabled capability that reaches
|
|
103
|
+
* its service through the launch environment.
|
|
104
|
+
*
|
|
105
|
+
* MinerU is switched on by the presence of its endpoint and Exa reads its key from
|
|
106
|
+
* the launch environment, so a capability the user selected is enabled here as well
|
|
107
|
+
* as in the profile layer. Only the names this interview owns are rewritten: a
|
|
108
|
+
* deployment that keeps its own assignments in the same file keeps them, and
|
|
109
|
+
* deselecting a capability removes the assignment that turned it on.
|
|
110
|
+
*
|
|
111
|
+
* @param answers - the interview's answers.
|
|
112
|
+
* @param existing - the file's current text, when it already exists.
|
|
113
|
+
* @returns the environment file's text.
|
|
114
|
+
*/
|
|
115
|
+
export function capabilityEnvironment(answers, existing = '') {
|
|
116
|
+
const enabled = new Set(answers.enabled);
|
|
117
|
+
const owned = new Set(CAPABILITY_ENV_NAMES);
|
|
118
|
+
const kept = existing.split('\n').filter((line) => {
|
|
119
|
+
const name = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=/.exec(line)?.[1];
|
|
120
|
+
return line.trim() !== '' && !line.trimStart().startsWith('#') && (name === undefined || !owned.has(name));
|
|
121
|
+
});
|
|
122
|
+
const written = [
|
|
123
|
+
'# Written by dsh-plus start from the capability interview. Enabling or',
|
|
124
|
+
'# disabling a capability rewrites the lines below; other lines are kept.',
|
|
125
|
+
];
|
|
126
|
+
if (enabled.has('mineru')) {
|
|
127
|
+
written.push('DSH_MINERU_ENDPOINT=' + (answers.mineruEndpoint ?? DEFAULT_MINERU_ENDPOINT));
|
|
128
|
+
}
|
|
129
|
+
if (enabled.has('exa') && answers.exaApiKey !== undefined) {
|
|
130
|
+
written.push('EXA_API_KEY=' + answers.exaApiKey);
|
|
131
|
+
}
|
|
132
|
+
return [...written, ...kept].join('\n') + '\n';
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* The profile patch layer that turns the selected capabilities on.
|
|
136
|
+
*
|
|
137
|
+
* The file is data the profile's loader merges, so enabling a capability writes a
|
|
138
|
+
* row rather than editing the deployment. An empty selection still writes the file,
|
|
139
|
+
* because a layer that exists and mounts nothing is how a previously enabled
|
|
140
|
+
* capability is turned back off.
|
|
141
|
+
*
|
|
142
|
+
* The profile's own layer is a separate file with its own name: a deployment that
|
|
143
|
+
* configured something by hand keeps it, and the capability rows are rewritten
|
|
144
|
+
* whole on every start.
|
|
145
|
+
*
|
|
146
|
+
* @param answers - the interview's answers.
|
|
147
|
+
* @returns the patch layer's YAML text.
|
|
148
|
+
*/
|
|
149
|
+
export function capabilityPatchLayer(answers) {
|
|
150
|
+
const enabled = new Set(answers.enabled);
|
|
151
|
+
const rows = [];
|
|
152
|
+
if (enabled.has('exa')) {
|
|
153
|
+
rows.push(' - id: web-search-exa', " name: '@deepseek-ai/dsh-web-search-exa'", ' config:', ' searchType: auto', ' numResults: 8');
|
|
154
|
+
}
|
|
155
|
+
if (enabled.has('computer-use')) {
|
|
156
|
+
rows.push(' - id: computer-use', " name: '@deepseek-ai/dsh-computer-use'", ' - id: computer-use-cua-driver-mcp', " name: '@deepseek-ai/dsh-experimental-computer-use-cua-driver-mcp'");
|
|
157
|
+
}
|
|
158
|
+
const lines = [
|
|
159
|
+
'# ' + CAPABILITY_MARKER + ' Enabling or disabling a capability',
|
|
160
|
+
'# rewrites this file; edits here are replaced.',
|
|
161
|
+
];
|
|
162
|
+
if (rows.length > 0)
|
|
163
|
+
lines.push('- insert:', ...rows);
|
|
164
|
+
if (enabled.has('exa')) {
|
|
165
|
+
lines.push('', '# Route web_search through Exa rather than the built-in provider.', '- id: web', ' config:', ' searchProvider: exa');
|
|
166
|
+
}
|
|
167
|
+
return lines.join('\n') + '\n';
|
|
168
|
+
}
|
|
169
|
+
/** Run one command, reporting a failure rather than throwing. */
|
|
170
|
+
function runStep(command, args) {
|
|
171
|
+
const result = spawnSync(command, [...args], { stdio: 'inherit' });
|
|
172
|
+
return result.status === 0;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Install and start the backing services the selected capabilities need.
|
|
176
|
+
*
|
|
177
|
+
* Enabling a capability means the deployment expects its service to answer, so the
|
|
178
|
+
* command that offers the choice is also the command that provides it. Each step
|
|
179
|
+
* reports what it is doing and, on failure, the command a user can run by hand —
|
|
180
|
+
* a missing runtime is a fact about the host, not a reason to abandon the install.
|
|
181
|
+
*
|
|
182
|
+
* @param answers - the interview's answers.
|
|
183
|
+
* @param home - the deployment home, where generated service units are recorded.
|
|
184
|
+
* @returns the ids whose service is ready.
|
|
185
|
+
*/
|
|
186
|
+
export async function installCapabilityServices(answers, home) {
|
|
187
|
+
const enabled = new Set(answers.enabled);
|
|
188
|
+
const ready = [];
|
|
189
|
+
// The record lets a later run name the capabilities this deployment enabled
|
|
190
|
+
// instead of interviewing again, and gives doctor something to check against.
|
|
191
|
+
mkdirSync(home, { recursive: true });
|
|
192
|
+
writeFileSync(join(home, CAPABILITY_RECORD), JSON.stringify({
|
|
193
|
+
enabled: [...answers.enabled],
|
|
194
|
+
...answers.mineruEndpoint === undefined ? {} : { mineruEndpoint: answers.mineruEndpoint },
|
|
195
|
+
}, null, 2) + '\n');
|
|
196
|
+
if (enabled.has('mineru')) {
|
|
197
|
+
console.log('');
|
|
198
|
+
console.log(' MinerU: installing the parser and starting its service...');
|
|
199
|
+
const installed = installMineru(home);
|
|
200
|
+
if (installed && startMineru(home)) {
|
|
201
|
+
ready.push('mineru');
|
|
202
|
+
console.log(' MinerU: ready at ' + (answers.mineruEndpoint ?? DEFAULT_MINERU_ENDPOINT));
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
console.log(' MinerU: not installed. Install it later with:');
|
|
206
|
+
console.log(' pip install -U "mineru[core]"');
|
|
207
|
+
console.log(' mineru-api --host 127.0.0.1 --port 8000');
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (enabled.has('officecli')) {
|
|
211
|
+
// OfficeCLI ships inside the mounted bundle and carries its own binary, so the
|
|
212
|
+
// capability needs no host step; the profile row is what turns it on.
|
|
213
|
+
ready.push('officecli');
|
|
214
|
+
}
|
|
215
|
+
if (enabled.has('computer-use')) {
|
|
216
|
+
if (desktopDriverAvailable()) {
|
|
217
|
+
ready.push('computer-use');
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
console.log('');
|
|
221
|
+
console.log(' computer-use: the cua driver was not found on PATH. The plugin is');
|
|
222
|
+
console.log(' still mounted, so it starts once the driver is installed:');
|
|
223
|
+
console.log(' install the cua-driver release for this architecture, then restart');
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return ready;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Install MinerU when it is absent and start its API service.
|
|
230
|
+
*
|
|
231
|
+
* MinerU is a Python package whose API server answers on a local port; the profile
|
|
232
|
+
* points at that port. The parser is installed into a virtual environment under the
|
|
233
|
+
* deployment home and an existing one is upgraded rather than skipped, so enabling
|
|
234
|
+
* the capability keeps the parser current without touching the system interpreter.
|
|
235
|
+
*
|
|
236
|
+
* @param home - the deployment home that owns the venv.
|
|
237
|
+
* @returns whether the package is installed; not whether it answered.
|
|
238
|
+
*/
|
|
239
|
+
function installMineru(home) {
|
|
240
|
+
const python = pythonInterpreter();
|
|
241
|
+
if (python === undefined) {
|
|
242
|
+
console.log(' MinerU: no Python interpreter found on PATH.');
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
// The venv keeps MinerU's torch and model dependencies away from the system
|
|
246
|
+
// interpreter, which is also what PEP 668 requires: a system pip refuses the install
|
|
247
|
+
// outright, and `--break-system-packages` would put a multi-gigabyte torch tree into
|
|
248
|
+
// the OS packages. `all` is the extra both the 3.x and 4.x lines publish; `core`
|
|
249
|
+
// existed only through 3.x and installing it on 4.x silently drops the extras.
|
|
250
|
+
const venv = join(home, '.mineru-venv');
|
|
251
|
+
if (!runStep(python, ['-m', 'venv', venv]))
|
|
252
|
+
return false;
|
|
253
|
+
const venvPython = join(venv, 'bin', 'python');
|
|
254
|
+
return runStep(venvPython, ['-m', 'pip', 'install', '-U', 'mineru[all]']);
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Start the MinerU API server as a background service when none answers yet.
|
|
258
|
+
*
|
|
259
|
+
* The service has to outlive the install command, so it is registered with the
|
|
260
|
+
* host's service manager when one is available and otherwise started detached.
|
|
261
|
+
*
|
|
262
|
+
* @returns whether the server is running after this call.
|
|
263
|
+
*/
|
|
264
|
+
function startMineru(home) {
|
|
265
|
+
const api = mineruApiBinary(home);
|
|
266
|
+
if (api === undefined)
|
|
267
|
+
return false;
|
|
268
|
+
if (mineruAnswers())
|
|
269
|
+
return true;
|
|
270
|
+
if (spawnSync('systemctl', ['--version'], { stdio: 'ignore' }).status === 0) {
|
|
271
|
+
writeFileSync('/etc/systemd/system/mineru-api.service', [
|
|
272
|
+
'[Unit]',
|
|
273
|
+
'Description=MinerU document parsing API for DeepSeek Harness Plus',
|
|
274
|
+
'After=network-online.target',
|
|
275
|
+
'Wants=network-online.target',
|
|
276
|
+
'',
|
|
277
|
+
'[Service]',
|
|
278
|
+
'Type=simple',
|
|
279
|
+
'User=root',
|
|
280
|
+
'ExecStart=' + api + ' --host 127.0.0.1 --port 8000',
|
|
281
|
+
'Restart=on-failure',
|
|
282
|
+
'RestartSec=2',
|
|
283
|
+
'TimeoutStopSec=15',
|
|
284
|
+
'',
|
|
285
|
+
'[Install]',
|
|
286
|
+
'WantedBy=multi-user.target',
|
|
287
|
+
'',
|
|
288
|
+
].join('\n'));
|
|
289
|
+
spawnSync('systemctl', ['daemon-reload'], { stdio: 'ignore' });
|
|
290
|
+
return spawnSync('systemctl', ['enable', '--now', 'mineru-api.service'], { stdio: 'ignore' }).status === 0;
|
|
291
|
+
}
|
|
292
|
+
console.log(' MinerU: start the API server with:');
|
|
293
|
+
console.log(' ' + api + ' --host 127.0.0.1 --port 8000');
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
/** The Python interpreter to install MinerU with, preferring python3. */
|
|
297
|
+
function pythonInterpreter() {
|
|
298
|
+
return ['python3', 'python'].find(candidate => spawnSync(candidate, ['--version'], { stdio: 'ignore' }).status === 0);
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* The MinerU API entry point, wherever the install placed it.
|
|
302
|
+
*
|
|
303
|
+
* The venv this command creates comes first: it is the interpreter the parser was
|
|
304
|
+
* installed into, so its `mineru-api` is the one that can import MinerU. A binary on
|
|
305
|
+
* PATH is a fallback for a deployment that installed MinerU some other way.
|
|
306
|
+
*
|
|
307
|
+
* @param home - the deployment home that owns the venv.
|
|
308
|
+
* @returns the command to run, or `undefined` when none answers.
|
|
309
|
+
*/
|
|
310
|
+
function mineruApiBinary(home) {
|
|
311
|
+
const candidates = [join(home, '.mineru-venv', 'bin', 'mineru-api'), 'mineru-api'];
|
|
312
|
+
return candidates.find(candidate => spawnSync(candidate, ['--help'], { stdio: 'ignore' }).status === 0);
|
|
313
|
+
}
|
|
314
|
+
/** Whether a MinerU API server already answers on the default port. */
|
|
315
|
+
function mineruAnswers() {
|
|
316
|
+
const probe = spawnSync('curl', ['-sf', '-o', '/dev/null', '--max-time', '3', 'http://127.0.0.1:8000/docs'], { stdio: 'ignore' });
|
|
317
|
+
return probe.status === 0;
|
|
318
|
+
}
|
|
319
|
+
/** Whether the Windows-side desktop driver answers from this shell. */
|
|
320
|
+
function desktopDriverAvailable() {
|
|
321
|
+
const probe = spawnSync('cua-driver', ['--version'], { stdio: 'ignore' });
|
|
322
|
+
return probe.status === 0;
|
|
323
|
+
}
|
|
324
|
+
//# sourceMappingURL=standalone-capabilities.js.map
|
|
@@ -13,10 +13,13 @@ import { createRequire } from 'node:module';
|
|
|
13
13
|
import { dirname, join } from 'node:path';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
15
|
import { newerVersion } from "./registry-versions.js";
|
|
16
|
+
import { CAPABILITY_ENV_FILE, CAPABILITY_MARKER, CAPABILITY_RECORD, capabilityEnvironment, capabilityPatchLayer, installCapabilityServices, interviewCapabilities, } from "./standalone-capabilities.js";
|
|
16
17
|
import { DEFAULT_PORT, STOP_GRACE_MILLISECONDS, choosePort, clearState, isRunning, portAvailable, readState, spawnServer, stateDirectory, waitForAuthenticatedUrl, waitForServer, writeState, } from "./standalone-server.js";
|
|
17
18
|
import { STANDALONE_PROFILE, applyProfileNpmPatches, ensureProfile, pnpmAvailable, pnpmInstallCommand, pnpmInstallCommands, registryOrder, readDistributionProfile, resolvePaths, } from "./standalone-profile.js";
|
|
18
19
|
/** Milliseconds a start waits for the server to answer before reporting failure. */
|
|
19
20
|
const READY_TIMEOUT_MILLISECONDS = 90_000;
|
|
21
|
+
/** Profile patch file the capability interview rewrites (the profile's user layer). */
|
|
22
|
+
const CAPABILITY_PATCH_FILE = 'cordis.patch.yml';
|
|
20
23
|
function parseStartOptions(argv) {
|
|
21
24
|
let port = DEFAULT_PORT;
|
|
22
25
|
let host = '127.0.0.1';
|
|
@@ -149,6 +152,24 @@ async function start(argv) {
|
|
|
149
152
|
for (const label of applyProfileNpmPatches(paths.distributionDirectory, paths.profileDirectory)) {
|
|
150
153
|
console.log('Applied the reviewed patch ' + label);
|
|
151
154
|
}
|
|
155
|
+
// The capability interview runs once, when the profile is created: a deployment
|
|
156
|
+
// that already answered keeps its answers, and a re-run only reports them. Asking
|
|
157
|
+
// on every start would make a restart look like a first install.
|
|
158
|
+
const needsInterview = created || !existsSync(join(paths.home, CAPABILITY_RECORD));
|
|
159
|
+
const answers = needsInterview
|
|
160
|
+
? await interviewCapabilities(true)
|
|
161
|
+
: undefined;
|
|
162
|
+
if (answers !== undefined) {
|
|
163
|
+
const ready = await installCapabilityServices(answers, paths.home);
|
|
164
|
+
writeCapabilityPatch(paths.profileDirectory, answers);
|
|
165
|
+
writeCapabilityEnvironment(paths.home, answers);
|
|
166
|
+
console.log(' Enabled: ' + (answers.enabled.length === 0 ? '(none)' : answers.enabled.join(', ')));
|
|
167
|
+
if (answers.enabled.length > 0)
|
|
168
|
+
console.log(' Services ready: ' + (ready.length === 0 ? '(none)' : ready.join(', ')));
|
|
169
|
+
if (answers.enabled.includes('exa') && answers.exaApiKey === undefined) {
|
|
170
|
+
console.log(' Exa: add EXA_API_KEY to ' + join(paths.home, CAPABILITY_ENV_FILE) + ' when you have a key.');
|
|
171
|
+
}
|
|
172
|
+
}
|
|
152
173
|
const entry = launcherEntry(anchor);
|
|
153
174
|
if (options.foreground)
|
|
154
175
|
return runForeground(entry, options.port, options.host, options.open);
|
|
@@ -282,6 +303,44 @@ async function update(argv) {
|
|
|
282
303
|
}
|
|
283
304
|
return 0;
|
|
284
305
|
}
|
|
306
|
+
/**
|
|
307
|
+
* Write the profile's capability patch layer.
|
|
308
|
+
*
|
|
309
|
+
* The profile's loader merges this file, so enabling a capability is a data change
|
|
310
|
+
* rather than an edit to the deployment. Writing it on every configured run also
|
|
311
|
+
* turns a capability back off when the interview no longer selects it.
|
|
312
|
+
*
|
|
313
|
+
* @param profileDirectory - the profile whose layer is replaced.
|
|
314
|
+
* @param answers - the interview's answers.
|
|
315
|
+
*/
|
|
316
|
+
function writeCapabilityPatch(profileDirectory, answers) {
|
|
317
|
+
const path = join(profileDirectory, CAPABILITY_PATCH_FILE);
|
|
318
|
+
// The profile has exactly one user layer, so the capability rows live in it. A file
|
|
319
|
+
// this command did not write belongs to the deployment, and replacing it would drop
|
|
320
|
+
// whatever the operator put there; keep it beside the new layer instead.
|
|
321
|
+
const existing = existsSync(path) ? readFileSync(path, 'utf8') : undefined;
|
|
322
|
+
if (existing !== undefined && !existing.includes(CAPABILITY_MARKER)) {
|
|
323
|
+
writeFileSync(path + '.before-capabilities', existing);
|
|
324
|
+
console.log(' Kept the existing profile layer at ' + CAPABILITY_PATCH_FILE + '.before-capabilities');
|
|
325
|
+
}
|
|
326
|
+
writeFileSync(path, capabilityPatchLayer(answers));
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Write the deployment's capability environment file.
|
|
330
|
+
*
|
|
331
|
+
* MinerU is switched on by the presence of its endpoint and Exa reads its key from
|
|
332
|
+
* the launch environment, so the answers reach those two through `$DSH_HOME/.env`
|
|
333
|
+
* rather than through the profile layer. The file is replaced whole on every
|
|
334
|
+
* configured start, which is also how a capability the user dropped stops applying.
|
|
335
|
+
*
|
|
336
|
+
* @param home - the deployment home whose env file the launcher reads.
|
|
337
|
+
* @param answers - the interview's answers.
|
|
338
|
+
*/
|
|
339
|
+
function writeCapabilityEnvironment(home, answers) {
|
|
340
|
+
const path = join(home, CAPABILITY_ENV_FILE);
|
|
341
|
+
const existing = existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
342
|
+
writeFileSync(path, capabilityEnvironment(answers, existing));
|
|
343
|
+
}
|
|
285
344
|
/** Ask one yes/no question on the terminal. */
|
|
286
345
|
function confirm(question) {
|
|
287
346
|
return new Promise((resolveAnswer) => {
|
package/package.json
CHANGED
|
@@ -3,28 +3,31 @@
|
|
|
3
3
|
"dsh-plus": "lib/bin.js"
|
|
4
4
|
},
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"@deepseek-ai/dsh-
|
|
6
|
+
"@deepseek-ai/dsh-computer-use": ">=0.1.6-alpha.1",
|
|
7
|
+
"@deepseek-ai/dsh-experimental-computer-use-cua-driver-mcp": ">=0.1.6-alpha.1",
|
|
8
|
+
"@deepseek-ai/dsh-tool-session-query": ">=0.1.6-alpha.1",
|
|
9
|
+
"@deepseek-ai/dsh-web-search-exa": ">=0.1.6-alpha.1",
|
|
7
10
|
"@sparkelf/dsh-client-ui-skill-center": ">=0.2.0-rc.9",
|
|
8
11
|
"@sparkelf/dsh-mobile-bridge": ">=0.2.11",
|
|
9
|
-
"@sparkelf/dsh-patch-better-sidebar-browser-url-seed": ">=0.2.0-rc.
|
|
10
|
-
"@sparkelf/dsh-patch-better-sidebar-html-preview-path": ">=0.2.0-rc.
|
|
11
|
-
"@sparkelf/dsh-patch-better-sidebar-main-view-session": ">=0.2.0-rc.
|
|
12
|
-
"@sparkelf/dsh-patch-better-sidebar-media-path": ">=0.2.0-rc.
|
|
13
|
-
"@sparkelf/dsh-patch-browser-auth-mode": ">=0.2.0-rc.
|
|
14
|
-
"@sparkelf/dsh-patch-composer-popover-boundaries": ">=0.2.0-rc.
|
|
15
|
-
"@sparkelf/dsh-patch-legacy-code-preset": ">=0.2.0-rc.
|
|
16
|
-
"@sparkelf/dsh-patch-mobile-journal-generation": ">=0.2.0-rc.
|
|
17
|
-
"@sparkelf/dsh-patch-officecli-deliverables": ">=0.2.0-rc.
|
|
18
|
-
"@sparkelf/dsh-patch-ptc-mcp-schema-types": ">=0.2.0-rc.
|
|
19
|
-
"@sparkelf/dsh-patch-responses-reasoning-status": ">=0.2.0-rc.
|
|
20
|
-
"@sparkelf/dsh-patch-session-export-chinese": ">=0.2.0-rc.
|
|
21
|
-
"@sparkelf/dsh-patch-session-format-legacy-restart": ">=0.2.0-rc.
|
|
22
|
-
"@sparkelf/dsh-patch-session-log-trajectory-toolbar": ">=0.2.0-rc.
|
|
23
|
-
"@sparkelf/dsh-patch-session-query-unindexable-session": ">=0.2.0-rc.
|
|
24
|
-
"@sparkelf/dsh-patch-subagent-settings-presets": ">=0.2.0-rc.
|
|
25
|
-
"@sparkelf/dsh-patch-web-base-path": ">=0.2.0-rc.
|
|
26
|
-
"@sparkelf/dsh-patch-workspace-storage-restore": ">=0.2.0-rc.
|
|
27
|
-
"@sparkelf/dsh-patch-wsl-native-open": ">=0.2.0-rc.
|
|
12
|
+
"@sparkelf/dsh-patch-better-sidebar-browser-url-seed": ">=0.2.0-rc.16",
|
|
13
|
+
"@sparkelf/dsh-patch-better-sidebar-html-preview-path": ">=0.2.0-rc.16",
|
|
14
|
+
"@sparkelf/dsh-patch-better-sidebar-main-view-session": ">=0.2.0-rc.16",
|
|
15
|
+
"@sparkelf/dsh-patch-better-sidebar-media-path": ">=0.2.0-rc.16",
|
|
16
|
+
"@sparkelf/dsh-patch-browser-auth-mode": ">=0.2.0-rc.16",
|
|
17
|
+
"@sparkelf/dsh-patch-composer-popover-boundaries": ">=0.2.0-rc.16",
|
|
18
|
+
"@sparkelf/dsh-patch-legacy-code-preset": ">=0.2.0-rc.16",
|
|
19
|
+
"@sparkelf/dsh-patch-mobile-journal-generation": ">=0.2.0-rc.16",
|
|
20
|
+
"@sparkelf/dsh-patch-officecli-deliverables": ">=0.2.0-rc.16",
|
|
21
|
+
"@sparkelf/dsh-patch-ptc-mcp-schema-types": ">=0.2.0-rc.16",
|
|
22
|
+
"@sparkelf/dsh-patch-responses-reasoning-status": ">=0.2.0-rc.16",
|
|
23
|
+
"@sparkelf/dsh-patch-session-export-chinese": ">=0.2.0-rc.16",
|
|
24
|
+
"@sparkelf/dsh-patch-session-format-legacy-restart": ">=0.2.0-rc.16",
|
|
25
|
+
"@sparkelf/dsh-patch-session-log-trajectory-toolbar": ">=0.2.0-rc.16",
|
|
26
|
+
"@sparkelf/dsh-patch-session-query-unindexable-session": ">=0.2.0-rc.16",
|
|
27
|
+
"@sparkelf/dsh-patch-subagent-settings-presets": ">=0.2.0-rc.16",
|
|
28
|
+
"@sparkelf/dsh-patch-web-base-path": ">=0.2.0-rc.16",
|
|
29
|
+
"@sparkelf/dsh-patch-workspace-storage-restore": ">=0.2.0-rc.16",
|
|
30
|
+
"@sparkelf/dsh-patch-wsl-native-open": ">=0.2.0-rc.16",
|
|
28
31
|
"@sparkelf/dsh-plugin-backup": ">=0.2.0-rc.8",
|
|
29
32
|
"@sparkelf/dsh-plugin-dataops": ">=0.2.0-rc.8",
|
|
30
33
|
"@sparkelf/dsh-plugin-mcp-credentials": ">=0.2.0-rc.8",
|
|
@@ -186,5 +189,5 @@
|
|
|
186
189
|
},
|
|
187
190
|
"type": "module",
|
|
188
191
|
"types": "lib/types/index.d.ts",
|
|
189
|
-
"version": "0.2.0-rc.
|
|
192
|
+
"version": "0.2.0-rc.16"
|
|
190
193
|
}
|