dsh-bots 0.0.1 → 0.2.10
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/LICENSE +21 -0
- package/README.md +8 -16
- package/cordis.patch.yml +5 -0
- package/lib/client.js +2199 -0
- package/lib/gateway.js +240 -0
- package/lib/index.js +508 -0
- package/lib/shared.js +37 -0
- package/lib/sse.js +249 -0
- package/lib/types/client.d.ts +35 -0
- package/lib/types/gateway.d.ts +32 -0
- package/lib/types/index.d.ts +210 -0
- package/lib/types/shared.d.ts +191 -0
- package/lib/types/sse.d.ts +79 -0
- package/lib/types/unread.d.ts +74 -0
- package/lib/types/version.d.ts +28 -0
- package/lib/types/workspace.d.ts +49 -0
- package/lib/unread.js +196 -0
- package/lib/version.js +109 -0
- package/lib/workspace.js +129 -0
- package/package.json +50 -14
package/lib/version.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal semver caret-range matching for the prerelease patterns this plugin
|
|
3
|
+
* actually uses (e.g. `^4.0.1`, `^0.1.1-rc.2`).
|
|
4
|
+
*
|
|
5
|
+
* Ported from the community dsh-plugin-template (MIT) and kept
|
|
6
|
+
* dependency-free: it implements the subset of node-semver semantics needed to
|
|
7
|
+
* turn a silent peer mismatch into a loud, actionable error.
|
|
8
|
+
* See tests/version.spec.ts for the behavior matrix.
|
|
9
|
+
* @module dsh-plugin-bots/version
|
|
10
|
+
*/
|
|
11
|
+
/** Parse `X.Y.Z` or `X.Y.Z-pre` into a comparable structure, or null. */
|
|
12
|
+
export function parseVersion(input) {
|
|
13
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(input.trim());
|
|
14
|
+
if (match === null)
|
|
15
|
+
return null;
|
|
16
|
+
const prerelease = match[4] !== undefined && match[4] !== '' ? match[4].split('.') : null;
|
|
17
|
+
return {
|
|
18
|
+
major: Number(match[1]),
|
|
19
|
+
minor: Number(match[2]),
|
|
20
|
+
patch: Number(match[3]),
|
|
21
|
+
prerelease,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function compareIdentifier(a, b) {
|
|
25
|
+
const aNumeric = /^\d+$/.test(a);
|
|
26
|
+
const bNumeric = /^\d+$/.test(b);
|
|
27
|
+
if (aNumeric && bNumeric) {
|
|
28
|
+
const diff = BigInt(a) - BigInt(b);
|
|
29
|
+
return diff === 0n ? 0 : diff > 0n ? 1 : -1;
|
|
30
|
+
}
|
|
31
|
+
if (aNumeric)
|
|
32
|
+
return -1; // numeric identifiers always sort lower
|
|
33
|
+
if (bNumeric)
|
|
34
|
+
return 1;
|
|
35
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
36
|
+
}
|
|
37
|
+
/** Compare two prerelease identifier lists; a stable version (null) is higher. */
|
|
38
|
+
export function comparePrerelease(a, b) {
|
|
39
|
+
if (a === null && b === null)
|
|
40
|
+
return 0;
|
|
41
|
+
if (a === null)
|
|
42
|
+
return 1;
|
|
43
|
+
if (b === null)
|
|
44
|
+
return -1;
|
|
45
|
+
const length = Math.max(a.length, b.length);
|
|
46
|
+
for (let i = 0; i < length; i++) {
|
|
47
|
+
const left = a[i];
|
|
48
|
+
const right = b[i];
|
|
49
|
+
if (left === undefined)
|
|
50
|
+
return -1; // shorter prerelease sorts lower
|
|
51
|
+
if (right === undefined)
|
|
52
|
+
return 1;
|
|
53
|
+
const diff = compareIdentifier(left, right);
|
|
54
|
+
if (diff !== 0)
|
|
55
|
+
return diff;
|
|
56
|
+
}
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
function tupleOf(version) {
|
|
60
|
+
return [version.major, version.minor, version.patch];
|
|
61
|
+
}
|
|
62
|
+
function compareTuple(a, b) {
|
|
63
|
+
for (let i = 0; i < 3; i++) {
|
|
64
|
+
const diff = a[i] - b[i];
|
|
65
|
+
if (diff !== 0)
|
|
66
|
+
return diff > 0 ? 1 : -1;
|
|
67
|
+
}
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
/** Upper exclusive bound of a caret range: ^0.1.0 → 0.2.0, ^1.2.3 → 2.0.0. */
|
|
71
|
+
export function caretUpperBound(version) {
|
|
72
|
+
if (version.major > 0)
|
|
73
|
+
return [version.major + 1, 0, 0];
|
|
74
|
+
if (version.minor > 0)
|
|
75
|
+
return [0, version.minor + 1, 0];
|
|
76
|
+
return [0, 0, version.patch + 1];
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Whether `version` satisfies the caret range `^X.Y.Z` or `^X.Y.Z-pre`.
|
|
80
|
+
* Mirrors node-semver for the subset this plugin declares.
|
|
81
|
+
*/
|
|
82
|
+
export function satisfiesCaret(version, range) {
|
|
83
|
+
const match = /^\^([0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?)$/.exec(range.trim());
|
|
84
|
+
if (match === null)
|
|
85
|
+
return false;
|
|
86
|
+
const parsed = parseVersion(version);
|
|
87
|
+
const parsedRange = parseVersion(match[1] ?? '');
|
|
88
|
+
if (parsed === null || parsedRange === null)
|
|
89
|
+
return false;
|
|
90
|
+
const versionTuple = tupleOf(parsed);
|
|
91
|
+
const rangeTuple = tupleOf(parsedRange);
|
|
92
|
+
const tupleDiff = compareTuple(versionTuple, rangeTuple);
|
|
93
|
+
// Lower bound.
|
|
94
|
+
if (tupleDiff < 0)
|
|
95
|
+
return false;
|
|
96
|
+
if (tupleDiff === 0) {
|
|
97
|
+
if (parsedRange.prerelease === null) {
|
|
98
|
+
// A range without a prerelease does not match prereleases of the same tuple.
|
|
99
|
+
if (parsed.prerelease !== null)
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
else if (comparePrerelease(parsed.prerelease, parsedRange.prerelease) < 0) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Upper bound (exclusive).
|
|
107
|
+
const upper = caretUpperBound(parsedRange);
|
|
108
|
+
return compareTuple(versionTuple, upper) < 0;
|
|
109
|
+
}
|
package/lib/workspace.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-agent workspace jail configuration bridge (plugin side).
|
|
3
|
+
*
|
|
4
|
+
* The ENGINE owns the isolation mechanism (`sdk-bots src/host/runner/
|
|
5
|
+
* agent-workspace-jail.ts`): when an agent's settings.json declares
|
|
6
|
+
* `workspaceRoot`, every shell that agent runs is wrapped in a generated
|
|
7
|
+
* macOS Seatbelt profile that denies file WRITES outside the agent's own
|
|
8
|
+
* workspace directory (+ allowPaths + OS temp). Reads stay unrestricted so
|
|
9
|
+
* shared blackboards remain readable. The jail resolves lazily per turn —
|
|
10
|
+
* editing settings.json takes effect on the agent's next turn, no restart.
|
|
11
|
+
*
|
|
12
|
+
* This module only mirrors the engine's config contract so the plugin can
|
|
13
|
+
* read and write it safely:
|
|
14
|
+
* `<dataDir>/agents/<agentId>/settings.json` →
|
|
15
|
+
* { workspaceRoot?: "/workspace/<slug>", workspaceAllowPaths?: string[] }
|
|
16
|
+
*
|
|
17
|
+
* Validation mirrors the engine exactly (SLUG_PATTERN, agentId pattern,
|
|
18
|
+
* virtual-prefix form) because the engine FAILS CLOSED on malformed config —
|
|
19
|
+
* a bad write would break the agent's turns, not silently run unjailed.
|
|
20
|
+
* @module dsh-bots/workspace
|
|
21
|
+
*/
|
|
22
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
23
|
+
import { join } from 'node:path';
|
|
24
|
+
/** Bot-facing virtual root prefix; the daemon maps it under the box root. */
|
|
25
|
+
export const WORKSPACE_VIRTUAL_PREFIX = '/workspace/';
|
|
26
|
+
/** Same shape as the engine's `AGENT_ID_FILENAME_PATTERN`. */
|
|
27
|
+
const AGENT_ID_PATTERN = /^[\w-]{1,128}$/;
|
|
28
|
+
/** Same shape as the engine's `SLUG_PATTERN` (unicode letters allowed: 录音师). */
|
|
29
|
+
const SLUG_PATTERN = /^[\p{L}\p{N}][\p{L}\p{N}._-]{0,78}$/u;
|
|
30
|
+
/** Read one agent's settings.json jail keys, preserving unknown fields. */
|
|
31
|
+
function readRawSettings(agentDir) {
|
|
32
|
+
const path = join(agentDir, 'settings.json');
|
|
33
|
+
if (!existsSync(path))
|
|
34
|
+
return {};
|
|
35
|
+
try {
|
|
36
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
37
|
+
return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
38
|
+
? parsed
|
|
39
|
+
: {};
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** Validate the `/workspace/<slug>` virtual form exactly like the engine. */
|
|
46
|
+
export function validateVirtualRoot(requested) {
|
|
47
|
+
if (typeof requested !== 'string' || !requested.startsWith(WORKSPACE_VIRTUAL_PREFIX)) {
|
|
48
|
+
throw new Error(`workspaceRoot 必须是 "${WORKSPACE_VIRTUAL_PREFIX}<slug>" 形式(如 /workspace/录音师)`);
|
|
49
|
+
}
|
|
50
|
+
const slug = requested.slice(WORKSPACE_VIRTUAL_PREFIX.length);
|
|
51
|
+
if (slug.includes('..') || slug.includes('/') || !SLUG_PATTERN.test(slug)) {
|
|
52
|
+
throw new Error(`workspaceRoot 的 slug 不合法(字母/数字/点/横线/下划线,1-79 位):"${slug}"`);
|
|
53
|
+
}
|
|
54
|
+
return requested;
|
|
55
|
+
}
|
|
56
|
+
/** Read one agent's jail config; null when the agent dir does not exist. */
|
|
57
|
+
export function readAgentWorkspace(dataDir, agentId) {
|
|
58
|
+
if (typeof agentId !== 'string' || !AGENT_ID_PATTERN.test(agentId))
|
|
59
|
+
return null;
|
|
60
|
+
const agentDir = join(dataDir, 'agents', agentId);
|
|
61
|
+
if (!existsSync(agentDir))
|
|
62
|
+
return null;
|
|
63
|
+
const raw = readRawSettings(agentDir);
|
|
64
|
+
const allowPaths = Array.isArray(raw.workspaceAllowPaths)
|
|
65
|
+
? raw.workspaceAllowPaths.filter((v) => typeof v === 'string' && v.trim() !== '')
|
|
66
|
+
: [];
|
|
67
|
+
return {
|
|
68
|
+
agentId,
|
|
69
|
+
workspaceRoot: typeof raw.workspaceRoot === 'string' && raw.workspaceRoot.trim() !== ''
|
|
70
|
+
? raw.workspaceRoot.trim()
|
|
71
|
+
: null,
|
|
72
|
+
allowPaths,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** Scan every agent directory for its jail config (missing settings → unjailed). */
|
|
76
|
+
export function listAgentWorkspaces(dataDir) {
|
|
77
|
+
const agentsDir = join(dataDir, 'agents');
|
|
78
|
+
let entries = [];
|
|
79
|
+
try {
|
|
80
|
+
entries = readdirSync(agentsDir);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
return entries
|
|
86
|
+
.filter((id) => AGENT_ID_PATTERN.test(id))
|
|
87
|
+
.sort()
|
|
88
|
+
.map((id) => readAgentWorkspace(dataDir, id) ?? { agentId: id, workspaceRoot: null, allowPaths: [] });
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Write one agent's jail keys into its settings.json, preserving every other
|
|
92
|
+
* field. Creates the agent dir when missing (a freshly created bot may not
|
|
93
|
+
* have its settings.json yet).
|
|
94
|
+
*/
|
|
95
|
+
export function setAgentWorkspace(dataDir, agentId, request) {
|
|
96
|
+
if (typeof agentId !== 'string' || !AGENT_ID_PATTERN.test(agentId)) {
|
|
97
|
+
throw new Error(`agentId 不合法:"${agentId}"`);
|
|
98
|
+
}
|
|
99
|
+
const agentDir = join(dataDir, 'agents', agentId);
|
|
100
|
+
const raw = readRawSettings(agentDir);
|
|
101
|
+
if (request.workspaceRoot === null || request.workspaceRoot === undefined || request.workspaceRoot === '') {
|
|
102
|
+
delete raw.workspaceRoot;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
raw.workspaceRoot = validateVirtualRoot(request.workspaceRoot);
|
|
106
|
+
}
|
|
107
|
+
if (request.allowPaths === undefined) {
|
|
108
|
+
// Leave existing allowPaths untouched unless explicitly replaced.
|
|
109
|
+
}
|
|
110
|
+
else if (Array.isArray(request.allowPaths)) {
|
|
111
|
+
const cleaned = request.allowPaths.filter((v) => typeof v === 'string' && v.trim() !== '');
|
|
112
|
+
if (cleaned.length === 0)
|
|
113
|
+
delete raw.workspaceAllowPaths;
|
|
114
|
+
else
|
|
115
|
+
raw.workspaceAllowPaths = cleaned;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
throw new Error('allowPaths 必须是字符串数组');
|
|
119
|
+
}
|
|
120
|
+
mkdirSync(agentDir, { recursive: true });
|
|
121
|
+
writeFileSync(join(agentDir, 'settings.json'), JSON.stringify(raw, null, 2) + '\n', 'utf8');
|
|
122
|
+
return {
|
|
123
|
+
agentId,
|
|
124
|
+
workspaceRoot: typeof raw.workspaceRoot === 'string' ? raw.workspaceRoot : null,
|
|
125
|
+
allowPaths: Array.isArray(raw.workspaceAllowPaths)
|
|
126
|
+
? raw.workspaceAllowPaths
|
|
127
|
+
: [],
|
|
128
|
+
};
|
|
129
|
+
}
|
package/package.json
CHANGED
|
@@ -1,21 +1,57 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-bots",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
6
|
-
"
|
|
3
|
+
"version": "0.2.10",
|
|
4
|
+
"description": "Multi-bot workbench for DeepSeek Harness: bridges the sdk-bots orchestration gateway (group chats, single bots) into the dsh web shell with official-styled UI.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"types": "lib/types/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./lib/types/index.d.ts",
|
|
11
|
+
"default": "./lib/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./client": "./lib/client.js",
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"lib",
|
|
18
|
+
"cordis.patch.yml",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc -p tsconfig.json && tsc -p tsconfig.client.json",
|
|
24
|
+
"prepare": "pnpm run build",
|
|
25
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
26
|
+
"test": "vitest run",
|
|
27
|
+
"test:integration": "node scripts/integration-test.mjs",
|
|
28
|
+
"test:dsh-smoke": "bash scripts/dsh-smoke.sh"
|
|
29
|
+
},
|
|
7
30
|
"keywords": [
|
|
8
|
-
"dsh",
|
|
9
|
-
"dsh-plugin",
|
|
10
31
|
"deepseek-harness",
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
32
|
+
"dsh-plugin",
|
|
33
|
+
"bots",
|
|
34
|
+
"multibot",
|
|
35
|
+
"cordis"
|
|
14
36
|
],
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
"
|
|
18
|
-
"
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
40
|
+
"@deepseek-ai/dsh-typert-protocol": "0.1.0-rc.6"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
44
|
+
"@deepseek-ai/dsh-typert-protocol": "0.1.1-rc.2",
|
|
45
|
+
"@types/node": "^22.20.0",
|
|
46
|
+
"typescript": "^6.0.3",
|
|
47
|
+
"vitest": "^4.1.8"
|
|
19
48
|
},
|
|
20
|
-
"
|
|
49
|
+
"dsh": {
|
|
50
|
+
"bundle": {
|
|
51
|
+
"patch": "./cordis.patch.yml"
|
|
52
|
+
},
|
|
53
|
+
"client": {
|
|
54
|
+
"platform": "web"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
21
57
|
}
|