@sessionbus/dsh 0.1.0-pre.2
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 +58 -0
- package/bin.mjs +18 -0
- package/docs/LANE-WITHOUT-TUI.md +7 -0
- package/install.mjs +178 -0
- package/launcher.mjs +27 -0
- package/package.json +39 -0
- package/plugin.cjs +388 -0
package/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# sessionbus for DSH
|
|
2
|
+
|
|
3
|
+
`@sessionbus/dsh` connects DSH roots to sessionbus and runs daemon-managed DSH lanes.
|
|
4
|
+
It depends on the exact `@sessionbus/kit` prerelease `0.1.0-pre.3`.
|
|
5
|
+
It supports DeepSeek Harness `0.1.5-rc.2` and later; tested versions are
|
|
6
|
+
`0.1.5-rc.2`, `0.1.6-alpha.1`, and `0.1.6-alpha.2`.
|
|
7
|
+
A DSH profile must install every DSH package at one uniform DSH version; adding
|
|
8
|
+
one prerelease package can otherwise pull newer prereleases through DSH's caret peers.
|
|
9
|
+
`@sessionbus/dsh` is a pkg.pr.new preview until a separately reviewed trusted-
|
|
10
|
+
publishing workflow exists; its first registry version must be published manually
|
|
11
|
+
before trusted publishing can be configured.
|
|
12
|
+
The plugin reads the token once, deletes it from process.env, and retains it
|
|
13
|
+
nowhere in the plugin; DSH's immutable launch snapshot keeps it for the process
|
|
14
|
+
lifetime (trusted host).
|
|
15
|
+
|
|
16
|
+
Create the base-only lane profile with:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
dsh plugin --profile sessionbus add @sessionbus/dsh && dsh plugin --profile sessionbus exec sessionbus-dsh-install
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Add the peer plugin to another profile, such as `web`, with:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
dsh plugin --profile web add @sessionbus/dsh && dsh plugin --profile web exec sessionbus-dsh-install --product dsh web
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The installer writes only profile-local rows and leaves an existing
|
|
29
|
+
`sessionbus` row's other fields unchanged while adding or updating its required
|
|
30
|
+
`product`. With no profile arguments it configures only the `sessionbus` lane
|
|
31
|
+
profile and derives product `sessionbus-dsh`. Use product `dashi` for the dashi
|
|
32
|
+
profile, `dsh` for a standalone web or custom peer profile, or another stable
|
|
33
|
+
operator-chosen identifier matching `^[a-z0-9][a-z0-9-]{0,31}$`.
|
|
34
|
+
Non-web profiles also receive the no-upload provider required by DSH's Session
|
|
35
|
+
Controller; ordinary text prompts work while file-upload receipts are rejected.
|
|
36
|
+
|
|
37
|
+
Register the daemon's `sessionbus-dsh` product command as the package's
|
|
38
|
+
`sessionbus-dsh` bin. With a launch token it selects the installed `sessionbus`
|
|
39
|
+
profile; without one it forwards arguments to `dsh` unchanged.
|
|
40
|
+
The daemon finds that command on `PATH`, so install this package alongside
|
|
41
|
+
`dsh` at the host level too—for example, run `pnpm add @sessionbus/dsh` in the
|
|
42
|
+
directory where `dsh` is installed—so both bins share one `node_modules/.bin`.
|
|
43
|
+
|
|
44
|
+
Uninstall without invoking DSH by running the installed bin from the profile:
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
pnpm --dir "$DSH_HOME/profiles/web" exec sessionbus-dsh-install --remove web
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
When the DSH CLI works, `dsh plugin --profile web exec
|
|
51
|
+
sessionbus-dsh-install --remove web` is the equivalent convenience form. The
|
|
52
|
+
installer removes the package first, then strips its managed `sessionbus` and
|
|
53
|
+
`file-uploads-none` rows.
|
|
54
|
+
|
|
55
|
+
`SESSIONBUS_GROUPS` configures peer identities only. Lane membership is owned
|
|
56
|
+
by the daemon; the plugin accepts and does not consume groups in `session.open`.
|
|
57
|
+
|
|
58
|
+
See [Lane without a TUI](docs/LANE-WITHOUT-TUI.md) for the daemon launch contract.
|
package/bin.mjs
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { install, remove } from "./install.mjs";
|
|
3
|
+
|
|
4
|
+
const profiles = [];
|
|
5
|
+
let product;
|
|
6
|
+
let removing = false;
|
|
7
|
+
try {
|
|
8
|
+
for (let index = 2; index < process.argv.length; index++) {
|
|
9
|
+
if (process.argv[index] === "--remove") removing = true;
|
|
10
|
+
else if (process.argv[index] !== "--product") profiles.push(process.argv[index]);
|
|
11
|
+
else if ((product = process.argv[++index]) === undefined) throw new Error("--product requires a value");
|
|
12
|
+
}
|
|
13
|
+
if (removing) remove(profiles);
|
|
14
|
+
else install(profiles, { product });
|
|
15
|
+
} catch (error) {
|
|
16
|
+
process.stderr.write(`sessionbus-dsh-install: ${error.message}\n`);
|
|
17
|
+
process.exitCode = 1;
|
|
18
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Lane without a TUI
|
|
2
|
+
|
|
3
|
+
The Sessionbus daemon starts a DSH lane directly; dashi is not involved. Its
|
|
4
|
+
`sessionbus-dsh` product command executes the package-owned `sessionbus-dsh` bin
|
|
5
|
+
with `SESSIONBUS_LAUNCH_TOKEN` set and supplies no CLI task. The bin selects
|
|
6
|
+
`dsh --profile sessionbus`; the profile's `product: sessionbus-dsh` row identifies
|
|
7
|
+
the launched product and its resident base-only DSH app.
|
package/install.mjs
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
|
|
9
|
+
const profilePatch = `- id: system-prompt
|
|
10
|
+
config:
|
|
11
|
+
persona: >-
|
|
12
|
+
You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}.
|
|
13
|
+
- id: session-title-llm
|
|
14
|
+
disabled: true
|
|
15
|
+
- id: permission
|
|
16
|
+
config:
|
|
17
|
+
presets:
|
|
18
|
+
read-only: { sandbox: read-only, approval: ask }
|
|
19
|
+
workspace-write: { sandbox: workspace-write, approval: ask }
|
|
20
|
+
workspace-write-noninteractive: { sandbox: workspace-write, approval: never }
|
|
21
|
+
danger-full-access: { sandbox: danger-full-access, approval: never }
|
|
22
|
+
|
|
23
|
+
- insert:
|
|
24
|
+
- { id: workspace, name: '@deepseek-ai/dsh-workspace' }
|
|
25
|
+
- { id: file-uploads-none, name: '@antst/dsh-file-uploads-none' }
|
|
26
|
+
- { id: session-controller, name: '@deepseek-ai/dsh-api-session-controller' }
|
|
27
|
+
- id: sessionbus
|
|
28
|
+
name: '@sessionbus/dsh'
|
|
29
|
+
config: { mode: lane, product: sessionbus-dsh }
|
|
30
|
+
`;
|
|
31
|
+
const peerPatch = (product) => `- insert:
|
|
32
|
+
- { id: sessionbus, name: '@sessionbus/dsh', config: { product: ${product} } }
|
|
33
|
+
`;
|
|
34
|
+
const noUploadsPatch = `- insert:
|
|
35
|
+
- { id: file-uploads-none, name: '@antst/dsh-file-uploads-none' }
|
|
36
|
+
`;
|
|
37
|
+
const sessionbusID = /(?:^\s*-\s*|[{,]\s*)['"]?id['"]?\s*:\s*['"]?sessionbus['"]?(?=\s|[,}]|$)/mu;
|
|
38
|
+
const noUploadsID = /(?:^\s*-\s*|[{,]\s*)['"]?id['"]?\s*:\s*['"]?file-uploads-none['"]?(?=\s|[,}]|$)/mu;
|
|
39
|
+
const productPattern = /^[a-z0-9][a-z0-9-]{0,31}$/u;
|
|
40
|
+
const ownedIDs = [sessionbusID, noUploadsID];
|
|
41
|
+
function writeChanged(file, body) {
|
|
42
|
+
if (!existsSync(file) || readFileSync(file, "utf8") !== body) writeFileSync(file, body);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function convergePatch(file, id, patch) {
|
|
46
|
+
const old = existsSync(file) ? readFileSync(file, "utf8") : "";
|
|
47
|
+
if (id.test(old)) return;
|
|
48
|
+
const empty = /^(?:\s*#.*\n)*\s*\[\]\s*$/u.test(old);
|
|
49
|
+
writeChanged(file, `${empty ? "" : old.trimEnd() + (old.trim() ? "\n\n" : "")}${patch}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function withProduct(body, product) {
|
|
53
|
+
const lines = body.split("\n");
|
|
54
|
+
const row = lines.findIndex((line) => sessionbusID.test(line));
|
|
55
|
+
if (row < 0) return body;
|
|
56
|
+
const value = /(['"]?product['"]?\s*:\s*)(?:'[^']*'|"[^"]*"|[^,\s}]+)/u;
|
|
57
|
+
const column = lines[row].search(/['"]?id['"]?\s*:/u);
|
|
58
|
+
const open = lines[row].lastIndexOf("{", column);
|
|
59
|
+
if (open >= 0) {
|
|
60
|
+
if (value.test(lines[row])) lines[row] = lines[row].replace(value, `$1${product}`);
|
|
61
|
+
else if (/['"]?config['"]?\s*:\s*\{/u.test(lines[row])) lines[row] = lines[row].replace(/(['"]?config['"]?\s*:\s*\{\s*)/u, `$1product: ${product}, `);
|
|
62
|
+
else {
|
|
63
|
+
let depth = 0, close = -1;
|
|
64
|
+
for (let index = open; index < lines[row].length; index++) {
|
|
65
|
+
if (lines[row][index] === "{") depth++;
|
|
66
|
+
if (lines[row][index] === "}" && --depth === 0) { close = index; break; }
|
|
67
|
+
}
|
|
68
|
+
lines[row] = `${lines[row].slice(0, close)}, config: { product: ${product} }${lines[row].slice(close)}`;
|
|
69
|
+
}
|
|
70
|
+
return lines.join("\n");
|
|
71
|
+
}
|
|
72
|
+
const indent = lines[row].match(/^\s*/u)[0].length;
|
|
73
|
+
let end = row + 1;
|
|
74
|
+
while (end < lines.length && !new RegExp(`^\\s{0,${indent}}-\\s+`, "u").test(lines[end])) end++;
|
|
75
|
+
for (let index = row; index < end; index++) {
|
|
76
|
+
if (value.test(lines[index])) { lines[index] = lines[index].replace(value, `$1${product}`); return lines.join("\n"); }
|
|
77
|
+
}
|
|
78
|
+
const config = lines.slice(row, end).findIndex((line) => /^\s*['"]?config['"]?\s*:/u.test(line));
|
|
79
|
+
if (config >= 0) {
|
|
80
|
+
const index = row + config;
|
|
81
|
+
if (lines[index].includes("{")) lines[index] = lines[index].replace(/\{\s*/u, `{ product: ${product}, `);
|
|
82
|
+
else lines.splice(index + 1, 0, `${lines[index].match(/^\s*/u)[0]} product: ${product}`);
|
|
83
|
+
} else {
|
|
84
|
+
if (end === lines.length && lines.at(-1) === "") end--;
|
|
85
|
+
lines.splice(end, 0, `${" ".repeat(indent + 2)}config: { product: ${product} }`);
|
|
86
|
+
}
|
|
87
|
+
return lines.join("\n");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function convergeSessionbus(file, product) {
|
|
91
|
+
const old = existsSync(file) ? readFileSync(file, "utf8") : "";
|
|
92
|
+
if (sessionbusID.test(old)) return writeChanged(file, withProduct(old, product));
|
|
93
|
+
convergePatch(file, sessionbusID, peerPatch(product));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function withoutOwnedRows(body) {
|
|
97
|
+
const lines = body.split(/(?<=\n)/u);
|
|
98
|
+
for (let row = lines.length - 1; row >= 0; row--) {
|
|
99
|
+
const text = lines[row].replace(/\r?\n$/u, "");
|
|
100
|
+
if (!ownedIDs.some((id) => id.test(text))) continue;
|
|
101
|
+
const indent = text.match(/^\s*/u)[0].length;
|
|
102
|
+
let end = row + 1;
|
|
103
|
+
if (!text.includes("{")) {
|
|
104
|
+
while (end < lines.length) {
|
|
105
|
+
const next = lines[end].replace(/\r?\n$/u, "");
|
|
106
|
+
const nextIndent = next.match(/^\s*/u)[0].length;
|
|
107
|
+
if ((/^\s*-\s+/u.test(next) || /^\s*#/u.test(next)) && nextIndent <= indent) break;
|
|
108
|
+
end++;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
lines.splice(row, end - row);
|
|
112
|
+
}
|
|
113
|
+
for (let row = lines.length - 1; row >= 0; row--) {
|
|
114
|
+
const match = /^(\s*)-\s+insert:\s*(?:#.*)?(?:\r?\n)?$/u.exec(lines[row]);
|
|
115
|
+
if (!match) continue;
|
|
116
|
+
let child = row + 1;
|
|
117
|
+
while (child < lines.length && /^\s*(?:#.*)?(?:\r?\n)?$/u.test(lines[child])) child++;
|
|
118
|
+
if (child === lines.length || lines[child].match(/^\s*/u)[0].length <= match[1].length) {
|
|
119
|
+
if (row > 0 && /^\s*(?:\r?\n)?$/u.test(lines[row - 1])) lines.splice(row - 1, 2);
|
|
120
|
+
else lines.splice(row, 1);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const result = lines.join("");
|
|
124
|
+
return result.trim() ? result : "[]\n";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function validProfile(name) {
|
|
128
|
+
return name && !name.includes("/") && !name.includes("\\") && name !== "." && name !== ".." && name !== "node_modules";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function install(profileNames = [], options = {}) {
|
|
132
|
+
const home = options.home || process.env.DSH_HOME || path.join(os.homedir(), ".dsh");
|
|
133
|
+
const root = options.root || path.dirname(fileURLToPath(import.meta.url));
|
|
134
|
+
const run = options.run || ((args) => spawnSync("dsh", args, { cwd: root, encoding: "utf8" }));
|
|
135
|
+
const names = [...new Set(profileNames.length ? profileNames : ["sessionbus"])];
|
|
136
|
+
if (options.product !== undefined && (typeof options.product !== "string" || !productPattern.test(options.product))) throw new Error(`invalid product ${JSON.stringify(options.product)}; expected ^[a-z0-9][a-z0-9-]{0,31}$`);
|
|
137
|
+
if (names.includes("sessionbus") && options.product !== undefined && options.product !== "sessionbus-dsh") throw new Error("the sessionbus profile product must be sessionbus-dsh");
|
|
138
|
+
if (names.some((name) => name !== "sessionbus") && options.product === undefined) throw new Error("--product is required for peer profiles");
|
|
139
|
+
for (const name of names) {
|
|
140
|
+
if (!validProfile(name)) throw new Error(`invalid profile name ${JSON.stringify(name)}`);
|
|
141
|
+
const profile = path.join(home, "profiles", name);
|
|
142
|
+
const manifestFile = path.join(profile, "package.json");
|
|
143
|
+
let manifest = existsSync(manifestFile) ? JSON.parse(readFileSync(manifestFile, "utf8")) : null;
|
|
144
|
+
if (!manifest?.dependencies?.["@sessionbus/dsh"]) {
|
|
145
|
+
const result = run(["plugin", "--profile", name, "add", "@sessionbus/dsh"]);
|
|
146
|
+
if (result.status !== 0) throw new Error(String(result.stderr || "dsh plugin add failed").trim());
|
|
147
|
+
manifest = JSON.parse(readFileSync(manifestFile, "utf8"));
|
|
148
|
+
}
|
|
149
|
+
if (name !== "sessionbus") {
|
|
150
|
+
const patch = path.join(profile, "cordis.patch.yml");
|
|
151
|
+
convergeSessionbus(patch, options.product);
|
|
152
|
+
if (name !== "web") convergePatch(patch, noUploadsID, noUploadsPatch);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
manifest = {
|
|
156
|
+
name: "dsh-profile-sessionbus",
|
|
157
|
+
private: true,
|
|
158
|
+
dependencies: { "@sessionbus/dsh": manifest.dependencies["@sessionbus/dsh"] },
|
|
159
|
+
dsh: { profile: { bundles: ["@deepseek-ai/dsh-base"], patchReload: "startup" } },
|
|
160
|
+
};
|
|
161
|
+
writeChanged(manifestFile, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
162
|
+
writeChanged(path.join(profile, "cordis.patch.yml"), profilePatch);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function remove(profileNames = [], options = {}) {
|
|
167
|
+
if (profileNames.length === 0) throw new Error("--remove requires at least one profile");
|
|
168
|
+
const home = options.home || process.env.DSH_HOME || path.join(os.homedir(), ".dsh");
|
|
169
|
+
const run = options.run || ((args, cwd) => spawnSync("pnpm", args, { cwd, encoding: "utf8" }));
|
|
170
|
+
for (const name of [...new Set(profileNames)]) {
|
|
171
|
+
if (!validProfile(name)) throw new Error(`invalid profile name ${JSON.stringify(name)}`);
|
|
172
|
+
const profile = path.join(home, "profiles", name);
|
|
173
|
+
const result = run(["remove", "@sessionbus/dsh"], profile);
|
|
174
|
+
if (result.status !== 0) throw new Error(String(result.stderr || result.error?.message || "pnpm remove failed").trim());
|
|
175
|
+
const patch = path.join(profile, "cordis.patch.yml");
|
|
176
|
+
if (existsSync(patch)) writeChanged(patch, withoutOwnedRows(readFileSync(patch, "utf8")));
|
|
177
|
+
}
|
|
178
|
+
}
|
package/launcher.mjs
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Signal and exit forwarding mirrors @antst/dashi-launcher's bin/dashi.js.
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
const args = process.env.SESSIONBUS_LAUNCH_TOKEN === undefined
|
|
6
|
+
? process.argv.slice(2)
|
|
7
|
+
: ["--profile", "sessionbus", ...process.argv.slice(2)];
|
|
8
|
+
const child = spawn("dsh", args, { stdio: "inherit" });
|
|
9
|
+
const signals = ["SIGINT", "SIGTERM"];
|
|
10
|
+
const forwards = signals.map((signal) => [signal, () => child.kill(signal)]);
|
|
11
|
+
for (const [signal, forward] of forwards) process.on(signal, forward);
|
|
12
|
+
|
|
13
|
+
let failed = false;
|
|
14
|
+
child.on("error", (error) => {
|
|
15
|
+
failed = true;
|
|
16
|
+
const missing = error.code === "ENOENT";
|
|
17
|
+
process.stderr.write(missing
|
|
18
|
+
? "sessionbus-dsh: dsh not found; install it with 'npm install --global @deepseek-ai/dsh'\n"
|
|
19
|
+
: `sessionbus-dsh: could not start dsh: ${error.message}\n`);
|
|
20
|
+
process.exitCode = missing ? 127 : 1;
|
|
21
|
+
});
|
|
22
|
+
child.on("close", (code, signal) => {
|
|
23
|
+
for (const [name, forward] of forwards) process.off(name, forward);
|
|
24
|
+
if (failed) return;
|
|
25
|
+
if (code !== null) process.exitCode = code;
|
|
26
|
+
else if (signal !== null) process.kill(process.pid, signal);
|
|
27
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sessionbus/dsh",
|
|
3
|
+
"version": "0.1.0-pre.2",
|
|
4
|
+
"type": "commonjs",
|
|
5
|
+
"main": "plugin.cjs",
|
|
6
|
+
"exports": "./plugin.cjs",
|
|
7
|
+
"bin": {
|
|
8
|
+
"sessionbus-dsh": "launcher.mjs",
|
|
9
|
+
"sessionbus-dsh-install": "bin.mjs"
|
|
10
|
+
},
|
|
11
|
+
"files": ["README.md", "docs/LANE-WITHOUT-TUI.md", "plugin.cjs", "launcher.mjs", "bin.mjs", "install.mjs"],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"test": "node --test plugin.test.cjs install.test.mjs launcher.test.mjs package.test.cjs rename.test.mjs"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@antst/dsh-file-uploads-none": "0.1.0-alpha.18",
|
|
17
|
+
"@sessionbus/kit": "0.1.0-pre.3"
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
21
|
+
"@deepseek-ai/cordis-plugin-loader": "^1.0.3",
|
|
22
|
+
"@deepseek-ai/dsh-agent": ">=0.1.5-rc.2",
|
|
23
|
+
"@deepseek-ai/dsh-api-session-controller": ">=0.1.5-rc.2",
|
|
24
|
+
"@deepseek-ai/dsh-cmdline": ">=0.1.5-rc.2",
|
|
25
|
+
"@deepseek-ai/dsh-commands": ">=0.1.5-rc.2",
|
|
26
|
+
"@deepseek-ai/dsh-launch-environment": ">=0.1.5-rc.2",
|
|
27
|
+
"@deepseek-ai/dsh-llm": ">=0.1.5-rc.2",
|
|
28
|
+
"@deepseek-ai/dsh-permission-presets": ">=0.1.5-rc.2",
|
|
29
|
+
"@deepseek-ai/dsh-session": ">=0.1.5-rc.2",
|
|
30
|
+
"@deepseek-ai/dsh-session-title": ">=0.1.5-rc.2",
|
|
31
|
+
"@deepseek-ai/dsh-tools": ">=0.1.5-rc.2",
|
|
32
|
+
"@deepseek-ai/dsh-workspace": ">=0.1.5-rc.2"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": { "access": "public" },
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/antst/sessionbus-dsh.git"
|
|
38
|
+
}
|
|
39
|
+
}
|
package/plugin.cjs
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const os = require("node:os");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const kit = require("@sessionbus/kit");
|
|
6
|
+
const version = require("./package.json").version;
|
|
7
|
+
const { ACTIONS } = kit;
|
|
8
|
+
|
|
9
|
+
// Match the MCP tool's closed union; the public kit validates each action.
|
|
10
|
+
function argumentSchema() {
|
|
11
|
+
const properties = {};
|
|
12
|
+
for (const field of ["session_id", "host", "message", "target", "group", "product", "name", "resume_session_id", "notify_target", "input", "run_id"]) properties[field] = { type: "string" };
|
|
13
|
+
for (const field of ["targets", "extra_groups"]) properties[field] = { type: "array", items: { type: "string" } };
|
|
14
|
+
for (const field of ["persistent", "notify", "forget"]) properties[field] = { type: "boolean" };
|
|
15
|
+
for (const field of ["auto_close_ms", "timeout_ms"]) properties[field] = { type: "integer" };
|
|
16
|
+
properties.idle_message = { type: "string", enum: ["stage", "run"] };
|
|
17
|
+
properties.trace = { type: "string", enum: ["off", "events", "content"] };
|
|
18
|
+
properties.mode = { type: "string", enum: ["off", "events", "content"] };
|
|
19
|
+
const open = {};
|
|
20
|
+
for (const field of ["cwd", "permission_mode", "model", "reasoning_effort"]) open[field] = { type: "string" };
|
|
21
|
+
open.arguments = { type: "array", items: { type: "string" } };
|
|
22
|
+
properties.open = { type: "object", additionalProperties: false, properties: open };
|
|
23
|
+
return { type: "object", additionalProperties: false, properties, description: "Use only the fields listed for the selected action in the tool description. send has no summary field; put the complete content in message." };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function assertKnownArguments(value, schema, location = "arguments") {
|
|
27
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`sessionbus ${location} must be an object`);
|
|
28
|
+
for (const field of Object.keys(value)) {
|
|
29
|
+
if (!Object.hasOwn(schema.properties, field)) throw new Error(`sessionbus ${location}.${field} is not supported`);
|
|
30
|
+
const property = schema.properties[field];
|
|
31
|
+
if (property.type === "object") assertKnownArguments(value[field], property, `${location}.${field}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const name = "sessionbus-dsh";
|
|
36
|
+
const productPattern = /^[a-z0-9][a-z0-9-]{0,31}$/u;
|
|
37
|
+
const inject = [
|
|
38
|
+
"agents", "appReady", "appExit", "commands", "permissionPresets",
|
|
39
|
+
"sessionController", "sessions", "sessionTitle", "tools",
|
|
40
|
+
];
|
|
41
|
+
function deferred() {
|
|
42
|
+
let resolve, reject;
|
|
43
|
+
const promise = new Promise((yes, no) => { resolve = yes; reject = no; });
|
|
44
|
+
return { promise, resolve, reject };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function text(value) {
|
|
48
|
+
return typeof value === "string" && value.length > 0 && value.trim() === value && !/[\0\r\n]/u.test(value);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function clean(error) {
|
|
52
|
+
return String(error?.message || error || "DSH plugin failed").replace(/[\0\r\n]/gu, " ").slice(0, 4096);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function readConfiguration(ctx, config = {}, ambient = process.env) {
|
|
56
|
+
if (config.product === undefined) throw new Error("product is required; re-run sessionbus-dsh-install --product <name> <profile>");
|
|
57
|
+
if (typeof config.product !== "string" || !productPattern.test(config.product)) throw new Error("product must match ^[a-z0-9][a-z0-9-]{0,31}$");
|
|
58
|
+
const launch = ctx.launchEnvironment || ctx.get?.("launchEnvironment");
|
|
59
|
+
const value = (key) => {
|
|
60
|
+
if (launch) return launch.get(key)?.value;
|
|
61
|
+
return Object.hasOwn(ambient, key) ? ambient[key] : undefined;
|
|
62
|
+
};
|
|
63
|
+
const token = value("SESSIONBUS_LAUNCH_TOKEN");
|
|
64
|
+
delete process.env.SESSIONBUS_LAUNCH_TOKEN;
|
|
65
|
+
const mode = token === undefined ? "peer" : "lane";
|
|
66
|
+
if (config.mode !== undefined && config.mode !== mode) throw new Error(`mode ${config.mode} conflicts with launch environment`);
|
|
67
|
+
let groups = mode === "lane" ? [] : Object.hasOwn(config, "groups") ? config.groups : JSON.parse(value("SESSIONBUS_GROUPS") || "[]");
|
|
68
|
+
if (!Array.isArray(groups) || groups.some((group) => !text(group)) || new Set(groups).size !== groups.length) throw new Error("groups are invalid");
|
|
69
|
+
const socket = Object.hasOwn(config, "socket") ? config.socket : value("SESSIONBUS_SOCKET") || path.join(value("XDG_STATE_HOME") || path.join(value("HOME") || os.homedir(), ".local/state"), "sessionbus/run/presence.sock");
|
|
70
|
+
const localKey = Object.hasOwn(config, "local_key") ? config.local_key : value("SESSIONBUS_LOCAL_KEY");
|
|
71
|
+
if (!text(socket) || localKey !== undefined && !text(localKey)) throw new Error("connection settings are invalid");
|
|
72
|
+
return { settings: { mode, product: config.product, groups: [...groups], socket, localKey }, token };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function captureContext(ctx) {
|
|
76
|
+
const services = Object.fromEntries(inject.map((service) => [service, ctx[service]]));
|
|
77
|
+
let launchEnvironment;
|
|
78
|
+
try { launchEnvironment = ctx.launchEnvironment || ctx.get?.("launchEnvironment"); } catch {}
|
|
79
|
+
const fiber = ctx.fiber;
|
|
80
|
+
return {
|
|
81
|
+
active: () => fiber?.uid !== null,
|
|
82
|
+
context: {
|
|
83
|
+
...services,
|
|
84
|
+
launchEnvironment,
|
|
85
|
+
on: ctx.on.bind(ctx),
|
|
86
|
+
effect: ctx.effect.bind(ctx),
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function settings(ctx, config = {}, ambient = process.env) {
|
|
92
|
+
return readConfiguration(ctx, config, ambient).settings;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function connectionEnvironment(config, token) {
|
|
96
|
+
return {
|
|
97
|
+
SESSIONBUS_SOCKET: config.socket,
|
|
98
|
+
...(config.localKey === undefined ? {} : { SESSIONBUS_LOCAL_KEY: config.localKey }),
|
|
99
|
+
...(token === undefined ? {} : { SESSIONBUS_LAUNCH_TOKEN: token }),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function nativeName(value) {
|
|
104
|
+
const at = value.lastIndexOf("@");
|
|
105
|
+
return at < 0 ? value : value.slice(0, at);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function textOf(message) {
|
|
109
|
+
return Array.isArray(message?.content) ? message.content.filter((part) => part?.type === "text" && typeof part.text === "string").map((part) => part.text).join("") : "";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function terminal(reason) {
|
|
113
|
+
const native_stop_reason = reason?.kind;
|
|
114
|
+
if (native_stop_reason === "completed") return { outcome: "completed", native_stop_reason };
|
|
115
|
+
if (native_stop_reason === "aborted" || native_stop_reason === "interrupted") return { outcome: "interrupted", native_stop_reason };
|
|
116
|
+
if (["blocked", "error", "max-tokens"].includes(native_stop_reason)) return { outcome: "failed", native_stop_reason };
|
|
117
|
+
throw new Error(`unknown DSH turn end reason ${JSON.stringify(reason)}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
class NativeSession {
|
|
121
|
+
constructor(ctx, createUserMessage) {
|
|
122
|
+
this.ctx = ctx;
|
|
123
|
+
this.createUserMessage = createUserMessage;
|
|
124
|
+
this.receipts = new Map();
|
|
125
|
+
this.removeEvents = ctx.on("session/event", (session, event) => this.event(session, event), { global: true });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async open(request) {
|
|
129
|
+
const options = request.open || {};
|
|
130
|
+
let resolved;
|
|
131
|
+
if (request.resume_session_id) {
|
|
132
|
+
resolved = await this.ctx.sessionController.resolveAgent(request.resume_session_id);
|
|
133
|
+
} else {
|
|
134
|
+
const created = await this.ctx.sessionController.create(options.cwd ? { cwd: options.cwd } : {});
|
|
135
|
+
resolved = await this.ctx.sessionController.resolveAgent(created.sessionId);
|
|
136
|
+
}
|
|
137
|
+
if (resolved.error) throw resolved.error;
|
|
138
|
+
const agent = resolved.agent;
|
|
139
|
+
const expected = request.resume_session_id || agent.id;
|
|
140
|
+
if (agent.id !== expected || agent.session.id !== expected) throw new Error("DSH resolved a different native session");
|
|
141
|
+
if (options.cwd !== undefined && agent.session.header.cwd !== options.cwd) throw new Error("DSH resolved a different native cwd");
|
|
142
|
+
await this.ctx.sessionController.rename({ sessionId: agent.id, title: nativeName(request.name) });
|
|
143
|
+
if (options.model !== undefined || options.reasoning_effort !== undefined) {
|
|
144
|
+
const model = options.model || `${agent.options?.provider || ""}/${agent.options?.model || ""}`;
|
|
145
|
+
const slash = model.indexOf("/");
|
|
146
|
+
if (slash < 1 || slash === model.length - 1) throw new Error(`unsupported value model=${model}`);
|
|
147
|
+
await this.ctx.sessionController.selectModel({ sessionId: agent.id, provider: model.slice(0, slash), model: model.slice(slash + 1), ...(options.reasoning_effort === undefined ? {} : { reasoningEffort: options.reasoning_effort }) });
|
|
148
|
+
}
|
|
149
|
+
if (options.permission_mode !== undefined) {
|
|
150
|
+
if (!this.ctx.permissionPresets.names.includes(options.permission_mode)) throw new Error(`unsupported value permission_mode=${options.permission_mode}`);
|
|
151
|
+
this.ctx.permissionPresets.set(agent.session, options.permission_mode);
|
|
152
|
+
}
|
|
153
|
+
await this.ctx.sessions.flush(agent.session);
|
|
154
|
+
this.agent = agent;
|
|
155
|
+
return { session_id: agent.id };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
message(body) {
|
|
159
|
+
return this.createUserMessage({ content: [{ type: "text", text: body }], source: { kind: "plugin", plugin: name, form: "relay" } });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
receipt(message, session, cancel) {
|
|
163
|
+
if (cancel?.aborted) throw cancel.reason || new Error("cancelled");
|
|
164
|
+
const accepted = { ...deferred(), session };
|
|
165
|
+
const abort = () => accepted.reject(cancel.reason || new Error("cancelled"));
|
|
166
|
+
cancel?.addEventListener("abort", abort, { once: true });
|
|
167
|
+
accepted.promise.catch(() => {});
|
|
168
|
+
accepted.close = () => {
|
|
169
|
+
cancel?.removeEventListener("abort", abort);
|
|
170
|
+
if (this.receipts.get(message.id) === accepted) this.receipts.delete(message.id);
|
|
171
|
+
};
|
|
172
|
+
this.receipts.set(message.id, accepted);
|
|
173
|
+
return accepted;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
event(session, event) {
|
|
177
|
+
if (event.type === "agent/inbox/spliced") {
|
|
178
|
+
for (const message of event.data.inserted || []) {
|
|
179
|
+
const receipt = this.receipts.get(message.id);
|
|
180
|
+
if (receipt?.session === session) receipt.resolve();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const run = this.active;
|
|
184
|
+
if (!run || session !== this.agent?.session) return;
|
|
185
|
+
if (event.type === "turn/start") run.openTurn = event.data.turn;
|
|
186
|
+
if (event.type === "user/message" && event.data.id === run.message.id) {
|
|
187
|
+
if (!Number.isSafeInteger(run.openTurn)) {
|
|
188
|
+
run.endError = new Error("DSH consumed input outside a turn");
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
run.turn = run.openTurn;
|
|
192
|
+
}
|
|
193
|
+
if (event.type === "assistant/message" && run.turn === event.data.turn) run.output += textOf(event.data.message);
|
|
194
|
+
if (event.type === "turn/end" && run.turn === event.data.turn) {
|
|
195
|
+
try { run.end = terminal(event.data.reason); }
|
|
196
|
+
catch (error) { run.endError = error; }
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async run(cancel, token, input) {
|
|
201
|
+
if (token.Interrupted()) return { outcome: "interrupted", result: "" };
|
|
202
|
+
const message = this.message(input);
|
|
203
|
+
const record = { message, openTurn: null, turn: null, output: "", cancelled: deferred() };
|
|
204
|
+
const receipt = this.receipt(message, this.agent.session, cancel);
|
|
205
|
+
this.active = record;
|
|
206
|
+
token.Native = record;
|
|
207
|
+
record.cancelled.promise.catch(() => {});
|
|
208
|
+
const abort = () => { record.cancelled.reject(cancel.reason || new Error("cancelled")); this.receipts.get(message.id)?.reject(cancel.reason || new Error("cancelled")); };
|
|
209
|
+
cancel.addEventListener("abort", abort, { once: true });
|
|
210
|
+
try {
|
|
211
|
+
try { this.agent.followup(message); }
|
|
212
|
+
catch (error) { receipt.reject(error); throw error; }
|
|
213
|
+
await receipt.promise;
|
|
214
|
+
const idle = this.agent.whenIdle();
|
|
215
|
+
idle.catch(() => {});
|
|
216
|
+
await Promise.race([idle, record.cancelled.promise]);
|
|
217
|
+
if (record.endError) throw record.endError;
|
|
218
|
+
if (record.end) return { ...record.end, result: record.output };
|
|
219
|
+
if (record.cancelReason) return { outcome: "interrupted", native_stop_reason: record.cancelReason, result: record.output };
|
|
220
|
+
throw new Error("DSH reached idle without turn/end");
|
|
221
|
+
} finally {
|
|
222
|
+
receipt.close();
|
|
223
|
+
cancel.removeEventListener("abort", abort);
|
|
224
|
+
if (this.active === record) this.active = null;
|
|
225
|
+
if (token.Native === record) token.Native = null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
interrupt(_cancel, token) {
|
|
230
|
+
if (token.Native) {
|
|
231
|
+
token.Native.cancelReason = "aborted:user";
|
|
232
|
+
this.agent.cancel({ kind: "user" }, { keepInbox: true });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async deliver(cancel, request, agent = this.agent) {
|
|
237
|
+
const message = this.message(request.body);
|
|
238
|
+
if (agent.status === "running") {
|
|
239
|
+
const receipt = this.receipt(message, agent.session, cancel);
|
|
240
|
+
try {
|
|
241
|
+
try { agent.steer(message); }
|
|
242
|
+
catch (error) { receipt.reject(error); throw error; }
|
|
243
|
+
await receipt.promise;
|
|
244
|
+
} finally { receipt.close(); }
|
|
245
|
+
} else {
|
|
246
|
+
const event = await agent.session.append("user/message", message, { surfaceOp: "append" });
|
|
247
|
+
if (event?.type !== "user/message" || event.data?.id !== message.id) throw new Error("DSH did not commit the delivered message");
|
|
248
|
+
}
|
|
249
|
+
return { disposition: "injected" };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async close() {
|
|
253
|
+
if (!this.agent) return;
|
|
254
|
+
if (this.agent.status === "running") this.agent.cancel({ kind: "disposed" });
|
|
255
|
+
await this.agent.whenIdle();
|
|
256
|
+
await this.ctx.sessions.flush(this.agent.session);
|
|
257
|
+
this.removeEvents();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function identity(ctx, agent, product, groups, title) {
|
|
262
|
+
const sessionID = String(agent?.session?.id || agent?.id || "");
|
|
263
|
+
const cwd = agent?.session?.header?.cwd;
|
|
264
|
+
if (!text(sessionID) || !text(cwd)) throw new Error("DSH root identity is incomplete");
|
|
265
|
+
const current = title === undefined ? ctx.sessionTitle.get(agent.session)?.title : title;
|
|
266
|
+
const provider = agent.options?.provider;
|
|
267
|
+
const model = agent.options?.model;
|
|
268
|
+
return { product, session_id: sessionID, name: text(current) ? current : sessionID, groups: [...groups], info: { cwd, ...(text(provider) && text(model) ? { model: `${provider}/${model}` } : {}) } };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function createRuntime(ctx, config, dependencies, prepared) {
|
|
272
|
+
const configured = prepared || readConfiguration(ctx, config, dependencies.ambient);
|
|
273
|
+
const values = configured.settings;
|
|
274
|
+
const active = dependencies.active || (() => true);
|
|
275
|
+
let launchToken = configured.token;
|
|
276
|
+
const native = new NativeSession(ctx, dependencies.createUserMessage);
|
|
277
|
+
const peers = new Map();
|
|
278
|
+
let worker;
|
|
279
|
+
let workerExit = Promise.resolve();
|
|
280
|
+
let ready = false;
|
|
281
|
+
let warned = false;
|
|
282
|
+
const warn = (error) => { if (active() && !warned) { warned = true; dependencies.stderr(`sessionbus: ${clean(error)}\n`); } };
|
|
283
|
+
const root = (agent) => ctx.agents.roots().includes(agent);
|
|
284
|
+
const present = (agent) => {
|
|
285
|
+
if (values.mode !== "peer" || !ready || !root(agent) || peers.has(agent)) return;
|
|
286
|
+
try {
|
|
287
|
+
const current = identity(ctx, agent, values.product, values.groups);
|
|
288
|
+
const peer = dependencies.connectPeer(current, (cancel, request) => native.deliver(cancel, request, agent), connectionEnvironment(values));
|
|
289
|
+
peers.set(agent, { peer, identity: current, rehello: Promise.resolve() });
|
|
290
|
+
} catch (error) { warn(error); }
|
|
291
|
+
};
|
|
292
|
+
const forget = (agent) => { peers.get(agent)?.peer.shutdown(); peers.delete(agent); };
|
|
293
|
+
let removeCreated = () => {}, removeDisposed = () => {}, removeTitle = () => {};
|
|
294
|
+
if (values.mode === "peer") {
|
|
295
|
+
removeCreated = ctx.on("agent/created", ({ agent }) => present(agent));
|
|
296
|
+
removeDisposed = ctx.on("agent/disposed", ({ agent }) => forget(agent));
|
|
297
|
+
removeTitle = ctx.on("session/event", (session, event) => {
|
|
298
|
+
if (event.type !== "session/title") return;
|
|
299
|
+
const agent = ctx.agents.get(session.id);
|
|
300
|
+
const record = peers.get(agent);
|
|
301
|
+
if (!record || agent.session !== session) return;
|
|
302
|
+
const next = identity(ctx, agent, values.product, values.groups, event.data.title);
|
|
303
|
+
record.identity = next;
|
|
304
|
+
record.rehello = record.rehello.then(() => record.peer.rehello({ name: next.name, info: next.info })).catch(warn);
|
|
305
|
+
}, { global: true });
|
|
306
|
+
}
|
|
307
|
+
const caller = (agent) => worker && native.agent === agent ? worker.caller : peers.get(agent)?.peer.caller;
|
|
308
|
+
const argumentsSchema = argumentSchema();
|
|
309
|
+
const execute = (argumentsValue, execution) => {
|
|
310
|
+
const client = caller(execution?.agent);
|
|
311
|
+
if (!client) throw new Error("sessionbus requires an exact live DSH root");
|
|
312
|
+
const args = argumentsValue.arguments || {};
|
|
313
|
+
assertKnownArguments(args, argumentsSchema);
|
|
314
|
+
return client.action(argumentsValue.action, args);
|
|
315
|
+
};
|
|
316
|
+
const removeTool = ctx.tools.register(dependencies.defineTool({
|
|
317
|
+
name: "sessionbus",
|
|
318
|
+
description: "List, message, spawn and control Sessionbus sessions. Use trace mode off, events or content to configure live parent tracing for a direct child; spawn trace sets its initial mode.",
|
|
319
|
+
parameters: { action: { type: "string", enum: ACTIONS, required: true }, arguments: argumentsSchema },
|
|
320
|
+
output: { schema: { type: "object", additionalProperties: true, properties: {} }, render: (_args, result) => [{ type: "text", text: JSON.stringify(result) }] },
|
|
321
|
+
execute,
|
|
322
|
+
}));
|
|
323
|
+
const removeCommand = ctx.commands.register({
|
|
324
|
+
name: "sessionbus",
|
|
325
|
+
description: "list sessionbus sessions",
|
|
326
|
+
handler: async (invocation) => ({ kind: "success", text: JSON.stringify(await execute({ action: "list", arguments: {} }, { agent: invocation.agent })) }),
|
|
327
|
+
});
|
|
328
|
+
const start = () => {
|
|
329
|
+
ready = true;
|
|
330
|
+
if (values.mode === "lane") {
|
|
331
|
+
const environment = connectionEnvironment(values, launchToken);
|
|
332
|
+
launchToken = undefined;
|
|
333
|
+
worker = dependencies.serveWorker({
|
|
334
|
+
hello: () => ({ product: values.product, version, supported_open_fields: ["cwd", "permission_mode", "model", "reasoning_effort"], extra_arguments: [] }),
|
|
335
|
+
open: (_cancel, request) => native.open(request),
|
|
336
|
+
run: (cancel, token, input) => native.run(cancel, token, input),
|
|
337
|
+
interrupt: (cancel, token) => native.interrupt(cancel, token),
|
|
338
|
+
deliver: (cancel, request) => native.deliver(cancel, request),
|
|
339
|
+
close: () => native.close(),
|
|
340
|
+
}, environment);
|
|
341
|
+
workerExit = worker.closed.then(async () => {
|
|
342
|
+
const failure = await worker.serving?.catch((error) => error);
|
|
343
|
+
if (!active()) return;
|
|
344
|
+
if (failure && !worker.opened) { warn(failure); ctx.appExit(1); } else ctx.appExit(0);
|
|
345
|
+
});
|
|
346
|
+
} else {
|
|
347
|
+
for (const agent of ctx.agents.roots()) present(agent);
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
const removeReady = ctx.appReady.onReady(start);
|
|
351
|
+
const close = () => {
|
|
352
|
+
removeReady(); removeCreated(); removeDisposed(); removeTitle(); removeCommand(); removeTool();
|
|
353
|
+
for (const agent of peers.keys()) forget(agent);
|
|
354
|
+
worker?.shutdown(); native.removeEvents();
|
|
355
|
+
};
|
|
356
|
+
ctx.effect(() => close, "sessionbus-dsh.lifecycle");
|
|
357
|
+
return { close, execute, native, peers, settings: values, start, get worker() { return worker; }, get workerExit() { return workerExit; } };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function activate(ctx, config = {}, dependencies = {}, prepared) {
|
|
361
|
+
const configured = prepared || readConfiguration(ctx, config, dependencies.ambient);
|
|
362
|
+
const [{ createUserMessage }, { defineTool }] = await Promise.all([
|
|
363
|
+
dependencies.createUserMessage ? { createUserMessage: dependencies.createUserMessage } : (dependencies.importLLM || (() => import("@deepseek-ai/dsh-llm")))(),
|
|
364
|
+
dependencies.defineTool ? { defineTool: dependencies.defineTool } : (dependencies.importTools || (() => import("@deepseek-ai/dsh-tools")))(),
|
|
365
|
+
]);
|
|
366
|
+
if (dependencies.active && !dependencies.active()) return;
|
|
367
|
+
return createRuntime(ctx, config, { ...kit, active: () => true, stderr: (line) => process.stderr.write(line), ...dependencies, createUserMessage, defineTool }, configured);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function apply(ctx, config = {}, dependencies = {}) {
|
|
371
|
+
let captured, prepared;
|
|
372
|
+
try {
|
|
373
|
+
captured = captureContext(ctx);
|
|
374
|
+
prepared = readConfiguration(captured.context, config, dependencies.ambient);
|
|
375
|
+
} catch (error) {
|
|
376
|
+
(dependencies.stderr || ((line) => process.stderr.write(line)))(`sessionbus: ${clean(error)}\n`);
|
|
377
|
+
try { ctx.appExit(1); } catch {}
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
const runtimeDependencies = { stderr: (line) => process.stderr.write(line), ...dependencies, active: captured.active };
|
|
381
|
+
void activate(captured.context, config, runtimeDependencies, prepared).catch((error) => {
|
|
382
|
+
if (!captured.active()) return;
|
|
383
|
+
runtimeDependencies.stderr(`sessionbus: ${clean(error)}\n`);
|
|
384
|
+
captured.context.appExit(1);
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
module.exports = { ACTIONS, NativeSession, activate, apply, connectionEnvironment, createRuntime, identity, inject, name, settings, terminal };
|