@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,118 @@
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
+ import { ENTRY_URL } from "../index.js";
34
+ import { toV1ModelMap } from "../models/mapping.js";
35
+ import { PROVIDER_ID } from "../protocol/envelope.js";
36
+ import { ensureV1ProviderRuntime } from "../provider/model.js";
37
+ import { API_KEY_ENV_VAR, API_KEY_METHOD_LABEL, PROVIDER_DISPLAY_NAME } from "./constants.js";
38
+ /**
39
+ * config hook 注入的 npm spec:从入口模块自身的加载路径推导(#37 真宿主验证定案)。
40
+ * v1 宿主对 plugin 列表的裸包名会归一化为 `<name>@latest` 再定 arborist 缓存目录,
41
+ * 而 provider 的 `npm` spec **原样**定目录——注入裸包名会装出第二个模块实例,
42
+ * 伪装状态与模型管线全部翻倍(违背「v1 无后台刷新」与伪装 per-key 状态的进程内
43
+ * 单例前提)。因此 spec 不能写死包名,按 v1 缓存目录布局
44
+ * `…/opencode/packages/<spec>/node_modules/…` 从实际加载路径提取:
45
+ * - 正常安装形态(plugin 裸包名 → @latest 归一化、版本钉死等):提取出的 `<spec>`
46
+ * 使 provider 解析命中同一缓存目录(目录存在即缓存命中,#11 实测)——同一模块
47
+ * 实例。
48
+ * - 其余形态(本地 file:// 直载开发、缓存布局漂移):回退入口自身 URL,免安装
49
+ * 直接 import(#11 实测 file:// 绝对路径形态可用;file: 相对路径残缺不可用,
50
+ * 不产出)。
51
+ * 推导在 server() 调用时进行(ENTRY_URL 此时必已初始化,规避环导 TDZ)。
52
+ */
53
+ export function selfNpmSpec(entryUrl) {
54
+ const marker = "/opencode/packages/";
55
+ const start = entryUrl.indexOf(marker);
56
+ if (entryUrl.startsWith("file:") && start !== -1) {
57
+ const from = start + marker.length;
58
+ const end = entryUrl.indexOf("/node_modules/", from);
59
+ if (end !== -1)
60
+ return entryUrl.slice(from, end);
61
+ }
62
+ return entryUrl;
63
+ }
64
+ // ---------------------------------------------------------------------------
65
+ // provider 块注入(非破坏合并)
66
+ // ---------------------------------------------------------------------------
67
+ /**
68
+ * 注入 provider 块:插件自举字段垫底、用户已写键一律优先。models 逐 id 合并——
69
+ * 用户手写的模型条目保留(覆盖同 id 的管线产物),其余由级联清单补齐。
70
+ */
71
+ function mergeProviderBlock(npmSpec, existing, models) {
72
+ const merged = {
73
+ name: PROVIDER_DISPLAY_NAME,
74
+ npm: npmSpec,
75
+ env: [API_KEY_ENV_VAR],
76
+ ...existing,
77
+ models: { ...models, ...existing?.models },
78
+ };
79
+ return merged;
80
+ }
81
+ // ---------------------------------------------------------------------------
82
+ // server():v1 插件入口
83
+ // ---------------------------------------------------------------------------
84
+ /**
85
+ * v1 插件入口(default.server):返回 config / auth hooks。纯函数——宿主可能在
86
+ * config 重载或 auth login 等场景重复调用,重放注入幂等(同级联 → 同块)。
87
+ */
88
+ export async function serverV1(_input, _options) {
89
+ // 自举 npm spec 按实际加载路径推导(见 selfNpmSpec);此时入口模块必已完成求值
90
+ const npmSpec = selfNpmSpec(ENTRY_URL);
91
+ return {
92
+ config: async (config) => {
93
+ const existing = config.provider?.[PROVIDER_ID];
94
+ // 用户 modelsUrls 通道(model-pipeline.md §1.3):仅用户已写时读取,作为
95
+ // v1 启动拉取的 config 通道初值(config > env > 默认列表)
96
+ const userModelsUrls = existing?.options?.modelsUrls;
97
+ // v1 启动协商:15s 总预算拉取一次(跨渠道共享),失败用快照;若运行时已被
98
+ // 工厂先行 / 内嵌 v2 setup 先行构造,幂等守卫使本调用退化为读当前级联
99
+ const cascade = await ensureV1ProviderRuntime({ modelsUrls: userModelsUrls });
100
+ config.provider ??= {};
101
+ config.provider[PROVIDER_ID] = mergeProviderBlock(npmSpec, existing, toV1ModelMap(cascade.models));
102
+ },
103
+ auth: {
104
+ provider: PROVIDER_ID,
105
+ loader: async (auth) => {
106
+ // 宿主传懒加载函数或现值(#11 原型实测两种形态);key 缺位 = 凭证记录损坏,
107
+ // 空 apiKey 按 auth > env 优先级压过 env,请求以 401 浮现——warn 指路重新登录
108
+ const resolved = (typeof auth === "function" ? await auth() : auth);
109
+ const key = resolved?.key;
110
+ if (!key) {
111
+ console.warn("[commandcode-go] 凭证记录存在但 key 为空,请求将返回 401;请重新 /connect 登录");
112
+ }
113
+ return { apiKey: key ?? "" };
114
+ },
115
+ methods: [{ type: "api", label: API_KEY_METHOD_LABEL }],
116
+ },
117
+ };
118
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * v2 宿主接线(#36;实测定案 #12/#5)。
3
+ *
4
+ * 用户 config 只写空壳 `{providers: {"commandcode-go": {}}}`,其余全部自举:
5
+ *
6
+ * - **catalog.transform 自指注册**(#12 S1 定案):provider.update 设
7
+ * `package = "aisdk:" + <入口模块 URL>`——宿主剥掉 `aisdk:` 前缀后原生 import
8
+ * 该文件,命中同一模块实例(instance seq 全程 = 1,伪装模块进程内状态安全
9
+ * 的前提);锚点取入口模块的 `import.meta.url`(ENTRY_URL)而非 glue 子模块的
10
+ * ——工厂判据「第一个 create* 导出」作用在入口模块上。`aisdk:<registry 包名>`
11
+ * 链路在 beta-18684 已死(UnsupportedPackageError),不用。模型按级联清单
12
+ * update-or-create 注册:首播 = 包内快照(启动零阻塞),后台拉取签名变化触发
13
+ * `catalog.reload()` 重放本 transform,届时 latestCascade 已是管线实时级联。
14
+ * - **integration.transform 认证**(#12 S2 定案):为全新 provider upsert
15
+ * integration(update 是 upsert,无需预存),注册 key 方法(/connect 粘贴)与
16
+ * env 方法;凭证由宿主解析后经工厂 `apiKey` 注入,优先级 credential > env。
17
+ * - **modelsUrls v2 settings 通道**:用户写的 `settings.modelsUrls` 由宿主合并进
18
+ * 工厂 options 顶层(beta-18684 实测:transform 草稿不带 config settings,插件
19
+ * 侧构造时不可见),首次工厂调用经管线 rebindModelsUrls 接入(config > env >
20
+ * 默认列表,原值不变零开销跳过);settings 壳其余字段原样保留不 clobber。
21
+ * - **测试边界**(testing.md §4 定案):glue 是全项目唯一无自动化测试的模块——
22
+ * mock 宿主 = 重写宿主;验证 = 真宿主(锁定 v2 beta 快照)验证 + #21 人工验收。
23
+ *
24
+ * 零依赖纪律(入口既定):不 import `@opencode-ai/plugin`,宿主 ctx 以本模块的
25
+ * 最小结构类型承接——字段名按 node_modules 实测的 beta d.ts 收窄到 glue 触达的
26
+ * 域,宿主漂移时真宿主验证即暴露,不为漂移预付兼容成本。
27
+ */
28
+ /**
29
+ * v2 插件入口(default.setup):自举 provider、模型与认证方法,并把共享运行时
30
+ * 接上宿主目录。幂等——插件热重载重跑 setup 时 transform 重放为 upsert、运行时
31
+ * 幂等构造、reload 回调重设到活 ctx。
32
+ */
33
+ export declare function setupV2(context: unknown): Promise<void>;
@@ -0,0 +1,110 @@
1
+ /**
2
+ * v2 宿主接线(#36;实测定案 #12/#5)。
3
+ *
4
+ * 用户 config 只写空壳 `{providers: {"commandcode-go": {}}}`,其余全部自举:
5
+ *
6
+ * - **catalog.transform 自指注册**(#12 S1 定案):provider.update 设
7
+ * `package = "aisdk:" + <入口模块 URL>`——宿主剥掉 `aisdk:` 前缀后原生 import
8
+ * 该文件,命中同一模块实例(instance seq 全程 = 1,伪装模块进程内状态安全
9
+ * 的前提);锚点取入口模块的 `import.meta.url`(ENTRY_URL)而非 glue 子模块的
10
+ * ——工厂判据「第一个 create* 导出」作用在入口模块上。`aisdk:<registry 包名>`
11
+ * 链路在 beta-18684 已死(UnsupportedPackageError),不用。模型按级联清单
12
+ * update-or-create 注册:首播 = 包内快照(启动零阻塞),后台拉取签名变化触发
13
+ * `catalog.reload()` 重放本 transform,届时 latestCascade 已是管线实时级联。
14
+ * - **integration.transform 认证**(#12 S2 定案):为全新 provider upsert
15
+ * integration(update 是 upsert,无需预存),注册 key 方法(/connect 粘贴)与
16
+ * env 方法;凭证由宿主解析后经工厂 `apiKey` 注入,优先级 credential > env。
17
+ * - **modelsUrls v2 settings 通道**:用户写的 `settings.modelsUrls` 由宿主合并进
18
+ * 工厂 options 顶层(beta-18684 实测:transform 草稿不带 config settings,插件
19
+ * 侧构造时不可见),首次工厂调用经管线 rebindModelsUrls 接入(config > env >
20
+ * 默认列表,原值不变零开销跳过);settings 壳其余字段原样保留不 clobber。
21
+ * - **测试边界**(testing.md §4 定案):glue 是全项目唯一无自动化测试的模块——
22
+ * mock 宿主 = 重写宿主;验证 = 真宿主(锁定 v2 beta 快照)验证 + #21 人工验收。
23
+ *
24
+ * 零依赖纪律(入口既定):不 import `@opencode-ai/plugin`,宿主 ctx 以本模块的
25
+ * 最小结构类型承接——字段名按 node_modules 实测的 beta d.ts 收窄到 glue 触达的
26
+ * 域,宿主漂移时真宿主验证即暴露,不为漂移预付兼容成本。
27
+ */
28
+ import { ENTRY_URL } from "../index.js";
29
+ import { toV2ModelFields } from "../models/mapping.js";
30
+ import { PROVIDER_ID } from "../protocol/envelope.js";
31
+ import { ensureProviderRuntime, latestCascade } from "../provider/model.js";
32
+ import { API_KEY_ENV_VAR, API_KEY_METHOD_LABEL, PROVIDER_DISPLAY_NAME } from "./constants.js";
33
+ // ---------------------------------------------------------------------------
34
+ // setup 接线
35
+ // ---------------------------------------------------------------------------
36
+ /**
37
+ * v2 插件入口(default.setup):自举 provider、模型与认证方法,并把共享运行时
38
+ * 接上宿主目录。幂等——插件热重载重跑 setup 时 transform 重放为 upsert、运行时
39
+ * 幂等构造、reload 回调重设到活 ctx。
40
+ */
41
+ export async function setupV2(context) {
42
+ const ctx = context;
43
+ await ctx.catalog.transform((draft) => {
44
+ draft.provider.update(PROVIDER_ID, (provider) => {
45
+ provider.name = PROVIDER_DISPLAY_NAME;
46
+ // 自指(#12 S1 定案):aisdk: 前缀强制(漏写即 UnsupportedPackageError);
47
+ // 锚点是入口模块 ENTRY_URL(create* 工厂判据所在),宿主再 import 同一路径
48
+ // 命中同一模块实例
49
+ provider.package = `aisdk:${ENTRY_URL}`;
50
+ provider.integrationID = PROVIDER_ID;
51
+ provider.activation = "auto";
52
+ });
53
+ // 模型注册(update-or-create):目录键与上游 modelID 同为 wire id(可含 `/`)。
54
+ // 首播 = 包内快照(启动零阻塞);后台拉取签名变化触发 catalog.reload() 重放本
55
+ // 回调,届时 latestCascade() 已是管线实时级联
56
+ for (const resolved of latestCascade().models.map(toV2ModelFields)) {
57
+ draft.model.update(PROVIDER_ID, resolved.id, (model) => {
58
+ model.id = resolved.id;
59
+ model.modelID = resolved.modelID;
60
+ model.name = resolved.name;
61
+ model.capabilities = {
62
+ tools: true,
63
+ input: [...resolved.capabilities.input],
64
+ output: [...resolved.capabilities.output],
65
+ };
66
+ model.limit = { context: resolved.limit.context, output: resolved.limit.output };
67
+ // 严格透传(model-pipeline.md §3.1):产物无档位即空数组,不造变体
68
+ model.variants = resolved.variants.map((variant) => ({
69
+ id: variant.id,
70
+ settings: { reasoningEffort: variant.settings.reasoningEffort },
71
+ }));
72
+ // 必填基线防御:字段归宿主 update-or-create 的 Model.Info 默认基线所有
73
+ // (cost 省略 = 不写价格,model-pipeline.md §3.2;基线缺位才补),已有值
74
+ // (含用户经 config 写入的 disabled)一律不覆盖
75
+ model.status ??= "active";
76
+ model.enabled ??= true;
77
+ model.time ??= { released: 0 };
78
+ });
79
+ }
80
+ });
81
+ // 认证方法(#12 S2 定案):integration.update 为全新 provider upsert;key 方法
82
+ // 带 /connect 输入框 label,env 方法声明环境变量;integrationID 与 providerID
83
+ // 同名——凭证解析终点是工厂 options.apiKey,优先级 credential > env 由宿主保证
84
+ await ctx.integration.transform((draft) => {
85
+ draft.update(PROVIDER_ID, (integration) => {
86
+ integration.name = PROVIDER_DISPLAY_NAME;
87
+ });
88
+ draft.method.update({
89
+ integrationID: PROVIDER_ID,
90
+ method: { type: "key", label: API_KEY_METHOD_LABEL },
91
+ });
92
+ draft.method.update({
93
+ integrationID: PROVIDER_ID,
94
+ method: { type: "env", names: [API_KEY_ENV_VAR] },
95
+ });
96
+ });
97
+ // 共享运行时(模型管线 + 伪装状态单例):注册全部完成后构造——变更回调只会晚于
98
+ // 此刻触发,reload 不会空放。构造时管线按 env/默认列表启动并后台首轮拉取(启动
99
+ // 零阻塞);modelsUrls 的 config 通道(settings.modelsUrls)在 beta-18684 的
100
+ // transform 草稿上不可见(宿主在目录构建后才合并,实测探针 settings=null),它
101
+ // 由宿主合并进工厂 options、经首次工厂调用 rebindModelsUrls 接入管线
102
+ ensureProviderRuntime({
103
+ onModelDataChange: () => {
104
+ ctx.catalog.reload().catch((error) => {
105
+ // reload 失败目录停在上一份级联:打 warn 留痕,不打断后台刷新节奏
106
+ console.warn(`[commandcode-go] catalog.reload 失败(${String(error)}),目录沿用上一份级联`);
107
+ });
108
+ },
109
+ });
110
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * @wallbreakerno4/opencode-commandcode 的入口骨架:单包三导出,v1/v2 双宿主共用。
3
+ *
4
+ * 形状即契约(防回归断言见 tests/package-shape.test.ts):
5
+ * - default `{ id, setup, server }`——v2 宿主读 `id` + `setup`(接线实现在
6
+ * src/host/v2.ts,#36);v1 宿主加载器在 default 带 `id` 时要求 `server` 也在场,
7
+ * 缺 `server` 则整模块跳过并忽略全部命名导出(#11 真机实测),三键缺一不可。
8
+ * v1 宿主是双轨形态:v1 加载器调 `server()`,内嵌 v2 运行时也调 `setup()`——
9
+ * 两条路共用同一模块实例与运行时单例,模式由先到者协商(接线实现 src/host/v1.ts,
10
+ * #37)。
11
+ * - `createCommandCode` 工厂——v1/v2 共用的 provider 运行时入口,宿主按「模块
12
+ * 第一个 `create*` 前缀导出」判据发现它,不得引入排位更靠前的 `create*` 导出;
13
+ * 实现与其 options/provider 契约类型见 src/provider/model.ts(#35 工厂装配)。
14
+ *
15
+ * 入口保持零运行时依赖:不 import `@opencode-ai/plugin`(v2 的 define() 是恒等
16
+ * 函数),避免被宿主 beta API 漂移绑架。
17
+ */
18
+ import { serverV1 } from "./host/v1.js";
19
+ import { setupV2 } from "./host/v2.js";
20
+ import type { V1Hooks } from "./host/v1.js";
21
+ /**
22
+ * 入口模块自身的 file URL:v2 自指(#12 S1 定案)的锚点。宿主的 aisdk 工厂
23
+ * 判据(「模块第一个 create* 前缀导出」)作用在**本入口模块**上,glue 子模块的
24
+ * `import.meta.url` 指向 dist/host/v2.js,作 package 会半个月厂都找不到——必须
25
+ * 以本入口的 URL 为准,且宿主以此路径再 import 时命中同一模块实例(单实例,
26
+ * 伪装进程内状态安全的前提)。
27
+ */
28
+ export declare const ENTRY_URL: string;
29
+ /**
30
+ * 单包三导出的 default 形状。
31
+ * id 为字面量类型:该标识在 v2 插件 id、v1 config 注入键、integrationID、模型 id
32
+ * 前缀 `commandcode-go/<wire>` 四处同名,写错任何一处即编译失败。
33
+ */
34
+ export interface CommandCodePlugin {
35
+ readonly id: "commandcode-go";
36
+ /** v2 插件入口:OpenCode 2 `setup(ctx)`(#36 宿主接线;v1 宿主的内嵌 v2 运行时同样调用) */
37
+ readonly setup: (context: unknown) => Promise<void> | void;
38
+ /** v1 插件入口:`server(input, options)` → hooks(options 为插件级配置,#37 宿主接线) */
39
+ readonly server: (input: unknown, options: unknown) => Promise<V1Hooks>;
40
+ }
41
+ export { createCommandCode, type CommandCodeFactoryOptions, type CommandCodeProvider } from "./provider/model.js";
42
+ declare const _default: {
43
+ id: "commandcode-go";
44
+ setup: typeof setupV2;
45
+ server: typeof serverV1;
46
+ };
47
+ export default _default;
package/dist/index.js ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @wallbreakerno4/opencode-commandcode 的入口骨架:单包三导出,v1/v2 双宿主共用。
3
+ *
4
+ * 形状即契约(防回归断言见 tests/package-shape.test.ts):
5
+ * - default `{ id, setup, server }`——v2 宿主读 `id` + `setup`(接线实现在
6
+ * src/host/v2.ts,#36);v1 宿主加载器在 default 带 `id` 时要求 `server` 也在场,
7
+ * 缺 `server` 则整模块跳过并忽略全部命名导出(#11 真机实测),三键缺一不可。
8
+ * v1 宿主是双轨形态:v1 加载器调 `server()`,内嵌 v2 运行时也调 `setup()`——
9
+ * 两条路共用同一模块实例与运行时单例,模式由先到者协商(接线实现 src/host/v1.ts,
10
+ * #37)。
11
+ * - `createCommandCode` 工厂——v1/v2 共用的 provider 运行时入口,宿主按「模块
12
+ * 第一个 `create*` 前缀导出」判据发现它,不得引入排位更靠前的 `create*` 导出;
13
+ * 实现与其 options/provider 契约类型见 src/provider/model.ts(#35 工厂装配)。
14
+ *
15
+ * 入口保持零运行时依赖:不 import `@opencode-ai/plugin`(v2 的 define() 是恒等
16
+ * 函数),避免被宿主 beta API 漂移绑架。
17
+ */
18
+ import { serverV1 } from "./host/v1.js";
19
+ import { setupV2 } from "./host/v2.js";
20
+ /**
21
+ * 入口模块自身的 file URL:v2 自指(#12 S1 定案)的锚点。宿主的 aisdk 工厂
22
+ * 判据(「模块第一个 create* 前缀导出」)作用在**本入口模块**上,glue 子模块的
23
+ * `import.meta.url` 指向 dist/host/v2.js,作 package 会半个月厂都找不到——必须
24
+ * 以本入口的 URL 为准,且宿主以此路径再 import 时命中同一模块实例(单实例,
25
+ * 伪装进程内状态安全的前提)。
26
+ */
27
+ export const ENTRY_URL = import.meta.url;
28
+ export { createCommandCode } from "./provider/model.js";
29
+ export default {
30
+ id: "commandcode-go",
31
+ setup: setupV2,
32
+ server: serverV1,
33
+ };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * `/provider/v1/models` 响应的运行时解析(契约:docs/spec/model-pipeline.md §0/§3,
3
+ * 事实层:docs/research/model-metadata-sources.md §二实测形状)。
4
+ *
5
+ * 该端点是发现(id 清单)、`context_length` 与 `name` 的权威(§3 字段表),匿名
6
+ * 拉取即可。OpenAI 兼容样板字段 `object` / `owned_by` 与响应生成时刻的动态时间戳
7
+ * `created` 在此剥除——`created` 一旦流入下游,变更签名(模型管线 II)就必然每次
8
+ * 误判「列表变了」,所以在数据入口处结构性剔除,而不是靠下游记得跳过它。
9
+ */
10
+ /** 单条发现:`name` / `contextLength` 现网 62/62 全有,类型上可选以容忍上游字段级缺失 */
11
+ export interface ApiModelEntry {
12
+ readonly id: string;
13
+ readonly name?: string;
14
+ readonly contextLength?: number;
15
+ }
16
+ export type ModelsApiParseResult = {
17
+ readonly ok: true;
18
+ readonly models: readonly ApiModelEntry[];
19
+ } | {
20
+ readonly ok: false;
21
+ readonly detail: string;
22
+ };
23
+ export declare function parseModelsApi(input: unknown): ModelsApiParseResult;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * `/provider/v1/models` 响应的运行时解析(契约:docs/spec/model-pipeline.md §0/§3,
3
+ * 事实层:docs/research/model-metadata-sources.md §二实测形状)。
4
+ *
5
+ * 该端点是发现(id 清单)、`context_length` 与 `name` 的权威(§3 字段表),匿名
6
+ * 拉取即可。OpenAI 兼容样板字段 `object` / `owned_by` 与响应生成时刻的动态时间戳
7
+ * `created` 在此剥除——`created` 一旦流入下游,变更签名(模型管线 II)就必然每次
8
+ * 误判「列表变了」,所以在数据入口处结构性剔除,而不是靠下游记得跳过它。
9
+ */
10
+ import { asRecord } from "../protocol/json.js";
11
+ export function parseModelsApi(input) {
12
+ const root = asRecord(input);
13
+ if (root === null)
14
+ return fail("顶层不是 JSON 对象");
15
+ const data = root["data"];
16
+ if (!Array.isArray(data))
17
+ return fail("data 必须是数组");
18
+ const models = [];
19
+ for (const [index, raw] of data.entries()) {
20
+ const entry = asRecord(raw);
21
+ if (entry === null)
22
+ return fail(`data[${index}] 不是对象`);
23
+ const id = entry["id"];
24
+ if (typeof id !== "string" || id.length === 0)
25
+ return fail(`data[${index}].id 必须是非空字符串`);
26
+ const name = entry["name"];
27
+ if (name !== undefined && typeof name !== "string")
28
+ return fail(`data[${index}]「${id}」.name 必须是字符串`);
29
+ const contextLength = entry["context_length"];
30
+ if (contextLength !== undefined &&
31
+ (typeof contextLength !== "number" || !Number.isFinite(contextLength) || contextLength <= 0)) {
32
+ return fail(`data[${index}]「${id}」.context_length 必须是正数`);
33
+ }
34
+ models.push({
35
+ id,
36
+ ...(name !== undefined ? { name } : {}),
37
+ ...(contextLength !== undefined ? { contextLength } : {}),
38
+ });
39
+ }
40
+ return { ok: true, models };
41
+ }
42
+ function fail(detail) {
43
+ return { ok: false, detail };
44
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * 构建产物 schema v1 的运行时解析(契约:docs/spec/model-pipeline.md §1)。
3
+ *
4
+ * 产物经分发渠道以 JSON 到达运行时(拉取与缓存归模型管线 II,#34),本模块只负责
5
+ * 把未知形状的已解析 JSON 变成可信的 `Artifact`。三条硬规则:
6
+ * - 同版本内只允许新增可选字段:未知字段一律忽略、不搬运进解析结果(向前兼容);
7
+ * - 遇到大于已知上限的 `schemaVersion`:整体弃用该产物(调用方降级到包内快照 +
8
+ * 告警),不做多版本兼容解析(§1.2);
9
+ * - 必填字段缺失或类型不对:整个产物判 malformed 整体弃用,不做逐模型挑拣——
10
+ * 快照 = 最后已知良好产物,坏产物宁可整层让位(§5 的产物级降级粒度)。
11
+ *
12
+ * 产物形状的唯一出口在构建侧 `scripts/build-models/emit.ts`(其 `Artifact` 类型
13
+ * 为组装权威);本文件为运行时侧独立定义——跨构建/运行时边界不共享编译单元,
14
+ * 两处字段须保持逐键兼容,改 schema 时两边同步。
15
+ */
16
+ export interface ArtifactModel {
17
+ readonly id: string;
18
+ readonly name: string;
19
+ readonly reasoning: boolean;
20
+ readonly inputModalities: readonly string[];
21
+ readonly efforts?: readonly string[];
22
+ readonly context: number;
23
+ readonly maxOutput: number;
24
+ }
25
+ export interface Artifact {
26
+ readonly schemaVersion: 1;
27
+ readonly generatedAt: string;
28
+ readonly sourceCliVersion: string;
29
+ readonly models: readonly ArtifactModel[];
30
+ }
31
+ export type ArtifactParseResult = {
32
+ readonly ok: true;
33
+ readonly artifact: Artifact;
34
+ } | {
35
+ readonly ok: false;
36
+ readonly error: ArtifactParseError;
37
+ };
38
+ export type ArtifactParseError = {
39
+ readonly reason: "future-version";
40
+ readonly schemaVersion: number;
41
+ } | {
42
+ readonly reason: "malformed";
43
+ readonly detail: string;
44
+ };
45
+ export declare function parseArtifact(input: unknown): ArtifactParseResult;
@@ -0,0 +1,108 @@
1
+ /**
2
+ * 构建产物 schema v1 的运行时解析(契约:docs/spec/model-pipeline.md §1)。
3
+ *
4
+ * 产物经分发渠道以 JSON 到达运行时(拉取与缓存归模型管线 II,#34),本模块只负责
5
+ * 把未知形状的已解析 JSON 变成可信的 `Artifact`。三条硬规则:
6
+ * - 同版本内只允许新增可选字段:未知字段一律忽略、不搬运进解析结果(向前兼容);
7
+ * - 遇到大于已知上限的 `schemaVersion`:整体弃用该产物(调用方降级到包内快照 +
8
+ * 告警),不做多版本兼容解析(§1.2);
9
+ * - 必填字段缺失或类型不对:整个产物判 malformed 整体弃用,不做逐模型挑拣——
10
+ * 快照 = 最后已知良好产物,坏产物宁可整层让位(§5 的产物级降级粒度)。
11
+ *
12
+ * 产物形状的唯一出口在构建侧 `scripts/build-models/emit.ts`(其 `Artifact` 类型
13
+ * 为组装权威);本文件为运行时侧独立定义——跨构建/运行时边界不共享编译单元,
14
+ * 两处字段须保持逐键兼容,改 schema 时两边同步。
15
+ */
16
+ import { asRecord } from "../protocol/json.js";
17
+ /** 运行时已知的产物 schema 上限;上游破坏性变更递增版本号后此处随之上调 */
18
+ const KNOWN_ARTIFACT_SCHEMA_VERSION = 1;
19
+ export function parseArtifact(input) {
20
+ const root = asRecord(input);
21
+ if (root === null)
22
+ return malformed("顶层不是 JSON 对象");
23
+ const version = root["schemaVersion"];
24
+ if (typeof version !== "number" || !Number.isInteger(version))
25
+ return malformed("schemaVersion 必须是整数");
26
+ if (version > KNOWN_ARTIFACT_SCHEMA_VERSION) {
27
+ return { ok: false, error: { reason: "future-version", schemaVersion: version } };
28
+ }
29
+ if (version !== KNOWN_ARTIFACT_SCHEMA_VERSION)
30
+ return malformed(`schemaVersion ${version} 低于已知下限`);
31
+ const generatedAt = root["generatedAt"];
32
+ if (typeof generatedAt !== "string" || generatedAt.length === 0)
33
+ return malformed("generatedAt 必须是非空字符串");
34
+ const sourceCliVersion = root["sourceCliVersion"];
35
+ if (typeof sourceCliVersion !== "string" || sourceCliVersion.length === 0) {
36
+ return malformed("sourceCliVersion 必须是非空字符串");
37
+ }
38
+ const rawModels = root["models"];
39
+ if (!Array.isArray(rawModels))
40
+ return malformed("models 必须是数组");
41
+ const models = [];
42
+ const seen = new Set();
43
+ for (const [index, raw] of rawModels.entries()) {
44
+ const entry = asRecord(raw);
45
+ if (entry === null)
46
+ return malformed(`models[${index}] 不是对象`);
47
+ const id = entry["id"];
48
+ if (typeof id !== "string" || id.length === 0)
49
+ return malformed(`models[${index}].id 必须是非空字符串`);
50
+ if (seen.has(id))
51
+ return malformed(`models[${index}].id「${id}」重复——id 是模型主键`);
52
+ seen.add(id);
53
+ const name = entry["name"];
54
+ if (typeof name !== "string" || name.length === 0)
55
+ return malformed(`models[${index}]「${id}」.name 必须是非空字符串`);
56
+ const reasoning = entry["reasoning"];
57
+ if (typeof reasoning !== "boolean")
58
+ return malformed(`models[${index}]「${id}」.reasoning 必须是布尔`);
59
+ const rawModalities = entry["inputModalities"];
60
+ const inputModalities = asStringArray(rawModalities);
61
+ if (inputModalities === null)
62
+ return malformed(`models[${index}]「${id}」.inputModalities 必须是字符串数组`);
63
+ const context = entry["context"];
64
+ if (typeof context !== "number" || !Number.isFinite(context) || context <= 0) {
65
+ return malformed(`models[${index}]「${id}」.context 必须是正数`);
66
+ }
67
+ const maxOutput = entry["maxOutput"];
68
+ if (typeof maxOutput !== "number" || !Number.isFinite(maxOutput) || maxOutput <= 0) {
69
+ return malformed(`models[${index}]「${id}」.maxOutput 必须是正数`);
70
+ }
71
+ const rawEfforts = entry["efforts"];
72
+ let efforts;
73
+ if (rawEfforts !== undefined) {
74
+ const parsed = asStringArray(rawEfforts);
75
+ if (parsed === null)
76
+ return malformed(`models[${index}]「${id}」.efforts 必须是字符串数组`);
77
+ efforts = parsed;
78
+ }
79
+ models.push({
80
+ id,
81
+ name,
82
+ reasoning,
83
+ inputModalities,
84
+ // efforts 无档位不写字段,与构建侧出口形状逐键一致
85
+ ...(efforts !== undefined ? { efforts } : {}),
86
+ context,
87
+ maxOutput,
88
+ });
89
+ }
90
+ return {
91
+ ok: true,
92
+ artifact: {
93
+ schemaVersion: 1,
94
+ generatedAt,
95
+ sourceCliVersion,
96
+ models,
97
+ },
98
+ };
99
+ }
100
+ function malformed(detail) {
101
+ return { ok: false, error: { reason: "malformed", detail } };
102
+ }
103
+ /** 字符串数组守卫:非数组或含非字符串项返回 null(空数组合法——语义由调用方定) */
104
+ function asStringArray(value) {
105
+ if (!Array.isArray(value))
106
+ return null;
107
+ return value.every((item) => typeof item === "string") ? value : null;
108
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * 运行时三层级联合并(契约:docs/spec/model-pipeline.md §3)。
3
+ *
4
+ * 原则:**每字段单一天窗**,权威 = 离网关事实最近的来源,包内快照永远最后(§3 字段表):
5
+ * - 发现(暴露哪些 id):API ∩ 产物层——产物拉取失败时快照顶替产物角色,交集规则
6
+ * 不变(「快照兜发现时同规则」);API 整体失败时发现退化为产物层 id 清单。
7
+ * - `context` / `name`:API 权威(`context_length` 是网关实际执行值;name 同为
8
+ * API 字段),字段级缺失时逐级回落产物 → 快照。
9
+ * - `efforts` / variants:产物层独供,无兜底链。
10
+ * - `reasoning` / `inputModalities` / `maxOutput`:产物层权威;「快照」兜底由产物
11
+ * 解析的整体弃用语义结构性保证——坏产物进不到本模块,只可能整层换成快照。
12
+ * - `tool_call`:常量 `true`,在宿主消费映射(mapping.ts)落键。
13
+ *
14
+ * 纯函数、无 I/O:拉取、TTL、变更签名与降级重试归模型管线 II(#34);本模块只把
15
+ * 已解析的层合并成宿主消费映射的直接输入,并暴露产物层来源供降级日志注明退到了哪层。
16
+ */
17
+ import type { ApiModelEntry } from "./api.js";
18
+ import type { Artifact } from "./artifact.js";
19
+ /**
20
+ * 级联解析后的单模型:字段取值已定天窗,是宿主消费映射(mapping.ts)的直接输入。
21
+ * 当前与 `ArtifactModel` 同构属有意为之——独立命名表达它是级联出口这个领域概念,
22
+ * 将来 API 独有字段落进运行时(如模型自报能力)后两边分化。
23
+ */
24
+ export interface ResolvedModel {
25
+ readonly id: string;
26
+ readonly name: string;
27
+ readonly reasoning: boolean;
28
+ readonly inputModalities: readonly string[];
29
+ readonly efforts?: readonly string[];
30
+ readonly context: number;
31
+ readonly maxOutput: number;
32
+ }
33
+ export interface ModelLayers {
34
+ /** `/provider/v1/models` 解析结果;缺省 = API 失败,发现退化为产物层 id 清单 */
35
+ readonly api?: readonly ApiModelEntry[];
36
+ /** 构建产物;缺省 = 拉取/解析失败,快照顶替产物角色 */
37
+ readonly artifact?: Artifact;
38
+ /** 包内快照:与产物同 schema、随插件发版内置,永远可用 */
39
+ readonly snapshot: Artifact;
40
+ }
41
+ /** 产物角色由谁扮演——模型管线 II 的降级日志据此注明退到了哪层(§5) */
42
+ export type ProductLayer = "artifact" | "snapshot";
43
+ export interface CascadeResult {
44
+ readonly models: readonly ResolvedModel[];
45
+ readonly productLayer: ProductLayer;
46
+ }
47
+ export declare function mergeModelLayers(layers: ModelLayers): CascadeResult;