@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
@@ -0,0 +1,157 @@
1
+ /**
2
+ * 伪装模块的状态与调度门面(disguise.md §1/§3/§5,#33):
3
+ *
4
+ * - **per-key 内存 KeyState**(`Map<apiKey, KeyState>`,key 为 Map 键原文、绝不
5
+ * 落盘绝不进日志——日志一律经 redactApiKey 短哈希):指纹、回退会话、预请求
6
+ * 调度;全局内存态(ccVersion、config 冻结)收在对应子模块;
7
+ * - **指纹进程内稳定(D1)**:KeyState 只增不删,会话轮换只换 session 不删指纹,
8
+ * 进程重启 = Map 清空、首见 key 重新采集并立即重报(MAXeaglet 同款重启语义);
9
+ * - **会话身份主路径(D3)**:doStream 调用 headers 中 `X-Session-Id` /
10
+ * `x-session-affinity`(v2 另有同值 `x-opencode-session`)任一存在即确定性派生
11
+ * `x-session-id`(session.ts 纯函数)——无需存储即保证同会话同 id、跨重启同 id;
12
+ * 多头同场值不一致 warn(「值漂移打日志」的落地;跨请求漂移无法与正常的新会话
13
+ * 区分,不告警防噪音)。全部缺失才落回退路径:per-key 随机 uuid v4、12h +
14
+ * 0~1h 抖动惰性轮换;
15
+ * - **预请求调度(D2/D5)**:startPreflight 对齐真实 CLI「每进程启动发一次」
16
+ * (幂等守卫);主请求前 maybeRunPreflight 做惰性补发/退避重试检查,到点后台
17
+ * 发射,主请求永不等待;
18
+ * - 全部出网与采集依赖经构造 options 注入(工厂 options.fetch、注入式 logger、
19
+ * ConfigRuntime、兜底链 getter),测试零后门。
20
+ */
21
+ import { randomUUID } from "node:crypto";
22
+ import { initialPreflightState } from "./backoff.js";
23
+ import { createFrozenConfigProvider } from "./config-freeze.js";
24
+ import { buildFingerprintBody, collectFingerprintComponents } from "./fingerprint.js";
25
+ import { consoleLogger } from "./logger.js";
26
+ import { runPreflight } from "./preflight.js";
27
+ import { deriveSessionId } from "./session.js";
28
+ import { redactIdentifier } from "./redact.js";
29
+ import { createVersionResolver } from "./version-runtime.js";
30
+ import { generateTraceparent } from "./traceparent.js";
31
+ /** 会话头查找优先级(session-visibility.md §6:v1 两键 + v2 同值第三键) */
32
+ const SESSION_HEADERS = ["X-Session-Id", "x-session-affinity", "x-opencode-session"];
33
+ /** 回退路径会话寿命:12h + 0~1h 抖动(§11 参数速查,惰性轮换) */
34
+ export const SESSION_TTL_BASE_MS = 12 * 60 * 60 * 1000;
35
+ export const SESSION_TTL_JITTER_MS = 60 * 60 * 1000;
36
+ /** 大小写不敏感收集会话头;返回按优先级的种子与去重后的命中值(值漂移告警用) */
37
+ function collectSessionSeeds(headers) {
38
+ const lower = new Map();
39
+ if (headers !== undefined) {
40
+ for (const [key, value] of Object.entries(headers)) {
41
+ if (typeof value === "string" && value.length > 0)
42
+ lower.set(key.toLowerCase(), value);
43
+ }
44
+ }
45
+ const values = [];
46
+ for (const name of SESSION_HEADERS) {
47
+ const value = lower.get(name.toLowerCase());
48
+ if (value !== undefined && !values.includes(value))
49
+ values.push(value);
50
+ }
51
+ return { seed: values[0], values };
52
+ }
53
+ export function createDisguiseState(options = {}) {
54
+ const logger = options.logger ?? consoleLogger();
55
+ const fetchImpl = options.fetch ?? globalThis.fetch;
56
+ const versionResolver = createVersionResolver({
57
+ fetch: fetchImpl,
58
+ logger,
59
+ artifactSourceCliVersion: options.artifactSourceCliVersion,
60
+ snapshotSourceCliVersion: options.snapshotSourceCliVersion,
61
+ });
62
+ const configProvider = createFrozenConfigProvider({
63
+ runtime: options.configRuntime,
64
+ logger,
65
+ });
66
+ const keys = new Map();
67
+ function ensureKeyState(apiKey) {
68
+ let state = keys.get(apiKey);
69
+ if (state === undefined) {
70
+ state = {
71
+ fingerprintBody: undefined,
72
+ session: undefined,
73
+ preflight: initialPreflightState(),
74
+ preflightInFlight: false,
75
+ startupPreflightStarted: false,
76
+ traceContexts: new Map(),
77
+ };
78
+ keys.set(apiKey, state);
79
+ }
80
+ return state;
81
+ }
82
+ function ensureFingerprintBody(state) {
83
+ state.fingerprintBody ??= collectFingerprintComponents()
84
+ .then(buildFingerprintBody)
85
+ .catch((error) => {
86
+ // 采集承诺内部全降级;真 reject 时清槽允许下轮重试,本轮按瞬时失败推进
87
+ state.fingerprintBody = undefined;
88
+ throw error;
89
+ });
90
+ return state.fingerprintBody;
91
+ }
92
+ function launchPreflight(state, apiKey) {
93
+ if (state.preflightInFlight)
94
+ return;
95
+ state.preflightInFlight = true;
96
+ void runPreflight({
97
+ apiKey,
98
+ preflight: state.preflight,
99
+ fetchImpl,
100
+ logger,
101
+ getVersion: () => versionResolver.getVersion(),
102
+ ensureFingerprintBody: () => ensureFingerprintBody(state),
103
+ }).finally(() => {
104
+ state.preflightInFlight = false;
105
+ });
106
+ }
107
+ return {
108
+ startPreflight(apiKey) {
109
+ const state = ensureKeyState(apiKey);
110
+ if (state.startupPreflightStarted)
111
+ return;
112
+ state.startupPreflightStarted = true;
113
+ launchPreflight(state, apiKey);
114
+ },
115
+ maybeRunPreflight(apiKey) {
116
+ const state = ensureKeyState(apiKey);
117
+ // 未到点(8–10h 补发窗口 / 退避与冷却未过)零开销返回;到点后台发射
118
+ if (Date.now() < state.preflight.nextAttemptAt)
119
+ return;
120
+ launchPreflight(state, apiKey);
121
+ },
122
+ resolveSessionId(apiKey, headers) {
123
+ const state = ensureKeyState(apiKey);
124
+ const { seed, values } = collectSessionSeeds(headers);
125
+ if (seed !== undefined) {
126
+ if (values.length > 1) {
127
+ // 主路径命中但多头值不一致(宿主改头名/漂移的异常信号),脱敏告警
128
+ logger.warn(`会话头取值不一致,按 ${SESSION_HEADERS[0]} 优先取用 ${values.map(redactIdentifier).join(" / ")}`);
129
+ }
130
+ return deriveSessionId(seed);
131
+ }
132
+ // 回退路径:无法区分会话边界,per-key 随机 uuid + 12h + 0~1h 抖动惰性轮换(尽力而为)
133
+ const existing = state.session;
134
+ if (existing !== undefined && Date.now() < existing.expiresAt)
135
+ return existing.id;
136
+ const next = {
137
+ id: randomUUID(),
138
+ expiresAt: Date.now() + SESSION_TTL_BASE_MS + Math.floor(Math.random() * SESSION_TTL_JITTER_MS),
139
+ };
140
+ state.session = next;
141
+ return next.id;
142
+ },
143
+ resolveTraceparent(apiKey, sessionId, turnKey) {
144
+ const state = ensureKeyState(apiKey);
145
+ const previous = state.traceContexts.get(sessionId);
146
+ const traceId = previous?.turnKey === turnKey ? previous.traceId : generateTraceparent().slice(3, 35);
147
+ state.traceContexts.set(sessionId, { turnKey, traceId });
148
+ return generateTraceparent(traceId);
149
+ },
150
+ getCommandCodeVersion() {
151
+ return versionResolver.getVersion();
152
+ },
153
+ getConfigBlock() {
154
+ return configProvider.get();
155
+ },
156
+ };
157
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * `traceparent` 头(disguise.md §10 照抄清单,#9 抓包实测吻合):
3
+ * `00-<32hex>-<16hex>-01`——版本恒 `00`(W3C Trace Context 当前版本)、
4
+ * trace-id 16 随机字节、parent-span-id 8 随机字节、flag 恒 `01`。
5
+ * 逐请求新造、跨请求零复用;只进 generate 主请求,不进预请求与 models。
6
+ */
7
+ export declare function generateTraceparent(traceId?: string): string;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `traceparent` 头(disguise.md §10 照抄清单,#9 抓包实测吻合):
3
+ * `00-<32hex>-<16hex>-01`——版本恒 `00`(W3C Trace Context 当前版本)、
4
+ * trace-id 16 随机字节、parent-span-id 8 随机字节、flag 恒 `01`。
5
+ * 逐请求新造、跨请求零复用;只进 generate 主请求,不进预请求与 models。
6
+ */
7
+ import { randomBytes } from "node:crypto";
8
+ export function generateTraceparent(traceId) {
9
+ return `00-${traceId ?? randomBytes(16).toString("hex")}-${randomBytes(8).toString("hex")}-01`;
10
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * 版本缓存落盘(disguise.md §6 / D1):伪装模块**唯一**的磁盘 IO。
3
+ * 路径 `<XDG_CACHE_HOME 或 ~/.cache>/opencode-commandcode/version-cache.json`,
4
+ * 内容 `{version, fetchedAt}`;写经临时文件 + rename 原子替换,读失败与损坏一律
5
+ * 视为无值(ENOENT 是首次运行的正常态,静默;其余异常按「落盘异常」warn)。
6
+ * key 与其他伪装状态与本文件无关——它们只活在内存(D1)。
7
+ */
8
+ import type { DisguiseLogger } from "./logger.js";
9
+ /** version-cache.json 的一行:上次成功拉取的 CLI 版本与拉取时刻(ISO 8601) */
10
+ export interface VersionCacheEntry {
11
+ readonly version: string;
12
+ readonly fetchedAt: string;
13
+ }
14
+ /** 缓存目录解析:XDG_CACHE_HOME 非空优先(含测试重定向,testing.md §3 认可的标准行为),否则 ~/.cache */
15
+ export declare function resolveVersionCacheDir(): string;
16
+ /** 缓存文件完整路径;env 每次现读(不缓存),重定向后同进程即刻生效 */
17
+ export declare function resolveVersionCachePath(): string;
18
+ /**
19
+ * 原子写:先落同目录临时文件再 rename(同目录保证同一文件系统,rename 原子)。
20
+ * 失败向上抛给调用方——缓存写失败不构成伪装失败,调用方降兜底 + warn。
21
+ */
22
+ export declare function writeVersionCache(entry: VersionCacheEntry): Promise<void>;
23
+ /**
24
+ * 读取上次成功拉取值;ENOENT(首次运行)静默返回 undefined,其余异常与损坏形状
25
+ * warn 后返回 undefined。日志经注入式 logger(§7),无 logger 时静默。
26
+ */
27
+ export declare function readVersionCache(logger?: DisguiseLogger): Promise<VersionCacheEntry | undefined>;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * 版本缓存落盘(disguise.md §6 / D1):伪装模块**唯一**的磁盘 IO。
3
+ * 路径 `<XDG_CACHE_HOME 或 ~/.cache>/opencode-commandcode/version-cache.json`,
4
+ * 内容 `{version, fetchedAt}`;写经临时文件 + rename 原子替换,读失败与损坏一律
5
+ * 视为无值(ENOENT 是首次运行的正常态,静默;其余异常按「落盘异常」warn)。
6
+ * key 与其他伪装状态与本文件无关——它们只活在内存(D1)。
7
+ */
8
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
9
+ import { homedir } from "node:os";
10
+ import { dirname, join } from "node:path";
11
+ /** 缓存目录解析:XDG_CACHE_HOME 非空优先(含测试重定向,testing.md §3 认可的标准行为),否则 ~/.cache */
12
+ export function resolveVersionCacheDir() {
13
+ return process.env["XDG_CACHE_HOME"] || join(homedir(), ".cache");
14
+ }
15
+ /** 缓存文件完整路径;env 每次现读(不缓存),重定向后同进程即刻生效 */
16
+ export function resolveVersionCachePath() {
17
+ return join(resolveVersionCacheDir(), "opencode-commandcode", "version-cache.json");
18
+ }
19
+ /**
20
+ * 原子写:先落同目录临时文件再 rename(同目录保证同一文件系统,rename 原子)。
21
+ * 失败向上抛给调用方——缓存写失败不构成伪装失败,调用方降兜底 + warn。
22
+ */
23
+ export async function writeVersionCache(entry) {
24
+ const path = resolveVersionCachePath();
25
+ const tmp = `${path}.tmp`;
26
+ await mkdir(dirname(path), { recursive: true });
27
+ await writeFile(tmp, JSON.stringify(entry), "utf8");
28
+ await rename(tmp, path);
29
+ }
30
+ /** 缓存形状守卫:{version, fetchedAt} 双非空字符串才可信,其余(损坏/缺字段)视同无值 */
31
+ function parseEntry(raw) {
32
+ let parsed;
33
+ try {
34
+ parsed = JSON.parse(raw);
35
+ }
36
+ catch {
37
+ return undefined;
38
+ }
39
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
40
+ return undefined;
41
+ const record = parsed;
42
+ const version = record["version"];
43
+ const fetchedAt = record["fetchedAt"];
44
+ if (typeof version !== "string" || version.length === 0)
45
+ return undefined;
46
+ if (typeof fetchedAt !== "string" || fetchedAt.length === 0)
47
+ return undefined;
48
+ return { version, fetchedAt };
49
+ }
50
+ /**
51
+ * 读取上次成功拉取值;ENOENT(首次运行)静默返回 undefined,其余异常与损坏形状
52
+ * warn 后返回 undefined。日志经注入式 logger(§7),无 logger 时静默。
53
+ */
54
+ export async function readVersionCache(logger) {
55
+ let raw;
56
+ try {
57
+ raw = await readFile(resolveVersionCachePath(), "utf8");
58
+ }
59
+ catch (error) {
60
+ if (error?.code === "ENOENT")
61
+ return undefined;
62
+ logger?.warn(`版本缓存读取失败,视同无值:${error instanceof Error ? error.message : String(error)}`);
63
+ return undefined;
64
+ }
65
+ const entry = parseEntry(raw);
66
+ if (entry === undefined)
67
+ logger?.warn(`版本缓存形状损坏,视同无值:${resolveVersionCachePath()}`);
68
+ return entry;
69
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * 版本头 `x-command-code-version` 状态机(disguise.md §6):
3
+ *
4
+ * - **数据源顺序**:npm registry 直连 → jsDelivr 镜像,各挂 1.5s 超时
5
+ * (`AbortSignal.timeout`),任一成功即为一手拉取值;
6
+ * - **初解析(D6b)**:进程内首次取值时启动拉取链并只等 1.5s——抢到用新值,
7
+ * 抢不到先用兜底值放行、链转后台跑完;修 fire-and-forget「初期请求带陈旧值」
8
+ * 的同时不让被墙的 npm 拖住首请求;
9
+ * - **24h 惰性刷新(D6c)**:每次取值比对「距上次**成功**拉取是否超 24h」,到点
10
+ * 后台触发拉取链。不挂 `setInterval`、不加抖动。全源失败时按字面语义每次取值
11
+ * 都会重试——全断即 fail-fast(fetch 即刻 reject),fire-and-forget 无主请求
12
+ * 代价,换网络恢复后的即时校准;
13
+ * - **重估时刻(#19)**:版本值只在初解析与 24h 刷新两处重算,兜底链的
14
+ * artifact/snapshot 层此刻才被读取——期间模型管线产物刷新不引起版本头翻转;
15
+ * - **兜底链**:竞速值(内存)→ 落盘缓存 → 产物 `sourceCliVersion`(只读内存
16
+ * getter,不触发拉取不等待)→ 包内快照 `sourceCliVersion`(version.ts 纯函数)。
17
+ * 一手拉取成功才写落盘缓存(③ 的值不回写,§6);写失败降兜底 + warn。
18
+ *
19
+ * 出网统一走注入的 fetch 接缝(testing.md §2:全部出网路径经工厂 options.fetch 注 mock)。
20
+ */
21
+ import type { FetchLike } from "../protocol/json.js";
22
+ import { type DisguiseLogger } from "./logger.js";
23
+ /** 初解析限时等待:超过即用兜底值放行,拉取链转后台 */
24
+ export declare const VERSION_RACE_MS = 1500;
25
+ /** 单一数据源(npm / jsDelivr)的超时上限 */
26
+ export declare const VERSION_SOURCE_TIMEOUT_MS = 1500;
27
+ /** 惰性刷新间隔(§11 参数速查:24h,无抖动) */
28
+ export declare const VERSION_REFRESH_INTERVAL_MS: number;
29
+ export interface VersionResolverOptions {
30
+ /** 出网接缝(工厂 options.fetch;缺省 globalThis.fetch) */
31
+ fetch?: FetchLike;
32
+ logger?: DisguiseLogger;
33
+ /** 兜底链 ③:模型管线内存中已拉取产物的 sourceCliVersion(不触发拉取、不等待) */
34
+ artifactSourceCliVersion?: () => string | undefined;
35
+ /** 兜底链 ④:包内快照的 sourceCliVersion(随插件发版更新) */
36
+ snapshotSourceCliVersion?: () => string | undefined;
37
+ }
38
+ export interface VersionResolver {
39
+ /** 当前版本值;初解析未完成时等待(≤1.5s 竞速),四层皆空返回 undefined(调用方决定放行形态) */
40
+ getVersion(): Promise<string | undefined>;
41
+ }
42
+ export declare function createVersionResolver(options?: VersionResolverOptions): VersionResolver;
@@ -0,0 +1,137 @@
1
+ /**
2
+ * 版本头 `x-command-code-version` 状态机(disguise.md §6):
3
+ *
4
+ * - **数据源顺序**:npm registry 直连 → jsDelivr 镜像,各挂 1.5s 超时
5
+ * (`AbortSignal.timeout`),任一成功即为一手拉取值;
6
+ * - **初解析(D6b)**:进程内首次取值时启动拉取链并只等 1.5s——抢到用新值,
7
+ * 抢不到先用兜底值放行、链转后台跑完;修 fire-and-forget「初期请求带陈旧值」
8
+ * 的同时不让被墙的 npm 拖住首请求;
9
+ * - **24h 惰性刷新(D6c)**:每次取值比对「距上次**成功**拉取是否超 24h」,到点
10
+ * 后台触发拉取链。不挂 `setInterval`、不加抖动。全源失败时按字面语义每次取值
11
+ * 都会重试——全断即 fail-fast(fetch 即刻 reject),fire-and-forget 无主请求
12
+ * 代价,换网络恢复后的即时校准;
13
+ * - **重估时刻(#19)**:版本值只在初解析与 24h 刷新两处重算,兜底链的
14
+ * artifact/snapshot 层此刻才被读取——期间模型管线产物刷新不引起版本头翻转;
15
+ * - **兜底链**:竞速值(内存)→ 落盘缓存 → 产物 `sourceCliVersion`(只读内存
16
+ * getter,不触发拉取不等待)→ 包内快照 `sourceCliVersion`(version.ts 纯函数)。
17
+ * 一手拉取成功才写落盘缓存(③ 的值不回写,§6);写失败降兜底 + warn。
18
+ *
19
+ * 出网统一走注入的 fetch 接缝(testing.md §2:全部出网路径经工厂 options.fetch 注 mock)。
20
+ */
21
+ import { consoleLogger } from "./logger.js";
22
+ import { resolveCommandCodeVersion } from "./version.js";
23
+ import { readVersionCache, writeVersionCache } from "./version-cache.js";
24
+ /** 初解析限时等待:超过即用兜底值放行,拉取链转后台 */
25
+ export const VERSION_RACE_MS = 1500;
26
+ /** 单一数据源(npm / jsDelivr)的超时上限 */
27
+ export const VERSION_SOURCE_TIMEOUT_MS = 1500;
28
+ /** 惰性刷新间隔(§11 参数速查:24h,无抖动) */
29
+ export const VERSION_REFRESH_INTERVAL_MS = 24 * 60 * 60 * 1000;
30
+ const NPM_REGISTRY_URL = "https://registry.npmjs.org/command-code/latest";
31
+ const JSDELIVR_URL = "https://cdn.jsdelivr.net/npm/command-code/package.json";
32
+ function asRecord(value) {
33
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
34
+ }
35
+ function delay(ms) {
36
+ return new Promise((resolve) => setTimeout(resolve, ms));
37
+ }
38
+ export function createVersionResolver(options = {}) {
39
+ const fetchImpl = options.fetch ?? globalThis.fetch;
40
+ const logger = options.logger ?? consoleLogger();
41
+ // 全部状态收在本闭包(§10 修写表:单一模块 state,非散装全局变量)
42
+ let raced;
43
+ let cachedEntry;
44
+ let resolved;
45
+ let cacheRead;
46
+ let initialResolution;
47
+ let chainInFlight;
48
+ /** 上次成功拉取的时刻(epoch ms);0 = 从未成功(D6c 字面语义:此时每次取值都视为到期) */
49
+ let lastFetchedAt = 0;
50
+ function recompute() {
51
+ resolved = resolveCommandCodeVersion({
52
+ raced,
53
+ cached: cachedEntry?.version,
54
+ artifact: options.artifactSourceCliVersion?.(),
55
+ snapshot: options.snapshotSourceCliVersion?.(),
56
+ });
57
+ }
58
+ function readCacheOnce() {
59
+ cacheRead ??= readVersionCache(logger).then((entry) => {
60
+ cachedEntry = entry;
61
+ });
62
+ return cacheRead;
63
+ }
64
+ /** 单一数据源拉取:GET packument/package.json 取 `version`;非 2xx / 形状不对 / 超时一律 reject */
65
+ async function fetchSourceVersion(url) {
66
+ const response = await fetchImpl(url, {
67
+ headers: { accept: "application/json" },
68
+ signal: AbortSignal.timeout(VERSION_SOURCE_TIMEOUT_MS),
69
+ });
70
+ if (!response.ok)
71
+ throw new Error(`HTTP ${response.status}`);
72
+ const body = await response.json();
73
+ const version = asRecord(body)?.["version"];
74
+ if (typeof version !== "string" || version.length === 0)
75
+ throw new Error("响应缺少 version 字段");
76
+ return version;
77
+ }
78
+ /**
79
+ * 拉取链:npm 直连 → jsDelivr 镜像,先到者胜。成功即更新内存与兜底链并重估
80
+ * (24h 刷新是规格允许的第二个重估时刻);写缓存失败只 warn,不影响内存值。
81
+ * 整体绝不 reject(调用方均为 fire-and-forget 或竞速等待,无 await 侧异常路径)。
82
+ */
83
+ function refreshChain() {
84
+ if (chainInFlight)
85
+ return chainInFlight;
86
+ const task = (async () => {
87
+ try {
88
+ let version;
89
+ try {
90
+ version = await fetchSourceVersion(NPM_REGISTRY_URL);
91
+ }
92
+ catch {
93
+ version = await fetchSourceVersion(JSDELIVR_URL).catch(() => undefined);
94
+ }
95
+ if (version === undefined)
96
+ return;
97
+ raced = version;
98
+ recompute(); // 内存值先行更新(主请求永不等待磁盘);落盘只服务下次进程的兜底链
99
+ const entry = { version, fetchedAt: new Date().toISOString() };
100
+ try {
101
+ await writeVersionCache(entry);
102
+ cachedEntry = entry;
103
+ }
104
+ catch (error) {
105
+ logger.warn(`版本缓存写入失败,降兜底值:${error instanceof Error ? error.message : String(error)}`);
106
+ }
107
+ lastFetchedAt = Date.now(); // D6c 字面:成功拉取才记账
108
+ logger.debug(`x-command-code-version 更新为 ${version}`);
109
+ }
110
+ finally {
111
+ chainInFlight = undefined;
112
+ }
113
+ })();
114
+ chainInFlight = task;
115
+ return task;
116
+ }
117
+ /** 初解析(仅一次):读盘与竞速并行,1.5s 限时等链,超时落兜底链取值放行 */
118
+ function resolveInitial() {
119
+ return (async () => {
120
+ await Promise.race([refreshChain(), delay(VERSION_RACE_MS)]);
121
+ await readCacheOnce();
122
+ recompute();
123
+ })();
124
+ }
125
+ return {
126
+ async getVersion() {
127
+ // 24h 惰性刷新(D6c 字面):距上次成功拉取超 24h 即后台触发;失败链在下次
128
+ // 取值时照常重试(全断即 fail-fast,无主请求代价);chainInFlight 去重保证
129
+ // 初解析期间与并发取值不重复发起
130
+ if (Date.now() - lastFetchedAt >= VERSION_REFRESH_INTERVAL_MS)
131
+ void refreshChain();
132
+ initialResolution ??= resolveInitial();
133
+ await initialResolution;
134
+ return resolved;
135
+ },
136
+ };
137
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * 版本头 `x-command-code-version` 兜底链取值(disguise.md §6,#19 修订全链序):
3
+ * ① npm/jsDelivr 竞速成功值(内存)→ ② 落盘缓存值 → ③ 运行时已拉取构建产物的
4
+ * `sourceCliVersion`(只读内存,不触发额外拉取、不等待,也不回写落盘缓存)→
5
+ * ④ 包内快照 `sourceCliVersion`。
6
+ *
7
+ * 本模块只定取值顺序;竞速、24h 惰性刷新与落盘读写归状态层(#33)。
8
+ * 空串视同无值——竞速失败或损坏缓存可能残留空串,不应短路后面的层。
9
+ */
10
+ export interface CommandCodeVersionChain {
11
+ /** ① npm/jsDelivr 竞速成功值(进程内存) */
12
+ raced?: string | undefined;
13
+ /** ② 落盘的「上次成功拉取值」(version-cache.json) */
14
+ cached?: string | undefined;
15
+ /** ③ 运行时已拉取构建产物的 sourceCliVersion(模型管线内存中已有才有值) */
16
+ artifact?: string | undefined;
17
+ /** ④ 包内快照的 sourceCliVersion(随插件发版更新) */
18
+ snapshot?: string | undefined;
19
+ }
20
+ /** 按兜底链序返回首个非空版本;四层皆空时 undefined(调用方决定放行形态) */
21
+ export declare function resolveCommandCodeVersion(chain: CommandCodeVersionChain): string | undefined;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * 版本头 `x-command-code-version` 兜底链取值(disguise.md §6,#19 修订全链序):
3
+ * ① npm/jsDelivr 竞速成功值(内存)→ ② 落盘缓存值 → ③ 运行时已拉取构建产物的
4
+ * `sourceCliVersion`(只读内存,不触发额外拉取、不等待,也不回写落盘缓存)→
5
+ * ④ 包内快照 `sourceCliVersion`。
6
+ *
7
+ * 本模块只定取值顺序;竞速、24h 惰性刷新与落盘读写归状态层(#33)。
8
+ * 空串视同无值——竞速失败或损坏缓存可能残留空串,不应短路后面的层。
9
+ */
10
+ /** 按兜底链序返回首个非空版本;四层皆空时 undefined(调用方决定放行形态) */
11
+ export function resolveCommandCodeVersion(chain) {
12
+ for (const value of [chain.raced, chain.cached, chain.artifact, chain.snapshot]) {
13
+ if (typeof value === "string" && value.length > 0)
14
+ return value;
15
+ }
16
+ return undefined;
17
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * v1/v2 glue 共享的宿主面常量(单源防漂移——v1 与 v2 宿主各自独立消费同一批
3
+ * 注册字段,复制粘贴会让「多处同名」不变量悄悄分叉)。
4
+ *
5
+ * 同名约束出处:CONTEXT.md——provider 显示名在 v1 /connect 列表项、v2 目录与
6
+ * provider 列表、v2 integration name 同用;认证方法 label 固定「Command Code
7
+ * API Key」,v1 auth hook 与 v2 integration key method 同文案。
8
+ */
9
+ /** provider 显示名「Command Code (Go)」(CONTEXT.md,两宿主同用) */
10
+ export declare const PROVIDER_DISPLAY_NAME = "Command Code (Go)";
11
+ /** 认证方法 label:「Command Code API Key」,/connect 认证输入框标题(CONTEXT.md) */
12
+ export declare const API_KEY_METHOD_LABEL = "Command Code API Key";
13
+ /** env 认证通道扫描的环境变量(与 .env.example 清单一致) */
14
+ export declare const API_KEY_ENV_VAR = "COMMANDCODE_API_KEY";
@@ -0,0 +1,14 @@
1
+ /**
2
+ * v1/v2 glue 共享的宿主面常量(单源防漂移——v1 与 v2 宿主各自独立消费同一批
3
+ * 注册字段,复制粘贴会让「多处同名」不变量悄悄分叉)。
4
+ *
5
+ * 同名约束出处:CONTEXT.md——provider 显示名在 v1 /connect 列表项、v2 目录与
6
+ * provider 列表、v2 integration name 同用;认证方法 label 固定「Command Code
7
+ * API Key」,v1 auth hook 与 v2 integration key method 同文案。
8
+ */
9
+ /** provider 显示名「Command Code (Go)」(CONTEXT.md,两宿主同用) */
10
+ export const PROVIDER_DISPLAY_NAME = "Command Code (Go)";
11
+ /** 认证方法 label:「Command Code API Key」,/connect 认证输入框标题(CONTEXT.md) */
12
+ export const API_KEY_METHOD_LABEL = "Command Code API Key";
13
+ /** env 认证通道扫描的环境变量(与 .env.example 清单一致) */
14
+ export const API_KEY_ENV_VAR = "COMMANDCODE_API_KEY";
@@ -0,0 +1,85 @@
1
+ /**
2
+ * v1 宿主接线(#37;实测定案 #11,原型证据在弃用分支 prototype/v1-host-loading)。
3
+ *
4
+ * 用户 config 只写一行 `plugin: ["@wallbreakerno4/opencode-commandcode"]`,其余全部
5
+ * 自举(与 v2 glue #36 共用工厂与运行时,宿主形态互不干扰):
6
+ *
7
+ * - **三合一入口**:`server(input, options)` 由 v1 加载器消费(default 带 id 时
8
+ * server 必须在场,否则整模块跳过,#11 实测),返回 `{config, auth}` hooks。
9
+ * v1.18.x 是双轨宿主——内嵌 v2 运行时也会以嵌入式 ctx 调 default.setup,但它
10
+ * 懒加载且晚于 v1 config hook(1.18.25 真宿主探针实测),管线模式由先到的 v1
11
+ * config hook 协商为 v1,setup 的构造调用幂等落空。
12
+ * - **config hook**:注入 `provider.commandcode-go = {npm, name, env, models}`。
13
+ * npm 指向本包安装 spec(推导方式见 selfNpmSpec——注入裸包名会装出第二个模块
14
+ * 实例);宿主经 arborist 缓存命中后 import 同一模块实例(目录存在即缓存命中,
15
+ * #11 实测),按「第一个 create* 导出」判得与 v2 共用的工厂。注入前先以用户
16
+ * 已写的 `options.modelsUrls` 做 v1 启动协商(拉取一次,15s 总预算,失败用快照,
17
+ * 此后无后台刷新——v1 无 reload 机制),模型清单 = 协商后的级联。
18
+ * - **非破坏合并**:用户已写键一律优先(npm/name/env 整键、options 整块、models
19
+ * 逐 id)——插件只补缺,绝不覆盖用户显式配置;`options` 全权归用户(modelsUrls
20
+ * 通道所在),插件不注入任何默认值。
21
+ * - **auth hook**:注册 `/connect` 登录项(label 固定「Command Code API Key」,
22
+ * CONTEXT.md);loader 仅在 auth.json 有该 provider 凭证记录时被宿主调用(无凭证
23
+ * 不触发,#11 实测),把凭证翻译成工厂 `apiKey`;优先级 auth > env 由宿主保证
24
+ * (与 v2 credential > env 一致)。
25
+ * - **测试边界**(testing.md §4 定案):hook 的宿主交互行为不入 bun test——mock
26
+ * 宿主 = 重写宿主,验证 = 真宿主 v1(latest 1.18.x)验证,全程 XDG 隔离;
27
+ * hooks 静态形状与 selfNpmSpec 纯函数推导在 bun test 内(testing.md §1.4)。
28
+ *
29
+ * 零依赖纪律(入口既定):不 import `@opencode-ai/plugin`,宿主对象以本模块的
30
+ * 最小结构类型承接——字段名按 v1.18.25 真宿主探针收窄到 glue 触达的域,宿主漂移
31
+ * 时真宿主验证即暴露,不为漂移预付兼容成本。
32
+ */
33
+ /**
34
+ * config hook 注入的 npm spec:从入口模块自身的加载路径推导(#37 真宿主验证定案)。
35
+ * v1 宿主对 plugin 列表的裸包名会归一化为 `<name>@latest` 再定 arborist 缓存目录,
36
+ * 而 provider 的 `npm` spec **原样**定目录——注入裸包名会装出第二个模块实例,
37
+ * 伪装状态与模型管线全部翻倍(违背「v1 无后台刷新」与伪装 per-key 状态的进程内
38
+ * 单例前提)。因此 spec 不能写死包名,按 v1 缓存目录布局
39
+ * `…/opencode/packages/<spec>/node_modules/…` 从实际加载路径提取:
40
+ * - 正常安装形态(plugin 裸包名 → @latest 归一化、版本钉死等):提取出的 `<spec>`
41
+ * 使 provider 解析命中同一缓存目录(目录存在即缓存命中,#11 实测)——同一模块
42
+ * 实例。
43
+ * - 其余形态(本地 file:// 直载开发、缓存布局漂移):回退入口自身 URL,免安装
44
+ * 直接 import(#11 实测 file:// 绝对路径形态可用;file: 相对路径残缺不可用,
45
+ * 不产出)。
46
+ * 推导在 server() 调用时进行(ENTRY_URL 此时必已初始化,规避环导 TDZ)。
47
+ */
48
+ export declare function selfNpmSpec(entryUrl: string): string;
49
+ /** v1 provider config 块:注入字段 + 用户可能已写的任意字段 */
50
+ export interface V1ProviderConfig {
51
+ npm?: string;
52
+ name?: string;
53
+ env?: string[];
54
+ /** options 块全权归用户(modelsUrls 通道所在);插件不注入、不合并、原样保留 */
55
+ options?: Record<string, unknown>;
56
+ models?: Record<string, unknown>;
57
+ }
58
+ /** v1 config hook 收到的宿主 config 对象:只触达 provider 域,其余字段零知识 */
59
+ export interface V1ConfigObject {
60
+ provider?: Record<string, V1ProviderConfig | undefined>;
61
+ }
62
+ /** v1 auth hook 的登录方式:/connect 列表项(api 类型 + label) */
63
+ export interface V1AuthMethod {
64
+ readonly type: "api";
65
+ readonly label: string;
66
+ }
67
+ /** v1 auth hook:provider 凭证通道注册(#11 实测消费行为;宿主以 (auth, provider) 调用) */
68
+ export interface V1AuthHook {
69
+ readonly provider: string;
70
+ /** 仅在 auth.json 有该 provider 凭证记录时被调;auth 参数可能是懒加载函数 */
71
+ readonly loader: (auth: unknown) => Promise<{
72
+ apiKey: string;
73
+ }>;
74
+ readonly methods: readonly V1AuthMethod[];
75
+ }
76
+ /** server() 返回的 v1 hooks(v1 加载器消费的全部面) */
77
+ export interface V1Hooks {
78
+ config: (config: V1ConfigObject) => Promise<void>;
79
+ auth: V1AuthHook;
80
+ }
81
+ /**
82
+ * v1 插件入口(default.server):返回 config / auth hooks。纯函数——宿主可能在
83
+ * config 重载或 auth login 等场景重复调用,重放注入幂等(同级联 → 同块)。
84
+ */
85
+ export declare function serverV1(_input: unknown, _options: unknown): Promise<V1Hooks>;