@open-pets/dsh 4.0.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 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, options?: OpenPetsDshOptions): void;
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, options = {}) {
6
- registerDshListeners(ctx, options);
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 three DSH listeners without taking ownership of their flow. */
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 three DSH listeners without taking ownership of their flow. */
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": "4.0.0",
3
+ "version": "4.1.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
7
- "url": "git+https://github.com/alvinunreal/openpets.git",
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": "4.0.0",
44
- "@open-pets/client": "4.0.0"
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"