dsh-llm-workbuddy 0.1.11 → 0.1.13

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/CHANGELOG.md ADDED
@@ -0,0 +1,42 @@
1
+ # Changelog
2
+
3
+ 本文件记录每个版本的改动。版本号遵循语义化版本(MAJOR.MINOR.PATCH),
4
+ 每次发版请同步 `package.json` 的 `version` 并打一个 `git tag`(如 `v0.1.13`),
5
+ 在 GitHub 创建 Release 时本文件即为更新说明来源。
6
+
7
+ ## [0.1.13] - 2026-08-28
8
+
9
+ ### Added
10
+ - 用量面板新增**积分消耗**统计。WorkBuddy 上游在每次对话的 usage 中返回
11
+ `credit` 字段(如 `deepseek-v4-pro` → `0.02`),插件此前丢弃了该字段;
12
+ 现将其写入本地台账,并在 📊 用量面板展示今日/累计/按模型的积分消耗。
13
+
14
+ ### Notes
15
+ - 代理未暴露账户余额接口,因此只能统计**已消耗**积分,无法显示剩余余额。
16
+ - 免费/折扣档模型上游回报 `credit: 0`,面板会提示"当前模型积分为 0"。
17
+
18
+ ## [0.1.12] - 2026-08-28
19
+
20
+ ### Fixed
21
+ - 恢复模型目录即白名单的过滤逻辑,模型选择器不再显示已下架/不可用的
22
+ 老模型(`deepseek-v3-1`、`glm-4.6`、`kimi-k2`、`minimax-m2.5`、
23
+ `kimi-k2-thinking`、`hunyuan-image-v3.0` 等)。
24
+
25
+ ### Added(随此前未提交批次一并发布)
26
+ - 本地 token 用量台账(`$DSH_HOME/llm-workbuddy/usage.jsonl`)与
27
+ `GET /api/workbuddy/usage` 接口,📊 用量面板展示今日/累计/按模型明细。
28
+ - 一键诊断端点 `POST /api/workbuddy/diagnose`,真实发请求探测模型能否出字。
29
+ - 请求图片压缩,确保上游接受多模态输入。
30
+
31
+ ## [0.1.11] 及更早
32
+
33
+ - `0.1.11`:适配代理动态模型列表(product.json 透传)。
34
+ - `0.1.10`:过滤已下架模型(glm-5.0/4.7/4.6、minimax-m2.5 等)。
35
+ - `0.1.9` 及之前:见 git 提交历史(`git log`)。
36
+
37
+ ---
38
+
39
+ <!-- 历史版本锚点(便于生成 Release 时对比区间) -->
40
+ [0.1.13]: https://github.com/zdk119746/dsh-llm-workbuddy/compare/v0.1.12...v0.1.13
41
+ [0.1.12]: https://github.com/zdk119746/dsh-llm-workbuddy/compare/v0.1.11...v0.1.12
42
+ [0.1.11]: https://github.com/zdk119746/dsh-llm-workbuddy/releases/tag/v0.1.11
package/README.md CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/dsh-llm-workbuddy.svg)](https://www.npmjs.com/package/dsh-llm-workbuddy)
4
4
  [![license](https://img.shields.io/npm/l/dsh-llm-workbuddy.svg)](https://github.com/zdk119746/dsh-llm-workbuddy/blob/main/LICENSE)
5
+ [![Changelog](https://img.shields.io/badge/changelog-CHANGELOG-blue)](./CHANGELOG.md)
5
6
 
6
7
  在 DeepSeek Harness 中使用你的 **WorkBuddy / CodeBuddy** 账号模型的 LLM 适配器插件。
7
8
 
@@ -199,6 +200,7 @@ Web 登录后终端脚本也读得到同一份会话。
199
200
  | `GET /api/workbuddy/status` | 读取会话文件(默认 `~/.codebuddy-session.json`,或配置的 `sessionFile`)的 `auth.expiresAt` 判断会话是否有效,并 `fetch` 代理 `/health` 判断 `proxyUp`;返回 JSON:`{ sessionFile, authenticated, expiresAt, account, proxyUp, tokenValid, loginScriptAvailable }`;非 GET 返回 405 |
200
201
  | `POST /api/workbuddy/login` | 若已有有效会话则直接返回 `alreadyLoggedIn`;否则用系统 `python3` `spawn` 包内 `login_workbuddy.py --session-file <sessionFile>`,从子进程 stdout 解析出 `authUrl` 立即返回 `{ authUrl, pending:true }`(设备流在后台继续,前端轮询 status 感知完成);非 POST 返回 405 |
201
202
  | `POST /api/workbuddy/diagnose` | **一键诊断**:真实探测健康状态,返回 `{ ok, session, health, chat, loginScriptAvailable, restartCommand }`。与 `/status` 不同,它除了探 `/health`,还会**真实发一次最小模型请求**(`chat.chatWorking`),能戳穿"胶囊显示成功但模型全 500"的假象;`ok:false` 时附带 `restartCommand`(自动区分本地 monorepo 布局与标准安装);非 POST 返回 405 |
203
+ | `GET /api/workbuddy/usage` | **用量统计**:读取本地 token 用量台账(`$DSH_HOME/llm-workbuddy/usage.jsonl`),返回 `{ today, byModel, total }`(今日/按模型/累计的 input/output tokens、请求次数,以及**积分消耗 `credit`**);非 GET 返回 405 |
202
204
 
203
205
  > 会话文件与登录脚本路径的解析顺序:
204
206
  > 1. 配置里显式指定的 `sessionFile` / `loginScript`;
@@ -214,6 +216,10 @@ Web 登录后终端脚本也读得到同一份会话。
214
216
  - 胶囊里有个 **🔍 诊断** 按钮:点它 `POST /api/workbuddy/diagnose`,弹出一个面板
215
217
  显示**真实健康状态**(登录、会话文件、代理进程、登录令牌、模型能否出字),
216
218
  发现问题时附带**可复制的重启命令**(一键复制到终端执行)。
219
+ - 胶囊里有个 **📊 用量** 按钮:点它 `GET /api/workbuddy/usage`,弹出一个面板
220
+ 显示**今日/累计 token 用量**、**积分消耗**(上游每次返回的 `credit` 累加)与
221
+ **按模型明细**(数据来自本地台账 `$DSH_HOME/llm-workbuddy/usage.jsonl`)。
222
+ 注:代理不暴露余额/剩余积分接口,只能统计**已消耗**积分,无法显示账户剩余。
217
223
  - 状态映射:
218
224
  - `authenticated && proxyUp` → 🟢 绿,显示 `WorkBuddy · <昵称>`
219
225
  - 否则 → 🔴 红,显示「登录」按钮;`proxyUp` 为 false 时额外提示 `代理未运行`
@@ -331,3 +337,18 @@ glm-5.0、minimax-m2.5 等,上游返回 `service info not found`)会被过
331
337
  > 第三方 workbuddy2api 代理**不在包内**,需单独安装(见「安装代理」章节)。
332
338
  > 仓库根目录的 `start-workbuddy.sh` / `login-workbuddy.sh` / `.workbuddy/` 是本地
333
339
  > 开发用的配套文件,不随 npm 包发布。
340
+
341
+ ## 更新日志 / Changelog
342
+
343
+ 每次发版的改动记录见 [CHANGELOG.md](./CHANGELOG.md)。
344
+
345
+ ### 发版流程(确保 GitHub 有更新说明)
346
+
347
+ 1. 改完代码后,在 `package.json` 里 `version` 自增(语义化版本)。
348
+ 2. 在 `CHANGELOG.md` 顶部补一段本次版本(`## [x.y.z] - 日期`)的 Added / Fixed / Changed。
349
+ 3. 提交并打 tag:`git commit -am "release: x.y.z"` 然后 `git tag vx.y.z`。
350
+ 4. `git push origin main --tags`,到 GitHub 用该 tag 创建 **Release**——
351
+ Release 的描述直接引用 CHANGELOG 对应段落,仓库页面就有了「改了啥」的说明。
352
+
353
+ > 注意:本机 `dsh web` 通过符号链接直接加载本地目录,因此**本地改动能立刻重启生效**,
354
+ > GitHub 上的版本只影响通过 `dsh plugin add dsh-llm-workbuddy` 安装的其他用户。
package/lib/client.js CHANGED
@@ -107,6 +107,18 @@ function runWidget() {
107
107
  }
108
108
  // 诊断按钮始终显示,便于随时自查真实健康状态。
109
109
  html += '<button class="wb-btn wb-diagnose" id="wb-diagnose" title="一键诊断">🔍</button>';
110
+ // 用量按钮:查看今日/累计 token 用量与模型明细。
111
+ html += '<button class="wb-btn wb-usage" id="wb-usage" title="用量统计">📊</button>';
112
+ // 签到按钮:✅ 今日已处理 / 🎁 今日未处理(点击立即领取;10 点后也会自动领取)。
113
+ var ck = status.checkin;
114
+ if (ck) {
115
+ var ckMsg = ck.lastResult && ck.lastResult.message ? "\n" + ck.lastResult.message : "";
116
+ var ckTitle = ck.handledToday ? "今日已处理签到" + ckMsg : "今日还没签到(每天 10 点后自动领取)\n点击立即领取";
117
+ var ckIcon = ck.handledToday && ck.lastResult && ck.lastResult.ok ? "✅" : "🎁";
118
+ html += '<button class="wb-btn wb-checkin" id="wb-checkin" title="' + esc(ckTitle) + '">' + ckIcon + "</button>";
119
+ }
120
+ // 刷新模型按钮:从代理重读模型列表(跟随 WorkBuddy 应用更新)并让模型选择器立即重载。
121
+ html += '<button class="wb-btn wb-models" id="wb-models" title="刷新模型列表">🔄</button>';
110
122
  if (!status.proxyUp) html += '<span class="wb-warn">代理未运行</span>';
111
123
  html += "</div>";
112
124
  host.innerHTML = html;
@@ -114,6 +126,12 @@ function runWidget() {
114
126
  if (btn) btn.addEventListener("click", onLogin);
115
127
  var dg = host.querySelector("#wb-diagnose");
116
128
  if (dg) dg.addEventListener("click", onDiagnose);
129
+ var us = host.querySelector("#wb-usage");
130
+ if (us) us.addEventListener("click", onUsage);
131
+ var ckBtn = host.querySelector("#wb-checkin");
132
+ if (ckBtn) ckBtn.addEventListener("click", onCheckin);
133
+ var mdBtn = host.querySelector("#wb-models");
134
+ if (mdBtn) mdBtn.addEventListener("click", onRefreshModels);
117
135
  }
118
136
 
119
137
  function refresh() {
@@ -206,12 +224,106 @@ function runWidget() {
206
224
  });
207
225
  }
208
226
 
227
+ /** 用量统计:读取 /api/workbuddy/usage 并展示今日/累计/模型明细。 */
228
+ function onUsage() {
229
+ var panel = document.getElementById("wb-usage-panel");
230
+ if (panel) { panel.remove(); return; }
231
+ var el = document.createElement("div");
232
+ el.id = "wb-usage-panel";
233
+ el.style.cssText =
234
+ "position:fixed;right:12px;bottom:44px;z-index:2147483647;width:340px;max-height:340px;overflow:auto;" +
235
+ "background:#0f172a;color:#e2e8f0;border:1px solid #334155;border-radius:8px;" +
236
+ "padding:10px;font:12px/1.5 system-ui,sans-serif;box-shadow:0 6px 20px rgba(0,0,0,.5);";
237
+ el.innerHTML = '<div class="wb-usage-title" style="font-weight:700;margin-bottom:6px">WorkBuddy 用量统计</div>' +
238
+ '<div class="wb-usage-body" style="color:#94a3b8">加载中…</div>';
239
+ document.body.appendChild(el);
240
+
241
+ fetch("/api/workbuddy/usage")
242
+ .then(function (r) { return r.ok ? r.json() : null; })
243
+ .then(function (d) {
244
+ if (!d) throw new Error("空响应");
245
+ var hasCredit = (d.total.credit || 0) > 0;
246
+ var lines = [];
247
+ lines.push("今日:输入 " + d.today.inputTokens + " / 输出 " + d.today.outputTokens + " tokens(" + d.today.requests + " 次请求)");
248
+ lines.push("累计:输入 " + d.total.inputTokens + " / 输出 " + d.total.outputTokens + " tokens(" + d.total.requests + " 次请求)");
249
+ if (hasCredit) {
250
+ lines.push("今日积分消耗:" + fmtCredit(d.today.credit) + " | 累计:" + fmtCredit(d.total.credit));
251
+ }
252
+ lines.push("");
253
+ lines.push("按模型明细:");
254
+ if (d.byModel.length === 0) lines.push(" 暂无数据");
255
+ for (var i = 0; i < d.byModel.length; i += 1) {
256
+ var m = d.byModel[i];
257
+ var extra = (m.credit || 0) > 0 ? ",积分 " + fmtCredit(m.credit) : "";
258
+ lines.push(" " + m.model + ":输入 " + m.inputTokens + " / 输出 " + m.outputTokens + "(" + m.requests + " 次" + extra + ")");
259
+ }
260
+ var note = hasCredit
261
+ ? "数据来源:本地用量台账(含上游每次返回的积分消耗;代理不暴露余额接口,无法显示剩余积分)"
262
+ : "数据来源:本地 token 用量台账(" + d.total.requests + " 次累计);当前模型为免费/折扣档,上游回报积分为 0";
263
+ el.innerHTML = '<div class="wb-usage-title" style="font-weight:700;margin-bottom:6px">WorkBuddy 用量统计</div>' +
264
+ '<div class="wb-usage-body" style="white-space:pre-wrap;word-break:break-word">' + esc(lines.join("\n")) + "</div>" +
265
+ '<div style="margin-top:8px;color:#64748b;font-size:11px">' + esc(note) + "</div>";
266
+ })
267
+ .catch(function (err) {
268
+ el.innerHTML = '<div class="wb-usage-title" style="font-weight:700;color:#f87171">用量加载失败</div>' +
269
+ '<div class="wb-usage-body">' + esc(String(err && err.message || err)) + "</div>";
270
+ });
271
+ }
272
+
209
273
  function esc(s) {
210
274
  return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) {
211
275
  return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c];
212
276
  });
213
277
  }
214
278
 
279
+ /** 格式化积分消耗:保留 2 位小数,去掉无意义的尾零。 */
280
+ function fmtCredit(n) {
281
+ var v = Number(n) || 0;
282
+ return (Math.round(v * 100) / 100).toString();
283
+ }
284
+
285
+ /** 手动领取今日签到(幂等:官方对已签到返回业务拒绝,不会出错)。 */
286
+ function onCheckin() {
287
+ var btn = host.querySelector("#wb-checkin");
288
+ if (!btn || btn.dataset.busy) return;
289
+ btn.dataset.busy = "1";
290
+ btn.textContent = "⏳";
291
+ fetch("/api/workbuddy/checkin", { method: "POST" })
292
+ .then(function (r) { return r.json(); })
293
+ .then(function (d) {
294
+ btn.textContent = d && d.ok ? "✅" : "🎁";
295
+ btn.title = (d && d.message) || "签到结果未知";
296
+ refresh();
297
+ })
298
+ .catch(function () {
299
+ btn.textContent = "🎁";
300
+ btn.title = "签到请求失败(代理未运行?)";
301
+ })
302
+ .then(function () { delete btn.dataset.busy; });
303
+ }
304
+
305
+ /** 刷新模型列表:清插件发现缓存 + 触发模型选择器立即重载。 */
306
+ function onRefreshModels() {
307
+ var btn = host.querySelector("#wb-models");
308
+ if (!btn || btn.dataset.busy) return;
309
+ btn.dataset.busy = "1";
310
+ btn.textContent = "…";
311
+ fetch("/api/workbuddy/refresh-models", { method: "POST" })
312
+ .then(function (r) { return r.json(); })
313
+ .then(function (d) {
314
+ btn.textContent = d && typeof d.count === "number" ? d.count + "✓" : "✓";
315
+ btn.title = d && d.announced
316
+ ? "已刷新:" + d.count + " 个模型(模型选择器已更新)"
317
+ : "已刷新模型缓存(事件通知失败,重开设置页可见新列表)";
318
+ setTimeout(function () { btn.textContent = "🔄"; }, 2000);
319
+ })
320
+ .catch(function () {
321
+ btn.textContent = "🔄";
322
+ btn.title = "刷新失败(代理未运行?)";
323
+ })
324
+ .then(function () { delete btn.dataset.busy; });
325
+ }
326
+
215
327
  refresh();
216
328
  timer = setInterval(refresh, POLL_MS);
217
329
  }
package/lib/index.js CHANGED
@@ -17,8 +17,8 @@
17
17
  * to a shipped static catalog when the proxy is not running.
18
18
  */
19
19
  import { spawn } from "node:child_process";
20
- import { existsSync, readFileSync } from "node:fs";
21
- import { dirname, resolve } from "node:path";
20
+ import { existsSync, mkdirSync, readFileSync, appendFileSync, openSync, truncateSync, writeSync, closeSync, writeFileSync } from "node:fs";
21
+ import { dirname, join, resolve } from "node:path";
22
22
  import { fileURLToPath } from "node:url";
23
23
  import z from "@deepseek-ai/schemastery";
24
24
  import {
@@ -62,6 +62,12 @@ const DEFAULT_CONTEXT_WINDOW = 200_000;
62
62
  /** Default per-request output-token cap. */
63
63
  const DEFAULT_MAX_TOKENS = 32_000;
64
64
 
65
+ /** Usage ledger: one JSONL line per completed request, under DSH_HOME. */
66
+ const USAGE_DIR_NAME = "llm-workbuddy";
67
+ const USAGE_FILE_NAME = "usage.jsonl";
68
+ /** Cap on how many historical lines the ledger keeps (avoid unbounded growth). */
69
+ const USAGE_MAX_LINES = 20_000;
70
+
65
71
  /** Live `/v1/models` discovery cache window and request timeout. */
66
72
  const DISCOVERY_TTL_MS = 30_000;
67
73
  const DISCOVERY_TIMEOUT_MS = 2_000;
@@ -139,6 +145,17 @@ function liveReasoningEffort(entry, catalogEffort) {
139
145
  return lower === "low" || lower === "medium" || lower === "high" ? lower : undefined;
140
146
  }
141
147
 
148
+ /**
149
+ * Extract validated input modalities from a live `/v1/models` entry
150
+ * (`input_modalities`, codex format). Unknown or missing values degrade to
151
+ * text-only, matching the official catalog's conservative default.
152
+ */
153
+ function liveInputModalities(entry) {
154
+ const mods = Array.isArray(entry?.input_modalities) ? entry.input_modalities : [];
155
+ const valid = mods.filter((modality) => modality === "text" || modality === "image");
156
+ return valid.length > 0 ? [...new Set(valid)] : ["text"];
157
+ }
158
+
142
159
 
143
160
  // #region serialize
144
161
 
@@ -337,14 +354,115 @@ function mapFinishReason(reason) {
337
354
  function mapUsage(usage) {
338
355
  const cacheRead = usage.prompt_tokens_details?.cached_tokens ?? usage.prompt_cache_hit_tokens;
339
356
  const reasoning = usage.completion_tokens_details?.reasoning_tokens;
357
+ // The upstream reports per-request `credit` consumption in the usage block
358
+ // (e.g. deepseek-v4-pro → 0.02). Free/discounted models report 0, so we
359
+ // only keep the field when it is a positive number — that is the real
360
+ // 积分 spend we want to surface (the proxy exposes no balance endpoint).
361
+ const credit = typeof usage.credit === "number" && usage.credit > 0 ? usage.credit : undefined;
340
362
  return {
341
363
  inputTokens: usage.prompt_tokens - (cacheRead ?? 0),
342
364
  outputTokens: usage.completion_tokens,
343
365
  ...(cacheRead !== undefined ? { cacheReadTokens: cacheRead } : {}),
344
366
  ...(reasoning !== undefined ? { reasoningTokens: reasoning } : {}),
367
+ ...(credit !== undefined ? { credit } : {}),
368
+ };
369
+ }
370
+
371
+ // #region usage ledger
372
+
373
+ /** Resolve the usage ledger file under DSH_HOME (falls back to ~/.dsh). */
374
+ function usageLedgerPath() {
375
+ const configured = process.env.DSH_HOME;
376
+ if (configured !== undefined && configured.trim().length > 0) {
377
+ return join(configured, USAGE_DIR_NAME, USAGE_FILE_NAME);
378
+ }
379
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? ".";
380
+ return join(home, ".dsh", USAGE_DIR_NAME, USAGE_FILE_NAME);
381
+ }
382
+
383
+ /** Append one completed request's token usage to the ledger (best-effort). */
384
+ function recordUsage(model, usage) {
385
+ if (usage === undefined) return;
386
+ try {
387
+ const file = usageLedgerPath();
388
+ mkdirSync(dirname(file), { recursive: true });
389
+ const line = JSON.stringify({
390
+ ts: Date.now(),
391
+ model,
392
+ input: usage.inputTokens ?? 0,
393
+ output: usage.outputTokens ?? 0,
394
+ cacheRead: usage.cacheReadTokens ?? 0,
395
+ reasoning: usage.reasoningTokens ?? 0,
396
+ credit: usage.credit ?? 0,
397
+ });
398
+ appendFileSync(file, `${line}\n`);
399
+ // Trim the ledger when it exceeds the cap: keep only the newest lines.
400
+ const size = existsSync(file) ? readFileSync(file, "utf8").split("\n").filter(Boolean).length : 0;
401
+ if (size > USAGE_MAX_LINES) {
402
+ const lines = readFileSync(file, "utf8").split("\n").filter(Boolean).slice(-USAGE_MAX_LINES);
403
+ const fd = openSync(file, "w");
404
+ try { writeSync(fd, `${lines.join("\n")}\n`); } finally { closeSync(fd); }
405
+ }
406
+ } catch {
407
+ // usage recording must never break the request path
408
+ }
409
+ }
410
+
411
+ /** Read the ledger and aggregate into daily/model breakdowns. */
412
+ function readUsageLedger() {
413
+ const file = usageLedgerPath();
414
+ const today = new Date();
415
+ today.setHours(0, 0, 0, 0);
416
+ const todayMs = today.getTime();
417
+ const byModel = new Map();
418
+ let todayInput = 0;
419
+ let todayOutput = 0;
420
+ let todayCredit = 0;
421
+ let todayRequests = 0;
422
+ let totalInput = 0;
423
+ let totalOutput = 0;
424
+ let totalCredit = 0;
425
+ let totalRequests = 0;
426
+ if (!existsSync(file)) {
427
+ return { today: { inputTokens: 0, outputTokens: 0, credit: 0, requests: 0 }, byModel: [], total: { inputTokens: 0, outputTokens: 0, credit: 0, requests: 0 } };
428
+ }
429
+ const lines = readFileSync(file, "utf8").split("\n").filter(Boolean);
430
+ for (const line of lines) {
431
+ let entry;
432
+ try { entry = JSON.parse(line); } catch { continue; }
433
+ const input = entry.input ?? 0;
434
+ const output = entry.output ?? 0;
435
+ const credit = entry.credit ?? 0;
436
+ totalInput += input;
437
+ totalOutput += output;
438
+ totalCredit += credit;
439
+ totalRequests += 1;
440
+ if ((entry.ts ?? 0) >= todayMs) {
441
+ todayInput += input;
442
+ todayOutput += output;
443
+ todayCredit += credit;
444
+ todayRequests += 1;
445
+ }
446
+ const model = String(entry.model ?? "unknown");
447
+ const agg = byModel.get(model) ?? { inputTokens: 0, outputTokens: 0, credit: 0, requests: 0 };
448
+ agg.inputTokens += input;
449
+ agg.outputTokens += output;
450
+ agg.credit += credit;
451
+ agg.requests += 1;
452
+ byModel.set(model, agg);
453
+ }
454
+ const byModelList = [...byModel.entries()]
455
+ .map(([model, agg]) => ({ model, ...agg }))
456
+ .sort((a, b) => b.credit - a.credit || b.inputTokens + b.outputTokens - (a.inputTokens + a.outputTokens));
457
+ return {
458
+ today: { inputTokens: todayInput, outputTokens: todayOutput, credit: todayCredit, requests: todayRequests },
459
+ byModel: byModelList,
460
+ total: { inputTokens: totalInput, outputTokens: totalOutput, credit: totalCredit, requests: totalRequests },
345
461
  };
346
462
  }
347
463
 
464
+ // #endregion
465
+
348
466
  /** Assemble the final ContentBlock for one open block. */
349
467
  function closeBlock(block) {
350
468
  switch (block.kind) {
@@ -443,7 +561,9 @@ async function* translate(payloads) {
443
561
  argumentsDelta: fragment,
444
562
  };
445
563
  }
446
- if (typeof choice.finish_reason === "string") pendingFinish = mapFinishReason(choice.finish_reason);
564
+ // Hy (Hunyuan) models emit `"finish_reason": ""` on every intermediate
565
+ // chunk; only treat a non-empty vocabulary value as the real finish.
566
+ if (typeof choice.finish_reason === "string" && choice.finish_reason.length > 0) pendingFinish = mapFinishReason(choice.finish_reason);
447
567
  }
448
568
  if (chunk.usage) pendingUsage = mapUsage(chunk.usage);
449
569
  }
@@ -515,6 +635,12 @@ export class WorkBuddyAdapter extends LlmAdapter {
515
635
  return { id: provider, name: "WorkBuddy" };
516
636
  }
517
637
 
638
+ /** Drop the discovery cache so the next listModels re-fetches from the proxy. */
639
+ refreshModels() {
640
+ this._cache = undefined;
641
+ this._liveMeta = undefined;
642
+ }
643
+
518
644
  async listModels(provider) {
519
645
  const connection = this.config.options();
520
646
  if (!connection.discovery) return connection.models.map((model) => modelInfo(provider, model));
@@ -544,18 +670,31 @@ export class WorkBuddyAdapter extends LlmAdapter {
544
670
  : typeof entry.display_name === "string"
545
671
  ? entry.display_name
546
672
  : undefined,
673
+ description: typeof entry.description === "string" && entry.description.length > 0
674
+ ? entry.description
675
+ : undefined,
676
+ modalities: liveInputModalities(entry),
677
+ contextWindow: Number.isInteger(entry.context_window) && entry.context_window > 0
678
+ ? entry.context_window
679
+ : undefined,
680
+ maxTokens: Number.isInteger(entry.max_context_window) && entry.max_context_window > 0
681
+ ? entry.max_context_window
682
+ : undefined,
547
683
  }))
548
684
  .filter((entry) => entry.id.length > 0);
549
685
  const merged = [];
550
686
  const seen = new Set();
687
+ const liveMeta = new Map();
551
688
  for (const entry of live) {
552
689
  if (seen.has(entry.id)) continue;
553
690
  seen.add(entry.id);
554
691
  const catalog = byId.get(entry.id);
555
- // Only keep models the static catalog declares. The proxy announces
556
- // retired models (glm-4.6v, hunyuan-image-v3.0, ...) that upstream
557
- // rejects with `service info not found`; the catalog doubles as the
558
- // whitelist so those never reach the UI.
692
+ // The static catalog doubles as a whitelist. The proxy still announces
693
+ // retired/legacy models (deepseek-v3-1, glm-4.6, glm-4.6v, kimi-k2,
694
+ // kimi-k2-thinking, minimax-m2.5, hunyuan-image-v3.0, ...) that the
695
+ // upstream rejects with `service info not found`. Drop any live entry
696
+ // that is not in the catalog so those never reach the UI; the catalog
697
+ // is the curated set that matches what the official client shows.
559
698
  if (catalog === undefined) continue;
560
699
  const effort = liveReasoningEffort(entry, catalog?.reasoningEffort);
561
700
  const reasoning = modelReasoningInfo(effort);
@@ -563,10 +702,13 @@ export class WorkBuddyAdapter extends LlmAdapter {
563
702
  provider,
564
703
  id: entry.id,
565
704
  name: entry.name ?? catalog?.name ?? entry.id,
566
- ...(catalog?.description !== undefined ? { description: catalog.description } : {}),
567
- inputModalities: catalog?.inputModalities ?? ["text"],
705
+ ...(catalog?.description !== undefined || entry.description !== undefined
706
+ ? { description: catalog?.description ?? entry.description }
707
+ : {}),
708
+ inputModalities: catalog?.inputModalities ?? entry.modalities,
568
709
  ...(reasoning === undefined ? {} : { reasoning }),
569
710
  });
711
+ liveMeta.set(entry.id, { contextWindow: entry.contextWindow, maxTokens: entry.maxTokens });
570
712
  }
571
713
  // Catalog entries the proxy did not announce (e.g. unauthenticated or
572
714
  // partial listing) stay selectable.
@@ -576,6 +718,7 @@ export class WorkBuddyAdapter extends LlmAdapter {
576
718
  }
577
719
  const result = merged.length > 0 ? merged : connection.models.map((model) => modelInfo(provider, model));
578
720
  this._cache = { at: now, models: result };
721
+ this._liveMeta = liveMeta;
579
722
  return result;
580
723
  } catch {
581
724
  return connection.models.map((model) => modelInfo(provider, model));
@@ -585,13 +728,18 @@ export class WorkBuddyAdapter extends LlmAdapter {
585
728
  resolveModel(provider, model, _signal) {
586
729
  const connection = this.config.options();
587
730
  const configured = connection.models.find((entry) => entry.id === model);
731
+ // Models discovered live (proxy-announced, not in the configured catalog)
732
+ // still get their platform-declared capacity from the last discovery.
733
+ const live = this._liveMeta?.get(model);
588
734
  const base = configured === undefined
589
735
  ? { provider, id: model, name: model, inputModalities: ["text"] }
590
736
  : modelInfo(provider, configured);
591
737
  return Promise.resolve({
592
738
  ...base,
593
- context: { contextWindow: configured?.contextWindow ?? connection.defaultContextWindow },
594
- defaultMaxTokens: configured?.maxTokens ?? connection.maxTokens,
739
+ context: {
740
+ contextWindow: configured?.contextWindow ?? live?.contextWindow ?? connection.defaultContextWindow,
741
+ },
742
+ defaultMaxTokens: configured?.maxTokens ?? live?.maxTokens ?? connection.maxTokens,
595
743
  });
596
744
  }
597
745
 
@@ -605,13 +753,16 @@ export class WorkBuddyAdapter extends LlmAdapter {
605
753
  );
606
754
  const iterator = this.request(options, watchdog.signal, connection, () => watchdog.pulse())[Symbol.asyncIterator]();
607
755
  let exhausted = false;
756
+ let usage;
608
757
  try {
609
758
  while (true) {
610
759
  const result = await watchdog.next(iterator);
611
760
  if (result.done) {
612
761
  exhausted = true;
762
+ if (usage !== undefined) recordUsage(options.model, usage);
613
763
  return;
614
764
  }
765
+ if (result.value?.type === "usage") usage = result.value.usage;
615
766
  yield result.value;
616
767
  }
617
768
  } catch (error) {
@@ -702,6 +853,10 @@ export const Config = z.object({
702
853
  models: z.array(catalogModel).default(DEFAULT_MODELS),
703
854
  streamIdleTimeoutMs: z.number().min(Number.MIN_VALUE).max(MAX_TIMER_DELAY_MS).default(DEFAULT_STREAM_IDLE_TIMEOUT_MS),
704
855
  discovery: z.boolean().default(true),
856
+ // Daily checkin: after `checkinAfterHour` local time, claim once per day if
857
+ // the official activity says today is unclaimed. Disable to check in manually.
858
+ autoCheckin: z.boolean().default(true),
859
+ checkinAfterHour: z.number().step(1).min(0).max(23).default(10),
705
860
  // Optional path overrides for the login helper. When unset, the plugin uses
706
861
  // the login_workbuddy.py bundled with this package and the default session
707
862
  // file location (~/.codebuddy-session.json).
@@ -764,6 +919,10 @@ export function resolveAdapterOptions(config) {
764
919
  if (!Number.isFinite(streamIdleTimeoutMs) || streamIdleTimeoutMs <= 0 || streamIdleTimeoutMs > MAX_TIMER_DELAY_MS) {
765
920
  throw new Error(`dsh-llm-workbuddy: streamIdleTimeoutMs must be a positive finite number no greater than ${MAX_TIMER_DELAY_MS}`);
766
921
  }
922
+ const checkinAfterHour = config.checkinAfterHour ?? 10;
923
+ if (!Number.isInteger(checkinAfterHour) || checkinAfterHour < 0 || checkinAfterHour > 23) {
924
+ throw new Error("dsh-llm-workbuddy: checkinAfterHour must be an integer between 0 and 23");
925
+ }
767
926
  return {
768
927
  baseURL,
769
928
  apiKey: config.apiKey,
@@ -772,6 +931,8 @@ export function resolveAdapterOptions(config) {
772
931
  models: resolveModels(config.models),
773
932
  streamIdleTimeoutMs,
774
933
  discovery: config.discovery ?? true,
934
+ autoCheckin: config.autoCheckin ?? true,
935
+ checkinAfterHour,
775
936
  loginScript: config.loginScript,
776
937
  sessionFile: config.sessionFile,
777
938
  };
@@ -779,6 +940,176 @@ export function resolveAdapterOptions(config) {
779
940
 
780
941
  // #endregion
781
942
 
943
+ // #region daily checkin
944
+
945
+ /** Checkin state file under DSH_HOME (same ledger dir as usage). */
946
+ const CHECKIN_FILE_NAME = "checkin.json";
947
+ /** How often the auto-claim scheduler wakes up. */
948
+ const CHECKIN_INTERVAL_MS = 30 * 60_000;
949
+ /** Request timeout for the proxy checkin endpoints. */
950
+ const CHECKIN_TIMEOUT_MS = 10_000;
951
+
952
+ /** Resolve the checkin state file path (same convention as usageLedgerPath). */
953
+ function checkinStatePath() {
954
+ const configured = process.env.DSH_HOME;
955
+ if (configured !== undefined && configured.trim().length > 0) {
956
+ return join(configured, USAGE_DIR_NAME, CHECKIN_FILE_NAME);
957
+ }
958
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? ".";
959
+ return join(home, ".dsh", USAGE_DIR_NAME, CHECKIN_FILE_NAME);
960
+ }
961
+
962
+ /** Read the persisted checkin state. Never throws. */
963
+ function readCheckinState() {
964
+ try {
965
+ return JSON.parse(readFileSync(checkinStatePath(), "utf-8"));
966
+ } catch {
967
+ return {};
968
+ }
969
+ }
970
+
971
+ /** Persist the checkin state (best-effort; failure must not break the caller). */
972
+ function writeCheckinState(state) {
973
+ try {
974
+ const file = checkinStatePath();
975
+ mkdirSync(dirname(file), { recursive: true });
976
+ writeFileSync(file, `${JSON.stringify(state, null, 2)}\n`);
977
+ } catch {
978
+ // persistence is advisory
979
+ }
980
+ }
981
+
982
+ /** Local calendar date (`YYYY-MM-DD`) for "did we already handle today". */
983
+ function localDateStr(date = new Date()) {
984
+ const y = date.getFullYear();
985
+ const m = String(date.getMonth() + 1).padStart(2, "0");
986
+ const d = String(date.getDate()).padStart(2, "0");
987
+ return `${y}-${m}-${d}`;
988
+ }
989
+
990
+ /**
991
+ * Call a proxy checkin endpoint. `method` defaults to GET (status probe);
992
+ * claiming passes "POST". Returns the parsed body or an error shape.
993
+ */
994
+ async function proxyCheckinCall(baseURL, path, method = "GET") {
995
+ const controller = new AbortController();
996
+ const timer = setTimeout(() => controller.abort(), CHECKIN_TIMEOUT_MS);
997
+ try {
998
+ const response = await fetch(`${baseURL.replace(/\/$/, "")}${path}`, {
999
+ method,
1000
+ ...(method === "POST" ? { headers: { "content-type": "application/json" }, body: "{}" } : {}),
1001
+ signal: controller.signal,
1002
+ });
1003
+ const body = await response.json().catch(() => null);
1004
+ if (!response.ok) {
1005
+ return { error: `HTTP ${response.status} (${method} ${path})`, ...(body ? { body } : {}) };
1006
+ }
1007
+ return body ?? { error: "empty response" };
1008
+ } catch (error) {
1009
+ return { error: String(error?.cause?.message ?? error?.message ?? error) };
1010
+ } finally {
1011
+ clearTimeout(timer);
1012
+ }
1013
+ }
1014
+
1015
+ /** Whether an upstream claim rejection means "already claimed today". */
1016
+ function alreadyClaimedMessage(msg) {
1017
+ return typeof msg === "string" && /(已签到|已经签到|already)/i.test(msg);
1018
+ }
1019
+
1020
+ /**
1021
+ * Run one auto-claim pass. Idempotent per local calendar day:
1022
+ * - before `checkinAfterHour` (default 10:00) local time: no-op;
1023
+ * - already handled today (persisted state): no-op;
1024
+ * - official status says already checked in: mark handled, done;
1025
+ * - activity not open (no status data): mark handled so we stop retrying today;
1026
+ * - otherwise claim, persisting the reward (credit, streak) on success.
1027
+ * Concurrent invocations collapse into the first caller via `inFlight`.
1028
+ * @returns the resulting state snapshot, or undefined when skipped/in-flight.
1029
+ */
1030
+ async function runCheckinOnce(baseURL, config, logger) {
1031
+ if (!config.autoCheckin) return undefined;
1032
+ const now = new Date();
1033
+ if (now.getHours() < config.checkinAfterHour) return undefined;
1034
+ const today = localDateStr(now);
1035
+ const state = readCheckinState();
1036
+ if (state.handledDate === today) return state;
1037
+ if (runCheckinOnce._inFlight) return undefined;
1038
+ runCheckinOnce._inFlight = true;
1039
+ try {
1040
+ const status = await proxyCheckinCall(baseURL, "/checkin-status");
1041
+ const data = status?.upstream?.data;
1042
+ if (!data) {
1043
+ // Activity closed / proxy unreachable / not authenticated: stop for today.
1044
+ const next = {
1045
+ ...state,
1046
+ handledDate: today,
1047
+ lastResult: { ok: false, message: status?.error ?? status?.upstream?.msg ?? "签到活动不可用", at: now.getTime() },
1048
+ };
1049
+ writeCheckinState(next);
1050
+ logger?.debug?.(`dsh-llm-workbuddy: checkin skipped today (${next.lastResult.message})`);
1051
+ return next;
1052
+ }
1053
+ if (data.today_checked_in) {
1054
+ const next = {
1055
+ ...state,
1056
+ handledDate: today,
1057
+ lastResult: { ok: true, message: "今日已签到(官方确认)", streakDays: data.streak_days, at: now.getTime() },
1058
+ };
1059
+ writeCheckinState(next);
1060
+ return next;
1061
+ }
1062
+ const claim = await proxyCheckinCall(baseURL, "/checkin", "POST");
1063
+ const payload = claim?.upstream?.data;
1064
+ // "Already claimed" rejections count as handled (the goal is covered).
1065
+ const ok = Boolean(payload) || alreadyClaimedMessage(claim?.upstream?.msg);
1066
+ const failureDetail = [claim?.upstream?.msg, claim?.error, claim?.body?.detail]
1067
+ .filter(Boolean).join(" ") || "签到失败";
1068
+ const next = {
1069
+ ...state,
1070
+ handledDate: today,
1071
+ lastResult: {
1072
+ ok,
1073
+ message: ok
1074
+ ? payload?.credit !== undefined
1075
+ ? `自动签到成功:+${payload.credit} 积分`
1076
+ : "今日已签到"
1077
+ : failureDetail,
1078
+ ...(payload?.credit !== undefined ? { credit: payload.credit } : {}),
1079
+ ...(payload?.streak_days !== undefined ? { streakDays: payload.streak_days } : {}),
1080
+ at: now.getTime(),
1081
+ },
1082
+ };
1083
+ writeCheckinState(next);
1084
+ logger?.info?.(`dsh-llm-workbuddy: ${next.lastResult.message}`);
1085
+ return next;
1086
+ } finally {
1087
+ runCheckinOnce._inFlight = false;
1088
+ }
1089
+ }
1090
+
1091
+ /**
1092
+ * Start the auto-claim scheduler: one immediate pass plus a low-frequency
1093
+ * interval. The interval survives plugin-lifetime only (disposed with the
1094
+ * fiber), and every pass re-reads the live config so settings hot-edit
1095
+ * (autoCheckin / checkinAfterHour) applies to the very next pass.
1096
+ */
1097
+ function startCheckinScheduler(ctx, options) {
1098
+ const tick = () => {
1099
+ const connection = options();
1100
+ runCheckinOnce(connection.baseURL, connection, ctx.logger).catch((error) => {
1101
+ ctx.logger.warn("dsh-llm-workbuddy: auto checkin pass failed");
1102
+ ctx.logger.warn(error);
1103
+ });
1104
+ };
1105
+ tick();
1106
+ const timer = setInterval(tick, CHECKIN_INTERVAL_MS);
1107
+ timer.unref?.();
1108
+ return () => clearInterval(timer);
1109
+ }
1110
+
1111
+ // #endregion
1112
+
782
1113
  // #region login status web API
783
1114
 
784
1115
  /**
@@ -948,14 +1279,18 @@ function diagnoseRestartCommand() {
948
1279
  }
949
1280
 
950
1281
  /**
951
- * Register `GET /api/workbuddy/status`, `POST /api/workbuddy/login`, and
952
- * `POST /api/workbuddy/diagnose` on the DSH web server. The login route spawns
953
- * `login_workbuddy.py`, parses the device-flow `authUrl` it prints to stdout,
954
- * and returns it so the browser can open it in a new tab. The widget then
955
- * polls `/status` until the session file appears and reports
956
- * `authenticated: true`.
1282
+ * Register the WorkBuddy web API routes on the DSH web server:
1283
+ * - `GET /api/workbuddy/status` login/proxy health + checkin digest;
1284
+ * - `POST /api/workbuddy/login` device-flow login kick-off;
1285
+ * - `POST /api/workbuddy/diagnose` real end-to-end health probe;
1286
+ * - `GET /api/workbuddy/usage` token usage ledger;
1287
+ * - `POST /api/workbuddy/refresh-models` drop the discovery cache, re-read the
1288
+ * proxy model list (which tracks the official WorkBuddy app), and announce
1289
+ * `llm/adapters-updated` so open model pickers reload immediately;
1290
+ * - `GET /api/workbuddy/checkin` today's checkin state (official query);
1291
+ * - `POST /api/workbuddy/checkin` claim now (idempotent upstream).
957
1292
  */
958
- function registerWorkbuddyRoutes(ctx, config) {
1293
+ function registerWorkbuddyRoutes(ctx, config, adapter, providersHandle) {
959
1294
  // Delay registration until the webServer service exists. The plugin keeps
960
1295
  // `webServer` OUT of `inject` so headless profiles (no HTTP surface) still
961
1296
  // load — but a synchronous `ctx.get("webServer")` probe at apply time races
@@ -983,11 +1318,20 @@ function registerWorkbuddyRoutes(ctx, config) {
983
1318
  }
984
1319
  const session = readSessionStatus(sessionFile);
985
1320
  const proxy = await probeProxy(baseURL);
1321
+ // Checkin digest comes from the persisted state only — never block the
1322
+ // 5s status poll with an upstream call.
1323
+ const checkinState = readCheckinState();
1324
+ const today = localDateStr();
986
1325
  res.writeHead(200, { "content-type": "application/json" });
987
1326
  res.end(JSON.stringify({
988
1327
  ...session,
989
1328
  ...proxy,
990
1329
  loginScriptAvailable: existsSync(loginScript),
1330
+ checkin: {
1331
+ autoEnabled: config?.autoCheckin ?? true,
1332
+ handledToday: checkinState.handledDate === today,
1333
+ lastResult: checkinState.lastResult ?? null,
1334
+ },
991
1335
  }));
992
1336
  },
993
1337
  }));
@@ -1058,6 +1402,101 @@ function registerWorkbuddyRoutes(ctx, config) {
1058
1402
  }));
1059
1403
  },
1060
1404
  }));
1405
+ webCtx.effect(() => webServer.register({
1406
+ kind: "exact",
1407
+ path: "/api/workbuddy/usage",
1408
+ async handler(req, res) {
1409
+ if (req.method !== "GET") {
1410
+ res.writeHead(405, { "content-type": "application/json" });
1411
+ res.end(JSON.stringify({ error: "method not allowed" }));
1412
+ return;
1413
+ }
1414
+ res.writeHead(200, { "content-type": "application/json" });
1415
+ res.end(JSON.stringify(readUsageLedger()));
1416
+ },
1417
+ }));
1418
+ webCtx.effect(() => webServer.register({
1419
+ kind: "exact",
1420
+ path: "/api/workbuddy/refresh-models",
1421
+ async handler(req, res) {
1422
+ if (req.method !== "POST") {
1423
+ res.writeHead(405, { "content-type": "application/json" });
1424
+ res.end(JSON.stringify({ error: "method not allowed" }));
1425
+ return;
1426
+ }
1427
+ // Drop the discovery cache so listModels re-reads the proxy, then
1428
+ // re-commit the (unchanged) configurable-provider entries: commit is
1429
+ // the one mutation point that publishes `llm/adapters-updated`, which
1430
+ // makes every open model picker reload immediately.
1431
+ adapter.refreshModels();
1432
+ let announced = true;
1433
+ try {
1434
+ providersHandle?.replace?.([{ provider: PROVIDER, displayName: "WorkBuddy", settingsNs: NS, settingsPath: [] }]);
1435
+ } catch (error) {
1436
+ announced = false;
1437
+ ctx.logger.warn("dsh-llm-workbuddy: failed to announce adapters-updated");
1438
+ ctx.logger.warn(error);
1439
+ }
1440
+ const models = await adapter.listModels(PROVIDER).catch(() => []);
1441
+ res.writeHead(200, { "content-type": "application/json" });
1442
+ res.end(JSON.stringify({ ok: true, announced, count: models.length, models }));
1443
+ },
1444
+ }));
1445
+ webCtx.effect(() => webServer.register({
1446
+ kind: "exact",
1447
+ path: "/api/workbuddy/checkin",
1448
+ async handler(req, res) {
1449
+ // GET: today's persisted state plus a fresh official status probe.
1450
+ if (req.method === "GET") {
1451
+ const state = readCheckinState();
1452
+ const upstream = await proxyCheckinCall(baseURL, "/checkin-status");
1453
+ const data = upstream?.upstream?.data ?? null;
1454
+ res.writeHead(200, { "content-type": "application/json" });
1455
+ res.end(JSON.stringify({
1456
+ handledToday: state.handledDate === localDateStr(),
1457
+ lastResult: state.lastResult ?? null,
1458
+ official: data === null ? null : {
1459
+ active: Boolean(data.active),
1460
+ todayCheckedIn: Boolean(data.today_checked_in),
1461
+ streakDays: data.streak_days ?? null,
1462
+ todayCredit: data.today_credit ?? null,
1463
+ streakBonusDays: data.streak_bonus_days ?? null,
1464
+ streakBonusCredit: data.streak_bonus_credit ?? null,
1465
+ },
1466
+ ...(upstream?.error ? { error: upstream.error } : {}),
1467
+ }));
1468
+ return;
1469
+ }
1470
+ // POST: claim now. Idempotent — upstream reports "already checked in"
1471
+ // as a business result rather than an error.
1472
+ if (req.method !== "POST") {
1473
+ res.writeHead(405, { "content-type": "application/json" });
1474
+ res.end(JSON.stringify({ error: "method not allowed" }));
1475
+ return;
1476
+ }
1477
+ const claim = await proxyCheckinCall(baseURL, "/checkin", "POST");
1478
+ const payload = claim?.upstream?.data;
1479
+ const ok = Boolean(payload) || alreadyClaimedMessage(claim?.upstream?.msg);
1480
+ const result = {
1481
+ ok,
1482
+ message: ok
1483
+ ? payload?.credit !== undefined
1484
+ ? `签到成功:+${payload.credit} 积分`
1485
+ : "今日已签到"
1486
+ : claim?.upstream?.msg ?? claim?.error ?? "签到失败",
1487
+ ...(payload ?? {}),
1488
+ };
1489
+ if (ok || claim?.upstream?.msg) {
1490
+ writeCheckinState({
1491
+ ...readCheckinState(),
1492
+ handledDate: ok ? localDateStr() : readCheckinState().handledDate,
1493
+ lastResult: { ok, message: result.message, at: Date.now() },
1494
+ });
1495
+ }
1496
+ res.writeHead(200, { "content-type": "application/json" });
1497
+ res.end(JSON.stringify(result));
1498
+ },
1499
+ }));
1061
1500
  });
1062
1501
  }
1063
1502
 
@@ -1104,14 +1543,15 @@ export function apply(ctx, config) {
1104
1543
  options,
1105
1544
  resolveAttachments: () => ctx.get("attachments"),
1106
1545
  });
1107
- ctx.llm.registerConfigurableProviders([{
1546
+ const providersHandle = ctx.llm.registerConfigurableProviders([{
1108
1547
  provider: PROVIDER,
1109
1548
  displayName: "WorkBuddy",
1110
1549
  settingsNs: NS,
1111
1550
  settingsPath: [],
1112
1551
  }]);
1113
1552
  ctx.llm.registerAdapter([PROVIDER], adapter);
1114
- registerWorkbuddyRoutes(ctx, config);
1553
+ registerWorkbuddyRoutes(ctx, config, adapter, providersHandle);
1554
+ ctx.effect(() => startCheckinScheduler(ctx, options));
1115
1555
  installSettingsSection(ctx, NS, Config, config, {
1116
1556
  setSource: (source) => {
1117
1557
  current = source;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-llm-workbuddy",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "WorkBuddy (via the local workbuddy2api proxy) LLM provider adapter for DeepSeek Harness, with a Web login-status widget",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -44,7 +44,8 @@
44
44
  "lib",
45
45
  "login_workbuddy.py",
46
46
  "cordis.patch.yml",
47
- "README.md"
47
+ "README.md",
48
+ "CHANGELOG.md"
48
49
  ],
49
50
  "peerDependencies": {
50
51
  "@deepseek-ai/cordis": "^4.0.1",