@dan-ai-studio/dshopencodego 0.1.47 → 0.1.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -247,7 +247,7 @@ npm test # contract tests against a real local HTTP mock gateway
247
247
 
248
248
  **Order matters**: `npm run build` must come before `npm test` — `tests/pack-contents.spec.ts` walks every `exports` target, and whether `lib/` exists decides whether that check runs or is silently skipped (it does not fail, it just stops protecting you). `package.json` no longer carries `prepare`, so `npm ci` will not build for you; all three CI workflows run `npm run build` explicitly.
249
249
 
250
- The suite covers configuration validation, the protocol ladder, gateway and online metadata, the catalog projection, the adapter on the wire (all three protocols), Remote contracts and services, session-header invariants, usage windows and the meter, the client controller and usage pill, both halves' mounting and registration, the error mapping for stream idle timeouts and aborts, the exit-code contract of the verification scripts, and several **structural invariants** (inject declarations match the source's reads, peer coverage holds both ways, the READMEs match the manifest, and the READMEs' claims about the upstream match the upstream actually installed). That is **45 test files** — a number `tests/readme-consistency.spec.ts` checks against the `tests/` directory, so it cannot drift.
250
+ The suite covers configuration validation, the protocol ladder, gateway and online metadata, the catalog projection, the adapter on the wire (all three protocols), Remote contracts and services, session-header invariants, usage windows and the meter, the client controller and usage pill, both halves' mounting and registration, the error mapping for stream idle timeouts and aborts, the exit-code contract of the verification scripts, and several **structural invariants** (inject declarations match the source's reads, peer coverage holds both ways, the READMEs match the manifest, and the READMEs' claims about the upstream match the upstream actually installed). That is **46 test files** — a number `tests/readme-consistency.spec.ts` checks against the `tests/` directory, so it cannot drift.
251
251
 
252
252
  ## Releasing (maintainers)
253
253
 
package/README.md CHANGED
@@ -247,7 +247,7 @@ npm test # 契约测试:真实 HTTP mock 网关,断言请求头/
247
247
 
248
248
  **顺序有讲究**:`npm run build` 要排在 `npm test` 之前——`tests/pack-contents.spec.ts` 会遍历 `exports` 的每个目标,`lib/` 存在与否决定这条检查是执行还是被跳过(不报错,但会少一层保护)。`package.json` 已移除 `prepare`,因此 `npm ci` 不会再替你隐式构建;三个 CI workflow 都显式跑了 `npm run build`。
249
249
 
250
- 测试覆盖配置校验、协议阶梯、网关与在线元数据、目录投影、适配器(三种协议的线上请求)、Remote 契约与服务、会话头不变量、用量窗口与计量、客户端控制器与用量组件、两半的挂载与登记、流空闲超时与中止的错误映射、验证脚本的退出码契约、以及几条**结构性不变量**(inject 声明与源码读取一致、peer 覆盖双向、README 与 manifest 一致、README 对上游的断言与实装的上游一致)。共 **45 个测试文件**——这个数字由 `tests/readme-consistency.spec.ts` 对着 `tests/` 目录校验,不会漂移。
250
+ 测试覆盖配置校验、协议阶梯、网关与在线元数据、目录投影、适配器(三种协议的线上请求)、Remote 契约与服务、会话头不变量、用量窗口与计量、客户端控制器与用量组件、两半的挂载与登记、流空闲超时与中止的错误映射、验证脚本的退出码契约、以及几条**结构性不变量**(inject 声明与源码读取一致、peer 覆盖双向、README 与 manifest 一致、README 对上游的断言与实装的上游一致)。共 **46 个测试文件**——这个数字由 `tests/readme-consistency.spec.ts` 对着 `tests/` 目录校验,不会漂移。
251
251
 
252
252
  ## 发布(维护者)
253
253
 
package/lib/index.js CHANGED
@@ -17788,8 +17788,8 @@ function getBunSandboxEnvValue(name2) {
17788
17788
  if (procEnvCache === null) {
17789
17789
  procEnvCache = /* @__PURE__ */ new Map();
17790
17790
  try {
17791
- const { readFileSync } = __require("node:fs");
17792
- const data = readFileSync("/proc/self/environ", "utf-8");
17791
+ const { readFileSync: readFileSync2 } = __require("node:fs");
17792
+ const data = readFileSync2("/proc/self/environ", "utf-8");
17793
17793
  for (const entry of data.split("\0")) {
17794
17794
  const idx = entry.indexOf("=");
17795
17795
  if (idx > 0) {
@@ -42462,6 +42462,8 @@ Check your ChatGPT usage: ${CHATGPT_USAGE_URL}` : errorMessage;
42462
42462
  });
42463
42463
 
42464
42464
  // src/index.ts
42465
+ import { homedir } from "node:os";
42466
+ import { join as join3 } from "node:path";
42465
42467
  import { credentialRef } from "@deepseek-ai/dsh-credentials";
42466
42468
  import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
42467
42469
  import * as llm2 from "@deepseek-ai/dsh-llm";
@@ -42626,6 +42628,78 @@ async function fetchModelIds(baseURL, signal) {
42626
42628
 
42627
42629
  // src/catalog/go-doc.ts
42628
42630
  import { attributionHeaders as attributionHeaders2, LlmError as LlmError3 } from "@deepseek-ai/dsh-llm";
42631
+
42632
+ // src/catalog/go-doc-discovery.ts
42633
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
42634
+ import { join as join2 } from "node:path";
42635
+ var DISCOVERY_URL = "https://api.github.com/repos/anomalyco/opencode/git/trees/dev?recursive=1";
42636
+ var DOCUMENT_NAME = /^go[^/]*\.mdx$/i;
42637
+ var DISCOVERY_MAX_BYTES = 8 * 1024 * 1024;
42638
+ var DISCOVERY_TIMEOUT_MS = 1e4;
42639
+ var CACHE_FILE = "go-doc-paths.json";
42640
+ var CACHE_VERSION = 1;
42641
+ function selectDocumentPaths(paths) {
42642
+ const selected = [];
42643
+ for (const path5 of paths) {
42644
+ const name2 = path5.slice(path5.lastIndexOf("/") + 1);
42645
+ if (!DOCUMENT_NAME.test(name2)) continue;
42646
+ if (!/(^|\/)(docs?|content|console)\//i.test(path5) && !/^packages\/web\/src\/content\/docs\//i.test(path5)) continue;
42647
+ selected.push(path5);
42648
+ }
42649
+ return selected.sort((left, right) => right.length - left.length);
42650
+ }
42651
+ function mirrorsOf(path5) {
42652
+ const encoded = path5.split("/").map((segment) => encodeURIComponent(segment)).join("/");
42653
+ return [
42654
+ `https://raw.githubusercontent.com/anomalyco/opencode/dev/${encoded}`,
42655
+ `https://cdn.jsdelivr.net/gh/anomalyco/opencode@dev/${encoded}`
42656
+ ];
42657
+ }
42658
+ async function discoverDocumentPaths(signal, fetchImpl = fetch) {
42659
+ const timeout = AbortSignal.timeout(DISCOVERY_TIMEOUT_MS);
42660
+ try {
42661
+ const response = await fetchImpl(DISCOVERY_URL, {
42662
+ headers: { accept: "application/vnd.github+json", "user-agent": "dshopencodego" },
42663
+ signal: signal === void 0 ? timeout : AbortSignal.any([signal, timeout])
42664
+ });
42665
+ if (!response.ok) return [];
42666
+ const body = await response.text();
42667
+ if (body.length > DISCOVERY_MAX_BYTES) return [];
42668
+ const listing = JSON.parse(body);
42669
+ const paths = (listing.tree ?? []).map((entry) => entry.path).filter((path5) => typeof path5 === "string");
42670
+ return selectDocumentPaths(paths);
42671
+ } catch {
42672
+ return [];
42673
+ }
42674
+ }
42675
+ function readCachedPaths(cacheDir) {
42676
+ if (cacheDir === void 0) return [];
42677
+ try {
42678
+ const parsed = JSON.parse(readFileSync(join2(cacheDir, CACHE_FILE), "utf8"));
42679
+ if (parsed.version !== CACHE_VERSION || !Array.isArray(parsed.paths)) return [];
42680
+ return parsed.paths.filter((path5) => typeof path5 === "string");
42681
+ } catch {
42682
+ return [];
42683
+ }
42684
+ }
42685
+ function rememberPaths(cacheDir, discovered) {
42686
+ const known = readCachedPaths(cacheDir);
42687
+ const merged = [];
42688
+ for (const path5 of [...discovered, ...known]) {
42689
+ if (!merged.includes(path5)) merged.push(path5);
42690
+ }
42691
+ if (cacheDir === void 0 || discovered.length === 0) return merged;
42692
+ try {
42693
+ mkdirSync(cacheDir, { recursive: true });
42694
+ const file = { version: CACHE_VERSION, paths: merged, updatedAt: Date.now() };
42695
+ writeFileSync(join2(cacheDir, CACHE_FILE), `${JSON.stringify(file, null, 2)}
42696
+ `, "utf8");
42697
+ } catch {
42698
+ }
42699
+ return merged;
42700
+ }
42701
+
42702
+ // src/catalog/go-doc.ts
42629
42703
  var GO_DOC_URLS = [
42630
42704
  "https://raw.githubusercontent.com/anomalyco/opencode/dev/services/www/src/docs/content/console/go.mdx",
42631
42705
  "https://cdn.jsdelivr.net/gh/anomalyco/opencode@dev/services/www/src/docs/content/console/go.mdx"
@@ -42725,18 +42799,46 @@ async function readSource(url, signal) {
42725
42799
  return { failure: `${url}: ${error instanceof Error ? error.message : String(error)}` };
42726
42800
  }
42727
42801
  }
42728
- async function fetchGoDocument(signal) {
42729
- const attempts = await Promise.all(GO_DOC_URLS.map((url) => readSource(url, signal)));
42802
+ async function fetchGoDocument(signal, options = {}) {
42730
42803
  const failures = [];
42731
- for (const attempt of attempts) {
42732
- if ("document" in attempt) return attempt.document;
42733
- failures.push(attempt.failure);
42804
+ const known = await readSources(GO_DOC_URLS, signal);
42805
+ if (known.document !== void 0) return known.document;
42806
+ failures.push(...known.failures);
42807
+ const remembered = readCachedPaths(options.cacheDir).filter((path5) => !GO_DOC_URLS.some((url) => url.endsWith(path5)));
42808
+ if (remembered.length > 0) {
42809
+ const cached = await readSources(remembered.flatMap(mirrorsOf), signal);
42810
+ if (cached.document !== void 0) return cached.document;
42811
+ failures.push(...cached.failures);
42812
+ }
42813
+ const discover = options.discover ?? discoverDocumentPaths;
42814
+ const discovered = await discover(signal);
42815
+ if (discovered.length === 0) {
42816
+ failures.push("the provider repository named no document");
42817
+ } else {
42818
+ const merged = rememberPaths(options.cacheDir, discovered);
42819
+ const fresh = merged.filter((path5) => !GO_DOC_URLS.some((url) => url.endsWith(path5))).flatMap(mirrorsOf);
42820
+ if (fresh.length === 0) {
42821
+ failures.push("the provider repository named the path that was already tried");
42822
+ } else {
42823
+ const result = await readSources(fresh, signal);
42824
+ if (result.document !== void 0) return result.document;
42825
+ failures.push(...result.failures);
42826
+ }
42734
42827
  }
42735
42828
  throw new LlmError3(
42736
42829
  `could not read the Go documentation (${failures.join("; ")})`,
42737
42830
  "DOCUMENT_UNAVAILABLE"
42738
42831
  );
42739
42832
  }
42833
+ async function readSources(urls, signal) {
42834
+ const attempts = await Promise.all(urls.map((url) => readSource(url, signal)));
42835
+ const failures = [];
42836
+ for (const attempt of attempts) {
42837
+ if ("document" in attempt) return { document: attempt.document, failures };
42838
+ failures.push(attempt.failure);
42839
+ }
42840
+ return { failures };
42841
+ }
42740
42842
 
42741
42843
  // src/catalog/protocol.ts
42742
42844
  var WIRE_PROTOCOLS = [
@@ -43074,7 +43176,7 @@ var OpencodeGoCatalog = class {
43074
43176
  */
43075
43177
  async build(signal) {
43076
43178
  const builtin = builtinModels(this.options.baseURL);
43077
- const readDocument = this.options.readDocument ?? fetchGoDocument;
43179
+ const readDocument = this.options.readDocument ?? ((signal2) => fetchGoDocument(signal2, { cacheDir: this.options.cacheDir }));
43078
43180
  const [listing, metadata, documentation] = await Promise.allSettled([
43079
43181
  fetchModelIds(this.options.baseURL),
43080
43182
  this.refreshMetadata(builtin),
@@ -45028,6 +45130,7 @@ var OpencodeGoAdapter = class extends LlmAdapter {
45028
45130
  },
45029
45131
  overrides: config.modelProtocols,
45030
45132
  ...this.options.readDocument === void 0 ? {} : { readDocument: this.options.readDocument },
45133
+ ...this.options.cacheDir === void 0 ? {} : { cacheDir: this.options.cacheDir },
45031
45134
  observers: {
45032
45135
  ...this.options.onFallback === void 0 ? {} : { onFallback: this.options.onFallback },
45033
45136
  ...this.options.onUnconfigured === void 0 ? {} : { onUnconfigured: this.options.onUnconfigured }
@@ -45764,7 +45867,7 @@ function createUpdateChecker(deps) {
45764
45867
  }
45765
45868
 
45766
45869
  // src/update/version.ts
45767
- var PLUGIN_VERSION = true ? "0.1.47" : "0.0.0-dev";
45870
+ var PLUGIN_VERSION = true ? "0.1.48" : "0.0.0-dev";
45768
45871
 
45769
45872
  // src/update/service.ts
45770
45873
  import { TypertRemoteService as TypertRemoteService2 } from "@deepseek-ai/dsh-typert-protocol";
@@ -45901,6 +46004,18 @@ var inject = ["llm"];
45901
46004
  function isLiveConfig(raw) {
45902
46005
  return typeof raw === "object" && raw !== null && typeof raw.enabled === "object" && typeof raw.enabled?.get === "function";
45903
46006
  }
46007
+ function resolveCacheDir(ctx) {
46008
+ const home = ctx.dshHomePath;
46009
+ if (typeof home === "function") {
46010
+ try {
46011
+ return home("cache", "@dan-ai-studio/dshopencodego");
46012
+ } catch {
46013
+ }
46014
+ }
46015
+ const configured = process.env.DSH_HOME?.trim();
46016
+ const root = configured !== void 0 && configured.length > 0 ? configured : join3(homedir(), ".dsh");
46017
+ return join3(root, "cache", "@dan-ai-studio/dshopencodego");
46018
+ }
45904
46019
  function apply(ctx, raw) {
45905
46020
  const live = isLiveConfig(raw) ? raw : void 0;
45906
46021
  const constant = live === void 0 ? PlainConfig(raw ?? {}) : void 0;
@@ -45921,6 +46036,7 @@ function apply(ctx, raw) {
45921
46036
  const adapter = new OpencodeGoAdapter({
45922
46037
  config: current,
45923
46038
  resolveApiKey,
46039
+ cacheDir: resolveCacheDir(ctx),
45924
46040
  imageAccess: {
45925
46041
  resolveAttachments: () => ctx.get("attachments"),
45926
46042
  resolveImageAccess: (attachments, ref) => llm2.resolveImageAttachmentAccess(
@@ -52,6 +52,13 @@ export interface OpencodeGoAdapterOptions {
52
52
  * a fixed document so the suite stays offline. Production omits this.
53
53
  */
54
54
  readonly readDocument?: () => Promise<GoDoc>;
55
+ /**
56
+ * Where the discovered provider-document paths are remembered.
57
+ *
58
+ * Absent means the plugin keeps them in memory only, which costs one
59
+ * discovery per start and nothing else.
60
+ */
61
+ readonly cacheDir?: string;
55
62
  /**
56
63
  * Observe the provider's own usage for one completed call. This is the only
57
64
  * honest source of token counts: the gateway's `/usage` endpoint reports
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Finding the provider's document without being told where it is.
3
+ *
4
+ * The document is the provider's own statement of each model's protocol, and
5
+ * the provider moves it: `go.mdx` lived at
6
+ * `packages/web/src/content/docs/go.mdx` until the `packages/web` tree was
7
+ * restructured, and the plugin then read 404 from both mirrors for as long as
8
+ * the old path was hardcoded. A hardcoded path is a promise the provider did
9
+ * not make.
10
+ *
11
+ * So the path is discovered instead of declared. The discovery is a read-only
12
+ * listing of the provider's own repository, and it runs only when every known
13
+ * path has already failed — the common case costs one request to a file that
14
+ * is there, and the rare case costs one listing.
15
+ *
16
+ * The paths that discovery finds are remembered on disk, as a *set of paths
17
+ * known to have existed*, not as one current path. That distinction matters:
18
+ * a cache holding a single path would hand back a stale path the moment the
19
+ * provider moved the file again, which is exactly the moment the cache is
20
+ * consulted. A set is only ever a list of candidates to try, so a stale entry
21
+ * costs one failed request and nothing else.
22
+ *
23
+ * @module @dan-ai-studio/dshopencodego/catalog/go-doc-discovery
24
+ */
25
+ /** A repository listing is large; the cap keeps a hostile response bounded. */
26
+ export declare const DISCOVERY_MAX_BYTES: number;
27
+ /**
28
+ * The repository paths a discovery run found.
29
+ *
30
+ * A path is only useful if it names the document this plugin parses, and the
31
+ * listing also contains unrelated `go.mdx` files in other packages' fixtures.
32
+ * The path must therefore sit under a documentation tree.
33
+ * @param paths - every repository path the listing mentioned.
34
+ * @returns the paths that name the provider's Go document.
35
+ */
36
+ export declare function selectDocumentPaths(paths: readonly string[]): readonly string[];
37
+ /**
38
+ * The mirrors a repository path can be read from.
39
+ *
40
+ * Two hosts, one file: the repository itself and a CDN that caches it. They
41
+ * fail independently, which is why both are tried.
42
+ * @param path - a repository-relative path.
43
+ * @returns the URLs to read that path from.
44
+ */
45
+ export declare function mirrorsOf(path: string): readonly string[];
46
+ /**
47
+ * Ask the provider's repository where its document is.
48
+ *
49
+ * The listing is unauthenticated, so it is rate-limited. That is why this runs
50
+ * only after every known path has failed, and why the paths it finds are
51
+ * remembered: the next process starts with the answer and never asks.
52
+ * @param signal - caller cancellation.
53
+ * @param fetchImpl - the fetch to use; injected for tests.
54
+ * @returns the repository paths that name the document; empty when the
55
+ * listing could not be read or named nothing.
56
+ */
57
+ export declare function discoverDocumentPaths(signal?: AbortSignal, fetchImpl?: typeof fetch): Promise<readonly string[]>;
58
+ /**
59
+ * The paths the cache remembers, most recently discovered first.
60
+ *
61
+ * A cache that cannot be read is an empty cache: the caller then tries the
62
+ * paths it knows and discovers again.
63
+ * @param cacheDir - the plugin's cache directory.
64
+ * @returns the remembered repository paths.
65
+ */
66
+ export declare function readCachedPaths(cacheDir: string | undefined): readonly string[];
67
+ /**
68
+ * Remember the paths a discovery run found.
69
+ *
70
+ * The known paths are merged rather than replaced, newest first, so a cache
71
+ * that a concurrent process also wrote does not lose the other process's
72
+ * entry. A cache that cannot be written is not an error: the paths stay in
73
+ * memory for this process and are rediscovered next time.
74
+ * @param cacheDir - the plugin's cache directory.
75
+ * @param discovered - the paths a discovery run found.
76
+ * @returns the merged set, in the order it should be tried.
77
+ */
78
+ export declare function rememberPaths(cacheDir: string | undefined, discovered: readonly string[]): readonly string[];
@@ -55,6 +55,23 @@ export interface GoDoc {
55
55
  * document does not carry the expected tables.
56
56
  */
57
57
  export declare function parseGoDocument(markdown: string): GoDoc;
58
+ /**
59
+ * How the document is found and where the answer is kept.
60
+ *
61
+ * The discovery is injectable so a test can decide what the provider's
62
+ * repository says without reaching the network — which is the only way this
63
+ * module can be exercised offline at all.
64
+ */
65
+ export interface GoDocOptions {
66
+ /** Where the discovered document paths are remembered. */
67
+ readonly cacheDir?: string | undefined;
68
+ /**
69
+ * Ask the provider's repository where its document is.
70
+ *
71
+ * Defaults to the real listing. A test injects a fixed answer.
72
+ */
73
+ readonly discover?: (signal?: AbortSignal) => Promise<readonly string[]>;
74
+ }
58
75
  /**
59
76
  * Fetch and parse the documentation, asking every source concurrently.
60
77
  *
@@ -64,9 +81,16 @@ export declare function parseGoDocument(markdown: string): GoDoc;
64
81
  * quota badge and the protocol fallback. `GO_DOC_URLS` order still decides:
65
82
  * the repository file is examined before its cache mirror, and a source that
66
83
  * fails is reported with its own reason.
84
+ *
85
+ * The known paths are tried first, then the paths a previous discovery
86
+ * remembered, and only then is the provider's repository asked where its
87
+ * document is. That order is what keeps the cost at one request in the common
88
+ * case while still surviving the provider moving the file: a path that is
89
+ * there is used, and a path that is not is replaced by a discovered one.
67
90
  * @param signal - caller cancellation; it reaches every source's fetch and
68
91
  * aborts all of them, so a cancelled discovery stops waiting on the network.
92
+ * @param options - where the discovered paths are remembered.
69
93
  * @returns the parsed document from the first source that carried one.
70
94
  * @throws {LlmError} `DOCUMENT_UNAVAILABLE` when every source failed.
71
95
  */
72
- export declare function fetchGoDocument(signal?: AbortSignal): Promise<GoDoc>;
96
+ export declare function fetchGoDocument(signal?: AbortSignal, options?: GoDocOptions): Promise<GoDoc>;
@@ -106,6 +106,14 @@ export interface CatalogOptions {
106
106
  * cancellation, forwarded to every documentation request.
107
107
  */
108
108
  readonly readDocument?: (signal?: AbortSignal) => Promise<GoDoc>;
109
+ /**
110
+ * Where the discovered document paths are remembered.
111
+ *
112
+ * Absent means the plugin cannot write a cache, and the paths are then only
113
+ * known to the running process. That costs one discovery per start, and
114
+ * nothing else.
115
+ */
116
+ readonly cacheDir?: string;
109
117
  }
110
118
  /**
111
119
  * One gateway's catalog. Requests reuse a cached snapshot; explicit discovery
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dan-ai-studio/dshopencodego",
3
- "version": "0.1.47",
3
+ "version": "0.1.48",
4
4
  "description": "OpenCode Go provider for DeepSeek Harness: live gateway catalog, per-conversation session header, usage",
5
5
  "type": "module",
6
6
  "license": "MIT",