@wallbreakerno4/opencode-commandcode 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.
Files changed (76) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +54 -0
  3. package/dist/disguise/backoff.d.ts +33 -0
  4. package/dist/disguise/backoff.js +43 -0
  5. package/dist/disguise/config-block.d.ts +71 -0
  6. package/dist/disguise/config-block.js +166 -0
  7. package/dist/disguise/config-freeze.d.ts +27 -0
  8. package/dist/disguise/config-freeze.js +87 -0
  9. package/dist/disguise/fingerprint.d.ts +44 -0
  10. package/dist/disguise/fingerprint.js +127 -0
  11. package/dist/disguise/hash.d.ts +8 -0
  12. package/dist/disguise/hash.js +13 -0
  13. package/dist/disguise/headers.d.ts +25 -0
  14. package/dist/disguise/headers.js +25 -0
  15. package/dist/disguise/logger.d.ts +14 -0
  16. package/dist/disguise/logger.js +18 -0
  17. package/dist/disguise/preflight.d.ts +74 -0
  18. package/dist/disguise/preflight.js +139 -0
  19. package/dist/disguise/redact.d.ts +12 -0
  20. package/dist/disguise/redact.js +17 -0
  21. package/dist/disguise/session.d.ts +11 -0
  22. package/dist/disguise/session.js +24 -0
  23. package/dist/disguise/slug.d.ts +12 -0
  24. package/dist/disguise/slug.js +47 -0
  25. package/dist/disguise/state.d.ts +62 -0
  26. package/dist/disguise/state.js +157 -0
  27. package/dist/disguise/traceparent.d.ts +7 -0
  28. package/dist/disguise/traceparent.js +10 -0
  29. package/dist/disguise/version-cache.d.ts +27 -0
  30. package/dist/disguise/version-cache.js +69 -0
  31. package/dist/disguise/version-runtime.d.ts +42 -0
  32. package/dist/disguise/version-runtime.js +137 -0
  33. package/dist/disguise/version.d.ts +21 -0
  34. package/dist/disguise/version.js +17 -0
  35. package/dist/host/constants.d.ts +14 -0
  36. package/dist/host/constants.js +14 -0
  37. package/dist/host/v1.d.ts +85 -0
  38. package/dist/host/v1.js +118 -0
  39. package/dist/host/v2.d.ts +33 -0
  40. package/dist/host/v2.js +110 -0
  41. package/dist/index.d.ts +47 -0
  42. package/dist/index.js +33 -0
  43. package/dist/models/api.d.ts +23 -0
  44. package/dist/models/api.js +44 -0
  45. package/dist/models/artifact.d.ts +45 -0
  46. package/dist/models/artifact.js +108 -0
  47. package/dist/models/cascade.d.ts +47 -0
  48. package/dist/models/cascade.js +40 -0
  49. package/dist/models/mapping.d.ts +66 -0
  50. package/dist/models/mapping.js +47 -0
  51. package/dist/models/pipeline.d.ts +89 -0
  52. package/dist/models/pipeline.js +331 -0
  53. package/dist/models/signature.d.ts +20 -0
  54. package/dist/models/signature.js +29 -0
  55. package/dist/models/snapshot.d.ts +15 -0
  56. package/dist/models/snapshot.js +16 -0
  57. package/dist/models/snapshot.json +535 -0
  58. package/dist/models/urls.d.ts +39 -0
  59. package/dist/models/urls.js +96 -0
  60. package/dist/protocol/envelope.d.ts +118 -0
  61. package/dist/protocol/envelope.js +331 -0
  62. package/dist/protocol/errors.d.ts +97 -0
  63. package/dist/protocol/errors.js +281 -0
  64. package/dist/protocol/generate.d.ts +53 -0
  65. package/dist/protocol/generate.js +173 -0
  66. package/dist/protocol/images.d.ts +45 -0
  67. package/dist/protocol/images.js +115 -0
  68. package/dist/protocol/json.d.ts +11 -0
  69. package/dist/protocol/json.js +8 -0
  70. package/dist/protocol/ndjson.d.ts +41 -0
  71. package/dist/protocol/ndjson.js +321 -0
  72. package/dist/protocol/watchdog.d.ts +37 -0
  73. package/dist/protocol/watchdog.js +61 -0
  74. package/dist/provider/model.d.ts +95 -0
  75. package/dist/provider/model.js +323 -0
  76. package/package.json +46 -0
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MAXeaglet
4
+ Copyright (c) 2026 WallBreakerNO4
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # @wallbreakerno4/opencode-commandcode
2
+
3
+ OpenCode provider 插件:把 Command Code Go plan 专用的 `/alpha/generate` 网关桥接成 OpenCode 原生 provider。安装插件即完成全部配置——provider(`commandcode-go`)、模型清单与认证方式由插件自举,无需手写任何 provider 定义。
4
+
5
+ ## 安装
6
+
7
+ > 前置:Command Code Go plan 订阅,并在 [Studio → API Keys](https://commandcode.ai/docs/studio) 生成 API key(与 CLI 同一把)。
8
+
9
+ ### OpenCode v2(beta)
10
+
11
+ ```bash
12
+ opencode2 plugin add @wallbreakerno4/opencode-commandcode
13
+ ```
14
+
15
+ 再在 `opencode.json` 合入一行 provider 空壳(beta 限制,需手动添加):
16
+
17
+ ```json
18
+ {
19
+ "providers": {
20
+ "commandcode-go": {}
21
+ }
22
+ }
23
+ ```
24
+
25
+ 启动 `opencode2` 后输入 `/connect`,选择 Command Code (Go) 粘贴 key 登录;
26
+ 或直接用环境变量(与 `/connect` 二选一):
27
+
28
+ ```bash
29
+ export COMMANDCODE_API_KEY=user_xxx
30
+ ```
31
+
32
+ > v2 处于 beta,命令名以当前快照为准;正式版更名后以官方文档为准。
33
+
34
+ ### OpenCode v1
35
+
36
+ `~/.config/opencode/opencode.json`(已有配置则合并)加一行:
37
+
38
+ ```json
39
+ {
40
+ "plugin": ["@wallbreakerno4/opencode-commandcode"]
41
+ }
42
+ ```
43
+
44
+ 启动 `opencode` 后输入 `/connect`,选择 Command Code (Go) 粘贴 key 登录;
45
+ 或直接用环境变量 `COMMANDCODE_API_KEY`(同上)。
46
+
47
+ ### 验证安装
48
+
49
+ ```bash
50
+ opencode2 run --model commandcode-go/deepseek/deepseek-v4-pro "hi" # v2
51
+ opencode run --model commandcode-go/deepseek/deepseek-v4-pro "hi" # v1
52
+ ```
53
+
54
+ 注意:`opencode2 models` 不加载插件,不能用来判断安装是否成功。
@@ -0,0 +1,33 @@
1
+ /**
2
+ * 预请求退避状态机(disguise.md §5 + §11 参数速查)。指纹记录与 lifecycle 两个
3
+ * 预请求合记一个结局(照抄 MAXeaglet 的耦合结构);任何结局都不影响主请求,
4
+ * 本模块只推进「下次允许尝试的时刻」。
5
+ *
6
+ * 结局三态:
7
+ * - success:`nextInitAt = now + 8h + rand×2h`(长驻补发节奏),`failCount = 0`
8
+ * - transient(网络错误 / 超时 / 5xx 任一):`now + min(60s × 2^failCount, 15min)`,`failCount++`
9
+ * - rejected(4xx 含 401/403 任一):长冷却 `now + 8h + rand×2h`——服务器明确说
10
+ * 「不」之后继续敲门更像机器人,安静是上策
11
+ *
12
+ * 时间直接取 `Date.now()`(testing.md:生产代码不加时钟注入参数,测试走 fake timers)。
13
+ */
14
+ /** 成功 / 4xx 长冷却的基础间隔:8h(ground truth 未获证实,保守保留,#9) */
15
+ export declare const SUCCESS_INTERVAL_MS: number;
16
+ /** 成功 / 长冷却的随机抖动上限:`floor(random × 2h)` 均匀分布 */
17
+ export declare const JITTER_MS: number;
18
+ /** 瞬时失败退避基数:60s × 2^failCount */
19
+ export declare const TRANSIENT_BASE_MS: number;
20
+ /** 瞬时失败退避封顶:15min */
21
+ export declare const TRANSIENT_CAP_MS: number;
22
+ /** 预请求单元(两请求合记)的结局分类(§5 表) */
23
+ export type PreflightOutcome = "success" | "transient" | "rejected";
24
+ export interface PreflightState {
25
+ /** 下次允许预请求的时刻(epoch ms);0 = 立即可试(首见 key / 重启归零)。
26
+ * 成功结局下即规格 §5 的 `nextInitAt`(长驻补发节奏),其余结局是重试时刻 */
27
+ nextAttemptAt: number;
28
+ /** 瞬时失败连续计数,驱动指数退避档位;成功清零 */
29
+ failCount: number;
30
+ }
31
+ export declare function initialPreflightState(): PreflightState;
32
+ /** 按结局推进状态;返回新状态对象,不改入参 */
33
+ export declare function applyPreflightOutcome(state: PreflightState, outcome: PreflightOutcome): PreflightState;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * 预请求退避状态机(disguise.md §5 + §11 参数速查)。指纹记录与 lifecycle 两个
3
+ * 预请求合记一个结局(照抄 MAXeaglet 的耦合结构);任何结局都不影响主请求,
4
+ * 本模块只推进「下次允许尝试的时刻」。
5
+ *
6
+ * 结局三态:
7
+ * - success:`nextInitAt = now + 8h + rand×2h`(长驻补发节奏),`failCount = 0`
8
+ * - transient(网络错误 / 超时 / 5xx 任一):`now + min(60s × 2^failCount, 15min)`,`failCount++`
9
+ * - rejected(4xx 含 401/403 任一):长冷却 `now + 8h + rand×2h`——服务器明确说
10
+ * 「不」之后继续敲门更像机器人,安静是上策
11
+ *
12
+ * 时间直接取 `Date.now()`(testing.md:生产代码不加时钟注入参数,测试走 fake timers)。
13
+ */
14
+ /** 成功 / 4xx 长冷却的基础间隔:8h(ground truth 未获证实,保守保留,#9) */
15
+ export const SUCCESS_INTERVAL_MS = 8 * 60 * 60 * 1000;
16
+ /** 成功 / 长冷却的随机抖动上限:`floor(random × 2h)` 均匀分布 */
17
+ export const JITTER_MS = 2 * 60 * 60 * 1000;
18
+ /** 瞬时失败退避基数:60s × 2^failCount */
19
+ export const TRANSIENT_BASE_MS = 60 * 1000;
20
+ /** 瞬时失败退避封顶:15min */
21
+ export const TRANSIENT_CAP_MS = 15 * 60 * 1000;
22
+ export function initialPreflightState() {
23
+ return { nextAttemptAt: 0, failCount: 0 };
24
+ }
25
+ /** 8h + floor(random × 2h) 均匀抖动(成功与 4xx 长冷却共用同一节奏) */
26
+ function jitteredInterval() {
27
+ return SUCCESS_INTERVAL_MS + Math.floor(Math.random() * JITTER_MS);
28
+ }
29
+ /** 按结局推进状态;返回新状态对象,不改入参 */
30
+ export function applyPreflightOutcome(state, outcome) {
31
+ const now = Date.now();
32
+ if (outcome === "transient") {
33
+ // 指数按旧计数取档:第 1 次失败 60s、第 2 次 120s……第 5 次起封顶 15min
34
+ const delay = Math.min(TRANSIENT_BASE_MS * 2 ** state.failCount, TRANSIENT_CAP_MS);
35
+ return { nextAttemptAt: now + delay, failCount: state.failCount + 1 };
36
+ }
37
+ if (outcome === "rejected") {
38
+ // 4xx 不动 failCount:规格只在成功时指明清零;长冷却本身远超任何退避档位,
39
+ // 计数去留的可观察差异仅在「4xx 冷却后紧跟瞬时失败」一档,保持最小语义
40
+ return { nextAttemptAt: now + jitteredInterval(), failCount: state.failCount };
41
+ }
42
+ return { nextAttemptAt: now + jitteredInterval(), failCount: 0 };
43
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * generate 信封 `config` 块九字段采集(disguise.md §9,#19 定稿):逐字段照抄官方
3
+ * CLI 实现(`command-code` 1.38.2 `buildServerConfig`,调研 `docs/research/cli-config-collection.md`
4
+ * 字节偏移级还原)。
5
+ *
6
+ * 官方语义照抄:structure 先于 git 判定(非 git 目录也有内容);git 查询顺序 await、
7
+ * 无并发;shell 输出 try/catch 全包,非零退出码 / 异常 → `""`,逐字段独立降级、
8
+ * 绝不抛错;非 git 仓库九字段**齐全**显式空值、不省略字段;`date` 恒 UTC;
9
+ * `mainBranch` 三级远程 HEAD 推断(从不直接取当前分支);`gitStatus` 空 / 失败 →
10
+ * `"Working tree clean"`(失败与干净工作区在 wire 上不可区分,照抄接受)。
11
+ *
12
+ * 两处防御偏离(§9.3,均不改请求线上形状):每条 git 查询加 2s 超时(官方无超时,
13
+ * 插件长驻进程);进程级冻结 + 24h 过期重采归状态层(#33),本模块只提供单次采集。
14
+ *
15
+ * runtime 接缝对应官方 `createNodeRuntime()` 的形状(cwd / platform / workspaceRoots /
16
+ * fs.readdir / shell.run),`homeDir()` 为本接口补充——官方 formatScopeDir 的 home 来源
17
+ * 在其 runtime 内部,此处显式化以便 scope 标签可测;测试注入 fake(testing.md §3 允许
18
+ * 的既有接缝注入)。
19
+ */
20
+ /** 每条 git 查询的防御超时(§11 参数速查:2s/条,到点按失败处理) */
21
+ export declare const GIT_QUERY_TIMEOUT_MS = 2000;
22
+ /** 信封 `date` 字段取值(§9.1):恒 UTC 的 `YYYY-MM-DD`(官方 `toISOString()` 即 UTC,非本地时区) */
23
+ export declare function currentUtcDate(): string;
24
+ /** 信封顶层 `permissionMode`:恒 `"standard"`(#9 抓包 ground truth,与 OpenCode 自身权限模式无关) */
25
+ export declare const PERMISSION_MODE = "standard";
26
+ /** 官方 structure 黑名单(调研偏移 1629355);点开头项与「滤点开头」规则重叠属官方冗余保险,照抄 */
27
+ export declare const STRUCTURE_BLACKLIST: ReadonlySet<string>;
28
+ /** 官方 runtime 抽象(buildServerConfig 的注入面);生产实现见 createNodeConfigRuntime */
29
+ export interface ConfigRuntime {
30
+ cwd(): string;
31
+ platform(): string;
32
+ /** scope 标签的 home 相对化需要;对应官方 formatScopeDir 的 homeDir 来源 */
33
+ homeDir(): string;
34
+ workspaceRoots(): string[];
35
+ readdir(path: string): Promise<string[]>;
36
+ shell: {
37
+ /** 执行命令 resolve stdout;非零退出码 / 进程起不来 / signal abort 一律 reject */
38
+ run(command: string, signal: AbortSignal): Promise<string>;
39
+ };
40
+ }
41
+ /** config 块九字段;键序即官方 buildServerConfig 的 return 序(wire 形状) */
42
+ export interface ConfigBlock {
43
+ workingDir: string;
44
+ date: string;
45
+ environment: string;
46
+ structure: string[];
47
+ isGitRepo: boolean;
48
+ currentBranch: string;
49
+ mainBranch: string;
50
+ gitStatus: string;
51
+ recentCommits: string[];
52
+ }
53
+ /**
54
+ * scope 标签的目录格式化(官方 formatScopeDir 语义):目录即 cwd → `"."`;在 cwd 下 →
55
+ * `"./xxx"`;即 home → `"~"`;在 home 下 → `"~/xxx"`;否则原样绝对路径。
56
+ */
57
+ export declare function formatScopeDir(dir: string, cwd: string, homeDir: string): string;
58
+ /**
59
+ * structure 采集(官方 readStructure 逐字):cwd 单层 readdir、无递归,滤点开头条目 +
60
+ * 黑名单,默认字典序(`.sort()`,大写在小写前),无数量上限;额外工作区目录(非 cwd)
61
+ * 格式化为 `scope:<路径>` 追加在数组末尾、不参与排序;readdir 抛异常 → 仅含 scope 标签。
62
+ */
63
+ export declare function collectStructure(runtime: ConfigRuntime): Promise<string[]>;
64
+ /** 单次采集 config 块九字段;任何子查询失败独立降级、整体绝不抛错(官方语义) */
65
+ export declare function collectConfigBlock(runtime: ConfigRuntime): Promise<ConfigBlock>;
66
+ /** 生产 runtime:官方 createNodeRuntime 同源取值(cwd = process.cwd() 等)。
67
+ * shell 用 exec 走 shell 解释(对齐官方 spawn {shell: true, windowsHide};exec 无
68
+ * detached 选项,该参数对一次性短命令无观察差异);stdout 无上限收集(官方无截断)。
69
+ * OpenCode 单工作区场景 workspaceRoots 仅 cwd(scope 标签自然为空);宿主侧若暴露
70
+ * 多根工作区由 glue 层追加。 */
71
+ export declare function createNodeConfigRuntime(): ConfigRuntime;
@@ -0,0 +1,166 @@
1
+ /**
2
+ * generate 信封 `config` 块九字段采集(disguise.md §9,#19 定稿):逐字段照抄官方
3
+ * CLI 实现(`command-code` 1.38.2 `buildServerConfig`,调研 `docs/research/cli-config-collection.md`
4
+ * 字节偏移级还原)。
5
+ *
6
+ * 官方语义照抄:structure 先于 git 判定(非 git 目录也有内容);git 查询顺序 await、
7
+ * 无并发;shell 输出 try/catch 全包,非零退出码 / 异常 → `""`,逐字段独立降级、
8
+ * 绝不抛错;非 git 仓库九字段**齐全**显式空值、不省略字段;`date` 恒 UTC;
9
+ * `mainBranch` 三级远程 HEAD 推断(从不直接取当前分支);`gitStatus` 空 / 失败 →
10
+ * `"Working tree clean"`(失败与干净工作区在 wire 上不可区分,照抄接受)。
11
+ *
12
+ * 两处防御偏离(§9.3,均不改请求线上形状):每条 git 查询加 2s 超时(官方无超时,
13
+ * 插件长驻进程);进程级冻结 + 24h 过期重采归状态层(#33),本模块只提供单次采集。
14
+ *
15
+ * runtime 接缝对应官方 `createNodeRuntime()` 的形状(cwd / platform / workspaceRoots /
16
+ * fs.readdir / shell.run),`homeDir()` 为本接口补充——官方 formatScopeDir 的 home 来源
17
+ * 在其 runtime 内部,此处显式化以便 scope 标签可测;测试注入 fake(testing.md §3 允许
18
+ * 的既有接缝注入)。
19
+ */
20
+ import { exec } from "node:child_process";
21
+ import { readdir as fsReaddir } from "node:fs/promises";
22
+ import { homedir } from "node:os";
23
+ import { promisify } from "node:util";
24
+ const execAsync = promisify(exec);
25
+ /** 每条 git 查询的防御超时(§11 参数速查:2s/条,到点按失败处理) */
26
+ export const GIT_QUERY_TIMEOUT_MS = 2000;
27
+ /** 信封 `date` 字段取值(§9.1):恒 UTC 的 `YYYY-MM-DD`(官方 `toISOString()` 即 UTC,非本地时区) */
28
+ export function currentUtcDate() {
29
+ return new Date().toISOString().split("T")[0] ?? "";
30
+ }
31
+ /** 信封顶层 `permissionMode`:恒 `"standard"`(#9 抓包 ground truth,与 OpenCode 自身权限模式无关) */
32
+ export const PERMISSION_MODE = "standard";
33
+ /** 官方 structure 黑名单(调研偏移 1629355);点开头项与「滤点开头」规则重叠属官方冗余保险,照抄 */
34
+ export const STRUCTURE_BLACKLIST = new Set([
35
+ "node_modules",
36
+ "dist",
37
+ "build",
38
+ ".git",
39
+ ".svn",
40
+ ".hg",
41
+ "coverage",
42
+ ".nyc_output",
43
+ ".cache",
44
+ "tmp",
45
+ "temp",
46
+ ".next",
47
+ ".nuxt",
48
+ "out",
49
+ ]);
50
+ /** 官方 shellOutput 等价物:成功 → stdout.trim();失败(reject)→ ""。超时经 signal 到点 reject,
51
+ * 与其他失败同形落入 ""。 */
52
+ async function shellOutput(runtime, command) {
53
+ try {
54
+ const stdout = await runtime.shell.run(command, AbortSignal.timeout(GIT_QUERY_TIMEOUT_MS));
55
+ return stdout.trim();
56
+ }
57
+ catch {
58
+ return "";
59
+ }
60
+ }
61
+ /** mainBranch 三级推断(调研 resolveMainBranch 逐字):symbolic-ref → branch -r → 兜底 main */
62
+ async function resolveMainBranch(runtime) {
63
+ const symbolicRef = await shellOutput(runtime, "git symbolic-ref --short refs/remotes/origin/HEAD");
64
+ if (symbolicRef)
65
+ return symbolicRef.replace(/^origin\//, "");
66
+ const remoteBranches = await shellOutput(runtime, "git branch -r");
67
+ return remoteBranches.includes("origin/main") ? "main" : remoteBranches.includes("origin/master") ? "master" : "main";
68
+ }
69
+ /**
70
+ * scope 标签的目录格式化(官方 formatScopeDir 语义):目录即 cwd → `"."`;在 cwd 下 →
71
+ * `"./xxx"`;即 home → `"~"`;在 home 下 → `"~/xxx"`;否则原样绝对路径。
72
+ */
73
+ export function formatScopeDir(dir, cwd, homeDir) {
74
+ const prefixOf = (base) => (base.endsWith("/") ? base : `${base}/`);
75
+ if (dir === cwd)
76
+ return ".";
77
+ if (dir.startsWith(prefixOf(cwd)))
78
+ return `./${dir.slice(prefixOf(cwd).length)}`;
79
+ if (dir === homeDir)
80
+ return "~";
81
+ if (dir.startsWith(prefixOf(homeDir)))
82
+ return `~/${dir.slice(prefixOf(homeDir).length)}`;
83
+ return dir;
84
+ }
85
+ /**
86
+ * structure 采集(官方 readStructure 逐字):cwd 单层 readdir、无递归,滤点开头条目 +
87
+ * 黑名单,默认字典序(`.sort()`,大写在小写前),无数量上限;额外工作区目录(非 cwd)
88
+ * 格式化为 `scope:<路径>` 追加在数组末尾、不参与排序;readdir 抛异常 → 仅含 scope 标签。
89
+ */
90
+ export async function collectStructure(runtime) {
91
+ const cwd = runtime.cwd();
92
+ const homeDir = runtime.homeDir();
93
+ const scopeLabels = runtime
94
+ .workspaceRoots()
95
+ .filter((root) => root !== cwd)
96
+ .map((root) => `scope:${formatScopeDir(root, cwd, homeDir)}`);
97
+ try {
98
+ const entries = await runtime.readdir(cwd);
99
+ return [
100
+ ...entries.filter((name) => !name.startsWith(".") && !STRUCTURE_BLACKLIST.has(name)).sort(),
101
+ ...scopeLabels,
102
+ ];
103
+ }
104
+ catch {
105
+ return scopeLabels;
106
+ }
107
+ }
108
+ /** 单次采集 config 块九字段;任何子查询失败独立降级、整体绝不抛错(官方语义) */
109
+ export async function collectConfigBlock(runtime) {
110
+ // 官方顺序:structure 最先(先于 git 判定),date/environment/workingDir 同步取
111
+ const structure = await collectStructure(runtime);
112
+ const date = currentUtcDate();
113
+ const environment = runtime.platform();
114
+ const workingDir = runtime.cwd();
115
+ const gitDir = await shellOutput(runtime, "git rev-parse --git-dir");
116
+ if (!gitDir) {
117
+ // 非 git 仓库 / git 未装同形:九字段齐全的显式空值,不省略字段(源码调研纠偏 #19)
118
+ return {
119
+ workingDir,
120
+ date,
121
+ environment,
122
+ structure,
123
+ isGitRepo: false,
124
+ currentBranch: "",
125
+ mainBranch: "",
126
+ gitStatus: "",
127
+ recentCommits: [],
128
+ };
129
+ }
130
+ const currentBranch = await shellOutput(runtime, "git branch --show-current");
131
+ const mainBranch = await resolveMainBranch(runtime);
132
+ const gitStatus = (await shellOutput(runtime, "git status --porcelain")) || "Working tree clean";
133
+ const logOutput = await shellOutput(runtime, "git log --oneline -3");
134
+ return {
135
+ workingDir,
136
+ date,
137
+ environment,
138
+ structure,
139
+ isGitRepo: true,
140
+ currentBranch,
141
+ mainBranch,
142
+ gitStatus,
143
+ recentCommits: logOutput ? logOutput.split("\n") : [],
144
+ };
145
+ }
146
+ /** 生产 runtime:官方 createNodeRuntime 同源取值(cwd = process.cwd() 等)。
147
+ * shell 用 exec 走 shell 解释(对齐官方 spawn {shell: true, windowsHide};exec 无
148
+ * detached 选项,该参数对一次性短命令无观察差异);stdout 无上限收集(官方无截断)。
149
+ * OpenCode 单工作区场景 workspaceRoots 仅 cwd(scope 标签自然为空);宿主侧若暴露
150
+ * 多根工作区由 glue 层追加。 */
151
+ export function createNodeConfigRuntime() {
152
+ return {
153
+ cwd: () => process.cwd(),
154
+ platform: () => process.platform,
155
+ homeDir: () => homedir(),
156
+ workspaceRoots: () => [process.cwd()],
157
+ readdir: (path) => fsReaddir(path),
158
+ shell: {
159
+ run: (command, signal) => execAsync(command, {
160
+ signal,
161
+ windowsHide: true,
162
+ maxBuffer: Infinity,
163
+ }).then(({ stdout }) => stdout),
164
+ },
165
+ };
166
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * config 块的进程级冻结与过期重采(disguise.md §9.3 偏离 b):
3
+ *
4
+ * - 官方语义照抄:进程级只构建一次,首次需要时构建并冻结复用,失败结果同样冻结;
5
+ * - 偏离 b:插件长驻可跨日,冻结值距构建超 24h 后由下一次取值触发**后台**重采——
6
+ * 期间沿用旧值、主请求不等待(与 §6 版本头惰性刷新同节奏,24h、无抖动);
7
+ * - 防御超时(偏离 a:git 查询 2s/条)在 config-block.ts 采集层实现,本模块不管。
8
+ *
9
+ * 采集经 ConfigRuntime 既有接缝注入(testing.md §3 认可),生产用
10
+ * createNodeConfigRuntime();采集层承诺整体绝不抛错,此处防御性 catch 仅覆盖
11
+ * 意外(采集器自身 bug),兜一个「非 git 九字段齐全」的显式空值形状让主请求照常发。
12
+ */
13
+ import { type DisguiseLogger } from "./logger.js";
14
+ import { type ConfigBlock, type ConfigRuntime } from "./config-block.js";
15
+ /** 冻结值过期重采间隔(§11 参数速查:24h,与版本头惰性刷新同节奏) */
16
+ export declare const CONFIG_REBUILD_INTERVAL_MS: number;
17
+ export interface FrozenConfigProvider {
18
+ /**
19
+ * 取冻结 config 块:首次调用构建并等待(官方「首次需要时构建」同语义);此后
20
+ * 同进程恒返回同一冻结对象;24h 过期后返回旧值并后台重采(主请求零等待)。
21
+ */
22
+ get(): Promise<ConfigBlock>;
23
+ }
24
+ export declare function createFrozenConfigProvider(options?: {
25
+ runtime?: ConfigRuntime;
26
+ logger?: DisguiseLogger;
27
+ }): FrozenConfigProvider;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * config 块的进程级冻结与过期重采(disguise.md §9.3 偏离 b):
3
+ *
4
+ * - 官方语义照抄:进程级只构建一次,首次需要时构建并冻结复用,失败结果同样冻结;
5
+ * - 偏离 b:插件长驻可跨日,冻结值距构建超 24h 后由下一次取值触发**后台**重采——
6
+ * 期间沿用旧值、主请求不等待(与 §6 版本头惰性刷新同节奏,24h、无抖动);
7
+ * - 防御超时(偏离 a:git 查询 2s/条)在 config-block.ts 采集层实现,本模块不管。
8
+ *
9
+ * 采集经 ConfigRuntime 既有接缝注入(testing.md §3 认可),生产用
10
+ * createNodeConfigRuntime();采集层承诺整体绝不抛错,此处防御性 catch 仅覆盖
11
+ * 意外(采集器自身 bug),兜一个「非 git 九字段齐全」的显式空值形状让主请求照常发。
12
+ */
13
+ import { consoleLogger } from "./logger.js";
14
+ import { collectConfigBlock, createNodeConfigRuntime, currentUtcDate, } from "./config-block.js";
15
+ /** 冻结值过期重采间隔(§11 参数速查:24h,与版本头惰性刷新同节奏) */
16
+ export const CONFIG_REBUILD_INTERVAL_MS = 24 * 60 * 60 * 1000;
17
+ /** 采集意外失败的兜底形状:与非 git 仓库九字段齐全的显式空值同形(§9.1)。
18
+ * cwd/platform 也包防御——工作目录被删时 process.cwd() 本身会 throw,兜底必须
19
+ * 永远可构造,否则主请求拿不到任何 config 块。 */
20
+ function emptyConfigBlock(runtime) {
21
+ const safe = (fn) => {
22
+ try {
23
+ return fn();
24
+ }
25
+ catch {
26
+ return "";
27
+ }
28
+ };
29
+ return {
30
+ workingDir: safe(() => runtime.cwd()),
31
+ date: currentUtcDate(),
32
+ environment: safe(() => runtime.platform()),
33
+ structure: [],
34
+ isGitRepo: false,
35
+ currentBranch: "",
36
+ mainBranch: "",
37
+ gitStatus: "",
38
+ recentCommits: [],
39
+ };
40
+ }
41
+ function deepFreeze(block) {
42
+ Object.freeze(block.structure);
43
+ Object.freeze(block.recentCommits);
44
+ return Object.freeze(block);
45
+ }
46
+ export function createFrozenConfigProvider(options = {}) {
47
+ const runtime = options.runtime ?? createNodeConfigRuntime();
48
+ const logger = options.logger ?? consoleLogger();
49
+ let frozen;
50
+ let builtAt = 0;
51
+ let rebuilding;
52
+ /** 单轮采集:collectConfigBlock 承诺整体绝不抛错;真抛(采集器自身缺陷)视为本轮采集失败 */
53
+ function build() {
54
+ return collectConfigBlock(runtime);
55
+ }
56
+ return {
57
+ async get() {
58
+ if (frozen === undefined) {
59
+ try {
60
+ frozen = deepFreeze(await build());
61
+ }
62
+ catch (error) {
63
+ // 首次构建意外失败:没有旧值可沿用,兜显式空值形状(主请求照常发)
64
+ logger.warn(`config 块首次构建意外失败,兜空值形状:${error instanceof Error ? error.message : String(error)}`);
65
+ frozen = deepFreeze(emptyConfigBlock(runtime));
66
+ }
67
+ builtAt = Date.now();
68
+ return frozen;
69
+ }
70
+ if (Date.now() - builtAt >= CONFIG_REBUILD_INTERVAL_MS && rebuilding === undefined) {
71
+ rebuilding = build()
72
+ .then((block) => {
73
+ frozen = deepFreeze(block);
74
+ builtAt = Date.now();
75
+ })
76
+ .catch((error) => {
77
+ // 重采意外失败沿用旧值(冻结语义),只记日志——旧数据好过空数据
78
+ logger.warn(`config 块重采失败,沿用旧值:${error instanceof Error ? error.message : String(error)}`);
79
+ })
80
+ .finally(() => {
81
+ rebuilding = undefined;
82
+ });
83
+ }
84
+ return frozen;
85
+ },
86
+ };
87
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * 设备指纹(disguise.md §8,#9 校准后如实上报真机真值)。
3
+ *
4
+ * - 15 字段 components 结构、thumbmark 联合哈希的拼接序与「五字段不入哈希」结构
5
+ * 照抄调研 §1.2–§1.3(服务端无从校验原像,结构同构无成本);
6
+ * - 输入换成真机真值:platform / 内核版本 / CPU / 内存 / 时区 / 网卡——真机真值
7
+ * 天然确定,指纹跨进程稳定随之免费获得(MAXeaglet 的 win32 随机池假人格弃用);
8
+ * - 采集器直接读系统 API(testing.md §1.5:不单测、不抽象注入接口,正确性归
9
+ * 真机人工验收);哈希纯函数照测(注入固定输入)。
10
+ */
11
+ /** components 15 字段(调研 §1.4);键序即 wire 序 */
12
+ export interface FingerprintComponents {
13
+ machineIdHash: string;
14
+ macHashes: string[];
15
+ osUserHash: string;
16
+ hostnameHash: string;
17
+ gitEmailHash: string;
18
+ platform: string;
19
+ arch: string;
20
+ osRelease: string;
21
+ cpuModel: string;
22
+ cpuCount: number;
23
+ memGiB: number;
24
+ isContainer: boolean;
25
+ timezone: string;
26
+ runtime: string;
27
+ collectorVersion: number;
28
+ }
29
+ /** `/alpha/fingerprint/record` 的 body 形状 */
30
+ export interface FingerprintBody {
31
+ thumbmark: string;
32
+ components: FingerprintComponents;
33
+ }
34
+ /**
35
+ * thumbmark 联合哈希:参与字段为 machineIdHash → macHashes(按序全部)→ osUserHash
36
+ * → hostnameHash → gitEmailHash → platform → osRelease → cpuModel → cpuCount →
37
+ * memGiB,分隔符 `|`;arch / timezone / isContainer / runtime / collectorVersion
38
+ * 五字段不入哈希(调研 §1.3,原样保留该实现取舍)。
39
+ */
40
+ export declare function computeThumbmark(components: FingerprintComponents): string;
41
+ /** 组装 record body:thumbmark 重算,components 原样搬运 */
42
+ export declare function buildFingerprintBody(components: FingerprintComponents): FingerprintBody;
43
+ /** 采集真机真值并计算各字段哈希与 thumbmark;进程启动时调用一次(disguise.md §1) */
44
+ export declare function collectFingerprintComponents(): Promise<FingerprintComponents>;