@wenathlan/devthink 1.1.15 → 2.0.2

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 (160) hide show
  1. package/CHANGELOG.md +1299 -0
  2. package/README.md +6 -0
  3. package/bridge.d.ts +698 -0
  4. package/bridge.d.ts.map +1 -0
  5. package/bridge.js +1877 -0
  6. package/bun.d.ts +8 -0
  7. package/bun.d.ts.map +1 -0
  8. package/bun.js +52329 -0
  9. package/caps/background.json +712 -0
  10. package/caps/cli.json +360 -0
  11. package/caps/library.json +3611 -0
  12. package/caps/mcp.json +126 -0
  13. package/caps/pagebridge.json +377 -0
  14. package/caps/popup.json +51 -0
  15. package/caps/sidepanel.json +522 -0
  16. package/checksums.txt +142 -0
  17. package/cli.d.ts +187 -0
  18. package/cli.d.ts.map +1 -0
  19. package/cli.js +18197 -0
  20. package/companion.d.ts +19 -0
  21. package/companion.d.ts.map +1 -0
  22. package/companion.js +389 -0
  23. package/crossbrowser.d.ts +277 -0
  24. package/crossbrowser.d.ts.map +1 -0
  25. package/crossbrowser.js +1191 -0
  26. package/dashdone.d.ts +273 -0
  27. package/dashdone.d.ts.map +1 -0
  28. package/dashdone.js +368 -0
  29. package/deno.d.ts +11 -0
  30. package/deno.d.ts.map +1 -0
  31. package/deno.js +52325 -0
  32. package/devthink.umd.js +52346 -0
  33. package/fixtures/bun.ts +7 -0
  34. package/fixtures/cjs.js +6 -0
  35. package/fixtures/deno.ts +6 -0
  36. package/fixtures/example-org-pagestate.json +22 -0
  37. package/fixtures/importers/automa-workflow.json +18 -0
  38. package/fixtures/importers/selenium-side.json +20 -0
  39. package/fixtures/importers/tabular-plan.csv +7 -0
  40. package/fixtures/importers/uivision-macro.json +10 -0
  41. package/fixtures/importers/v1-plan.json +13 -0
  42. package/fixtures/mcp-client.mjs +14 -0
  43. package/fixtures/pages/checkout.html +192 -0
  44. package/fixtures/pages/dashboard.html +110 -0
  45. package/fixtures/pages/feed.html +113 -0
  46. package/fixtures/pages/product-grid.html +150 -0
  47. package/fixtures/plans/form-inventory-plan.json +11 -0
  48. package/fixtures/plans/release-notes-plan.json +11 -0
  49. package/fixtures/recipes/agentscompete.json +11 -0
  50. package/fixtures/recipes/agentsforms.json +11 -0
  51. package/fixtures/recipes/agentsmonitor.json +11 -0
  52. package/fixtures/recipes/agentsreview.json +29 -0
  53. package/fixtures/recipes/agentswarm.json +11 -0
  54. package/fixtures/recipes/fillcalendar.json +12 -0
  55. package/fixtures/recipes/fillcheckout.json +13 -0
  56. package/fixtures/recipes/filldependentdropdowns.json +12 -0
  57. package/fixtures/recipes/fillfromcsv.json +13 -0
  58. package/fixtures/recipes/fillloginform.json +12 -0
  59. package/fixtures/recipes/fillmultistepwizard.json +13 -0
  60. package/fixtures/recipes/fillregistration.json +12 -0
  61. package/fixtures/recipes/fillsearchfilters.json +12 -0
  62. package/fixtures/recipes/filltypeahead.json +11 -0
  63. package/fixtures/recipes/monitoralerts.json +12 -0
  64. package/fixtures/recipes/monitoravailability.json +11 -0
  65. package/fixtures/recipes/monitornetworkquiet.json +12 -0
  66. package/fixtures/recipes/monitorpagechanges.json +11 -0
  67. package/fixtures/recipes/monitorpricechange.json +11 -0
  68. package/fixtures/recipes/monitorschedule.json +12 -0
  69. package/fixtures/recipes/scrapedynamicprices.json +13 -0
  70. package/fixtures/recipes/scrapeimagegallery.json +12 -0
  71. package/fixtures/recipes/scrapeinfinitefeed.json +13 -0
  72. package/fixtures/recipes/scrapenestedlists.json +12 -0
  73. package/fixtures/recipes/scrapeproductgrid.json +12 -0
  74. package/fixtures/recipes/scrapereviews.json +12 -0
  75. package/fixtures/recipes/scrapesearchresults.json +12 -0
  76. package/fixtures/recipes/scrapetablepagination.json +12 -0
  77. package/fixtures/recipes/testaccessibility.json +34 -0
  78. package/fixtures/recipes/testconsoleerrors.json +12 -0
  79. package/fixtures/recipes/testdeeplinks.json +12 -0
  80. package/fixtures/recipes/testformvalidation.json +13 -0
  81. package/fixtures/recipes/testlayoutshift.json +12 -0
  82. package/fixtures/recipes/testlinkcheck.json +12 -0
  83. package/fixtures/recipes/testloadtiming.json +12 -0
  84. package/fixtures/recipes/testvisualdiff.json +12 -0
  85. package/fixtures/workflows/changelog-digest-workflow.json +13 -0
  86. package/fixtures/workflows/form-survey-workflow.json +13 -0
  87. package/gallery.json +706 -0
  88. package/hardening.d.ts +81 -0
  89. package/hardening.d.ts.map +1 -0
  90. package/hardening.js +188 -0
  91. package/headless.d.ts +114 -0
  92. package/headless.d.ts.map +1 -0
  93. package/headless.js +11522 -0
  94. package/http.d.ts +636 -0
  95. package/http.d.ts.map +1 -0
  96. package/http.js +2165 -0
  97. package/index.cjs +54682 -0
  98. package/index.d.ts +40 -0
  99. package/index.d.ts.map +1 -0
  100. package/index.js +52310 -0
  101. package/index.neutral.js +46343 -0
  102. package/manifest.json +199 -0
  103. package/mcp.d.ts +501 -0
  104. package/mcp.d.ts.map +1 -0
  105. package/mcp.js +20515 -0
  106. package/memory.d.ts +2397 -0
  107. package/memory.d.ts.map +1 -0
  108. package/memory.js +5782 -0
  109. package/nativehost.template.json +10 -0
  110. package/node.cjs +54686 -0
  111. package/node.d.ts +8 -0
  112. package/node.d.ts.map +1 -0
  113. package/pack.d.ts +211 -0
  114. package/pack.d.ts.map +1 -0
  115. package/pack.js +805 -0
  116. package/package.json +101 -51
  117. package/policy.d.ts +1854 -0
  118. package/policy.d.ts.map +1 -0
  119. package/policy.js +12580 -0
  120. package/progress.d.ts +390 -0
  121. package/progress.d.ts.map +1 -0
  122. package/progress.js +645 -0
  123. package/protocol.d.ts +2336 -0
  124. package/protocol.d.ts.map +1 -0
  125. package/protocol.js +8334 -0
  126. package/schemas/audit.schema.json +226 -0
  127. package/schemas/capability.schema.json +113 -0
  128. package/schemas/envelope.schema.json +162 -0
  129. package/schemas/memory.schema.json +51 -0
  130. package/schemas/observation.schema.json +113 -0
  131. package/schemas/plan.schema.json +208 -0
  132. package/schemas/progress.schema.json +126 -0
  133. package/schemas/proposal.schema.json +68 -0
  134. package/schemas/session.schema.json +82 -0
  135. package/schemas/tool.schema.json +94 -0
  136. package/server.d.ts +435 -0
  137. package/server.d.ts.map +1 -0
  138. package/server.js +28872 -0
  139. package/umd-example.html +18 -0
  140. package/umd.d.ts +6 -0
  141. package/umd.d.ts.map +1 -0
  142. package/build.ts +0 -41
  143. package/check-secrets.ts +0 -52
  144. package/compatibility.ts +0 -37
  145. package/config.ts +0 -246
  146. package/devthink.ts +0 -397
  147. package/identity.ts +0 -156
  148. package/ids.ts +0 -22
  149. package/memory.ts +0 -95
  150. package/modes.ts +0 -43
  151. package/plugin.ts +0 -68
  152. package/providers.ts +0 -179
  153. package/server.ts +0 -264
  154. package/session.ts +0 -155
  155. package/storage.ts +0 -82
  156. package/stream.ts +0 -130
  157. package/sync.ts +0 -27
  158. package/terminal-ui.tsx +0 -155
  159. package/ui.ts +0 -143
  160. package/workspace.ts +0 -20
@@ -0,0 +1,18 @@
1
+ <!-- devthink 2.0.2 umd example page — GPL-3.0-only -->
2
+ <!doctype html>
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="utf-8">
6
+ <title>Devthink umd example</title>
7
+ </head>
8
+ <body>
9
+ <h1>Devthink umd script tag example</h1>
10
+ <p>This page loads the umd bundle from a file url and prints the bundle stamp of the running library; the consent gates stay intact inside the browser bundle.</p>
11
+ <pre id="out">loading…</pre>
12
+ <script src="./devthink.umd.js"></script>
13
+ <script>
14
+ const out = document.getElementById("out");
15
+ out.textContent = JSON.stringify(window.devthink.bundlestamp(), null, 2) + "\n" + Object.keys(window.devthink).length + " exports on window.devthink";
16
+ </script>
17
+ </body>
18
+ </html>
package/umd.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Umd entry of the 1.1.67 family, restamped by the 1.1.81 library modes and sunset at 2.0.0.
3
+ * The shared core for script tag consumers: the build emits this entry as the global devthink bundle of the browser platform target, tree shakable imports, the whole library behind one script tag with the consent gates intact. The 2.0.0 sunset removed the deprecated uppercase Devthink global the deprecation window carried: the envelope exposes exactly the lowercase devthink surface the frozen library contract declares.
4
+ */
5
+ export * from "./index.js";
6
+ //# sourceMappingURL=umd.d.ts.map
package/umd.d.ts.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"umd.d.ts","sourceRoot":"","sources":["../umd.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,cAAc,YAAY,CAAC"}
package/build.ts DELETED
@@ -1,41 +0,0 @@
1
- import { readFileSync } from "node:fs";
2
- import { arch, platform } from "node:process";
3
-
4
- type BunRuntime = {
5
- build: (options: Record<string, unknown>) => Promise<{ success: boolean; logs: unknown[] }>;
6
- };
7
-
8
- function bunRuntime(): BunRuntime {
9
- const runtime = (globalThis as unknown as { Bun?: BunRuntime }).Bun;
10
- if (!runtime) throw new Error("Bun is required to compile DevThink binaries.");
11
- return runtime;
12
- }
13
-
14
- function flag(name: string): string | undefined {
15
- const index = process.argv.indexOf(name);
16
- return index >= 0 ? process.argv[index + 1] : undefined;
17
- }
18
-
19
- function packageVersion(): string {
20
- const packageData = JSON.parse(readFileSync(new URL("./package.json", import.meta.url), "utf8")) as { version?: string };
21
- return packageData.version || "0.0.0";
22
- }
23
-
24
- function localCompileTarget(): string {
25
- const os = platform === "darwin" ? "darwin" : platform === "win32" ? "windows" : "linux";
26
- const cpu = arch === "arm64" ? "arm64" : "x64";
27
- return `bun-${os}-${cpu}`;
28
- }
29
-
30
- export async function buildBinary(): Promise<void> {
31
- const runtime = bunRuntime();
32
- const target = flag("--target") || process.env.BUN_TARGET || localCompileTarget();
33
- const outfile = flag("--outfile") || process.env.DEVTHINK_OUTFILE || `release/devthink-${target}`;
34
- const result = await runtime.build({ entrypoints: ["./devthink.ts"], target, define: { "process.env.DEVTHINK_VERSION": JSON.stringify(packageVersion()) }, compile: { target, outfile }, minify: true, sourcemap: "none" });
35
- if (!result.success) throw new Error(`Bun compilation failed: ${JSON.stringify(result.logs)}`);
36
- console.log(`Built ${outfile}`);
37
- }
38
-
39
- const directEntry = process.argv[1]?.endsWith("build.ts") || process.argv[1]?.endsWith("build");
40
- const bunEntry = (import.meta as ImportMeta & { main?: boolean }).main === true;
41
- if (directEntry || bunEntry) buildBinary().catch((error: unknown) => { console.error(error instanceof Error ? error.message : "Build failed."); process.exitCode = 1; });
package/check-secrets.ts DELETED
@@ -1,52 +0,0 @@
1
- import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
- import { join } from "node:path";
3
-
4
- const patterns = [
5
- /sk-[A-Za-z0-9_-]{20,}/g,
6
- /AIza[A-Za-z0-9_-]{20,}/g,
7
- /xox[baprs]-[A-Za-z0-9-]{16,}/g,
8
- /gh[pousr]_[A-Za-z0-9_]{20,}/g,
9
- /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/g,
10
- /eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}/g,
11
- ];
12
-
13
- function productFiles(root: string): string[] {
14
- const files: string[] = [];
15
- const visit = (path: string) => {
16
- if (!existsSync(path)) return;
17
- const stat = statSync(path);
18
- if (stat.isFile()) return files.push(path);
19
- for (const entry of readdirSync(path)) {
20
- if (entry === "node_modules" || entry === ".git" || entry === "docs" || entry === "dist" || entry === "release") continue;
21
- visit(join(path, entry));
22
- }
23
- };
24
- visit(join(root, "devthink.ts"));
25
- for (const name of ["config.ts", "providers.ts", "stream.ts", "session.ts", "memory.ts", "modes.ts", "plugin.ts", "server.ts", "ui.ts", "build.ts", "check-secrets.ts", "package.json", "tsconfig.json", "README.md", "CHANGELOG.md", "SECURITY.md"]) visit(join(root, name));
26
- visit(join(root, ".github"));
27
- visit(join(root, "tests"));
28
- return files.filter((path) => existsSync(path));
29
- }
30
-
31
- export function scanSecrets(root = process.cwd()): string[] {
32
- const findings: string[] = [];
33
- for (const file of productFiles(root)) {
34
- const text = readFileSync(file, "utf8");
35
- for (const pattern of patterns) {
36
- if (pattern.test(text)) findings.push(`${file}: ${pattern}`);
37
- pattern.lastIndex = 0;
38
- }
39
- }
40
- return findings;
41
- }
42
-
43
- const directEntry = process.argv[1]?.endsWith("check-secrets.ts") || process.argv[1]?.endsWith("check-secrets");
44
- const bunEntry = (import.meta as ImportMeta & { main?: boolean }).main === true;
45
- if (directEntry || bunEntry) {
46
- const findings = scanSecrets();
47
- if (findings.length) {
48
- console.error("Potential embedded credentials detected:");
49
- for (const finding of findings) console.error(finding);
50
- process.exitCode = 1;
51
- } else console.log("Secret scan passed.");
52
- }
package/compatibility.ts DELETED
@@ -1,37 +0,0 @@
1
- /**
2
- * Shared TypeScript compatibility primitives derived from the converted docs logic.
3
- * These helpers intentionally exclude browser automation, CAPTCHA handling and credential capture.
4
- */
5
-
6
- /** Identifies the public wire formats normalized by DevThink. */
7
- export type ProviderProtocol = "openai" | "anthropic" | "google";
8
-
9
- /** Returns bounded exponential retry timing for a transient request failure. */
10
- export function retryDelay(attempt: number, baseMs = 250, maximumMs = 4_000): number {
11
- return Math.min(maximumMs, baseMs * 2 ** Math.max(0, attempt));
12
- }
13
-
14
- /** Removes a bearer value from an error message before it reaches a terminal or browser surface. */
15
- export function redactProviderError(message: string): string {
16
- return message.replace(/bearer\s+[^\s]+/gi, "bearer [redacted]").slice(0, 500);
17
- }
18
-
19
- /** Accepts a browser origin only when it exactly matches an explicit local configuration entry. */
20
- export function isAllowedOrigin(origin: string | undefined, allowedOrigins: readonly string[] | undefined): boolean {
21
- return Boolean(origin && allowedOrigins?.includes(origin));
22
- }
23
-
24
- /** Verifies that a user-operated synchronization endpoint uses HTTPS. */
25
- export function isSecureRemoteEndpoint(value: string | undefined): boolean {
26
- try {
27
- return Boolean(value && new URL(value).protocol === "https:");
28
- } catch {
29
- return false;
30
- }
31
- }
32
-
33
- /** Normalizes a static-site base path for repository-scoped GitHub Pages deployments. */
34
- export function normalizeBasePath(value: string | undefined, repository: string): string {
35
- const candidate = value?.trim() || `/${repository}/`;
36
- return `/${candidate.replace(/^\/+|\/+$/g, "")}/`;
37
- }
package/config.ts DELETED
@@ -1,246 +0,0 @@
1
- import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
- import { homedir, platform } from "node:os";
3
- import { join } from "node:path";
4
-
5
- export type DevThinkConfig = {
6
- activeProvider?: string;
7
- activeModel?: string;
8
- provider?: string;
9
- model?: string;
10
- mode?: string;
11
- baseUrl?: string;
12
- apiKey?: string;
13
- anthropicApiKey?: string;
14
- googleApiKey?: string;
15
- zaiApiKey?: string;
16
- temperature?: number;
17
- maxTokens?: number;
18
- projectMemory?: boolean;
19
- externalMemory?: boolean;
20
- fallbackProviders?: string[];
21
- providers?: Record<string, {
22
- baseUrl?: string;
23
- apiKey?: string;
24
- model?: string;
25
- transport?: "official" | "openai-compatible" | "local-gateway";
26
- auth?: { kind: "api-key" | "bearer" | "oauth"; value?: string; expiresAt?: number };
27
- }>;
28
- gateway?: { mode?: "embedded"; stream?: boolean; host?: string };
29
- web?: { enabled?: boolean; pagesUrl?: string; gatewayUrl?: string; allowedOrigins?: string[]; remoteSync?: { enabled?: boolean; endpoint?: string } };
30
- };
31
-
32
- export type AuthKind = "api-key" | "bearer" | "oauth";
33
- export type AuthCredential = { kind: AuthKind; value?: string; accessToken?: string; refreshToken?: string; expiresAt?: number; resourceUrl?: string; updatedAt: string };
34
- export type DevThinkAuth = { version: 1; providers: Record<string, AuthCredential> };
35
-
36
- export type DevThinkPaths = {
37
- home: string;
38
- config: string;
39
- auth: string;
40
- identity: string;
41
- pairings: string;
42
- legacyConfig: string;
43
- sessions: string;
44
- workspaces: string;
45
- database: string;
46
- legacyDatabase: string;
47
- memory: string;
48
- logs: string;
49
- };
50
-
51
- function resolveHome(): string {
52
- const override = process.env.DEVTHINK_HOME?.trim();
53
- if (override) return override;
54
- if (platform() === "win32") return join(process.env.APPDATA || homedir(), "devthink");
55
- if (platform() === "darwin") return join(homedir(), "Library", "Application Support", "devthink");
56
- return join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "devthink");
57
- }
58
-
59
- export function resolvePaths(root = resolveHome()): DevThinkPaths {
60
- return { home: root, config: join(root, "devthink.json"), auth: join(root, "auth.json"), identity: join(root, "identity.json"), pairings: join(root, "pairings.json"), legacyConfig: join(root, "config.json"), sessions: join(root, "sessions"), workspaces: join(root, "workspaces"), database: join(root, "devthink.db"), legacyDatabase: join(root, "devthink.sqlite"), memory: join(root, "memory"), logs: join(root, "logs") };
61
- }
62
-
63
- export function ensurePaths(paths = resolvePaths()): DevThinkPaths {
64
- for (const path of [paths.home, paths.sessions, paths.workspaces, paths.memory, paths.logs]) mkdirSync(path, { recursive: true });
65
- if (!existsSync(paths.database) && existsSync(paths.legacyDatabase)) {
66
- try { copyFileSync(paths.legacyDatabase, paths.database); } catch { /* The JSON session records remain the safe compatibility fallback. */ }
67
- }
68
- return paths;
69
- }
70
-
71
- function isRecord(value: unknown): value is Record<string, unknown> {
72
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
73
- }
74
-
75
- function writeJson(path: string, value: unknown, mode?: number): void {
76
- const temporary = `${path}.${process.pid}.${Date.now()}.tmp`;
77
- writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, mode ? { encoding: "utf8", mode } : "utf8");
78
- renameSync(temporary, path);
79
- }
80
-
81
- export function readConfig(paths = resolvePaths()): DevThinkConfig {
82
- try {
83
- const candidate = existsSync(paths.config) ? paths.config : paths.legacyConfig;
84
- if (!existsSync(candidate)) return {};
85
- const parsed: unknown = JSON.parse(readFileSync(candidate, "utf8"));
86
- if (!isRecord(parsed)) return {};
87
- const config = parsed as DevThinkConfig;
88
- return { ...config, activeProvider: config.activeProvider || config.provider, activeModel: config.activeModel || config.model };
89
- } catch {
90
- return {};
91
- }
92
- }
93
-
94
- export function saveConfig(config: DevThinkConfig, paths = resolvePaths()): void {
95
- ensurePaths(paths);
96
- writeJson(paths.config, { ...config, activeProvider: config.activeProvider || config.provider, activeModel: config.activeModel || config.model, gateway: { mode: "embedded" as const, stream: true, ...config.gateway } });
97
- }
98
-
99
- const forbiddenAuthFields = new Set(["cookie", "cookies", "fingerprint", "useragent", "sessiontoken", "captcha", "browserprofile"]);
100
-
101
- function validCredential(value: unknown): AuthCredential | undefined {
102
- if (!isRecord(value) || Object.keys(value).some((key) => forbiddenAuthFields.has(key.toLowerCase()))) return undefined;
103
- const kind = String(value.kind) as AuthKind;
104
- if (!["api-key", "bearer", "oauth"].includes(kind)) return undefined;
105
- const token = typeof value.value === "string" ? value.value : undefined;
106
- const accessToken = typeof value.accessToken === "string" ? value.accessToken : undefined;
107
- const refreshToken = typeof value.refreshToken === "string" ? value.refreshToken : undefined;
108
- if ((kind === "oauth" && !accessToken) || (kind !== "oauth" && !token)) return undefined;
109
- return { kind, ...(token ? { value: token } : {}), ...(accessToken ? { accessToken } : {}), ...(refreshToken ? { refreshToken } : {}), ...(typeof value.expiresAt === "number" ? { expiresAt: value.expiresAt } : {}), ...(typeof value.resourceUrl === "string" ? { resourceUrl: value.resourceUrl } : {}), updatedAt: typeof value.updatedAt === "string" ? value.updatedAt : new Date().toISOString() };
110
- }
111
-
112
- export function readAuth(paths = resolvePaths()): DevThinkAuth {
113
- try {
114
- if (!existsSync(paths.auth)) return { version: 1, providers: {} };
115
- const parsed: unknown = JSON.parse(readFileSync(paths.auth, "utf8"));
116
- if (!isRecord(parsed) || !isRecord(parsed.providers)) return { version: 1, providers: {} };
117
- const providers: Record<string, AuthCredential> = {};
118
- for (const [provider, raw] of Object.entries(parsed.providers)) {
119
- const credential = validCredential(raw);
120
- if (credential) providers[provider.toLowerCase()] = credential;
121
- }
122
- return { version: 1, providers };
123
- } catch {
124
- return { version: 1, providers: {} };
125
- }
126
- }
127
-
128
- export function saveAuth(auth: DevThinkAuth, paths = resolvePaths()): void {
129
- ensurePaths(paths);
130
- const providers: Record<string, AuthCredential> = {};
131
- for (const [provider, raw] of Object.entries(auth.providers)) {
132
- const credential = validCredential(raw);
133
- if (credential) providers[provider.toLowerCase()] = credential;
134
- }
135
- writeJson(paths.auth, { version: 1, providers }, 0o600);
136
- }
137
-
138
- export function setAuthCredential(provider: string, credential: Omit<AuthCredential, "updatedAt">, paths = resolvePaths()): DevThinkAuth {
139
- const next = { ...credential, updatedAt: new Date().toISOString() };
140
- if (!validCredential(next)) throw new Error("Credential must use an official api-key, bearer, or OAuth shape without browser-session fields.");
141
- const current = readAuth(paths);
142
- const auth = { version: 1 as const, providers: { ...current.providers, [provider.toLowerCase()]: next } };
143
- saveAuth(auth, paths);
144
- return auth;
145
- }
146
-
147
- export function clearAuthCredential(provider: string, paths = resolvePaths()): DevThinkAuth {
148
- const current = readAuth(paths);
149
- const providers = { ...current.providers };
150
- delete providers[provider.toLowerCase()];
151
- const auth = { version: 1 as const, providers };
152
- saveAuth(auth, paths);
153
- return auth;
154
- }
155
-
156
- export function migrateLegacyCredentials(config: DevThinkConfig, paths = resolvePaths()): { config: DevThinkConfig; migrated: string[] } {
157
- const current = readAuth(paths);
158
- const providers = { ...current.providers };
159
- const nextConfig: DevThinkConfig = { ...config, providers: { ...config.providers } };
160
- const migrated: string[] = [];
161
- for (const [provider, settings] of Object.entries(config.providers || {})) {
162
- const legacy = settings.auth?.value || settings.apiKey;
163
- if (!legacy || providers[provider]) continue;
164
- const kind = settings.auth?.kind || "api-key";
165
- providers[provider] = kind === "oauth" ? { kind, accessToken: legacy, expiresAt: settings.auth?.expiresAt, updatedAt: new Date().toISOString() } : { kind, value: legacy, expiresAt: settings.auth?.expiresAt, updatedAt: new Date().toISOString() };
166
- const { auth: _auth, apiKey: _apiKey, ...rest } = settings;
167
- nextConfig.providers![provider] = rest;
168
- migrated.push(provider);
169
- }
170
- for (const [provider, property] of Object.entries({ openai: "apiKey", anthropic: "anthropicApiKey", google: "googleApiKey", zai: "zaiApiKey" })) {
171
- const value = nextConfig[property as keyof DevThinkConfig];
172
- if (typeof value === "string" && value && !providers[provider]) {
173
- providers[provider] = { kind: "api-key", value, updatedAt: new Date().toISOString() };
174
- delete (nextConfig as Record<string, unknown>)[property];
175
- migrated.push(provider);
176
- }
177
- }
178
- saveAuth({ version: 1, providers }, paths);
179
- saveConfig(nextConfig, paths);
180
- return { config: nextConfig, migrated };
181
- }
182
-
183
- export function getConfigValue(config: DevThinkConfig, key: string): unknown {
184
- return (config as Record<string, unknown>)[key];
185
- }
186
-
187
- export function setConfigValue(config: DevThinkConfig, key: string, value: unknown): DevThinkConfig {
188
- return { ...config, [key]: value };
189
- }
190
-
191
- export function resolveCredential(provider: string, config: DevThinkConfig, paths = resolvePaths()): string | undefined {
192
- const auth = readAuth(paths).providers[provider.toLowerCase()];
193
- if (auth) {
194
- if (auth.kind === "oauth" && auth.expiresAt && auth.expiresAt <= Date.now()) throw new Error(`Official OAuth credential for ${provider} has expired. Reauthenticate with the provider-owned flow.`);
195
- return auth.kind === "oauth" ? auth.accessToken : auth.value;
196
- }
197
- const envNames: Record<string, string[]> = { openai: ["OPENAI_API_KEY"], anthropic: ["ANTHROPIC_API_KEY"], google: ["GOOGLE_API_KEY", "GEMINI_API_KEY"], zai: ["ZAI_API_KEY"], qwen: ["QWEN_API_KEY"], openrouter: ["OPENROUTER_API_KEY"], deepseek: ["DEEPSEEK_API_KEY"], groq: ["GROQ_API_KEY"], mistral: ["MISTRAL_API_KEY"], xai: ["XAI_API_KEY"], ollama: ["OLLAMA_API_KEY"], mimo: ["MIMO_API_KEY"] };
198
- for (const name of envNames[provider.toLowerCase()] || []) {
199
- const value = process.env[name]?.trim();
200
- if (value) return value;
201
- }
202
- const property = `${provider}ApiKey` as keyof DevThinkConfig;
203
- const nested = config.providers?.[provider];
204
- const legacy = config[property];
205
- const legacyValue = typeof legacy === "string" ? legacy : undefined;
206
- return nested?.auth?.value || nested?.apiKey || legacyValue || (provider === (config.activeProvider || config.provider) ? config.apiKey : undefined) || process.env.DEVTHINK_API_KEY?.trim();
207
- }
208
-
209
- export function redactValue(value: unknown): unknown {
210
- if (typeof value !== "string") return value;
211
- if (value.length < 8) return "[redacted]";
212
- return `${value.slice(0, 3)}...${value.slice(-3)}`;
213
- }
214
-
215
- export function redactAuth(auth: DevThinkAuth): Record<string, unknown> {
216
- return { version: auth.version, providers: Object.fromEntries(Object.entries(auth.providers).map(([provider, credential]) => [provider, { kind: credential.kind, ...(credential.value ? { value: redactValue(credential.value) } : {}), ...(credential.accessToken ? { accessToken: redactValue(credential.accessToken) } : {}), ...(credential.refreshToken ? { refreshToken: redactValue(credential.refreshToken) } : {}), ...(credential.expiresAt ? { expiresAt: credential.expiresAt } : {}), ...(credential.resourceUrl ? { resourceUrl: credential.resourceUrl } : {}), updatedAt: credential.updatedAt }])) };
217
- }
218
-
219
- export function redactConfig(config: DevThinkConfig): Record<string, unknown> {
220
- const sensitive = new Set(["apiKey", "anthropicApiKey", "googleApiKey", "zaiApiKey"]);
221
- return Object.fromEntries(Object.entries(config).map(([key, value]) => {
222
- if (key === "providers" && value && typeof value === "object") {
223
- const providers = Object.fromEntries(Object.entries(value as Record<string, Record<string, unknown>>).map(([id, settings]) => [id, Object.fromEntries(Object.entries(settings).map(([setting, item]) => {
224
- if (setting === "apiKey") return [setting, redactValue(item)];
225
- if (setting === "auth" && item && typeof item === "object") {
226
- const auth = item as Record<string, unknown>;
227
- return [setting, { ...auth, value: redactValue(auth.value) }];
228
- }
229
- return [setting, item];
230
- }))]));
231
- return [key, providers];
232
- }
233
- return [key, sensitive.has(key) || key.toLowerCase().endsWith("apikey") ? redactValue(value) : value];
234
- }));
235
- }
236
-
237
- export function parseConfigValue(raw: string): unknown {
238
- if (raw === "true") return true;
239
- if (raw === "false") return false;
240
- if (/^-?\d+$/.test(raw)) return Number.parseInt(raw, 10);
241
- if (/^-?\d+\.\d+$/.test(raw)) return Number.parseFloat(raw);
242
- if (raw.startsWith("[") || raw.startsWith("{")) {
243
- try { return JSON.parse(raw); } catch { return raw; }
244
- }
245
- return raw;
246
- }