paseo-dsh-direct 0.2.4 → 0.2.6
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/README.md +7 -0
- package/README.zh.md +7 -0
- package/index.server.ts +2 -0
- package/package.json +1 -1
- package/server/bridge-client.ts +6 -7
- package/server/dsh-spawn.ts +134 -0
- package/server/dsh-version.ts +2 -3
- package/server/provider.ts +3 -1
- package/server/timeline.ts +2 -14
package/README.md
CHANGED
|
@@ -15,6 +15,13 @@ In Paseo, open **Settings → Plugins → Install Plugin** and paste either the
|
|
|
15
15
|
| `0.2.x` | `npm:paseo-dsh-direct` | `https://github.com/Jecvay/paseo-dsh-direct` |
|
|
16
16
|
| `0.1.x` | `npm:paseo-dsh-direct@dsh-0.1` | CLI only: `paseo plugin install https://github.com/Jecvay/paseo-dsh-direct --ref release/0.1` |
|
|
17
17
|
|
|
18
|
+
To update, run on the daemon machine (the app's plugin menu has no update entry):
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
paseo plugin update paseo-dsh-direct --check # see if a new version exists
|
|
22
|
+
paseo plugin update paseo-dsh-direct --yes # update to the latest
|
|
23
|
+
```
|
|
24
|
+
|
|
18
25
|
More: [compatibility](docs/en/compatibility.md) · [CLI and local install, environment variables](docs/en/installation.md)
|
|
19
26
|
|
|
20
27
|
## Why "Direct"
|
package/README.zh.md
CHANGED
|
@@ -15,6 +15,13 @@
|
|
|
15
15
|
| `0.2.x` | `npm:paseo-dsh-direct` | `https://github.com/Jecvay/paseo-dsh-direct` |
|
|
16
16
|
| `0.1.x` | `npm:paseo-dsh-direct@dsh-0.1` | 只能用命令行:`paseo plugin install https://github.com/Jecvay/paseo-dsh-direct --ref release/0.1` |
|
|
17
17
|
|
|
18
|
+
更新插件在 daemon 所在机器上运行(App 的插件菜单里没有更新入口):
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
paseo plugin update paseo-dsh-direct --check # 看有没有新版本
|
|
22
|
+
paseo plugin update paseo-dsh-direct --yes # 更新到最新
|
|
23
|
+
```
|
|
24
|
+
|
|
18
25
|
更多:[兼容性说明](docs/compatibility.md) · [命令行安装、本地目录安装、环境变量](docs/plugin-guide.md)
|
|
19
26
|
|
|
20
27
|
## 为什么叫 Direct
|
package/index.server.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { PluginServerContext } from "@getpaseo/plugin/server";
|
|
2
2
|
import { launchDshBridge } from "./server/bridge-client.js";
|
|
3
|
+
import { defaultDshCommand } from "./server/dsh-spawn.js";
|
|
3
4
|
import { DSH_BRIDGE_SOURCE } from "./server/generated-bridge.js";
|
|
4
5
|
import { createDshProvider } from "./server/provider.js";
|
|
5
6
|
|
|
@@ -8,6 +9,7 @@ export default function contribute(server: PluginServerContext) {
|
|
|
8
9
|
server.registerProvider(
|
|
9
10
|
createDshProvider({
|
|
10
11
|
executable,
|
|
12
|
+
command: defaultDshCommand(process.env),
|
|
11
13
|
createBridge: (launch) =>
|
|
12
14
|
launchDshBridge({
|
|
13
15
|
...launch,
|
package/package.json
CHANGED
package/server/bridge-client.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ChildProcessWithoutNullStreams } from "node:child_process";
|
|
2
2
|
import { mkdtemp, rm, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import { homedir, tmpdir } from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { createInterface } from "node:readline";
|
|
6
6
|
import { detectDshVersion } from "./dsh-version.js";
|
|
7
|
+
import { spawnDsh } from "./dsh-spawn.js";
|
|
7
8
|
import type { ProviderLaunch } from "@getpaseo/plugin/server/provider";
|
|
8
9
|
import type {
|
|
9
10
|
BridgeInitializeResult,
|
|
@@ -182,10 +183,10 @@ function runProfileInit(
|
|
|
182
183
|
timeoutMs: number,
|
|
183
184
|
): Promise<void> {
|
|
184
185
|
return new Promise((resolve, reject) => {
|
|
185
|
-
const child =
|
|
186
|
+
const child = spawnDsh(
|
|
186
187
|
executable,
|
|
187
188
|
[...leadingArgs, "--profile", profile, "--from-default-profile", DEFAULT_PROFILE_TEMPLATE, "--dump-config"],
|
|
188
|
-
{ env, stdio: ["ignore", "ignore", "pipe"]
|
|
189
|
+
{ env, stdio: ["ignore", "ignore", "pipe"] },
|
|
189
190
|
);
|
|
190
191
|
let stderr = "";
|
|
191
192
|
child.stderr.setEncoding("utf8");
|
|
@@ -293,10 +294,9 @@ function dumpProfileConfig(
|
|
|
293
294
|
env: Readonly<Record<string, string | undefined>>,
|
|
294
295
|
): Promise<string> {
|
|
295
296
|
return new Promise((resolve, reject) => {
|
|
296
|
-
const child =
|
|
297
|
+
const child = spawnDsh(launch.command, [...launch.args, "--profile", profile, "--dump-config"], {
|
|
297
298
|
env,
|
|
298
299
|
stdio: ["ignore", "pipe", "ignore"],
|
|
299
|
-
windowsHide: true,
|
|
300
300
|
});
|
|
301
301
|
let stdout = "";
|
|
302
302
|
child.stdout.setEncoding("utf8");
|
|
@@ -360,10 +360,9 @@ export async function launchDshBridge(options: LaunchBridgeOptions): Promise<Dsh
|
|
|
360
360
|
{ mode: 0o600 },
|
|
361
361
|
);
|
|
362
362
|
|
|
363
|
-
const child =
|
|
363
|
+
const child = spawnDsh(launch.command, [...launch.args, "--profile", profile, "--patch", patchPath], {
|
|
364
364
|
env,
|
|
365
365
|
stdio: ["pipe", "pipe", "pipe"],
|
|
366
|
-
windowsHide: true,
|
|
367
366
|
});
|
|
368
367
|
const client = new JsonlBridgeClient(child, directory, launch);
|
|
369
368
|
try {
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { spawn, type SpawnOptions } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
type Env = Readonly<Record<string, string | undefined>>;
|
|
6
|
+
|
|
7
|
+
/** A command line `child_process.spawn` can start without a shell. */
|
|
8
|
+
export interface DshSpawn {
|
|
9
|
+
command: string;
|
|
10
|
+
args: string[];
|
|
11
|
+
/** The environment to spawn with: the launch environment, plus what the resolved command needs. */
|
|
12
|
+
env: Env;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** The host facts the resolution depends on, injected so the Windows branch is testable anywhere. */
|
|
16
|
+
export interface DshSpawnHost {
|
|
17
|
+
platform: NodeJS.Platform;
|
|
18
|
+
exists(file: string): boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const defaultHost: DshSpawnHost = { platform: process.platform, exists: existsSync };
|
|
22
|
+
const DESKTOP_EXE = "DeepSeek Harness.exe";
|
|
23
|
+
const WRAPPER_EXTENSIONS = [".cmd", ".ps1"];
|
|
24
|
+
/** Extensions Node finds itself when spawning a bare name; a match means a real executable wins. */
|
|
25
|
+
const EXECUTABLE_EXTENSIONS = [".com", ".exe"];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Spawns dsh without a shell: `spawn` with `command` / `args` / `env` replaced
|
|
29
|
+
* by `resolveDshSpawn(command, args, options.env)` and `windowsHide` set. Every
|
|
30
|
+
* dsh process the plugin starts goes through here.
|
|
31
|
+
*/
|
|
32
|
+
export const spawnDsh = ((command: string, args: readonly string[], options: SpawnOptions) => {
|
|
33
|
+
const target = resolveDshSpawn(command, args, options.env ?? process.env);
|
|
34
|
+
return spawn(target.command, target.args, { ...options, env: target.env, windowsHide: true });
|
|
35
|
+
}) as typeof spawn;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The spawn for a dsh command chosen by `resolveDshLaunch`, with `args` being
|
|
39
|
+
* the launch's leading arguments followed by dsh's own. Other platforms, real
|
|
40
|
+
* executables and unrecognised layouts pass through unchanged; on Windows an
|
|
41
|
+
* npm or DSH Desktop `dsh.cmd` / `dsh.ps1` wrapper becomes the command it runs,
|
|
42
|
+
* and a bare `dsh` not on `PATH` falls back to DSH Desktop's default install.
|
|
43
|
+
* Layouts and reasons: `docs/server-architecture.md` (dsh 的选择).
|
|
44
|
+
*/
|
|
45
|
+
export function resolveDshSpawn(
|
|
46
|
+
command: string,
|
|
47
|
+
args: readonly string[],
|
|
48
|
+
env: Env,
|
|
49
|
+
host: DshSpawnHost = defaultHost,
|
|
50
|
+
): DshSpawn {
|
|
51
|
+
const passthrough = { command, args: [...args], env };
|
|
52
|
+
if (host.platform !== "win32") return passthrough;
|
|
53
|
+
if (path.win32.basename(command).toLowerCase() === DESKTOP_EXE.toLowerCase()) {
|
|
54
|
+
return desktopSpawn(path.win32.dirname(command), args, env, host) ?? passthrough;
|
|
55
|
+
}
|
|
56
|
+
const found = findCommand(command, env, host);
|
|
57
|
+
if (found.kind === "executable") return passthrough;
|
|
58
|
+
if (found.kind === "missing") {
|
|
59
|
+
return (command.toLowerCase() === "dsh" ? desktopFallback(args, env, host) : undefined) ?? passthrough;
|
|
60
|
+
}
|
|
61
|
+
const directory = path.win32.dirname(found.path);
|
|
62
|
+
return (
|
|
63
|
+
npmSpawn(directory, args, env, host) ??
|
|
64
|
+
// <install>\resources\runtime\cli\bin\dsh.cmd
|
|
65
|
+
desktopSpawn(path.win32.resolve(directory, "..", "..", "..", ".."), args, env, host) ??
|
|
66
|
+
passthrough
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The command the provider registers with Paseo. Paseo resolves it on `PATH`
|
|
72
|
+
* before the plugin sees it, so on Windows, when `PATH` has no `dsh` but DSH
|
|
73
|
+
* Desktop is installed in its default location, the desktop executable is
|
|
74
|
+
* registered instead; `resolveDshSpawn` then runs it as dsh. Otherwise `dsh`.
|
|
75
|
+
*/
|
|
76
|
+
export function defaultDshCommand(env: Env, host: DshSpawnHost = defaultHost): string {
|
|
77
|
+
if (host.platform !== "win32" || findCommand("dsh", env, host).kind !== "missing") return "dsh";
|
|
78
|
+
return desktopFallback([], env, host)?.command ?? "dsh";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** npm's layout: `node <wrapper dir>\node_modules\@deepseek-ai\dsh\lib\bin.js`, preferring a `node.exe` beside the wrapper. */
|
|
82
|
+
function npmSpawn(directory: string, args: readonly string[], env: Env, host: DshSpawnHost): DshSpawn | undefined {
|
|
83
|
+
const binJs = path.win32.join(directory, "node_modules", "@deepseek-ai", "dsh", "lib", "bin.js");
|
|
84
|
+
if (!host.exists(binJs)) return undefined;
|
|
85
|
+
const bundledNode = path.win32.join(directory, "node.exe");
|
|
86
|
+
return { command: host.exists(bundledNode) ? bundledNode : "node", args: [binJs, ...args], env };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** DSH Desktop's default per-user install, `%LOCALAPPDATA%\Programs\DeepSeek Harness`, when present. */
|
|
90
|
+
function desktopFallback(args: readonly string[], env: Env, host: DshSpawnHost): DshSpawn | undefined {
|
|
91
|
+
const local = windowsEnv(env, "LOCALAPPDATA");
|
|
92
|
+
return local ? desktopSpawn(path.win32.join(local, "Programs", "DeepSeek Harness"), args, env, host) : undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** DSH Desktop's own CLI, as its `dsh.cmd` runs it, when `install` holds a desktop install. */
|
|
96
|
+
function desktopSpawn(install: string, args: readonly string[], env: Env, host: DshSpawnHost): DshSpawn | undefined {
|
|
97
|
+
const exe = path.win32.join(install, DESKTOP_EXE);
|
|
98
|
+
const asar = path.win32.join(install, "resources", "app.asar");
|
|
99
|
+
if (!host.exists(exe) || !host.exists(asar)) return undefined;
|
|
100
|
+
const cli = path.win32.join(asar, "dsh", "node_modules", "@deepseek-ai", "dsh-desktop-host", "lib", "cli.js");
|
|
101
|
+
return { command: exe, args: ["--expose-internals", cli, ...args], env: { ...env, ELECTRON_RUN_AS_NODE: "1" } };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
type Found = { kind: "executable" } | { kind: "wrapper"; path: string } | { kind: "missing" };
|
|
105
|
+
|
|
106
|
+
/** What `command` names: a wrapper script, a real executable Node finds first, or nothing. */
|
|
107
|
+
function findCommand(command: string, env: Env, host: DshSpawnHost): Found {
|
|
108
|
+
const extension = path.win32.extname(command).toLowerCase();
|
|
109
|
+
if (extension !== "" && !WRAPPER_EXTENSIONS.includes(extension)) return { kind: "executable" };
|
|
110
|
+
const candidates = (base: string) => (extension ? [base] : WRAPPER_EXTENSIONS.map((suffix) => base + suffix));
|
|
111
|
+
const wrapperAt = (base: string): Found => {
|
|
112
|
+
const wrapper = candidates(base).find((candidate) => host.exists(candidate));
|
|
113
|
+
return wrapper === undefined ? { kind: "missing" } : { kind: "wrapper", path: wrapper };
|
|
114
|
+
};
|
|
115
|
+
if (/[\\/]/.test(command)) return wrapperAt(command);
|
|
116
|
+
for (const directory of (windowsEnv(env, "PATH") ?? "").split(";")) {
|
|
117
|
+
if (!directory.trim()) continue;
|
|
118
|
+
const base = path.win32.join(directory.trim().replace(/^"|"$/g, ""), command);
|
|
119
|
+
if (extension === "" && EXECUTABLE_EXTENSIONS.some((suffix) => host.exists(base + suffix))) {
|
|
120
|
+
return { kind: "executable" };
|
|
121
|
+
}
|
|
122
|
+
const found = wrapperAt(base);
|
|
123
|
+
if (found.kind === "wrapper") return found;
|
|
124
|
+
}
|
|
125
|
+
return { kind: "missing" };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Windows environment names are case-insensitive (`Path` as well as `PATH`). */
|
|
129
|
+
function windowsEnv(env: Env, key: string): string | undefined {
|
|
130
|
+
for (const [name, value] of Object.entries(env)) {
|
|
131
|
+
if (name.toUpperCase() === key && value) return value;
|
|
132
|
+
}
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
package/server/dsh-version.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawnDsh } from "./dsh-spawn.js";
|
|
2
2
|
|
|
3
3
|
const DEFAULT_TIMEOUT_MS = 3_000;
|
|
4
4
|
|
|
@@ -41,10 +41,9 @@ function runVersionProbe(executable: string, options: DetectDshVersionOptions):
|
|
|
41
41
|
|
|
42
42
|
let child;
|
|
43
43
|
try {
|
|
44
|
-
child =
|
|
44
|
+
child = spawnDsh(executable, [...(options.args ?? []), "--version"], {
|
|
45
45
|
env: options.env ?? process.env,
|
|
46
46
|
stdio: ["ignore", "pipe", "ignore"],
|
|
47
|
-
windowsHide: true,
|
|
48
47
|
});
|
|
49
48
|
} catch {
|
|
50
49
|
// Synchronous spawn failure (e.g. the executable does not exist).
|
package/server/provider.ts
CHANGED
|
@@ -63,6 +63,8 @@ interface DshProviderOptions {
|
|
|
63
63
|
createBridge(launch?: SessionLaunch): Promise<DshBridge>;
|
|
64
64
|
/** The `PASEO_DSH_EXECUTABLE` value; it outranks the `launch` Paseo resolves. `status()` and `createBridge` use the same precedence. */
|
|
65
65
|
executable?: string;
|
|
66
|
+
/** The command registered with Paseo; defaults to `dsh` (see `defaultDshCommand`). */
|
|
67
|
+
command?: string;
|
|
66
68
|
/** Detects the local dsh version for the startup compatibility check; defaults to spawning `<exe> --version`. */
|
|
67
69
|
detectDshVersion?: DetectVersion;
|
|
68
70
|
}
|
|
@@ -103,7 +105,7 @@ export function createDshProvider(options: DshProviderOptions): ProviderRegistra
|
|
|
103
105
|
icon: "dsh.svg",
|
|
104
106
|
getCatalogCacheKey: async (options) =>
|
|
105
107
|
options.scope === "workspace" ? `dsh-pi:${options.cwd}` : "dsh-pi:global",
|
|
106
|
-
command: ["dsh"],
|
|
108
|
+
command: [options.command ?? "dsh"],
|
|
107
109
|
async status(request) {
|
|
108
110
|
return dshStatus(
|
|
109
111
|
resolveDshLaunch(options.executable, request?.launch),
|
package/server/timeline.ts
CHANGED
|
@@ -263,20 +263,8 @@ export class TimelineProjector {
|
|
|
263
263
|
projected.push(timeline(this.paseoSessionId, item, timestamp));
|
|
264
264
|
}
|
|
265
265
|
});
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
projected.push({
|
|
269
|
-
type: "session.usage",
|
|
270
|
-
sessionId: this.paseoSessionId,
|
|
271
|
-
usage: {
|
|
272
|
-
inputTokens: numberValue(usage.inputTokens) ?? numberValue(usage.input_tokens),
|
|
273
|
-
cachedInputTokens:
|
|
274
|
-
numberValue(usage.cachedInputTokens) ?? numberValue(usage.cached_input_tokens),
|
|
275
|
-
outputTokens: numberValue(usage.outputTokens) ?? numberValue(usage.output_tokens),
|
|
276
|
-
totalCostUsd: numberValue(usage.totalCostUsd) ?? numberValue(usage.total_cost_usd),
|
|
277
|
-
},
|
|
278
|
-
});
|
|
279
|
-
}
|
|
266
|
+
// No session.usage here: Paseo replaces `lastUsage` whole, and a per-message
|
|
267
|
+
// reading would overwrite the bridge's complete one with fewer fields.
|
|
280
268
|
return projected;
|
|
281
269
|
}
|
|
282
270
|
|