dsh-workbuddy-connect 0.2.1

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/lib/index.js ADDED
@@ -0,0 +1,556 @@
1
+ import { _ as WorkBuddyCredentialStore, a as readHostHeartbeat, b as workbuddyOwnAuthPath, d as prepareChatBody, f as regionOf, g as WORKBUDDY_AUTH_FILE_ENV, h as WORKBUDDY_AUTH_FILENAME, i as processStartTimeMs, l as WorkBuddyUpstreamClient, m as WorkBuddyCatalog, n as clearHostHeartbeat, o as workbuddyHostHeartbeatPath, p as FALLBACK_WORKBUDDY_MODELS, r as isHeartbeatProcessAlive, s as writeHostHeartbeat, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, u as classifyUpstreamError, v as defaultDesktopAuthPath, y as parseWorkBuddyAuth } from "./host-heartbeat-DehQgrb9.js";
2
+ import z from "@deepseek-ai/schemastery";
3
+ import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
4
+ import { createProvider } from "@earendil-works/pi-ai";
5
+ import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
6
+ import { resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
7
+ import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
8
+ import { randomBytes, timingSafeEqual } from "node:crypto";
9
+ import { createServer } from "node:http";
10
+ import { Readable } from "node:stream";
11
+ //#region src/adapter.ts
12
+ /**
13
+ * The `workbuddy` pi-ai provider: one loopback-backed adapter registered
14
+ * into the Harness LLM seam, assembled from public `dsh-llm-pi-ai`
15
+ * extension points the way `dsh-codex-connect` assembles its Codex route.
16
+ *
17
+ * @module dsh-workbuddy-connect/adapter
18
+ */
19
+ /** Provider route this bundle owns. */
20
+ const WORKBUDDY_PROVIDER = "workbuddy";
21
+ /** Provider idle ceiling while one stream read is outstanding. */
22
+ const WORKBUDDY_STREAM_IDLE_TIMEOUT_MS = 3e5;
23
+ /**
24
+ * Image-request budgets at the dsh-llm-pi-ai defaults; the profile type made
25
+ * them required in 0.1.1-rc.2. WorkBuddy routes are text-only today, so these
26
+ * bounds never bite unless image input is added to the catalog.
27
+ */
28
+ const REQUEST_IMAGE_BUDGETS = {
29
+ maxRequestImageBytes: 20971520,
30
+ requestImagePixelBudget: 4194304,
31
+ requestImageMaxBytes: 1048576
32
+ };
33
+ /**
34
+ * Inert pi-ai auth plane. The workbuddy route authenticates only through the
35
+ * shim shared secret resolved per request by `resolveApiKey`, so pi-ai's own
36
+ * credential lifecycle and ambient discovery must never manufacture a
37
+ * credential for it. `PiAiAdapterOptions.auth` is required since 0.1.1-rc.2;
38
+ * every ambient question here answers "nothing stored, nothing set".
39
+ */
40
+ const INERT_AUTH = {
41
+ credentials: {
42
+ async read() {},
43
+ async list() {
44
+ return [];
45
+ },
46
+ async modify() {
47
+ throw new Error("dsh-workbuddy-connect: the workbuddy route has no pi-ai credential lifecycle");
48
+ },
49
+ async delete() {}
50
+ },
51
+ authContext: {
52
+ async env() {},
53
+ async fileExists() {
54
+ return false;
55
+ }
56
+ }
57
+ };
58
+ /** No per-token pricing is knowable for a subscription quota; report zero. */
59
+ const NO_COST = {
60
+ input: 0,
61
+ output: 0,
62
+ cacheRead: 0,
63
+ cacheWrite: 0
64
+ };
65
+ /** Build one pi-ai model descriptor pointing at the loopback shim. */
66
+ function toPiModel(info, baseUrl) {
67
+ return {
68
+ id: info.id,
69
+ name: info.name,
70
+ api: "openai-completions",
71
+ provider: WORKBUDDY_PROVIDER,
72
+ baseUrl,
73
+ input: ["text"],
74
+ cost: NO_COST,
75
+ contextWindow: info.contextWindow,
76
+ maxTokens: info.maxTokens
77
+ };
78
+ }
79
+ /**
80
+ * Assemble the adapter. The provider's `getModels` reads the live catalog,
81
+ * and every model's `baseUrl` is re-resolved per read so the shim's
82
+ * ephemeral port applies from the first snapshot after startup.
83
+ */
84
+ function createWorkBuddyAdapter(options) {
85
+ const { shim, store, catalog, resolveAttachments } = options;
86
+ const buildModels = () => {
87
+ const baseUrl = `${shim.baseUrl()}/v1`;
88
+ return catalog.current().map((info) => toPiModel(info, baseUrl));
89
+ };
90
+ const provider = {
91
+ ...createProvider({
92
+ id: WORKBUDDY_PROVIDER,
93
+ name: "WorkBuddy",
94
+ auth: { apiKey: {
95
+ name: "WorkBuddy OAuth bearer token",
96
+ async resolve({ credential }) {
97
+ const apiKey = credential?.key;
98
+ return apiKey === void 0 || apiKey.length === 0 ? void 0 : {
99
+ auth: { apiKey },
100
+ source: "WorkBuddy"
101
+ };
102
+ }
103
+ } },
104
+ models: buildModels(),
105
+ api: openAICompletionsApi()
106
+ }),
107
+ getModels: () => buildModels()
108
+ };
109
+ const profile = {
110
+ provider: WORKBUDDY_PROVIDER,
111
+ displayName: "WorkBuddy",
112
+ streamIdleTimeoutMs: WORKBUDDY_STREAM_IDLE_TIMEOUT_MS,
113
+ retryPolicy: resolveRetryPolicy(void 0, "dsh-workbuddy-connect retryPolicy"),
114
+ configuredMaxTokens: /* @__PURE__ */ new Map(),
115
+ ...REQUEST_IMAGE_BUDGETS,
116
+ piProvider: provider
117
+ };
118
+ let profiles = /* @__PURE__ */ new Map([[WORKBUDDY_PROVIDER, profile]]);
119
+ return {
120
+ adapter: new PiAiAdapter({
121
+ profiles: () => profiles,
122
+ auth: INERT_AUTH,
123
+ resolveApiKey: async () => shim.token(),
124
+ ...resolveAttachments === void 0 ? {} : { resolveAttachments }
125
+ }),
126
+ invalidate: () => {
127
+ profiles = /* @__PURE__ */ new Map([[WORKBUDDY_PROVIDER, profile]]);
128
+ }
129
+ };
130
+ }
131
+ //#endregion
132
+ //#region src/shim.ts
133
+ /**
134
+ * Loopback OpenAI-compatible endpoint. The pi-ai provider points here; the
135
+ * shim applies the WorkBuddy wire quirks (forced streaming, string
136
+ * `tool_choice`, CLI-shaped headers) and forwards to the real upstream.
137
+ * It binds 127.0.0.1 only and never serves another interface.
138
+ *
139
+ * Inbound hardening: the loopback bind alone is not a trust boundary (any
140
+ * local process or a DNS-rebinding page can reach 127.0.0.1), so every
141
+ * request must carry a loopback Host header, browser-sent Origins must be
142
+ * loopback, chat POSTs must be application/json, and the Authorization
143
+ * header must carry the shim's per-process shared secret. The plugin's
144
+ * own client satisfies all four by construction; local attackers cannot
145
+ * read the secret out of the plugin process's memory.
146
+ *
147
+ * @module dsh-workbuddy-connect/shim
148
+ */
149
+ const REQUEST_BODY_LIMIT = 67108864;
150
+ /** Loopback hostnames the shim's own in-process client uses. */
151
+ const LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
152
+ "127.0.0.1",
153
+ "localhost",
154
+ "[::1]"
155
+ ]);
156
+ /** Strip the optional :port from a Host header value, IPv6-bracket aware. */
157
+ function hostnameOfHost(host) {
158
+ let hostname = host.trim().toLowerCase();
159
+ if (hostname.startsWith("[")) {
160
+ const end = hostname.indexOf("]");
161
+ return end === -1 ? hostname : hostname.slice(0, end + 1);
162
+ }
163
+ const colon = hostname.lastIndexOf(":");
164
+ if (colon !== -1 && /^\d+$/.test(hostname.slice(colon + 1))) hostname = hostname.slice(0, colon);
165
+ return hostname;
166
+ }
167
+ /**
168
+ * The request's Host header must name the loopback interface. A DNS-rebinding
169
+ * page (attacker domain re-resolved to 127.0.0.1) sends its own domain in
170
+ * Host, so this check drops those before any routing happens.
171
+ */
172
+ function hostIsLoopback(host) {
173
+ if (host === void 0 || host.trim() === "") return false;
174
+ return LOOPBACK_HOSTS.has(hostnameOfHost(host));
175
+ }
176
+ /**
177
+ * A browser-sent Origin (present header) must be loopback. Non-browser
178
+ * clients (the plugin's own fetch calls) send no Origin at all and pass.
179
+ */
180
+ function originIsLoopback(origin) {
181
+ if (origin === void 0 || origin.trim() === "") return true;
182
+ try {
183
+ const { hostname } = new URL(origin);
184
+ return LOOPBACK_HOSTS.has(hostname) || hostname === "::1";
185
+ } catch {
186
+ return false;
187
+ }
188
+ }
189
+ /** Chat-completion POSTs must carry a JSON body type (simple-request CSRF drops here). */
190
+ function isJsonContentType(req) {
191
+ const type = req.headers["content-type"];
192
+ return typeof type === "string" && type.trim().toLowerCase().startsWith("application/json");
193
+ }
194
+ /** HTTP status each upstream failure class surfaces as. */
195
+ const KIND_STATUS = {
196
+ hard_credit: 402,
197
+ soft_rate: 429,
198
+ session_dead: 401,
199
+ not_found: 502,
200
+ server: 502,
201
+ client: 400
202
+ };
203
+ function writeJson(res, status, body) {
204
+ const payload = JSON.stringify(body);
205
+ res.writeHead(status, {
206
+ "Content-Type": "application/json",
207
+ "Content-Length": Buffer.byteLength(payload)
208
+ });
209
+ res.end(payload);
210
+ }
211
+ function writeOpenAIError(res, status, kind, message) {
212
+ writeJson(res, status, { error: {
213
+ message,
214
+ type: kind,
215
+ code: kind
216
+ } });
217
+ }
218
+ /** Read a request body with a size cap; over-limit bodies fail the request. */
219
+ function readBody(req) {
220
+ return new Promise((resolve, reject) => {
221
+ const chunks = [];
222
+ let size = 0;
223
+ req.on("data", (chunk) => {
224
+ size += chunk.length;
225
+ if (size > REQUEST_BODY_LIMIT) {
226
+ reject(/* @__PURE__ */ new Error("request body too large"));
227
+ req.destroy();
228
+ return;
229
+ }
230
+ chunks.push(chunk);
231
+ });
232
+ req.on("end", () => resolve(Buffer.concat(chunks)));
233
+ req.on("error", reject);
234
+ });
235
+ }
236
+ /**
237
+ * Start the loopback endpoint. Requests carry any bearer; the loopback bind
238
+ * is the boundary, and the upstream credential comes from the store alone.
239
+ */
240
+ function createWorkBuddyShim(options) {
241
+ const { store, client, catalog } = options;
242
+ const logger = options.logger;
243
+ const SHARED_SECRET = randomBytes(32).toString("base64url");
244
+ /** Constant-time bearer check; absent or mismatched bearers are rejected. */
245
+ function bearerOk(req) {
246
+ const header = req.headers.authorization;
247
+ if (typeof header !== "string") return false;
248
+ const match = /^Bearer\s+(.+)$/i.exec(header.trim());
249
+ if (match === null) return false;
250
+ const presented = match[1];
251
+ const expected = SHARED_SECRET;
252
+ const a = Buffer.from(presented);
253
+ const b = Buffer.from(expected);
254
+ if (a.length !== b.length) return false;
255
+ return timingSafeEqual(a, b);
256
+ }
257
+ const server = createServer((req, res) => {
258
+ handle(req, res);
259
+ });
260
+ const ready = new Promise((resolve, reject) => {
261
+ server.once("listening", () => resolve());
262
+ server.once("error", reject);
263
+ });
264
+ server.listen(0, "127.0.0.1");
265
+ const baseUrl = () => {
266
+ const address = server.address();
267
+ if (address === null || typeof address === "string") throw new Error("workbuddy shim has no listening address");
268
+ return `http://127.0.0.1:${address.port}`;
269
+ };
270
+ async function handle(req, res) {
271
+ try {
272
+ if (!hostIsLoopback(req.headers.host)) {
273
+ writeOpenAIError(res, 403, "host_not_allowed", "Host header must name the loopback interface");
274
+ return;
275
+ }
276
+ if (!originIsLoopback(req.headers.origin)) {
277
+ writeOpenAIError(res, 403, "origin_not_allowed", "Origin must be a loopback origin");
278
+ return;
279
+ }
280
+ if (!bearerOk(req)) {
281
+ writeOpenAIError(res, 401, "unauthorized", "missing or invalid Authorization bearer");
282
+ return;
283
+ }
284
+ const url = req.url ?? "/";
285
+ if (req.method === "GET" && (url === "/healthz" || url === "/healthz/")) {
286
+ writeJson(res, 200, { ok: true });
287
+ return;
288
+ }
289
+ if (req.method === "GET" && (url === "/v1/models" || url === "/v1/models/")) {
290
+ writeJson(res, 200, {
291
+ object: "list",
292
+ data: catalog.current().map((model) => ({
293
+ id: model.id,
294
+ object: "model",
295
+ created: 0,
296
+ owned_by: "workbuddy"
297
+ }))
298
+ });
299
+ return;
300
+ }
301
+ if (req.method === "POST" && (url === "/v1/chat/completions" || url === "/v1/chat/completions/")) {
302
+ await chatCompletions(req, res);
303
+ return;
304
+ }
305
+ writeOpenAIError(res, 404, "not_found", `no such route: ${req.method} ${url}`);
306
+ } catch (error) {
307
+ if (!res.headersSent) writeOpenAIError(res, 500, "internal", String(error));
308
+ else res.end();
309
+ }
310
+ }
311
+ async function chatCompletions(req, res) {
312
+ if (!isJsonContentType(req)) {
313
+ writeOpenAIError(res, 415, "unsupported_media_type", "Content-Type must be application/json");
314
+ return;
315
+ }
316
+ let credential;
317
+ try {
318
+ credential = await store.resolve();
319
+ } catch (error) {
320
+ writeOpenAIError(res, 401, "not_signed_in", String(error));
321
+ return;
322
+ }
323
+ const raw = (await readBody(req)).toString("utf8");
324
+ const prepared = prepareChatBody(raw);
325
+ const controller = new AbortController();
326
+ req.on("close", () => controller.abort());
327
+ const result = await client.chatStream(credential, prepared, controller.signal);
328
+ if (!result.ok) {
329
+ writeOpenAIError(res, KIND_STATUS[result.kind], result.kind, `workbuddy upstream ${result.kind} (http ${result.status}): ${result.message.slice(0, 400)}`);
330
+ return;
331
+ }
332
+ res.writeHead(200, {
333
+ "Content-Type": "text/event-stream",
334
+ "Cache-Control": "no-cache",
335
+ "Connection": "keep-alive",
336
+ "X-Accel-Buffering": "no"
337
+ });
338
+ let sawDone = false;
339
+ const body = Readable.fromWeb(result.response.body);
340
+ body.on("data", (chunk) => {
341
+ if (chunk.includes("[DONE]")) sawDone = true;
342
+ });
343
+ body.on("error", (error) => {
344
+ logger?.warn("dsh-workbuddy-connect: upstream stream failed mid-flight", error);
345
+ if (!sawDone && res.writable) res.end("data: [DONE]\n\n");
346
+ });
347
+ body.pipe(res);
348
+ }
349
+ return {
350
+ ready,
351
+ baseUrl,
352
+ token: () => SHARED_SECRET,
353
+ close: () => new Promise((resolve, reject) => {
354
+ server.close(() => resolve());
355
+ server.closeAllConnections();
356
+ server.once("error", reject);
357
+ })
358
+ };
359
+ }
360
+ //#endregion
361
+ //#region src/status-paths.ts
362
+ /** Node-free constants and types shared by the Host and browser halves. */
363
+ /** Plugin-owned status endpoint consumed by its browser half. */
364
+ const WORKBUDDY_STATUS_PATH = "/plugins/dsh-workbuddy-connect/status";
365
+ //#endregion
366
+ //#region src/web-status.ts
367
+ /** Redact token-like content before it crosses to the browser. */
368
+ function safeMessage(error) {
369
+ return (error instanceof Error ? error.message : String(error)).replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/gu, "[redacted token]").replace(/(\b(?:code|token|refresh_token|access_token)=)[^&\s]+/giu, "$1[redacted]").slice(0, 500);
370
+ }
371
+ function json(res, status, body) {
372
+ const payload = JSON.stringify(body);
373
+ res.writeHead(status, {
374
+ "Content-Type": "application/json",
375
+ "Content-Length": Buffer.byteLength(payload)
376
+ });
377
+ res.end(payload);
378
+ }
379
+ /** Loopback browser origins only; other devices are refused until trusted origins exist. */
380
+ function loopbackOrigin(req) {
381
+ const origin = req.headers.origin;
382
+ if (origin === void 0) return true;
383
+ try {
384
+ const { hostname } = new URL(origin);
385
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname === "::1";
386
+ } catch {
387
+ return false;
388
+ }
389
+ }
390
+ /**
391
+ * Assemble the card's status document. Sign-in state is read-only; credit is
392
+ * a live billing answer whose failure degrades to `creditsError` rather than
393
+ * failing the whole document.
394
+ */
395
+ async function workBuddyWebStatus(deps) {
396
+ const authStatus = await deps.store.status();
397
+ if (authStatus.state !== "signed-in") return { status: "signed-out" };
398
+ const status = {
399
+ status: "signed-in",
400
+ ...authStatus.nickname === void 0 ? {} : { nickname: authStatus.nickname },
401
+ ...authStatus.domain === void 0 || authStatus.domain === "" ? {} : { domain: authStatus.domain },
402
+ ...authStatus.source === void 0 ? {} : { source: authStatus.source },
403
+ ...authStatus.expiresAtMs === void 0 ? {} : { expiresAt: authStatus.expiresAtMs }
404
+ };
405
+ try {
406
+ const credential = await deps.store.current();
407
+ if (credential !== void 0) {
408
+ const credits = await deps.client.fetchCredits(credential);
409
+ return {
410
+ ...status,
411
+ credits
412
+ };
413
+ }
414
+ } catch (error) {
415
+ return {
416
+ ...status,
417
+ creditsError: safeMessage(error)
418
+ };
419
+ }
420
+ return status;
421
+ }
422
+ /** Mount the GET status route on an optional webServer context. */
423
+ function registerWorkBuddyStatusRoute(ctx, deps) {
424
+ ctx.effect(() => {
425
+ const dispose = ctx.webServer.register({
426
+ kind: "exact",
427
+ path: WORKBUDDY_STATUS_PATH,
428
+ handler: async (req, res) => {
429
+ if (req.method !== "GET") {
430
+ json(res, 405, { error: "method not allowed" });
431
+ return;
432
+ }
433
+ if (!loopbackOrigin(req)) {
434
+ json(res, 403, { error: "origin-not-trusted" });
435
+ return;
436
+ }
437
+ try {
438
+ json(res, 200, await workBuddyWebStatus(deps));
439
+ } catch (error) {
440
+ json(res, 500, { error: safeMessage(error) });
441
+ }
442
+ }
443
+ });
444
+ return () => {
445
+ dispose();
446
+ };
447
+ }, "dsh-workbuddy-connect: Web status route");
448
+ }
449
+ //#endregion
450
+ //#region src/index.ts
451
+ /** Stable Cordis plugin name. */
452
+ const name = "llm-workbuddy";
453
+ /** The model registry required before the provider can register. */
454
+ const inject = ["llm"];
455
+ /** Settings namespace reserved for the future configuration card. */
456
+ const WORKBUDDY_SETTINGS_NS = settingsNamespace("workbuddy");
457
+ const Config = z.object({ authFile: z.string().description("WorkBuddy desktop auth file (defaults to the app's own location)") });
458
+ /**
459
+ * Start the loopback endpoint, register the `workbuddy` provider, and
460
+ * refresh the model catalog from the upstream once credentials allow it.
461
+ * The static fallback catalog serves from the first moment, so an offline
462
+ * upstream never leaves the provider empty.
463
+ */
464
+ function apply(ctx, config) {
465
+ const client = new WorkBuddyUpstreamClient();
466
+ const store = new WorkBuddyCredentialStore({
467
+ ...config.authFile === void 0 ? {} : { desktopPath: config.authFile },
468
+ refresh: (credential) => client.refreshToken(credential)
469
+ });
470
+ const catalog = new WorkBuddyCatalog();
471
+ const shim = createWorkBuddyShim({
472
+ store,
473
+ client,
474
+ catalog,
475
+ logger: ctx.logger
476
+ });
477
+ ctx.inject(["webServer"], (webCtx) => registerWorkBuddyStatusRoute(webCtx, {
478
+ store,
479
+ client
480
+ }));
481
+ let current = () => config;
482
+ installSettingsSection(ctx, WORKBUDDY_SETTINGS_NS, Config, config, {
483
+ setSource(source) {
484
+ current = source;
485
+ },
486
+ onChange() {
487
+ const next = current().authFile;
488
+ store.setDesktopPath(next);
489
+ }
490
+ });
491
+ let stopped = false;
492
+ ctx.effect(() => () => {
493
+ stopped = true;
494
+ shim.close();
495
+ clearHostHeartbeat();
496
+ });
497
+ shim.ready.then(() => {
498
+ if (stopped) return;
499
+ let invalidate;
500
+ try {
501
+ const workbuddy = createWorkBuddyAdapter({
502
+ shim,
503
+ store,
504
+ catalog,
505
+ resolveAttachments: () => ctx.get("attachments")
506
+ });
507
+ invalidate = workbuddy.invalidate;
508
+ let releaseAdapter;
509
+ let releaseDirectory;
510
+ try {
511
+ releaseAdapter = ctx.llm.registerAdapter([WORKBUDDY_PROVIDER], workbuddy.adapter);
512
+ releaseDirectory = ctx.llm.registerConfigurableProviders([{
513
+ provider: WORKBUDDY_PROVIDER,
514
+ displayName: "WorkBuddy",
515
+ settingsNs: WORKBUDDY_SETTINGS_NS,
516
+ settingsPath: [],
517
+ declared: false
518
+ }]);
519
+ } finally {
520
+ if (releaseAdapter === void 0 || releaseDirectory === void 0) {
521
+ releaseAdapter?.();
522
+ releaseDirectory?.();
523
+ }
524
+ }
525
+ try {
526
+ ctx.effect(() => () => {
527
+ releaseAdapter?.();
528
+ releaseDirectory?.();
529
+ });
530
+ } catch {
531
+ releaseAdapter?.();
532
+ releaseDirectory?.();
533
+ }
534
+ writeHostHeartbeat();
535
+ } catch (error) {
536
+ ctx.logger.error("dsh-workbuddy-connect: provider registration failed", error);
537
+ return;
538
+ }
539
+ (async () => {
540
+ try {
541
+ const credential = await store.current();
542
+ if (credential === void 0 || stopped) return;
543
+ const models = await client.fetchModels(credential);
544
+ if (stopped) return;
545
+ catalog.set([...models]);
546
+ invalidate?.();
547
+ } catch (error) {
548
+ ctx.logger.warn("dsh-workbuddy-connect: dynamic model catalog unavailable; serving the static fallback list", error);
549
+ }
550
+ })();
551
+ }).catch((error) => {
552
+ ctx.logger.error("dsh-workbuddy-connect: loopback endpoint failed to start; provider not registered", error);
553
+ });
554
+ }
555
+ //#endregion
556
+ export { Config, FALLBACK_WORKBUDDY_MODELS, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_PROVIDER, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WorkBuddyCatalog, WorkBuddyCredentialStore, WorkBuddyUpstreamClient, apply, classifyUpstreamError, clearHostHeartbeat, createWorkBuddyAdapter, createWorkBuddyShim, defaultDesktopAuthPath, inject, isHeartbeatProcessAlive, name, parseWorkBuddyAuth, prepareChatBody, processStartTimeMs, readHostHeartbeat, regionOf, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath };
package/package.json ADDED
@@ -0,0 +1,102 @@
1
+ {
2
+ "name": "dsh-workbuddy-connect",
3
+ "displayName": "DSH WorkBuddy Connect",
4
+ "description": "将 WorkBuddy 桌面 App 包含的模型自动接入 DeepSeek Harness — bring WorkBuddy desktop app models into DeepSeek Harness with zero configuration.",
5
+ "version": "0.2.1",
6
+ "author": "corrinehu",
7
+ "keywords": [
8
+ "dsh-plugin",
9
+ "deepseek-harness",
10
+ "deepseek",
11
+ "workbuddy",
12
+ "model-provider"
13
+ ],
14
+ "type": "module",
15
+ "engines": {
16
+ "node": "^22.19.0 || >=24.0.0"
17
+ },
18
+ "main": "lib/index.js",
19
+ "types": "lib/index.d.ts",
20
+ "bin": {
21
+ "dsh-workbuddy-connect": "lib/bin.js"
22
+ },
23
+ "scripts": {
24
+ "build": "tsdown",
25
+ "test": "vitest run",
26
+ "typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.client.json",
27
+ "check": "pnpm run typecheck && pnpm run test && pnpm run build"
28
+ },
29
+ "exports": {
30
+ ".": {
31
+ "types": "./lib/index.d.ts",
32
+ "default": "./lib/index.js"
33
+ },
34
+ "./client": "./lib/client.js",
35
+ "./cordis.patch.yml": "./cordis.patch.yml",
36
+ "./package.json": "./package.json"
37
+ },
38
+ "files": [
39
+ "lib",
40
+ "cordis.patch.yml",
41
+ "README.md",
42
+ "README.en.md",
43
+ "LICENSE"
44
+ ],
45
+ "license": "MIT",
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/corrinehu/dsh-workbuddy-connect.git"
49
+ },
50
+ "homepage": "https://github.com/corrinehu/dsh-workbuddy-connect#readme",
51
+ "bugs": "https://github.com/corrinehu/dsh-workbuddy-connect/issues",
52
+ "dsh": {
53
+ "bundle": {
54
+ "patch": "./cordis.patch.yml"
55
+ },
56
+ "client": {
57
+ "inject": [
58
+ "@deepseek-ai/dsh-client-runtime",
59
+ "@deepseek-ai/dsh-client-ui-slots",
60
+ "@deepseek-ai/dsh-client-ui-settings-plugins",
61
+ "@deepseek-ai/dsh-client-locale"
62
+ ],
63
+ "platform": "web"
64
+ }
65
+ },
66
+ "peerDependencies": {
67
+ "@earendil-works/pi-ai": "0.82.1",
68
+ "@deepseek-ai/cordis": "^4.0.1-rc.1",
69
+ "@deepseek-ai/dsh-atomic-write": "^0.1.1-rc.2",
70
+ "@deepseek-ai/dsh-attachment": "^0.1.1-rc.2",
71
+ "@deepseek-ai/dsh-home-paths": "^0.1.1-rc.2",
72
+ "@deepseek-ai/dsh-host-webserver": "^0.1.1-rc.2",
73
+ "@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
74
+ "@deepseek-ai/dsh-llm-pi-ai": "^0.1.1-rc.2",
75
+ "@deepseek-ai/dsh-settings": "^0.1.1-rc.2",
76
+ "@deepseek-ai/schemastery": "^3.18.1-rc.1",
77
+ "react": "^18.2.0"
78
+ },
79
+ "devDependencies": {
80
+ "@earendil-works/pi-ai": "0.82.1",
81
+ "@deepseek-ai/cordis": "^4.0.1",
82
+ "@deepseek-ai/dsh-atomic-write": "^0.1.1-rc.2",
83
+ "@deepseek-ai/dsh-attachment": "^0.1.1-rc.2",
84
+ "@deepseek-ai/dsh-client-locale": "^0.1.1-rc.2",
85
+ "@deepseek-ai/dsh-client-runtime": "^0.1.1-rc.2",
86
+ "@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.1-rc.2",
87
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.1-rc.2",
88
+ "@deepseek-ai/dsh-credentials": "^0.1.1-rc.2",
89
+ "@deepseek-ai/dsh-home-paths": "^0.1.1-rc.2",
90
+ "@deepseek-ai/dsh-host-webserver": "^0.1.1-rc.2",
91
+ "@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
92
+ "@deepseek-ai/dsh-llm-pi-ai": "^0.1.1-rc.2",
93
+ "@deepseek-ai/dsh-settings": "^0.1.1-rc.2",
94
+ "@deepseek-ai/schemastery": "3.18.1",
95
+ "@types/node": "^22.20.0",
96
+ "@types/react": "~18.3.1",
97
+ "react": "^18.2.0",
98
+ "tsdown": "^0.22.2",
99
+ "typescript": "^5.9.3",
100
+ "vitest": "^4.1.8"
101
+ }
102
+ }