dsh-llm-codebuddy 1.3.4 → 1.3.5

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 CHANGED
@@ -1,7 +1,8 @@
1
1
  # DSH CodeBuddy Provider
2
2
 
3
- 为 DeepSeek Harness(DSH)增加 `CodeBuddy 中国区` Provider。安装后可直接在
4
- DSH WebUI 中填写 API Key、获取模型、调整模型参数并使用 CodeBuddy 模型。
3
+ 为 DeepSeek Harness(DSH)增加 `CodeBuddy 中国区` Provider。支持 WorkBuddy
4
+ API Key CodeBuddy 中国站账号令牌两种认证方式,并可在 DSH WebUI 中获取、
5
+ 选择和调整模型。
5
6
 
6
7
  > [!IMPORTANT]
7
8
  > API Key 由 **WorkBuddy** 提供,本插件使用该 Key 调用供 CodeBuddy 使用的模型服务。
@@ -10,8 +11,10 @@ DSH WebUI 中填写 API Key、获取模型、调整模型参数并使用 CodeBud
10
11
 
11
12
  ## 功能
12
13
 
13
- - WebUI 中直接添加 `CodeBuddy 中国区`;
14
- - 只需填写从 WorkBuddy 获取的 API Key;
14
+ - WebUI 中直接添加 `CodeBuddy 中国区`;
15
+ - 支持从 WorkBuddy 获取的 API Key;
16
+ - API Key 输入框旁可显式切换 `API Key` / `令牌登录`;
17
+ - 支持从 WebUI 拉起浏览器登录 CodeBuddy 中国站,并自动复用、刷新登录令牌;
15
18
  - 自动获取 CodeBuddy 当前可用模型;
16
19
  - 支持编辑模型 ID、名称、上下文窗口和最大输出 Token;
17
20
  - 支持添加、删除模型以及重新同步模型目录;
@@ -24,10 +27,10 @@ DSH WebUI 中填写 API Key、获取模型、调整模型参数并使用 CodeBud
24
27
 
25
28
  - Windows、Linux 或 macOS;
26
29
  - Node.js `>= 22.19.0`;
27
- - 已安装 DSH;
28
- - 已验证 DSH `0.1.0-rc.6`。
29
-
30
- 安装器已自带 DSH 所需的 `pnpm`,无需全局安装。
30
+ - 已安装 DSH;
31
+ - 已验证 DSH `0.1.0-rc.6`。
32
+
33
+ 安装器已自带 DSH 所需的 `pnpm`,无需全局安装;令牌登录也不需要安装 CodeBuddy CLI。
31
34
 
32
35
  > DSH 仍处于预发布阶段。未来版本如果调整插件接口,本插件可能需要同步升级;
33
36
  > DSH 普通更新不会覆盖本插件。
@@ -51,9 +54,35 @@ dsh plugin --profile headless add dsh-llm-codebuddy@latest
51
54
 
52
55
  只使用 WebUI 时,仅执行第一条即可。
53
56
 
54
- ## WebUI 配置
55
-
56
- 1. 打开“设置 → 模型”。
57
+ ## WebUI 配置
58
+
59
+ ### CodeBuddy 账号令牌登录(新用户推荐)
60
+
61
+ 1. 打开“设置 → 模型”。
62
+ 2. 添加或编辑 `CodeBuddy 中国区`。
63
+ 3. 点击 API Key 输入框右侧的“令牌登录”。
64
+ 4. 在自动打开的浏览器中完成 CodeBuddy 中国站登录。
65
+ 5. WebUI 显示“令牌已登录”后即可获取模型并保存自定义目录。
66
+
67
+ 如果 WebUI 无法打开浏览器,也可以使用命令行入口:
68
+
69
+ ```powershell
70
+ npx --yes dsh-llm-codebuddy@latest login
71
+ ```
72
+
73
+ 插件会调用 CodeBuddy 官方认证流程。完成登录后,插件会自动把
74
+ `CodeBuddy 中国区` Provider 切换为令牌模式,无需 API Key。点击同一行的
75
+ “API Key”可以切回 API Key 模式;已经保存的 API Key 不会被删除。
76
+ 两种凭据彼此独立保留,之后再次点击“令牌登录”会优先复用已保存令牌;仅在没有
77
+ 令牌或主动重新登录时才需要再次打开浏览器。
78
+
79
+ 插件直接调用 CodeBuddy 中国站官方网页登录和令牌刷新接口,不依赖本机
80
+ `codebuddy` 命令。访问令牌和刷新令牌保存在 DSH 凭据服务中,不会写入
81
+ `settings.yaml` 或模型目录。
82
+
83
+ ### WorkBuddy API Key
84
+
85
+ 1. 打开“设置 → 模型”。
57
86
  2. 点击“添加提供方”。
58
87
  3. 选择 `CodeBuddy 中国区`。
59
88
  4. 输入从 WorkBuddy 获取的 API Key 并保存。
@@ -116,7 +145,7 @@ npx --yes dsh-llm-codebuddy@latest uninstall
116
145
  2. 只删除 `llm-pi-ai.providers.codebuddy-cn` 配置;
117
146
  3. 保留其他 Provider 和 DSH 设置;
118
147
  4. 从 `web`、`headless` Profile 移除插件;
119
- 5. 保留 API Key 凭据,方便以后重新安装。
148
+ 5. 保留 API Key 和登录令牌凭据,方便以后重新安装。
120
149
 
121
150
  完成后重启 DSH。备份文件名类似:
122
151
 
@@ -141,28 +170,43 @@ dsh plugin --profile web list --depth 0
141
170
  确认 API Key 来自 WorkBuddy 且仍然有效,然后重新输入 Key 并点击“获取可用模型”。
142
171
  接口临时不可用时,插件仍会提供内置模型目录。
143
172
 
144
- ### 为什么别人能看到某个模型,我这里看不到
173
+ ### 为什么别人能看到某个模型,我这里看不到
145
174
 
146
175
  模型权限与 API Key 绑定。插件只导入 `/v3/config` 中 `agents[name=cli].models` 为
147
176
  当前 Key 返回的模型;更换 Key 后请重新点击“获取可用模型”。插件不会强行显示
148
- 当前 Key 未授权的模型。
149
-
150
- ## 开发文档
151
-
152
- 需要开发其他 Agent 或 Provider 时,请阅读
153
- [CodeBuddy 调用 WorkBuddy API 开发文档](./docs/CodeBuddy调用WorkBuddy-API开发文档.md)。
154
-
155
- ### 卸载后仍显示旧页面
156
-
157
- 关闭正在运行的 DSH,再重新启动。已经运行的进程不会自动卸载内存中的插件。
158
-
159
- ## 工作原理
177
+ 当前 Key 未授权的模型。
178
+
179
+ ### 调用时报 `500 status code (no body)`
180
+
181
+ 先更新到最新版并重启 DSH:
182
+
183
+ ```powershell
184
+ npx --yes dsh-llm-codebuddy@latest install
185
+ ```
186
+
187
+ 旧版本可能被 DSH 覆盖 CodeBuddy 请求标识,导致服务端拒绝请求;最新版已在插件请求层
188
+ 恢复 CodeBuddy 官方标识,同时适用于 API Key 和令牌模式。
189
+
190
+ ### 卸载后仍显示旧页面
191
+
192
+ 关闭正在运行的 DSH,再重新启动。已经运行的进程不会自动卸载内存中的插件。
193
+
194
+ ## 开发文档
195
+
196
+ 需要开发其他 Agent 或 Provider 时,请阅读
197
+ [反向代理调用 WorkBuddy API 开发文档](./docs/反向代理调用WorkBuddy-API开发文档.md)。
198
+
199
+ ## 工作原理
160
200
 
161
201
  ```text
162
- WorkBuddy 提供 API Key
163
-
164
- DSH Agent → 本插件 → CodeBuddy /v2/chat/completions
165
- ↘ CodeBuddy /v3/config(获取模型)
202
+ WorkBuddy 提供 API Key
203
+
204
+ DSH Agent → 本插件 → CodeBuddy /v2/chat/completions
205
+ ↘ CodeBuddy /v3/config(获取模型)
206
+
207
+ CodeBuddy 中国站网页登录 → 本插件保存并刷新 DSH 登录凭据
208
+ ↓ 当前 access token
209
+ DSH Agent → 本插件 → CodeBuddy API(Authorization: Bearer)
166
210
  ```
167
211
 
168
212
  - DSH:负责 Agent 循环、上下文、工具调用和权限;
package/cli.js CHANGED
@@ -6,6 +6,7 @@ import { createRequire } from "node:module";
6
6
  import { delimiter, dirname, join, resolve } from "node:path";
7
7
  import { spawnSync } from "node:child_process";
8
8
  import { parseDocument } from "yaml";
9
+ import { CODEBUDDY_SESSION_REF, loginCodeBuddy, parseCodeBuddySession, serializeCodeBuddySession } from "./codebuddy-auth.js";
9
10
 
10
11
  const PACKAGE = "dsh-llm-codebuddy";
11
12
  const PACKAGE_VERSION = JSON.parse(readFileSync(new URL("./package.json", import.meta.url), "utf8")).version;
@@ -110,6 +111,43 @@ function cleanSettings(file) {
110
111
  return backup;
111
112
  }
112
113
 
114
+ function enableTokenLogin(home = dshHome()) {
115
+ const file = join(home, "settings.yaml");
116
+ const source = existsSync(file) ? readFileSync(file, "utf8") : "{}\n";
117
+ const document = parseDocument(source);
118
+ if (document.errors.length) throw new Error(`无法解析 ${file}:${document.errors[0].message}`);
119
+ if (document.hasIn(PROVIDER_PATH)) document.deleteIn([...PROVIDER_PATH, "apiKeyEnv"]);
120
+ else document.setIn(PROVIDER_PATH, {});
121
+ if (existsSync(file)) {
122
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
123
+ copyFileSync(file, `${file}.codebuddy-backup-${stamp}`);
124
+ }
125
+ writeYamlDocument(file, document);
126
+ }
127
+
128
+ function storeLoginSession(session, home = dshHome()) {
129
+ const file = join(home, ".credentials.yaml");
130
+ const document = parseDocument(existsSync(file) ? readFileSync(file, "utf8") : "{}\n");
131
+ if (document.errors.length) throw new Error(`无法解析 ${file}:${document.errors[0].message}`);
132
+ document.set(CODEBUDDY_SESSION_REF, serializeCodeBuddySession(session));
133
+ if (existsSync(file)) {
134
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
135
+ copyFileSync(file, `${file}.codebuddy-backup-${stamp}`);
136
+ }
137
+ writeYamlDocument(file, document);
138
+ }
139
+
140
+ async function login() {
141
+ console.log("正在打开 CodeBuddy 中国站网页登录(无需安装 CodeBuddy CLI)……");
142
+ const session = await loginCodeBuddy((url, opened) => {
143
+ if (opened) console.log("浏览器登录页已打开,请在浏览器中完成登录。");
144
+ else console.log(`无法自动打开浏览器,请手动访问:${url}`);
145
+ });
146
+ storeLoginSession(session);
147
+ enableTokenLogin();
148
+ console.log("CodeBuddy 令牌登录成功,Provider 已切换为令牌模式。请重启 DSH。");
149
+ }
150
+
113
151
  function install() {
114
152
  for (const profile of ["web", "headless"]) {
115
153
  runDsh(["plugin", "--profile", profile, "list", "--depth", "0"]);
@@ -129,7 +167,7 @@ function uninstall(home = dshHome()) {
129
167
  cleanPnpmWorkspace(workspace);
130
168
  }
131
169
  console.log(backup ? `CodeBuddy 配置已清理,备份:${backup}` : "未发现 CodeBuddy Provider 配置。");
132
- console.log("插件已卸载,API Key 凭据保持不变。请重启 DSH。");
170
+ console.log("插件已卸载,API Key 和登录令牌凭据保持不变。请重启 DSH。");
133
171
  }
134
172
 
135
173
  function selfTest() {
@@ -137,11 +175,29 @@ function selfTest() {
137
175
  try {
138
176
  const file = join(root, "settings.yaml");
139
177
  writeFileSync(file, "llm-pi-ai:\n providers:\n opencode-go:\n apiKeyEnv: OPENCODE_GO_API_KEY\n codebuddy-cn:\n apiKeyEnv: CODEBUDDY_CN_API_KEY\n", "utf8");
178
+ enableTokenLogin(root);
179
+ const tokenMode = parseDocument(readFileSync(file, "utf8"));
180
+ if (tokenMode.hasIn([...PROVIDER_PATH, "apiKeyEnv"]) || !tokenMode.hasIn(PROVIDER_PATH)) {
181
+ throw new Error("token login settings self-test failed");
182
+ }
140
183
  const backup = cleanSettings(file);
141
184
  const result = parseDocument(readFileSync(file, "utf8"));
142
185
  if (!backup || !existsSync(backup) || result.hasIn(PROVIDER_PATH) || !result.hasIn(["llm-pi-ai", "providers", "opencode-go"])) {
143
186
  throw new Error("uninstall settings cleanup self-test failed");
144
187
  }
188
+ enableTokenLogin(root);
189
+ if (!parseDocument(readFileSync(file, "utf8")).hasIn(PROVIDER_PATH)) {
190
+ throw new Error("token login provider creation self-test failed");
191
+ }
192
+ const sampleSession = {
193
+ auth: { accessToken: "test-access-token", refreshToken: "test-refresh-token", expiresAt: Date.now() + 60_000 },
194
+ account: { userId: "test-user" },
195
+ };
196
+ storeLoginSession(sampleSession, root);
197
+ const storedSession = parseDocument(readFileSync(join(root, ".credentials.yaml"), "utf8")).get(CODEBUDDY_SESSION_REF);
198
+ if (parseCodeBuddySession(storedSession).account.userId !== "test-user") {
199
+ throw new Error("token credential storage self-test failed");
200
+ }
145
201
  const workspace = join(root, "pnpm-workspace.yaml");
146
202
  writeFileSync(workspace, "packages:\n - .\nallowBuilds:\n '@google/genai': true\n protobufjs: pending\n", "utf8");
147
203
  withPnpmBuildPolicy(workspace, () => {
@@ -172,8 +228,9 @@ function selfTest() {
172
228
  const command = process.argv[2];
173
229
  if (command === "install") install();
174
230
  else if (command === "uninstall") uninstall();
231
+ else if (command === "login") await login();
175
232
  else if (command === "--self-test") selfTest();
176
233
  else {
177
- console.log("用法:dsh-llm-codebuddy <install|uninstall>");
234
+ console.log("用法:dsh-llm-codebuddy <install|login|uninstall>");
178
235
  process.exitCode = 1;
179
236
  }
package/client.js ADDED
@@ -0,0 +1,152 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-llm-codebuddy",
3
+ factory: () => {
4
+ const ROUTE = "/dsh-llm-codebuddy/auth";
5
+ const MARKER = "data-codebuddy-auth-switch";
6
+
7
+ function button(text) {
8
+ const element = document.createElement("button");
9
+ element.type = "button";
10
+ element.textContent = text;
11
+ Object.assign(element.style, {
12
+ minHeight: "44px",
13
+ padding: "0 12px",
14
+ border: "1px solid var(--dsw-border-subtle, #d0d5dd)",
15
+ borderRadius: "8px",
16
+ background: "var(--dsw-surface-subtle, transparent)",
17
+ color: "inherit",
18
+ cursor: "pointer",
19
+ whiteSpace: "nowrap",
20
+ });
21
+ return element;
22
+ }
23
+
24
+ function isCodeBuddy(input) {
25
+ const editor = input.parentElement?.parentElement;
26
+ if (!editor) return false;
27
+ if (editor.textContent?.includes("codebuddy-cn")) return true;
28
+ return editor.parentElement?.querySelector('select[aria-label="提供方"]')?.value === "codebuddy-cn";
29
+ }
30
+
31
+ function applyMode(input, keyButton, tokenButton, message, status) {
32
+ const token = status.mode === "token";
33
+ input.disabled = token;
34
+ input.placeholder = token ? "当前使用 CodeBuddy 账号令牌" : input.dataset.codebuddyPlaceholder || "输入 API 密钥";
35
+ keyButton.setAttribute("aria-pressed", String(!token));
36
+ tokenButton.setAttribute("aria-pressed", String(token));
37
+ keyButton.style.background = !token ? "var(--dsw-accent-subtle, #eef4ff)" : "var(--dsw-surface-subtle, transparent)";
38
+ tokenButton.style.background = token ? "var(--dsw-accent-subtle, #eef4ff)" : "var(--dsw-surface-subtle, transparent)";
39
+ message.textContent = token ? status.authenticated ? "令牌已登录" : "令牌缺失,请重新登录" : "";
40
+ message.style.color = token && !status.authenticated ? "var(--dsw-text-danger, #c62828)" : "var(--dsw-text-success, #2e7d32)";
41
+ }
42
+
43
+ async function request(path) {
44
+ const response = await fetch(`${ROUTE}/${path}`, { method: "POST" });
45
+ const body = await response.json();
46
+ if (!response.ok || !body.ok) {
47
+ const error = new Error(body.message || `请求失败(${response.status})`);
48
+ error.status = response.status;
49
+ throw error;
50
+ }
51
+ return body;
52
+ }
53
+
54
+ function mount(input) {
55
+ const field = input.parentElement;
56
+ if (!field || field.querySelector(`[${MARKER}]`)) return;
57
+ field.setAttribute("data-codebuddy-auth-field", "");
58
+ input.dataset.codebuddyPlaceholder = input.placeholder;
59
+ const controls = document.createElement("div");
60
+ controls.setAttribute(MARKER, "");
61
+ controls.setAttribute("role", "group");
62
+ controls.setAttribute("aria-label", "CodeBuddy 认证方式");
63
+ Object.assign(controls.style, { display: "flex", alignItems: "center", gap: "8px", flexWrap: "wrap" });
64
+ const keyButton = button("API Key");
65
+ const tokenButton = button("令牌登录");
66
+ const message = document.createElement("span");
67
+ message.setAttribute("role", "status");
68
+ message.setAttribute("aria-live", "polite");
69
+ Object.assign(message.style, { fontSize: "12px", minHeight: "18px" });
70
+ controls.append(keyButton, tokenButton, message);
71
+ field.append(controls);
72
+ let current = { mode: "api-key", authenticated: false };
73
+ const render = (status) => {
74
+ current = { ...current, ...status };
75
+ applyMode(input, keyButton, tokenButton, message, current);
76
+ };
77
+
78
+ const setBusy = (busy) => {
79
+ keyButton.disabled = busy;
80
+ tokenButton.disabled = busy;
81
+ keyButton.style.cursor = busy ? "progress" : "pointer";
82
+ tokenButton.style.cursor = busy ? "progress" : "pointer";
83
+ };
84
+ keyButton.addEventListener("click", async () => {
85
+ setBusy(true);
86
+ message.textContent = "正在切换…";
87
+ try {
88
+ render(await request("api-key"));
89
+ input.focus();
90
+ } catch (error) {
91
+ message.textContent = error instanceof Error ? error.message : "切换失败";
92
+ message.style.color = "var(--dsw-text-danger, #c62828)";
93
+ } finally {
94
+ setBusy(false);
95
+ }
96
+ });
97
+ tokenButton.addEventListener("click", async () => {
98
+ setBusy(true);
99
+ tokenButton.textContent = "等待浏览器登录…";
100
+ message.textContent = "请在浏览器中完成 CodeBuddy 中国站登录";
101
+ try {
102
+ const path = current.mode === "api-key" && current.authenticated ? "token" : "login";
103
+ render(await request(path));
104
+ } catch (error) {
105
+ message.textContent = error instanceof Error ? error.message : "登录失败";
106
+ message.style.color = "var(--dsw-text-danger, #c62828)";
107
+ } finally {
108
+ tokenButton.textContent = "令牌登录";
109
+ setBusy(false);
110
+ }
111
+ });
112
+ fetch(`${ROUTE}/status`, { cache: "no-store" })
113
+ .then((response) => response.json())
114
+ .then(render)
115
+ .catch(() => {
116
+ message.textContent = "认证状态读取失败";
117
+ message.style.color = "var(--dsw-text-danger, #c62828)";
118
+ });
119
+ }
120
+
121
+ function enhance() {
122
+ for (const input of document.querySelectorAll('input[aria-label="API 密钥"]')) {
123
+ if (isCodeBuddy(input)) mount(input);
124
+ }
125
+ }
126
+
127
+ function apply(ctx) {
128
+ ctx.effect(() => {
129
+ const style = document.createElement("style");
130
+ style.textContent = `
131
+ [data-codebuddy-auth-field] { display: grid !important; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; }
132
+ [data-codebuddy-auth-field] > span:first-child { grid-column: 1 / -1; }
133
+ [data-codebuddy-auth-field] > input[aria-label="API 密钥"] { grid-column: 1; min-width: 0; }
134
+ [data-codebuddy-auth-field] > [data-codebuddy-auth-switch] { grid-column: 2; }
135
+ @media (max-width: 640px) { [data-codebuddy-auth-field] > [data-codebuddy-auth-switch] { grid-column: 1 / -1; } }
136
+ `;
137
+ document.head.append(style);
138
+ const observer = new MutationObserver(enhance);
139
+ observer.observe(document.body, { childList: true, subtree: true });
140
+ document.addEventListener("change", enhance, true);
141
+ enhance();
142
+ return () => {
143
+ style.remove();
144
+ observer.disconnect();
145
+ document.removeEventListener("change", enhance, true);
146
+ };
147
+ }, "llm-codebuddy: auth switch");
148
+ }
149
+
150
+ return { name: "dsh-llm-codebuddy-client", inject: [], apply };
151
+ },
152
+ });
@@ -0,0 +1,186 @@
1
+ import { spawn } from "node:child_process";
2
+ import { setTimeout as delay } from "node:timers/promises";
3
+
4
+ export const CODEBUDDY_SESSION_REF = "CODEBUDDY_LOGIN_SESSION";
5
+
6
+ const BASE_URL = "https://copilot.tencent.com/v2/plugin";
7
+ const USER_AGENT = "CLI/unknown CodeBuddy/2.137.1";
8
+ const REQUEST_HEADERS = {
9
+ accept: "application/json",
10
+ "content-type": "application/json",
11
+ "user-agent": USER_AGENT,
12
+ "x-product": "SaaS",
13
+ };
14
+ const NO_ACCOUNT_HEADERS = {
15
+ "X-No-Authorization": "true",
16
+ "X-No-User-Id": "true",
17
+ "X-No-Enterprise-Id": "true",
18
+ "X-No-Department-Info": "true",
19
+ };
20
+ const NO_ID_HEADERS = {
21
+ "X-No-User-Id": "true",
22
+ "X-No-Enterprise-Id": "true",
23
+ "X-No-Department-Info": "true",
24
+ };
25
+
26
+ function calculateExpiresAt(auth, now = Date.now()) {
27
+ const result = { ...auth };
28
+ if (!result.expiresAt && Number.isFinite(result.expiresIn)) result.expiresAt = now + result.expiresIn * 1000;
29
+ if (!result.refreshExpiresAt && Number.isFinite(result.refreshExpiresIn)) result.refreshExpiresAt = now + result.refreshExpiresIn * 1000;
30
+ return result;
31
+ }
32
+
33
+ async function responseBody(response, action) {
34
+ try {
35
+ return await response.json();
36
+ } catch (error) {
37
+ throw new Error(`${action}返回了无法解析的数据`, { cause: error });
38
+ }
39
+ }
40
+
41
+ async function request(path, options, action) {
42
+ let response;
43
+ try {
44
+ response = await fetch(`${BASE_URL}${path}`, options);
45
+ } catch (error) {
46
+ if (options.signal?.aborted) throw new Error(`${action}已取消`, { cause: error });
47
+ throw new Error(`${action}无法连接 CodeBuddy 中国站`, { cause: error });
48
+ }
49
+ const body = await responseBody(response, action);
50
+ if (!response.ok || body?.code !== 0) throw new Error(`${action}失败(${body?.message ?? body?.msg ?? response.status})`);
51
+ return body.data;
52
+ }
53
+
54
+ function enterpriseHeaders(session) {
55
+ const enterpriseId = session.account?.enterpriseId;
56
+ return {
57
+ ...(enterpriseId ? { "X-Enterprise-Id": enterpriseId, "X-Tenant-Id": enterpriseId } : {}),
58
+ ...(session.auth?.domain ? { "X-Domain": session.auth.domain } : {}),
59
+ };
60
+ }
61
+
62
+ async function poll(path, headers, action, timeoutMs, signal) {
63
+ const deadline = Date.now() + timeoutMs;
64
+ while (Date.now() < deadline) {
65
+ await delay(1000, undefined, { signal });
66
+ let response;
67
+ try {
68
+ response = await fetch(`${BASE_URL}${path}`, { headers: { ...REQUEST_HEADERS, ...headers }, signal });
69
+ } catch (error) {
70
+ if (signal?.aborted) throw new Error(`${action}已取消`, { cause: error });
71
+ continue;
72
+ }
73
+ const body = await responseBody(response, action);
74
+ if (response.ok && body?.code === 0 && body.data) return body.data;
75
+ if (response.status === 401 || response.status === 403) throw new Error(`${action}失败(${body?.message ?? body?.msg ?? response.status})`);
76
+ }
77
+ throw new Error(`${action}超时`);
78
+ }
79
+
80
+ function openBrowser(url) {
81
+ const [command, args] = process.platform === "win32"
82
+ ? ["rundll32.exe", ["url.dll,FileProtocolHandler", url]]
83
+ : process.platform === "darwin"
84
+ ? ["open", [url]]
85
+ : ["xdg-open", [url]];
86
+ return new Promise((resolve, reject) => {
87
+ const child = spawn(command, args, { detached: true, stdio: "ignore", windowsHide: true });
88
+ child.once("spawn", () => {
89
+ child.unref();
90
+ resolve();
91
+ });
92
+ child.once("error", reject);
93
+ });
94
+ }
95
+
96
+ function normalizeAccount(account) {
97
+ return {
98
+ ...(account?.userId || account?.uid ? { userId: account.userId ?? account.uid } : {}),
99
+ ...(account?.enterpriseId || account?.tenantId ? { enterpriseId: account.enterpriseId ?? account.tenantId } : {}),
100
+ };
101
+ }
102
+
103
+ export async function loginCodeBuddy(onAuthUrl, signal) {
104
+ const state = await request("/auth/state?platform=CLI", {
105
+ method: "POST",
106
+ headers: { ...REQUEST_HEADERS, ...NO_ACCOUNT_HEADERS },
107
+ body: "{}",
108
+ signal,
109
+ }, "创建 CodeBuddy 登录会话");
110
+ if (!state?.state || !state?.authUrl) throw new Error("CodeBuddy 登录接口没有返回登录地址");
111
+ try {
112
+ await openBrowser(state.authUrl);
113
+ onAuthUrl?.(state.authUrl, true);
114
+ } catch {
115
+ onAuthUrl?.(state.authUrl, false);
116
+ }
117
+ const auth = calculateExpiresAt(await poll(
118
+ `/auth/token?state=${encodeURIComponent(state.state)}`,
119
+ NO_ACCOUNT_HEADERS,
120
+ "等待 CodeBuddy 登录",
121
+ 10 * 60_000,
122
+ signal,
123
+ ));
124
+ if (!auth.accessToken || !auth.refreshToken) throw new Error("CodeBuddy 登录接口没有返回完整令牌");
125
+ const account = await poll(
126
+ `/login/account?state=${encodeURIComponent(state.state)}`,
127
+ { ...enterpriseHeaders({ auth }), authorization: `Bearer ${auth.accessToken}`, ...NO_ID_HEADERS },
128
+ "获取 CodeBuddy 账号",
129
+ 60_000,
130
+ signal,
131
+ );
132
+ return { auth, account: normalizeAccount(account) };
133
+ }
134
+
135
+ export async function refreshCodeBuddySession(session, signal) {
136
+ if (!session?.auth?.refreshToken) throw new Error("CodeBuddy 登录会话缺少刷新令牌,请重新登录");
137
+ const auth = await request("/auth/token/refresh", {
138
+ method: "POST",
139
+ headers: {
140
+ ...REQUEST_HEADERS,
141
+ ...enterpriseHeaders(session),
142
+ "X-Refresh-Token": session.auth.refreshToken,
143
+ "X-Auth-Refresh-Source": "plugin",
144
+ },
145
+ body: "{}",
146
+ signal,
147
+ }, "刷新 CodeBuddy 登录令牌");
148
+ const fresh = calculateExpiresAt(auth);
149
+ const merged = { ...session.auth, ...fresh, refreshToken: fresh?.refreshToken ?? session.auth.refreshToken };
150
+ if (!merged.accessToken) throw new Error("CodeBuddy 刷新接口没有返回访问令牌");
151
+ return { auth: merged, account: normalizeAccount(session.account) };
152
+ }
153
+
154
+ export function serializeCodeBuddySession(session) {
155
+ if (!session?.auth?.accessToken || !session?.auth?.refreshToken) throw new Error("CodeBuddy 登录会话无效");
156
+ return JSON.stringify({ auth: calculateExpiresAt(session.auth), account: normalizeAccount(session.account) });
157
+ }
158
+
159
+ export function parseCodeBuddySession(value) {
160
+ let session;
161
+ try {
162
+ session = JSON.parse(value);
163
+ } catch (error) {
164
+ throw new Error("CodeBuddy 登录凭据已损坏,请重新登录", { cause: error });
165
+ }
166
+ if (!session?.auth?.accessToken || !session?.auth?.refreshToken) throw new Error("CodeBuddy 登录凭据不完整,请重新登录");
167
+ return { auth: calculateExpiresAt(session.auth), account: normalizeAccount(session.account) };
168
+ }
169
+
170
+ export function sessionNeedsRefresh(session, now = Date.now()) {
171
+ const expiresAt = Number(session?.auth?.expiresAt);
172
+ if (Number.isFinite(expiresAt)) return expiresAt <= now + 2 * 60_000;
173
+ try {
174
+ const payload = JSON.parse(Buffer.from(session.auth.accessToken.split(".")[1], "base64url").toString("utf8"));
175
+ return Number.isFinite(payload.exp) ? payload.exp * 1000 <= now + 2 * 60_000 : true;
176
+ } catch {
177
+ return true;
178
+ }
179
+ }
180
+
181
+ export function sessionCacheDeadline(session, now = Date.now()) {
182
+ const expiresAt = Number(session?.auth?.expiresAt);
183
+ return Number.isFinite(expiresAt)
184
+ ? Math.max(now, Math.min(expiresAt - 2 * 60_000, now + 30 * 60_000))
185
+ : now + 5 * 60_000;
186
+ }
@@ -0,0 +1,98 @@
1
+ import { credentialRef } from "@deepseek-ai/dsh-credentials";
2
+ import { CODEBUDDY_SESSION_REF, loginCodeBuddy, serializeCodeBuddySession } from "./codebuddy-auth.js";
3
+
4
+ const PROVIDER = "codebuddy-cn";
5
+ const API_KEY_ENV = "CODEBUDDY_API_KEY";
6
+ const ROUTE = "/dsh-llm-codebuddy/auth";
7
+
8
+ export function authenticationMode(config) {
9
+ const profile = config?.providers?.[PROVIDER];
10
+ return profile && profile.apiKeyEnv === undefined ? "token" : "api-key";
11
+ }
12
+
13
+ function json(res, status, body) {
14
+ res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
15
+ res.end(JSON.stringify(body));
16
+ }
17
+
18
+ function localPost(req) {
19
+ const address = req.socket.remoteAddress;
20
+ const loopback = address === "127.0.0.1" || address === "::1" || address === "::ffff:127.0.0.1";
21
+ if (!loopback) return false;
22
+ const origin = req.headers.origin;
23
+ if (!origin) return req.headers["sec-fetch-site"] === "same-origin";
24
+ try {
25
+ return ["127.0.0.1", "localhost", "[::1]"].includes(new URL(origin).hostname);
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+
31
+ async function setMode(settings, mode) {
32
+ const config = settings.get("llm-pi-ai");
33
+ const exists = Object.hasOwn(config?.providers ?? {}, PROVIDER);
34
+ const path = ["providers", PROVIDER];
35
+ if (!exists) {
36
+ await settings.mutate("llm-pi-ai", [{ op: "set", path, value: mode === "token" ? {} : { apiKeyEnv: API_KEY_ENV } }]);
37
+ return;
38
+ }
39
+ await settings.mutate("llm-pi-ai", [{
40
+ op: mode === "token" ? "unset" : "set",
41
+ path: [...path, "apiKeyEnv"],
42
+ ...(mode === "api-key" ? { value: API_KEY_ENV } : {}),
43
+ }]);
44
+ }
45
+
46
+ export function installCodeBuddyWeb(ctx) {
47
+ ctx.inject(["webServer", "settings", "credentials"], (webCtx) => {
48
+ let loginPromise;
49
+ const currentState = async () => ({
50
+ ok: true,
51
+ mode: authenticationMode(webCtx.settings.get("llm-pi-ai")),
52
+ authenticated: (await webCtx.credentials.describe(credentialRef(CODEBUDDY_SESSION_REF))).configured,
53
+ });
54
+ const status = async (_req, res) => {
55
+ json(res, 200, await currentState());
56
+ };
57
+ const apiKey = async (req, res) => {
58
+ if (req.method !== "POST") return json(res, 405, { ok: false, message: "Method not allowed" });
59
+ if (!localPost(req)) return json(res, 403, { ok: false, message: "只允许从本机 DSH 页面切换认证方式" });
60
+ await setMode(webCtx.settings, "api-key");
61
+ json(res, 200, await currentState());
62
+ };
63
+ const token = async (req, res) => {
64
+ if (req.method !== "POST") return json(res, 405, { ok: false, message: "Method not allowed" });
65
+ if (!localPost(req)) return json(res, 403, { ok: false, message: "只允许从本机 DSH 页面切换认证方式" });
66
+ const state = await currentState();
67
+ if (!state.authenticated) return json(res, 409, { ok: false, message: "尚未保存 CodeBuddy 登录令牌" });
68
+ await setMode(webCtx.settings, "token");
69
+ json(res, 200, await currentState());
70
+ };
71
+ const login = async (req, res) => {
72
+ if (req.method !== "POST") return json(res, 405, { ok: false, message: "Method not allowed" });
73
+ if (!localPost(req)) return json(res, 403, { ok: false, message: "只允许从本机 DSH 页面登录" });
74
+ try {
75
+ loginPromise ??= (async () => {
76
+ const session = await loginCodeBuddy();
77
+ await webCtx.credentials.set(credentialRef(CODEBUDDY_SESSION_REF), serializeCodeBuddySession(session));
78
+ await setMode(webCtx.settings, "token");
79
+ })().finally(() => {
80
+ loginPromise = undefined;
81
+ });
82
+ await loginPromise;
83
+ json(res, 200, { ok: true, mode: "token", authenticated: true });
84
+ } catch (error) {
85
+ json(res, 500, { ok: false, message: error instanceof Error ? error.message : "CodeBuddy 登录失败" });
86
+ }
87
+ };
88
+ webCtx.effect(() => {
89
+ const dispose = [
90
+ webCtx.webServer.register({ kind: "exact", path: `${ROUTE}/status`, handler: status }),
91
+ webCtx.webServer.register({ kind: "exact", path: `${ROUTE}/api-key`, handler: apiKey }),
92
+ webCtx.webServer.register({ kind: "exact", path: `${ROUTE}/token`, handler: token }),
93
+ webCtx.webServer.register({ kind: "exact", path: `${ROUTE}/login`, handler: login }),
94
+ ];
95
+ return () => dispose.forEach((fn) => fn());
96
+ }, "llm-codebuddy: web login routes");
97
+ });
98
+ }
package/index.js CHANGED
@@ -6,6 +6,15 @@ import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-sett
6
6
  import { createProvider } from "@earendil-works/pi-ai";
7
7
  import * as openAICompletionsApi from "@earendil-works/pi-ai/api/openai-completions";
8
8
  import { builtinProviders } from "@earendil-works/pi-ai/providers/all";
9
+ import {
10
+ CODEBUDDY_SESSION_REF,
11
+ parseCodeBuddySession,
12
+ refreshCodeBuddySession,
13
+ serializeCodeBuddySession,
14
+ sessionCacheDeadline,
15
+ sessionNeedsRefresh,
16
+ } from "./codebuddy-auth.js";
17
+ import { installCodeBuddyWeb } from "./codebuddy-web.js";
9
18
 
10
19
  export { Config };
11
20
 
@@ -18,7 +27,7 @@ const DISPLAY_NAME = "CodeBuddy 中国区";
18
27
  const API_KEY_ENV = "CODEBUDDY_API_KEY";
19
28
  const BASE_URL = "https://copilot.tencent.com/v2";
20
29
  const CONFIG_URL = "https://copilot.tencent.com/v3/config";
21
- const USER_AGENT = "CLI/unknown CodeBuddy/2.136.0";
30
+ const USER_AGENT = "CLI/unknown CodeBuddy/2.137.1";
22
31
  const STREAM_IDLE_TIMEOUT_MS = 300_000;
23
32
  const NO_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
24
33
  const EFFORTS = ["minimal", "low", "medium", "high", "xhigh", "max"];
@@ -31,6 +40,16 @@ const COMPAT = {
31
40
  thinkingFormat: "openai",
32
41
  };
33
42
 
43
+ function codeBuddyRequestOptions(options) {
44
+ return { ...options, headers: { ...(options?.headers ?? {}), "user-agent": USER_AGENT } };
45
+ }
46
+
47
+ const codeBuddyApi = {
48
+ ...openAICompletionsApi,
49
+ stream: (model, context, options) => openAICompletionsApi.stream(model, context, codeBuddyRequestOptions(options)),
50
+ streamSimple: (model, context, options) => openAICompletionsApi.streamSimple(model, context, codeBuddyRequestOptions(options)),
51
+ };
52
+
34
53
  const FALLBACK_MODELS = [
35
54
  ["hy3", "Hy3", 192000, 64000, true],
36
55
  ["glm-5.2", "GLM-5.2", 1000000, 48000, false],
@@ -133,13 +152,18 @@ function modelsFromConfig(data) {
133
152
  });
134
153
  }
135
154
 
136
- async function fetchCodeBuddyModels(apiKey, signal) {
155
+ function authenticationHeaders(credential) {
156
+ const value = assertUsableApiKey(credential.value, name, credential.ref ?? API_KEY_ENV);
157
+ return credential.kind === "bearer" ? { authorization: `Bearer ${value}` } : { "x-api-key": value };
158
+ }
159
+
160
+ async function fetchCodeBuddyModels(credential, signal) {
137
161
  let response;
138
162
  try {
139
163
  response = await fetch(CONFIG_URL, {
140
164
  headers: {
141
165
  accept: "application/json",
142
- "x-api-key": assertUsableApiKey(apiKey, name, API_KEY_ENV),
166
+ ...authenticationHeaders(credential),
143
167
  "user-agent": USER_AGENT,
144
168
  "x-product": "SaaS",
145
169
  },
@@ -164,7 +188,7 @@ function codeBuddyProvider(models, auth) {
164
188
  baseUrl: BASE_URL,
165
189
  auth,
166
190
  models,
167
- api: openAICompletionsApi,
191
+ api: codeBuddyApi,
168
192
  });
169
193
  }
170
194
 
@@ -172,6 +196,7 @@ function resolvedProfile(provider, source, piProvider, configuredMaxTokens = new
172
196
  const apiKeyEnv = source.apiKeyEnv === undefined ? undefined : credentialRef(source.apiKeyEnv);
173
197
  return {
174
198
  ...source,
199
+ headers: runtimeHeaders(source.headers),
175
200
  provider,
176
201
  displayName: source.displayName ?? piProvider.name ?? provider,
177
202
  ...(apiKeyEnv === undefined ? {} : { apiKeyEnv }),
@@ -216,15 +241,30 @@ function selectCodeBuddyModels(base, entries) {
216
241
  });
217
242
  }
218
243
 
219
- export const __testing = Object.freeze({ modelsFromConfig, selectCodeBuddyModels });
244
+ function ownsProvider(provider, builtins) {
245
+ return provider === PROVIDER || builtins.has(provider);
246
+ }
247
+
248
+ function runtimeHeaders(headers) {
249
+ return { ...(headers ?? {}) };
250
+ }
251
+
252
+ function codeBuddySource(config, source) {
253
+ return Object.hasOwn(config?.providers ?? {}, PROVIDER) ? source : { ...source, apiKeyEnv: source.apiKeyEnv ?? API_KEY_ENV };
254
+ }
255
+
256
+ export const __testing = Object.freeze({ authenticationHeaders, codeBuddyRequestOptions, codeBuddySource, modelsFromConfig, ownsProvider, runtimeHeaders, selectCodeBuddyModels });
220
257
 
221
258
  export function apply(ctx, config) {
259
+ installCodeBuddyWeb(ctx);
222
260
  let current = () => config;
223
261
  let remoteModels;
224
262
  let generation = 0;
225
263
  let memoRaw;
226
264
  let memoGeneration = -1;
227
265
  let memoized;
266
+ let loginSession;
267
+ let loginSessionPromise;
228
268
  const builtins = new Map(builtinProviders().map((provider) => [provider.id, provider]));
229
269
  const apiKeyAuth = builtins.get("deepseek")?.auth;
230
270
  if (!apiKeyAuth) throw new Error(`${name}: pi-ai DeepSeek auth helper is unavailable`);
@@ -245,20 +285,20 @@ export function apply(ctx, config) {
245
285
  if (memoRaw === current() && memoGeneration === generation && memoized) return memoized;
246
286
  const result = new Map();
247
287
  for (const [provider, source] of Object.entries(raw.providers)) {
288
+ if (!ownsProvider(provider, builtins)) continue;
248
289
  if (provider === PROVIDER) {
290
+ const sourceWithAuth = codeBuddySource(current(), source);
249
291
  const models = selectCodeBuddyModels(remoteModels ?? FALLBACK_MODELS, source.models);
250
292
  const configured = new Map((source.models ?? []).flatMap((model) =>
251
293
  Number.isSafeInteger(model.maxTokens) && model.maxTokens > 0 ? [[model.id, model.maxTokens]] : [],
252
294
  ));
253
295
  result.set(provider, resolvedProfile(provider, {
254
- ...source,
255
- apiKeyEnv: source.apiKeyEnv ?? API_KEY_ENV,
296
+ ...sourceWithAuth,
256
297
  displayName: DISPLAY_NAME,
257
298
  }, codeBuddyProvider(models, apiKeyAuth), configured));
258
299
  continue;
259
300
  }
260
301
  const base = builtins.get(provider);
261
- if (!base) throw new Error(`${name}: 不支持非内置 Provider "${provider}"`);
262
302
  const selected = selectBuiltinModels(base, source.models);
263
303
  const configured = new Map((source.models ?? []).flatMap((model) =>
264
304
  Number.isSafeInteger(model.maxTokens) && model.maxTokens > 0 ? [[model.id, model.maxTokens]] : [],
@@ -271,15 +311,54 @@ export function apply(ctx, config) {
271
311
  return result;
272
312
  };
273
313
 
274
- const resolveApiKey = async (provider, profile) => {
314
+ const resolveLoginSession = async () => {
315
+ if (loginSession?.expiresAt > Date.now()) return loginSession;
316
+ loginSessionPromise ??= (async () => {
317
+ const credentials = ctx.get("credentials");
318
+ const ref = credentialRef(CODEBUDDY_SESSION_REF);
319
+ const stored = await credentials?.resolve(ref);
320
+ const value = stored?.value ?? launchEnvironmentOf(ctx).get(ref)?.value;
321
+ if (!value) throw new Error("未找到 CodeBuddy 登录凭据");
322
+ let session = parseCodeBuddySession(value);
323
+ if (sessionNeedsRefresh(session)) {
324
+ session = await refreshCodeBuddySession(session);
325
+ await credentials?.set(ref, serializeCodeBuddySession(session));
326
+ }
327
+ return { ...session, expiresAt: sessionCacheDeadline(session) };
328
+ })().finally(() => {
329
+ loginSessionPromise = undefined;
330
+ });
331
+ loginSession = await loginSessionPromise;
332
+ return loginSession;
333
+ };
334
+
335
+ const resolveCredential = async (provider, profile) => {
275
336
  const ref = profile.apiKeyEnv;
276
- if (!ref) return undefined;
337
+ if (!ref && provider === PROVIDER) {
338
+ let session;
339
+ try {
340
+ session = await resolveLoginSession();
341
+ } catch (error) {
342
+ throw new LlmError(`${name}: 未找到可用的 CodeBuddy 登录令牌,请运行 dsh-llm-codebuddy login`, "MISSING_CREDENTIAL", { cause: error });
343
+ }
344
+ profile.headers ??= {};
345
+ if (session.account.userId) profile.headers["X-User-Id"] = session.account.userId;
346
+ if (session.account.enterpriseId) {
347
+ profile.headers["X-Enterprise-Id"] = session.account.enterpriseId;
348
+ profile.headers["X-Tenant-Id"] = session.account.enterpriseId;
349
+ }
350
+ if (session.auth.domain) profile.headers["X-Domain"] = session.auth.domain;
351
+ return { value: assertUsableApiKey(session.auth.accessToken, name, "CodeBuddy login session"), kind: "bearer" };
352
+ }
353
+ if (!ref) return { value: undefined, kind: "none" };
277
354
  const stored = await ctx.get("credentials")?.resolve(ref);
278
355
  const value = stored?.value ?? launchEnvironmentOf(ctx).get(ref)?.value;
279
- if (value) return assertUsableApiKey(value, name, ref);
356
+ if (value) return { value: assertUsableApiKey(value, name, ref), kind: "api-key", ref };
280
357
  throw new LlmError(`${name}: Provider "${provider}" 缺少 API Key,请在 WebUI 的模型设置中填写`, "MISSING_CREDENTIAL");
281
358
  };
282
359
 
360
+ const resolveApiKey = async (provider, profile) => (await resolveCredential(provider, profile)).value;
361
+
283
362
  const adapter = new PiAiAdapter({
284
363
  profiles,
285
364
  resolveApiKey,
@@ -301,8 +380,8 @@ export function apply(ctx, config) {
301
380
  refreshPromise ??= (async () => {
302
381
  try {
303
382
  const profile = profiles().get(PROVIDER);
304
- const apiKey = await resolveApiKey(PROVIDER, profile);
305
- remoteModels = await fetchCodeBuddyModels(apiKey);
383
+ const credential = await resolveCredential(PROVIDER, profile);
384
+ remoteModels = await fetchCodeBuddyModels(credential);
306
385
  generation += 1;
307
386
  } catch {
308
387
  // Keep the built-in catalog available while the key or network is absent.
@@ -333,8 +412,10 @@ export function apply(ctx, config) {
333
412
  ctx.llm.registerModelDiscovery(NS, async (request) => {
334
413
  if (request.provider === PROVIDER) {
335
414
  const profile = profiles().get(PROVIDER);
336
- const apiKey = request.apiKey ?? await resolveApiKey(PROVIDER, profile);
337
- remoteModels = await fetchCodeBuddyModels(apiKey, request.signal);
415
+ const credential = request.apiKey
416
+ ? { value: request.apiKey, kind: "api-key", ref: API_KEY_ENV }
417
+ : await resolveCredential(PROVIDER, profile);
418
+ remoteModels = await fetchCodeBuddyModels(credential, request.signal);
338
419
  generation += 1;
339
420
  return remoteModels.map((model) => ({
340
421
  id: model.id,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-llm-codebuddy",
3
- "version": "1.3.4",
4
- "description": "使用 WorkBuddy API Key DeepSeek Harness 接入 CodeBuddy 模型",
3
+ "version": "1.3.5",
4
+ "description": "通过 WorkBuddy API Key CodeBuddy 登录令牌为 DeepSeek Harness 接入 CodeBuddy 模型",
5
5
  "author": "Axiaohungry",
6
6
  "keywords": [
7
7
  "deepseek-harness",
@@ -14,7 +14,9 @@
14
14
  "type": "module",
15
15
  "main": "index.js",
16
16
  "exports": {
17
- ".": "./index.js"
17
+ ".": "./index.js",
18
+ "./client": "./client.js",
19
+ "./package.json": "./package.json"
18
20
  },
19
21
  "bin": {
20
22
  "dsh-llm-codebuddy": "cli.js"
@@ -29,6 +31,9 @@
29
31
  },
30
32
  "files": [
31
33
  "index.js",
34
+ "codebuddy-auth.js",
35
+ "codebuddy-web.js",
36
+ "client.js",
32
37
  "cli.js",
33
38
  "cordis.patch.yml",
34
39
  "docs",
@@ -36,9 +41,13 @@
36
41
  "LICENSE"
37
42
  ],
38
43
  "scripts": {
39
- "check": "node --check index.js && node --check cli.js && node --test test.js && node cli.js --self-test"
44
+ "check": "node --check index.js && node --check codebuddy-auth.js && node --check codebuddy-web.js && node --check client.js && node --check cli.js && node --test test.js && node cli.js --self-test"
40
45
  },
41
46
  "dsh": {
47
+ "client": {
48
+ "inject": [],
49
+ "platform": "web"
50
+ },
42
51
  "bundle": {
43
52
  "patch": "./cordis.patch.yml"
44
53
  }