@open-pets/dsh 3.5.0 → 4.1.0
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 +55 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +4 -2
- package/dist/runtime.d.ts +15 -2
- package/dist/runtime.js +125 -1
- package/package.json +4 -4
package/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# @open-pets/dsh
|
|
2
|
+
|
|
3
|
+
Cordis plugin connecting the [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) agent runtime directly to the [OpenPets](https://openpets.dev) desktop companion.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Lifecycle Reactions**: Synchronizes DSH agent status (`running` ⇄ `idle`), errors, and approval requests with companion emotions.
|
|
8
|
+
- **Status Stream Awareness**: Extracts `⏵` status lines from assistant stream chunks and displays them in companion speech bubbles.
|
|
9
|
+
- **Tool Operation Reactions**: Maps `bash` executions (`running`), `edit`/`write` operations (`editing`), and `read` operations (`working`).
|
|
10
|
+
- **Toggleable Configuration**: Enable or disable per profile or dynamically via Cordis plugin configuration.
|
|
11
|
+
- **Safe & Fail-safe**: Automatically sanitizes paths and messages, strips URLs/secrets, throttles speech rate, and isolates IPC failures so the agent loop never stalls.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
Add to your DSH profile or user configuration (`~/.dsh/cordis.patch.yml`):
|
|
16
|
+
|
|
17
|
+
```yaml
|
|
18
|
+
- insert:
|
|
19
|
+
- id: plugin-openpets-dsh
|
|
20
|
+
name: '@open-pets/dsh'
|
|
21
|
+
disabled: false
|
|
22
|
+
config:
|
|
23
|
+
enabled: true
|
|
24
|
+
minSpeechIntervalMs: 1200
|
|
25
|
+
enableStatusLineStream: true
|
|
26
|
+
enableToolEvents: true
|
|
27
|
+
enableApprovalEvents: true
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Configuration Options
|
|
31
|
+
|
|
32
|
+
| Option | Type | Default | Description |
|
|
33
|
+
|---|---|---|---|
|
|
34
|
+
| `enabled` | `boolean` | `true` | Master switch to enable or disable listener registration. |
|
|
35
|
+
| `minSpeechIntervalMs` | `number` | `1200` | Minimum interval (in ms) between speech bubble notifications. |
|
|
36
|
+
| `enableStatusLineStream` | `boolean` | `true` | Forward `⏵` status lines from assistant stream chunks. |
|
|
37
|
+
| `enableToolEvents` | `boolean` | `true` | React to tool executions (`bash`, `edit`, `write`, `read`). |
|
|
38
|
+
| `enableApprovalEvents` | `boolean` | `true` | Announce when an approval request is waiting for human permission. |
|
|
39
|
+
|
|
40
|
+
## Event Mappings
|
|
41
|
+
|
|
42
|
+
| DSH Event | OpenPets Reaction | Speech Behavior |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| `agent/status: running` | `thinking` | Curated thinking speech |
|
|
45
|
+
| `agent/status: idle` | `success` | Curated success speech (suppressed for 5s after errors) |
|
|
46
|
+
| `agent/error` | `error` | Curated error speech |
|
|
47
|
+
| `approval/request` | `waiting` | Curated permission speech |
|
|
48
|
+
| `agent/assistant-stream` | `working` | Sanitized `⏵` status line |
|
|
49
|
+
| `tools/result (bash)` | `running` | Sanitized tool description |
|
|
50
|
+
| `tools/result (edit/write)` | `editing` | Sanitized file action |
|
|
51
|
+
| `tools/result (read)` | `working` | Sanitized read action |
|
|
52
|
+
|
|
53
|
+
## License
|
|
54
|
+
|
|
55
|
+
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,11 @@ import type { Context } from "@deepseek-ai/cordis";
|
|
|
2
2
|
export { classifyDshEvent, createOpenPetsDshClient, createOpenPetsDshRuntime, registerDshListeners, type DshCordisApi, type DshAgentStatus, type DshEventName, type DshEventDecision, type OpenPetsDshOptions, type OpenPetsDshRuntime, } from "./runtime.js";
|
|
3
3
|
import { type OpenPetsDshOptions } from "./runtime.js";
|
|
4
4
|
export declare const name = "@open-pets/dsh";
|
|
5
|
+
/** Configuration options for the OpenPets DSH plugin. */
|
|
6
|
+
export interface Config extends OpenPetsDshOptions {
|
|
7
|
+
/** Master switch to enable or disable OpenPets notifications. Default: true. */
|
|
8
|
+
enabled?: boolean;
|
|
9
|
+
}
|
|
5
10
|
/** Install the OpenPets listeners into the DSH Cordis context. */
|
|
6
|
-
export declare function apply(ctx: Context,
|
|
11
|
+
export declare function apply(ctx: Context, config?: Config): void;
|
|
7
12
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,9 @@ export { classifyDshEvent, createOpenPetsDshClient, createOpenPetsDshRuntime, re
|
|
|
2
2
|
import { registerDshListeners } from "./runtime.js";
|
|
3
3
|
export const name = "@open-pets/dsh";
|
|
4
4
|
/** Install the OpenPets listeners into the DSH Cordis context. */
|
|
5
|
-
export function apply(ctx,
|
|
6
|
-
|
|
5
|
+
export function apply(ctx, config = {}) {
|
|
6
|
+
if (config.enabled === false)
|
|
7
|
+
return;
|
|
8
|
+
registerDshListeners(ctx, config);
|
|
7
9
|
}
|
|
8
10
|
//# sourceMappingURL=index.js.map
|
package/dist/runtime.d.ts
CHANGED
|
@@ -1,24 +1,37 @@
|
|
|
1
1
|
import { type HookSpeechCategory } from "@open-pets/agent-events";
|
|
2
2
|
import { type OpenPetsClient, type OpenPetsReaction } from "@open-pets/client";
|
|
3
3
|
import type { Context } from "@deepseek-ai/cordis";
|
|
4
|
-
export type DshEventName = "agent/status" | "agent/error" | "approval/request";
|
|
4
|
+
export type DshEventName = "agent/status" | "agent/error" | "approval/request" | "agent/assistant-stream" | "tools/result";
|
|
5
5
|
export type DshAgentStatus = "running" | "idle";
|
|
6
6
|
export interface DshEventDecision {
|
|
7
7
|
readonly reaction: OpenPetsReaction;
|
|
8
8
|
readonly speechCategory: HookSpeechCategory;
|
|
9
9
|
}
|
|
10
10
|
export interface OpenPetsDshOptions {
|
|
11
|
+
/** Master toggle to enable or disable OpenPets integration. Default: true. */
|
|
12
|
+
readonly enabled?: boolean;
|
|
11
13
|
readonly clientFactory?: () => OpenPetsClient;
|
|
12
14
|
readonly schedule?: (work: () => Promise<void>) => void | Promise<void>;
|
|
13
15
|
readonly random?: () => number;
|
|
14
16
|
readonly now?: () => number;
|
|
17
|
+
/** Minimum time between speech messages in milliseconds. Default: 1200. */
|
|
18
|
+
readonly minSpeechIntervalMs?: number;
|
|
19
|
+
/** Forward ⏵ status lines from assistant stream. Default: true. */
|
|
20
|
+
readonly enableStatusLineStream?: boolean;
|
|
21
|
+
/** Forward tool events. Default: true. */
|
|
22
|
+
readonly enableToolEvents?: boolean;
|
|
23
|
+
/** Announce when an approval request is waiting. Default: true. */
|
|
24
|
+
readonly enableApprovalEvents?: boolean;
|
|
15
25
|
}
|
|
16
26
|
export interface OpenPetsDshRuntime {
|
|
17
27
|
readonly handleStatus: (event: unknown) => void;
|
|
18
28
|
readonly handleEvent: (eventName: DshEventName, event?: unknown) => void;
|
|
19
29
|
readonly handleApproval: () => void;
|
|
30
|
+
readonly handleStream: (payload: unknown) => void;
|
|
31
|
+
readonly handleToolResult: (exec: unknown, result?: unknown) => void;
|
|
20
32
|
}
|
|
21
33
|
export type DshCordisApi = Pick<Context, "on">;
|
|
34
|
+
export declare function sanitizeStatusLine(text: string): string | null;
|
|
22
35
|
/**
|
|
23
36
|
* Map only the categorical values defined by DSH. Event payloads are
|
|
24
37
|
* deliberately not inspected or forwarded. For agent/status, only the
|
|
@@ -27,6 +40,6 @@ export type DshCordisApi = Pick<Context, "on">;
|
|
|
27
40
|
export declare function classifyDshEvent(eventName: DshEventName | string, event?: unknown): DshEventDecision | undefined;
|
|
28
41
|
export declare function createOpenPetsDshRuntime(options?: OpenPetsDshOptions): OpenPetsDshRuntime;
|
|
29
42
|
export declare function createOpenPetsDshClient(): OpenPetsClient;
|
|
30
|
-
/** Register the
|
|
43
|
+
/** Register the DSH listeners without taking ownership of their flow. */
|
|
31
44
|
export declare function registerDshListeners(cordis: DshCordisApi, options?: OpenPetsDshOptions): OpenPetsDshRuntime;
|
|
32
45
|
//# sourceMappingURL=runtime.d.ts.map
|
package/dist/runtime.js
CHANGED
|
@@ -2,6 +2,26 @@ import { pickHookSpeech, validateHookSpeech } from "@open-pets/agent-events";
|
|
|
2
2
|
import { createOpenPetsClient } from "@open-pets/client";
|
|
3
3
|
const automaticTimeoutMs = 500;
|
|
4
4
|
const errorSuccessSuppressionMs = 5_000;
|
|
5
|
+
const minSpeechIntervalMs = 1_200;
|
|
6
|
+
export function sanitizeStatusLine(text) {
|
|
7
|
+
if (!text || typeof text !== "string")
|
|
8
|
+
return null;
|
|
9
|
+
let s = text.replace(/[\r\n]+/g, " ").trim();
|
|
10
|
+
s = s.replace(/https?:\/\/\S+/gi, "");
|
|
11
|
+
s = s.replace(/[`{};]/g, "");
|
|
12
|
+
s = s.replace(/(?:^|\s)(?:~|\.{1,2}|[A-Za-z]:)?[\\/]\S+\/([^\s/]+)/g, " $1");
|
|
13
|
+
if (s.length > 100)
|
|
14
|
+
s = s.slice(0, 97) + "...";
|
|
15
|
+
s = s.trim();
|
|
16
|
+
if (s.length === 0)
|
|
17
|
+
return null;
|
|
18
|
+
try {
|
|
19
|
+
return validateHookSpeech(s);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
5
25
|
/**
|
|
6
26
|
* Map only the categorical values defined by DSH. Event payloads are
|
|
7
27
|
* deliberately not inspected or forwarded. For agent/status, only the
|
|
@@ -25,8 +45,15 @@ export function classifyDshEvent(eventName, event) {
|
|
|
25
45
|
export function createOpenPetsDshRuntime(options = {}) {
|
|
26
46
|
const clientFactory = options.clientFactory ?? createOpenPetsDshClient;
|
|
27
47
|
const schedule = options.schedule ?? defaultSchedule;
|
|
48
|
+
const minSpeech = options.minSpeechIntervalMs ?? minSpeechIntervalMs;
|
|
49
|
+
const enableStatusLineStream = options.enableStatusLineStream !== false;
|
|
50
|
+
const enableToolEvents = options.enableToolEvents !== false;
|
|
51
|
+
const enableApprovalEvents = options.enableApprovalEvents !== false;
|
|
28
52
|
let client;
|
|
29
53
|
let recentErrorAt = Number.NEGATIVE_INFINITY;
|
|
54
|
+
let lastCustomSpeechAt = Number.NEGATIVE_INFINITY;
|
|
55
|
+
let streamBuffer = "";
|
|
56
|
+
let statusExtractedForTurn = false;
|
|
30
57
|
const getClient = () => {
|
|
31
58
|
client ??= clientFactory();
|
|
32
59
|
return client;
|
|
@@ -59,16 +86,109 @@ export function createOpenPetsDshRuntime(options = {}) {
|
|
|
59
86
|
// A scheduler supplied by the host is optional infrastructure.
|
|
60
87
|
}
|
|
61
88
|
};
|
|
89
|
+
const dispatchCustom = (message, reaction) => {
|
|
90
|
+
const now = options.now?.() ?? Date.now();
|
|
91
|
+
if (now - lastCustomSpeechAt < minSpeech)
|
|
92
|
+
return;
|
|
93
|
+
lastCustomSpeechAt = now;
|
|
94
|
+
const work = async () => {
|
|
95
|
+
try {
|
|
96
|
+
const validated = validateHookSpeech(message);
|
|
97
|
+
await getClient().say(validated, { reaction });
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// Automatic agent listeners must never affect the DSH operation.
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
try {
|
|
104
|
+
const scheduled = schedule(work);
|
|
105
|
+
if (isPromiseLike(scheduled))
|
|
106
|
+
void scheduled.catch(() => undefined);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Ignored
|
|
110
|
+
}
|
|
111
|
+
};
|
|
62
112
|
return {
|
|
63
113
|
handleStatus(event) {
|
|
114
|
+
const status = typeof event === "string" ? event : getStatusCategory(event);
|
|
115
|
+
if (status === "running") {
|
|
116
|
+
streamBuffer = "";
|
|
117
|
+
statusExtractedForTurn = false;
|
|
118
|
+
}
|
|
64
119
|
dispatch(classifyDshEvent("agent/status", event));
|
|
65
120
|
},
|
|
66
121
|
handleEvent(eventName, event) {
|
|
67
122
|
dispatch(classifyDshEvent(eventName, event));
|
|
68
123
|
},
|
|
69
124
|
handleApproval() {
|
|
125
|
+
if (!enableApprovalEvents)
|
|
126
|
+
return;
|
|
70
127
|
dispatch(classifyDshEvent("approval/request"));
|
|
71
128
|
},
|
|
129
|
+
handleStream(payload) {
|
|
130
|
+
if (!enableStatusLineStream)
|
|
131
|
+
return;
|
|
132
|
+
if (!payload || typeof payload !== "object")
|
|
133
|
+
return;
|
|
134
|
+
const frame = payload.frame;
|
|
135
|
+
if (!frame)
|
|
136
|
+
return;
|
|
137
|
+
if (frame.type === "start") {
|
|
138
|
+
streamBuffer = "";
|
|
139
|
+
statusExtractedForTurn = false;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (frame.type === "end") {
|
|
143
|
+
streamBuffer = "";
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (frame.type === "chunk" && frame.chunk?.type === "text-delta" && typeof frame.chunk.text === "string") {
|
|
147
|
+
if (statusExtractedForTurn)
|
|
148
|
+
return;
|
|
149
|
+
streamBuffer += frame.chunk.text;
|
|
150
|
+
const arrowIndex = streamBuffer.search(/[⏵▶]/);
|
|
151
|
+
if (arrowIndex !== -1) {
|
|
152
|
+
const newlineIndex = streamBuffer.indexOf("\n", arrowIndex);
|
|
153
|
+
if (newlineIndex !== -1) {
|
|
154
|
+
statusExtractedForTurn = true;
|
|
155
|
+
const rawStatus = streamBuffer.slice(arrowIndex + 1, newlineIndex).trim();
|
|
156
|
+
const clean = sanitizeStatusLine(rawStatus);
|
|
157
|
+
if (clean) {
|
|
158
|
+
dispatchCustom(clean, "working");
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
handleToolResult(exec) {
|
|
165
|
+
if (!enableToolEvents)
|
|
166
|
+
return;
|
|
167
|
+
if (!exec || typeof exec !== "object")
|
|
168
|
+
return;
|
|
169
|
+
const e = exec;
|
|
170
|
+
if (!e.name)
|
|
171
|
+
return;
|
|
172
|
+
const args = e.arguments ?? e.params;
|
|
173
|
+
if (e.name === "bash" && args?.description) {
|
|
174
|
+
const clean = sanitizeStatusLine(String(args.description));
|
|
175
|
+
if (clean)
|
|
176
|
+
dispatchCustom(clean, "running");
|
|
177
|
+
}
|
|
178
|
+
else if (e.name === "edit" || e.name === "write") {
|
|
179
|
+
const filePath = args?.file_path ? String(args.file_path).split(/[\\/]/).pop() : "";
|
|
180
|
+
const action = e.name === "edit" ? "Chỉnh sửa" : "Ghi file";
|
|
181
|
+
const clean = sanitizeStatusLine(filePath ? `${action} ${filePath}` : action);
|
|
182
|
+
if (clean)
|
|
183
|
+
dispatchCustom(clean, "editing");
|
|
184
|
+
}
|
|
185
|
+
else if (e.name === "read") {
|
|
186
|
+
const filePath = args?.file_path ? String(args.file_path).split(/[\\/]/).pop() : "";
|
|
187
|
+
const clean = sanitizeStatusLine(filePath ? `Đọc ${filePath}` : "Đang đọc file");
|
|
188
|
+
if (clean)
|
|
189
|
+
dispatchCustom(clean, "working");
|
|
190
|
+
}
|
|
191
|
+
},
|
|
72
192
|
};
|
|
73
193
|
}
|
|
74
194
|
export function createOpenPetsDshClient() {
|
|
@@ -78,9 +198,11 @@ export function createOpenPetsDshClient() {
|
|
|
78
198
|
responseTimeoutMs: automaticTimeoutMs,
|
|
79
199
|
});
|
|
80
200
|
}
|
|
81
|
-
/** Register the
|
|
201
|
+
/** Register the DSH listeners without taking ownership of their flow. */
|
|
82
202
|
export function registerDshListeners(cordis, options = {}) {
|
|
83
203
|
const runtime = createOpenPetsDshRuntime(options);
|
|
204
|
+
if (options.enabled === false)
|
|
205
|
+
return runtime;
|
|
84
206
|
const on = cordis.on;
|
|
85
207
|
on("agent/status", (event) => runtime.handleStatus(event));
|
|
86
208
|
on("agent/error", () => runtime.handleEvent("agent/error"));
|
|
@@ -88,6 +210,8 @@ export function registerDshListeners(cordis, options = {}) {
|
|
|
88
210
|
runtime.handleApproval();
|
|
89
211
|
return typeof next === "function" ? next() : undefined;
|
|
90
212
|
});
|
|
213
|
+
on("agent/assistant-stream", (payload) => runtime.handleStream(payload));
|
|
214
|
+
on("tools/result", (exec, result) => runtime.handleToolResult(exec, result));
|
|
91
215
|
return runtime;
|
|
92
216
|
}
|
|
93
217
|
function defaultSchedule(work) {
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-pets/dsh",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
|
-
"url": "git+https://github.com/
|
|
7
|
+
"url": "git+https://github.com/OpenPetsHQ/openpets.git",
|
|
8
8
|
"directory": "packages/dsh"
|
|
9
9
|
},
|
|
10
10
|
"type": "module",
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"./package.json": "./package.json"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@open-pets/agent-events": "
|
|
44
|
-
"@open-pets/client": "
|
|
43
|
+
"@open-pets/agent-events": "4.1.0",
|
|
44
|
+
"@open-pets/client": "4.1.0"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"@deepseek-ai/cordis": "^4.0.1"
|