dsh-qoder-connect 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Corrine Hu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.en.md ADDED
@@ -0,0 +1,147 @@
1
+ # DSH Qoder Connect
2
+
3
+ **English** | [简体中文](./README.md)
4
+
5
+ A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH) plugin that brings your **Qoder subscription** models into DSH through a Personal Access Token (PAT). One plugin serves both Qoder product lines — each with its own settings card, provider id, credential file and saved catalog, never mixed:
6
+
7
+ | Variant | Provider id | Card title | Where the PAT is minted |
8
+ |---|---|---|---|
9
+ | China (国内版) | `qoder` | Qoder | qoder.com.cn |
10
+ | International (国际版) | `qoder-global` | Qoder Global | qoder.com |
11
+
12
+ Configure only what you use: a variant with no saved PAT shows no model group and never interferes with the other one. Streaming, reasoning content and tool calls ride the plugin's built-in Qoder transport; the conversation loop, compaction and permissions stay Harness-owned.
13
+
14
+ ## Features
15
+
16
+ - **PAT: save it and it works** — paste a PAT into the card and press *Save*; the token is validated live against that variant's region before anything is written, and the model group appears immediately — no DSH restart. Once signed in, *Replace PAT* (the new token overwrites only after it validates, so a mistyped paste can never strand a working credential) and *Clear PAT* are one click away.
17
+ - **Two independent variants** — `qoder` (china) and `qoder-global` (global) keep separate credential files, routes and saved catalogs: a token minted on qoder.com does not work against the China deployment and vice versa.
18
+ - **Three-tier model catalog (live → saved → fallback)** — the card states where the list on screen came from: *“Model list updated …”* (fetched now), *“Showing the saved model list from …”* (this account's last successful fetch, restored after a restart or a failed fetch), or *“Showing the built-in model list (not yet updated from Qoder)”* (the roster compiled into the plugin). The reason of the most recent failed fetch is shown too, and a *Refresh model list* button sits right there.
19
+ - **One-toggle context window** — each variant card's *Context window* tab lists every model's capacity (the default and the largest declared window) and carries its own *“Use the largest declared context window”* preference (on by default): on, requests declare the maximum (e.g. Qwen3.8-Max at 1M); off, the default (200K). The two variants keep independent toggle state.
20
+ - **Sidebar quota card + credit details** — the *“Qoder sidebar display”* card toggles a per-variant sidebar quota widget (off by default; each toggle needs its variant's PAT saved) with one shared refresh interval (default 5 min, minimum 1 min). Clicking the sidebar widget opens the *Qoder quota* panel: one row per credit package — *“Remaining / Total + bar | Expires”* — plus the cycle share and reset time. Click the same widget again to close; click the other to switch variants.
21
+ - **Rate display `x<priceFactor>`** — each model name is suffixed with the upstream-reported price multiplier (e.g. `Some Model · x0.79`, free is `x0`), spelled `x<n>` from the catalog's `price_factor`. Display only — it never changes the request; models whose rate the upstream did not report simply show no suffix.
22
+
23
+ ## Screenshots
24
+
25
+ ### Sidebar quota widgets & credit details
26
+
27
+ Once enabled on the *“Qoder sidebar display”* card, a per-variant quota widget sits at the sidebar's foot; clicking it opens the *Qoder quota* panel with one row per credit package, switchable between the two variants at any time.
28
+
29
+ **China (after clicking the sidebar widget):**
30
+
31
+ ![China sidebar quota details](assets/1.png)
32
+
33
+ **International (after clicking the sidebar widget):**
34
+
35
+ ![International sidebar quota details](assets/2.png)
36
+
37
+ ### Sidebar toggles
38
+
39
+ The *“Qoder sidebar display”* card controls both widgets' visibility and the shared refresh interval:
40
+
41
+ ![Sidebar display toggles](assets/3.png)
42
+
43
+ ### Picking a model in a session
44
+
45
+ Once a PAT is saved, the model picker gains a Qoder group with per-model rate multipliers (`x0.5`, `x0`, …):
46
+
47
+ ![Model picker in a session](assets/4.png)
48
+
49
+ ### Context-window settings
50
+
51
+ Each variant card's *Context window* tab lists capacities per model under the *“Use the largest declared context window”* preference:
52
+
53
+ **China:**
54
+
55
+ ![China context-window settings](assets/5.png)
56
+
57
+ **International:**
58
+
59
+ ![International context-window settings](assets/6.png)
60
+
61
+ ## Install
62
+
63
+ Prerequisites:
64
+
65
+ - DSH core `0.1.5-rc.1` or newer (this plugin peers on the `@deepseek-ai/*` `^0.1.5-rc.1` line);
66
+ - Node.js `^22.19.0 || >=24.0.0` (per `package.json` engines);
67
+ - your own Qoder account and at least one Personal Access Token.
68
+
69
+ ```sh
70
+ # From GitHub (recommended)
71
+ dsh plugin --profile web add github:masknull/dsh-qoder-connect
72
+ # Or from npm, once published
73
+ dsh plugin --profile web add dsh-qoder-connect
74
+ ```
75
+
76
+ Swap the `--profile` value for the profile you use (`web` / `desktop` / `tui`, e.g. `dsh plugin --profile desktop add dsh-qoder-connect`) — data stays inside that profile, so web / desktop / tui never collide. On a terminal-only profile (TUI) there is no browser card: manage the PAT and check status through the bundled CLI instead; the settings card and sidebar widgets require Web or Desktop.
77
+
78
+ ## Configuration
79
+
80
+ ### Mint a PAT
81
+
82
+ - **Qoder (China)**: sign in at qoder.com.cn → account settings → Personal Access Token.
83
+ - **Qoder Global**: sign in at qoder.com → account settings → Personal Access Token.
84
+
85
+ (The card repeats the same guidance: *“Generate a PAT in your qoder.com account settings (Account → Personal Access Token), then paste it here.”*)
86
+
87
+ ### Save it on the card
88
+
89
+ DSH → Settings → Plugins → the variant's card: paste the PAT into the password field and press *Save* (*“Validating and saving…”* while it runs). The token is validated against that variant's region first — a refusal saves nothing and the card says *“That PAT was rejected — generate a new one in your account settings and try again.”*. Do this on both cards to run both groups side by side.
90
+
91
+ ### Environment-variable fallback
92
+
93
+ For headless setups, the plugin reads an environment fallback **only when no credential file exists**:
94
+
95
+ | Variant | Environment variable |
96
+ |---|---|
97
+ | `qoder` (china) | `QODER_CN_PERSONAL_ACCESS_TOKEN` |
98
+ | `qoder-global` | `QODER_PERSONAL_ACCESS_TOKEN` |
99
+
100
+ The precedence is **file > env**: as soon as a valid credential file is saved, a stray environment token stops mattering (the saved token always wins). Env-sourced credentials show as *source: environment variable* on the card, with no saved-at time.
101
+
102
+ ## Data & privacy
103
+
104
+ ### File layout
105
+
106
+ Everything the plugin owns lives in one data directory: `<profile>/.dsh-qoder-connect/` — credentials at the root, rebuildable caches under `state/` (so “clear the cache” can never touch a credential):
107
+
108
+ ```text
109
+ <profile>/.dsh-qoder-connect/
110
+ ├── .qoder-auth.json # China PAT ({version:2, pat, region, savedAt})
111
+ ├── .qoder-global-auth.json # Global PAT
112
+ └── state/
113
+ ├── .qoder-catalog.json # China per-account saved catalog
114
+ ├── .qoder-global-catalog.json
115
+ ├── .qoder-probe.json # China reasoning-effort probe records
116
+ ├── .qoder-global-probe.json
117
+ ├── .qoder-host-heartbeat.json # host heartbeat (doctor uses it)
118
+ └── .qoder-machine-id # transport machine-id seed (never written to ~/.qoder)
119
+ ```
120
+
121
+ The directory follows the profile that declares this plugin (found under `$DSH_HOME/profiles/`), falling back to the Harness home when none can be determined; `DSH_QODER_DATA_DIR` overrides it explicitly.
122
+
123
+ ## Development
124
+
125
+ ```sh
126
+ pnpm install
127
+ pnpm run build # tsdown → lib/ (host bundle + client bundle)
128
+ pnpm test # vitest run (tests/**/*.spec.ts)
129
+ pnpm run test:qoder # node --test (tests/qoder/*.test.ts, the Qoder transport suite)
130
+ pnpm run typecheck # tsc across the host and client tsconfigs
131
+ pnpm run check # typecheck + test + test:qoder + build in one gate
132
+ ```
133
+
134
+ The upstream `node:test` suite for the Qoder transport layer now lives in `tests/qoder/` (run it with `pnpm run test:qoder`; the pristine `reference-qoder-tests/` copy has been removed) — `package.json` scripts are the source of truth.
135
+
136
+ ## Provenance & license
137
+
138
+ - Derived from [masknull/dsh-workbuddy-connect](https://github.com/masknull/dsh-workbuddy-connect) (MIT) — the connect-plugin skeleton this project was refactored from.
139
+ - Qoder transport layer ported from [mo-n/dsh-provider-qoder](https://github.com/mo-n/dsh-provider-qoder) (MIT).
140
+
141
+ Released under the [MIT](./LICENSE) license; see [NOTICE](./NOTICE) for the upstream attributions. This is a community adapter — not affiliated with, authorized by, or endorsed by Qoder or DeepSeek, and not an official implementation.
142
+
143
+ ## Disclaimer
144
+
145
+ - Qoder's endpoints come from the upstream repositories: upstream may change, rate-limit or block them at any time, breaking part or all of this plugin with no compatibility window to promise.
146
+ - For personal learning and research only; this tool drives **your own** Qoder account. Do not use it commercially or beyond reasonable personal use. You are responsible for complying with Qoder's terms of service and for any consequence — account restriction, quota loss, service interruption — of using this project.
147
+ - The authors are not liable for any direct or indirect loss arising from use or misuse of this project. The names Qoder, DeepSeek and related marks belong to their respective owners and appear here only to describe compatibility.
package/README.md ADDED
@@ -0,0 +1,148 @@
1
+ # DSH Qoder Connect
2
+
3
+ [English](./README.en.md) | **简体中文**
4
+
5
+ 把 **Qoder 订阅**的模型以个人访问令牌(PAT)接入 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(DSH)的社区插件。一个插件同时服务两条 Qoder 产品线,各自一张设置卡片、一个 provider、一套凭证与模型目录,互不混用:
6
+
7
+ | 变体 | provider id | 卡片标题(中文界面) | PAT 生成站点 |
8
+ |---|---|---|---|
9
+ | 国内版 | `qoder` | Qoder(国内版) | qoder.com.cn |
10
+ | 国际版 | `qoder-global` | Qoder Global(国际版) | qoder.com |
11
+
12
+ 只需要哪一版就只配哪一版:未保存 PAT 的变体不显示模型分组,另一版完全不受影响。流式输出、推理内容、工具调用走插件内置的 Qoder 传输层;对话循环、压缩与权限始终由 DSH 本体掌控。
13
+
14
+ ## 功能
15
+
16
+ - **PAT 即存即用**:在卡片上粘贴 PAT 点「保存」,插件先对该变体的区域实时校验,通过才落盘,模型分组立即出现,无需重启 DSH。已登录后可「更换 PAT」(新令牌校验通过才覆盖,粘贴打错不会把可用凭据弄丢)与「清除 PAT」。
17
+ - **双变体独立配置**:`qoder`(china)与 `qoder-global`(global)各有自己的凭证文件、路由与已保存目录——qoder.com 生成的令牌对中国区无效,反之亦然,两套永不互串。
18
+ - **模型目录三级降级(live → saved → fallback)**:卡片明示当前列表来自哪里——「模型列表更新于 …」(实时拉取)、「当前显示已保存的模型列表,更新于 …」(本账号上次成功目录,重启或断网后顶上来)、「当前显示内置模型列表(尚未从 Qoder 更新)」(编译进插件的兜底名单);最近一次拉取失败时附带原因,并可在卡片上手动「刷新模型列表」。
19
+ - **上下文窗口一键切换**:两张变体卡片的「上下文窗口」页列出每个模型的容量(默认值与上游声明的最大值),并各自提供「使用上游声明的最大上下文窗口」开关(默认勾选):勾选按最大窗口(如 Qwen3.8-Max 的 1M)发请求,不勾选按默认窗口(200K),两版开关状态独立保存、互不影响。
20
+ - **侧栏额度展示 + 额度明细**:「Qoder 侧栏展示」卡片可分别为两版开启侧栏底部的额度小卡(默认关,开启前需为该变体保存 PAT),共享一个刷新间隔(默认 5 分钟,最小 1 分钟)。点侧栏小卡在中间面板打开「Qoder 额度」明细:按资源包逐行给出「剩余 / 总量 + 进度条|到期时间」,合计占比与本轮重置时间一并展示;再点同一张卡关闭,点另一张切换到对应版本。
21
+ - **倍率显示 `x<priceFactor>`**:模型名后直接拼上游报价的价格倍率(如 `某模型 · x0.79`,免费为 `x0`),来自目录接口的 `price_factor` 字段,仅作展示、不影响请求;上游未报倍率的模型不显示后缀,不会凭空补一个数。
22
+
23
+ ## 界面一览
24
+
25
+ ### 侧栏额度小卡与额度明细
26
+
27
+ 在「Qoder 侧栏展示」卡片开启后,侧栏底部出现该变体的额度小卡;点击小卡在中间面板打开「Qoder 额度」明细,按套餐逐行展示,可随时在两版之间切换。
28
+
29
+ **国内版(侧栏点击后):**
30
+
31
+ ![国内版侧栏额度明细](assets/1.png)
32
+
33
+ **国际版(侧栏点击后):**
34
+
35
+ ![国际版侧栏额度明细](assets/2.png)
36
+
37
+ ### 侧栏开关设置
38
+
39
+ 「Qoder 侧栏展示」卡片控制两张侧栏小卡的显隐与刷新间隔(间隔对两卡同时生效):
40
+
41
+ ![侧栏展示开关设置](assets/3.png)
42
+
43
+ ### 会话内选择模型
44
+
45
+ 保存 PAT 后,模型选择器出现 Qoder 分组,模型名后跟随价格倍率(`x0.5`、`x0` 等):
46
+
47
+ ![会话模型选择器](assets/4.png)
48
+
49
+ ### 上下文窗口设置
50
+
51
+ 「上下文窗口」页按模型列出容量与最大声明值,顶部是「使用上游声明的最大上下文窗口」开关:
52
+
53
+ **国内版:**
54
+
55
+ ![国内版上下文窗口设置](assets/5.png)
56
+
57
+ **国际版:**
58
+
59
+ ![国际版上下文窗口设置](assets/6.png)
60
+
61
+ ## 安装
62
+
63
+ 前置条件:
64
+
65
+ - DSH 核心 `0.1.5-rc.1` 及以上(本插件 peer 依赖 `@deepseek-ai/*` `^0.1.5-rc.1` 线);
66
+ - Node.js `^22.19.0 || >=24.0.0`(`package.json` engines);
67
+ - 你自己的 Qoder 账号,以及至少一枚个人访问令牌。
68
+
69
+ ```sh
70
+ # 从 GitHub 安装(推荐)
71
+ dsh plugin --profile web add github:masknull/dsh-qoder-connect
72
+ # 或已发布 npm 后从 npm 安装
73
+ dsh plugin --profile web add dsh-qoder-connect
74
+ ```
75
+
76
+ 按你使用的 profile 换 `--profile` 的值(`web` / `desktop` / `tui`,如 `dsh plugin --profile desktop add dsh-qoder-connect`)。装到哪个 profile,数据就落在哪个 profile 目录里,web / desktop / tui 各自独立。没有浏览器卡片的终端环境(TUI)下依旧可用 CLI 保存 PAT 并驱动模型;设置卡片与侧栏额度卡需要 Web/Desktop 界面。
77
+
78
+ ## 配置与使用
79
+
80
+ ### 生成 PAT
81
+
82
+ - **Qoder(中国版)**:登录 qoder.com.cn → 账号设置 → 个人访问令牌,生成后复制。
83
+ - **Qoder Global(国际版)**:登录 qoder.com → 账号设置 → 个人访问令牌,生成后复制。
84
+
85
+ (卡片内也有同样指引:「请先在 qoder.com.cn 的账号设置(账号 → 个人访问令牌)中生成 PAT,再粘贴到下方输入框。」)
86
+
87
+ ### 在卡片上保存
88
+
89
+ DSH → 设置 → 插件 → 对应卡片,展开后把 PAT 粘贴进密码输入框,点「保存」(期间显示「校验并保存中…」)。保存动作会先经 Qoder 校验令牌对该变体的区域有效,失败则不落盘并提示「PAT 无效或已过期 — 请到账号设置重新生成后再试。」两版各填各的卡片即可两组并存。
90
+
91
+ ### 环境变量兜底
92
+
93
+ 不方便走卡片的场景可用环境变量:**未保存过凭证文件**时插件读取 env 兜底——
94
+
95
+ | 变体 | 环境变量 |
96
+ |---|---|
97
+ | `qoder`(china) | `QODER_CN_PERSONAL_ACCESS_TOKEN` |
98
+ | `qoder-global` | `QODER_PERSONAL_ACCESS_TOKEN` |
99
+
100
+ 优先级为 **文件 > env**:只要该变体保存过有效的凭证文件,环境变量就不再生效(保存的令牌永远压过游离的 env)。env 来源的令牌在卡片上标为「来源:环境变量」,且没有保存时间。
101
+
102
+ ## 数据与隐私
103
+
104
+ ### 文件布局
105
+
106
+ 插件的全部自有文件收在数据目录一处:`<profile>/.dsh-qoder-connect/`(凭据在根,可重建的缓存在 `state/` 子目录,「清缓存」永远碰不到凭据):
107
+
108
+ ```text
109
+ <profile>/.dsh-qoder-connect/
110
+ ├── .qoder-auth.json # 中国版 PAT({version:2, pat, region, savedAt})
111
+ ├── .qoder-global-auth.json # 国际版 PAT
112
+ └── state/
113
+ ├── .qoder-catalog.json # 中国版按账号保存的模型目录
114
+ ├── .qoder-global-catalog.json
115
+ ├── .qoder-probe.json # 中国版推理档位探测记录
116
+ ├── .qoder-global-probe.json
117
+ ├── .qoder-host-heartbeat.json # host 心跳(doctor 判断宿主是否在跑)
118
+ └── .qoder-machine-id # 传输层机器标识种子(不落 ~/.qoder)
119
+ ```
120
+
121
+ 数据目录按 profile 归属(在 `$DSH_HOME/profiles/` 下找声明本插件的 profile),无法确定时回落到 Harness home;环境变量 `DSH_QODER_DATA_DIR` 可显式覆盖。
122
+
123
+
124
+ ## 开发
125
+
126
+ ```sh
127
+ pnpm install
128
+ pnpm run build # tsdown 产出 lib/(宿主 bundle + client bundle)
129
+ pnpm test # vitest run(tests/**/*.spec.ts)
130
+ pnpm run test:qoder # node --test(tests/qoder/*.test.ts,Qoder 传输层用例)
131
+ pnpm run typecheck # tsc:宿主 + client 两份 tsconfig
132
+ pnpm run check # typecheck + test + test:qoder + build 一条命令全过
133
+ ```
134
+
135
+ Qoder 传输层的上游 `node:test` 用例已移植进 `tests/qoder/`(用 `pnpm run test:qoder` 跑,原件 `reference-qoder-tests/` 已移除);命令以 `package.json` scripts 为准。
136
+
137
+ ## 出处与许可
138
+
139
+ - 骨架与 connect 机制改造自 [masknull/dsh-workbuddy-connect](https://github.com/masknull/dsh-workbuddy-connect)(MIT);
140
+ - Qoder 传输层移植自 [mo-n/dsh-provider-qoder](https://github.com/mo-n/dsh-provider-qoder)(MIT)。
141
+
142
+ 本项目按 [MIT](./LICENSE) 许可发布,双上游出处详见 [NOTICE](./NOTICE)。本仓库与 Qoder、DeepSeek 官方均无关联,是社区适配器——非官方实现。
143
+
144
+ ## 免责声明
145
+
146
+ - Qoder 的接口来自上游仓库:上游随时可能改动、限流或封禁调用方,导致本插件部分或全部功能失效;届时可能只有等社区跟进,不保证兼容窗口。
147
+ - 本工具仅供个人学习研究,用于驱动**使用者自己的** Qoder 账号;请勿用于商业转售或任何超出个人合理使用的场景。使用即表示你自行承担账号被限制、额度损失、服务中断等一切后果,并遵守 Qoder 的服务条款。
148
+ - 作者不对因使用或滥用本插件产生的任何直接或间接损失负责。文中出现的 Qoder、DeepSeek 等名称与商标归各自权利人所有,仅作兼容性描述之用。
package/assets/1.png ADDED
Binary file
package/assets/2.png ADDED
Binary file
package/assets/3.png ADDED
Binary file
package/assets/4.png ADDED
Binary file
package/assets/5.png ADDED
Binary file
package/assets/6.png ADDED
Binary file
@@ -0,0 +1,6 @@
1
+ # Register the Qoder provider without changing the profile's current
2
+ # default model. Credentials are Personal Access Tokens (PATs) the user
3
+ # pastes into the plugin's card; no OAuth or device flow is started.
4
+ - insert:
5
+ - id: llm-qoder
6
+ name: dsh-qoder-connect
package/lib/bin.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ //#region src/bin.d.ts
2
+ /** Standalone status/diagnostics/PAT CLI for the dsh-qoder-connect bundle. */
3
+ /** Execute one boot-free command. */
4
+ declare function run(argv: readonly string[]): Promise<number>;
5
+ //#endregion
6
+ export { run };
package/lib/bin.js ADDED
@@ -0,0 +1,291 @@
1
+ #!/usr/bin/env node
2
+ import { B as qoderCredentialIdentity, D as FALLBACK_QODER_MODELS, E as qoderCatalogPath, R as QoderCredentialStore, T as QoderCatalogStore, b as createQoderTransport, f as QODER_CONNECT_VERSION, i as variantFor, l as qoderHostHeartbeatPath, m as QoderUpstreamClient, r as QODER_VARIANTS, s as isHeartbeatProcessAlive, t as CHINA_VARIANT, u as readHostHeartbeat, y as validateApiKey } from "./variants-jbFhMKO8.js";
3
+ import { readFile } from "node:fs/promises";
4
+ import { realpathSync } from "node:fs";
5
+ import { fileURLToPath } from "node:url";
6
+ //#region src/bin.ts
7
+ /** Standalone status/diagnostics/PAT CLI for the dsh-qoder-connect bundle. */
8
+ const JSON_SCHEMA_VERSION = 1;
9
+ /** Remove token-like strings from an unexpected diagnostic message. */
10
+ function safeMessage(error) {
11
+ 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|pat)=)[^&\s]+/giu, "$1[redacted]");
12
+ }
13
+ function printHelp() {
14
+ process.stdout.write([
15
+ "Usage: dsh-qoder-connect <doctor|status|pat|logout|catalog> [--provider <id>] [--json] [--file <path>]",
16
+ "",
17
+ " doctor secret-free environment diagnostics",
18
+ " status credential state and remaining Qoder credit",
19
+ " pat set validate and store a Personal Access Token (arg, --file, or stdin)",
20
+ " pat clear remove the stored credential",
21
+ " logout alias for `pat clear`",
22
+ " catalog refresh re-fetch the model list and save it for this account",
23
+ "",
24
+ " --provider which product to act on; defaults to qoder",
25
+ ` one of: ${QODER_VARIANTS.map((variant) => variant.id).join(", ")}`,
26
+ " --json emit one secret-free JSON document (doctor/status only)",
27
+ " --file file to read the token from with `pat set`; \"-\" reads standard input",
28
+ "",
29
+ " A token can also arrive as the positional argument after `pat set`.",
30
+ " Environment fallback: QODER_CN_PERSONAL_ACCESS_TOKEN (qoder) or",
31
+ " QODER_PERSONAL_ACCESS_TOKEN (qoder-global) when no file is saved.",
32
+ ""
33
+ ].join("\n"));
34
+ }
35
+ function printJson(value) {
36
+ process.stdout.write(`${JSON.stringify(value)}\n`);
37
+ }
38
+ /** One variant's store. */
39
+ function makeStore(variant) {
40
+ return new QoderCredentialStore({ variant });
41
+ }
42
+ /** One variant's store plus the upstream client riding its transport. */
43
+ function makeClient(store, variant) {
44
+ const transport = createQoderTransport({
45
+ region: variant.region,
46
+ resolvePat: () => store.patPromise()
47
+ });
48
+ return new QoderUpstreamClient({
49
+ region: variant.region,
50
+ providerId: variant.id,
51
+ getPat: () => store.patPromise(),
52
+ transport
53
+ });
54
+ }
55
+ async function doctor(jsonOutput, variant) {
56
+ const store = makeStore(variant);
57
+ const status = await store.status();
58
+ const heartbeat = await readHostHeartbeat();
59
+ const hostAlive = heartbeat !== void 0 && isHeartbeatProcessAlive(heartbeat);
60
+ const report = {
61
+ schemaVersion: JSON_SCHEMA_VERSION,
62
+ package: "dsh-qoder-connect",
63
+ version: QODER_CONNECT_VERSION,
64
+ node: process.version,
65
+ provider: variant.id,
66
+ displayName: variant.displayName,
67
+ region: variant.region,
68
+ credentialFile: store.ownAuthPath(),
69
+ hostHeartbeat: {
70
+ path: qoderHostHeartbeatPath(),
71
+ present: heartbeat !== void 0,
72
+ ...heartbeat === void 0 ? {} : {
73
+ registeredAt: heartbeat.registeredAt,
74
+ pid: heartbeat.pid
75
+ },
76
+ processAlive: hostAlive
77
+ },
78
+ signIn: status.state,
79
+ ...status.pat === void 0 ? {} : { pat: status.pat },
80
+ ...status.reason === void 0 ? {} : { reason: status.reason },
81
+ fallbackModels: FALLBACK_QODER_MODELS.length,
82
+ hints: [...status.state === "configured" ? [] : [`Save a token with \`dsh-qoder-connect pat set <token> --provider ${variant.id}\`, or from the plugin's settings card.`], ...hostAlive ? [] : ["Host bundle not running in this DSH profile (or the process exited). The browser card is unavailable until DSH starts the plugin; the pat command above still works."]]
83
+ };
84
+ if (jsonOutput) printJson(report);
85
+ else process.stdout.write([
86
+ `${variant.displayName} Connect ${QODER_CONNECT_VERSION} on ${process.version}`,
87
+ `Region: ${report.region}`,
88
+ `Credential file: ${report.credentialFile}`,
89
+ `Host bundle: ${hostAlive ? `running (pid ${heartbeat.pid})` : heartbeat !== void 0 ? "stale heartbeat (process exited)" : "not started"}`,
90
+ `Credential: ${status.state === "configured" ? `configured (${status.pat?.source ?? "file"}${status.pat?.patTail === void 0 ? "" : `, tail …${status.pat.patTail}`})` : "missing"}`,
91
+ ...status.reason === void 0 ? [] : [`Note: ${status.reason}`],
92
+ `Static fallback models: ${report.fallbackModels}`,
93
+ ...report.hints.map((hint) => `Hint: ${hint}`),
94
+ ""
95
+ ].join("\n"));
96
+ return status.state === "configured" ? 0 : 1;
97
+ }
98
+ async function status(jsonOutput, variant) {
99
+ const store = makeStore(variant);
100
+ const auth = await store.status();
101
+ const base = {
102
+ schemaVersion: JSON_SCHEMA_VERSION,
103
+ package: "dsh-qoder-connect",
104
+ version: QODER_CONNECT_VERSION,
105
+ provider: variant.id,
106
+ region: variant.region,
107
+ credentialFile: auth.filePath,
108
+ signIn: auth.state,
109
+ ...auth.pat === void 0 ? {} : { pat: auth.pat },
110
+ ...auth.reason === void 0 ? {} : { reason: auth.reason }
111
+ };
112
+ if (auth.state !== "configured") {
113
+ if (jsonOutput) printJson({
114
+ ...base,
115
+ credits: void 0
116
+ });
117
+ else process.stdout.write([
118
+ `${variant.displayName}: no credential configured at ${auth.filePath}`,
119
+ ...auth.reason === void 0 ? [] : [`Note: ${auth.reason}`],
120
+ "Save a token with `pat set` or from the plugin’s settings card.",
121
+ ""
122
+ ].join("\n"));
123
+ return 1;
124
+ }
125
+ const client = makeClient(store, variant);
126
+ try {
127
+ const credits = await client.fetchCredits();
128
+ if (jsonOutput) printJson({
129
+ ...base,
130
+ credits
131
+ });
132
+ else process.stdout.write([
133
+ `${variant.displayName} (${variant.region}): token from ${auth.pat?.source ?? "file"}${auth.pat?.patTail === void 0 ? "" : `, tail …${auth.pat.patTail}`}`,
134
+ credits.unlimited ? "Quota: unlimited for the current cycle" : `Quota: ${Math.max(0, 100 - credits.total).toFixed(1)}% remaining of the cycle allowance`,
135
+ ...credits.accounts.map((account) => ` ${account.packageName}: ${account.remain}/${account.size}${account.unlimited ? " (unlimited)" : ""}`),
136
+ ...credits.cycleResetTime === void 0 ? [] : [` resets: ${credits.cycleResetTime}`],
137
+ ""
138
+ ].join("\n"));
139
+ return 0;
140
+ } catch (error) {
141
+ if (jsonOutput) printJson({
142
+ ...base,
143
+ creditsError: safeMessage(error)
144
+ });
145
+ else process.stdout.write(`${variant.displayName}: credential stored, but the quota call failed: ${safeMessage(error)}\n`);
146
+ return 1;
147
+ }
148
+ }
149
+ /** Read a token from the positional arg, --file, or standard input. */
150
+ async function readPat(source, rest) {
151
+ const inline = rest.find((argument) => !argument.startsWith("--"));
152
+ const pat = (source === void 0 ? inline ?? await readStdin() : source === "-" ? await readStdin() : await readFile(source, "utf8"))?.trim() ?? "";
153
+ if (pat === "") throw new Error("no token given: pass it as an argument, via --file <path>, or on standard input");
154
+ return pat;
155
+ }
156
+ async function readStdin() {
157
+ const chunks = [];
158
+ for await (const chunk of process.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
159
+ return Buffer.concat(chunks).toString("utf8");
160
+ }
161
+ async function patSet(variant, file, rest) {
162
+ const store = makeStore(variant);
163
+ const pat = await readPat(file, rest);
164
+ process.stdout.write(`Validating a token against ${variant.displayName} (${variant.region})…\n`);
165
+ if (!await validateApiKey(pat, variant.region)) {
166
+ process.stderr.write(`dsh-qoder-connect: the token was refused by ${variant.displayName} (${variant.region}). Nothing was saved.\n`);
167
+ process.stderr.write("If this token belongs to the other Qoder product, use --provider for that one.\n");
168
+ return 2;
169
+ }
170
+ await store.save(pat, "cli");
171
+ const status = await store.status();
172
+ process.stdout.write([
173
+ `Saved to ${store.ownAuthPath()}${status.pat?.patTail === void 0 ? "" : ` (tail …${status.pat.patTail})`}`,
174
+ "The running plugin picks the token up on its next sweep; no restart needed.",
175
+ ""
176
+ ].join("\n"));
177
+ return 0;
178
+ }
179
+ async function patClear(variant) {
180
+ const store = makeStore(variant);
181
+ await store.clear();
182
+ const status = await store.status();
183
+ process.stdout.write(status.state === "configured" ? `${variant.displayName} Connect: removed the stored file, but a token is still active from the environment (${status.pat?.source ?? "env"}).\n` : `${variant.displayName} Connect: signed out; removed ${store.ownAuthPath()}\n`);
184
+ return 0;
185
+ }
186
+ async function catalogRefresh(variant) {
187
+ const store = makeStore(variant);
188
+ const credential = await store.resolve();
189
+ const client = makeClient(store, variant);
190
+ const models = await client.fetchModels();
191
+ const identity = qoderCredentialIdentity(credential);
192
+ const fetch = client.lastCatalog;
193
+ new QoderCatalogStore({ path: qoderCatalogPath(variant.catalogFilename) }).set(identity, {
194
+ source: fetch?.source ?? "unknown",
195
+ fetchedAtMs: fetch?.fetchedAtMs ?? Date.now(),
196
+ models: [...models]
197
+ });
198
+ process.stdout.write(`${variant.displayName}: fetched ${models.length} models; saved for account ${identity}.\n`);
199
+ return 0;
200
+ }
201
+ /** Execute one boot-free command. */
202
+ async function run(argv) {
203
+ if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
204
+ printHelp();
205
+ return 0;
206
+ }
207
+ const [rawAction, ...flags] = argv;
208
+ if (![
209
+ "doctor",
210
+ "status",
211
+ "pat",
212
+ "logout",
213
+ "catalog"
214
+ ].includes(rawAction)) {
215
+ process.stderr.write(`dsh-qoder-connect: expected doctor, status, pat, logout, or catalog; got ${JSON.stringify(rawAction)}\n`);
216
+ return 1;
217
+ }
218
+ const action = rawAction;
219
+ const jsonOutput = flags.includes("--json");
220
+ let providerId;
221
+ let file;
222
+ const rest = [];
223
+ for (let index = 0; index < flags.length; index += 1) {
224
+ const flag = flags[index];
225
+ if (flag === "--provider") {
226
+ providerId = flags[index + 1];
227
+ index += 1;
228
+ continue;
229
+ }
230
+ if (flag.startsWith("--provider=")) {
231
+ providerId = flag.slice(11);
232
+ continue;
233
+ }
234
+ if (flag === "--file") {
235
+ file = flags[index + 1];
236
+ index += 1;
237
+ continue;
238
+ }
239
+ if (flag.startsWith("--file=")) {
240
+ file = flag.slice(7);
241
+ continue;
242
+ }
243
+ rest.push(flag);
244
+ }
245
+ const variant = providerId === void 0 ? CHINA_VARIANT : variantFor(providerId);
246
+ if (variant === void 0) {
247
+ process.stderr.write(`dsh-qoder-connect: unknown provider ${JSON.stringify(providerId)}; expected one of ${QODER_VARIANTS.map((v) => v.id).join(", ")}\n`);
248
+ return 1;
249
+ }
250
+ let patCommand;
251
+ let catalogCommand;
252
+ if (action === "pat") {
253
+ const sub = rest.shift();
254
+ patCommand = sub === "set" || sub === "clear" ? sub : void 0;
255
+ if (patCommand === void 0) {
256
+ process.stderr.write(`dsh-qoder-connect: pat needs set or clear; got ${JSON.stringify(sub ?? "")}\n`);
257
+ return 1;
258
+ }
259
+ }
260
+ if (action === "catalog") {
261
+ const sub = rest.shift();
262
+ catalogCommand = sub === "refresh" ? sub : void 0;
263
+ if (catalogCommand === void 0) {
264
+ process.stderr.write(`dsh-qoder-connect: catalog needs refresh; got ${JSON.stringify(sub ?? "")}\n`);
265
+ return 1;
266
+ }
267
+ }
268
+ if (rest.filter((flag) => flag !== "--json" && !flag.startsWith("--")).length > 0 || jsonOutput && action !== "doctor" && action !== "status") {
269
+ process.stderr.write(`dsh-qoder-connect: invalid options for ${action}: ${flags.join(" ")}\n`);
270
+ return 1;
271
+ }
272
+ if (action !== "pat" && file !== void 0) {
273
+ process.stderr.write(`dsh-qoder-connect: --file applies to pat set, not ${action}\n`);
274
+ return 1;
275
+ }
276
+ try {
277
+ switch (action) {
278
+ case "doctor": return await doctor(jsonOutput, variant);
279
+ case "status": return await status(jsonOutput, variant);
280
+ case "pat": return patCommand === "set" ? await patSet(variant, file, rest) : await patClear(variant);
281
+ case "logout": return await patClear(variant);
282
+ case "catalog": return await catalogRefresh(variant);
283
+ }
284
+ } catch (error) {
285
+ process.stderr.write(`dsh-qoder-connect: ${action} failed: ${safeMessage(error)}\n`);
286
+ return 1;
287
+ }
288
+ }
289
+ if (process.argv[1] !== void 0 && fileURLToPath(import.meta.url) === realpathSync(process.argv[1])) process.exitCode = await run(process.argv.slice(2));
290
+ //#endregion
291
+ export { run };