@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
package/devthink.ts DELETED
@@ -1,397 +0,0 @@
1
- #!/usr/bin/env bun
2
-
3
- import { readFileSync } from "node:fs";
4
- import { createInterface } from "node:readline/promises";
5
- import { stdin, stdout } from "node:process";
6
- import { clearAuthCredential, ensurePaths, getConfigValue, migrateLegacyCredentials, parseConfigValue, readAuth, readConfig, redactAuth, redactConfig, resolvePaths, saveConfig, setAuthCredential, setConfigValue, type DevThinkConfig } from "./config.ts";
7
- import { createMemoryStore, memorySummary } from "./memory.ts";
8
- import { listModes, modePrompt, resolveMode } from "./modes.ts";
9
- import { listModels, listProviders, streamChat } from "./providers.ts";
10
- import { appendMessage, createSession, exportSession, listSessions, loadSession, type Session } from "./session.ts";
11
- import { createPairing, createPairingLink, getIdentity, pairingStatus, revokeBrowserSessions, setIdentityUserId } from "./identity.ts";
12
- import { readPreferences, savePreference } from "./storage.ts";
13
- import type { ChatEvent } from "./stream.ts";
14
- import { banner, box, colors, formatConfig, formatEvent, statusBar } from "./ui.ts";
15
- import { startServer } from "./server.ts";
16
- import { exportLocalSnapshot, remoteSyncStatus } from "./sync.ts";
17
-
18
- export type ParsedArgs = { flags: Record<string, string | boolean>; positional: string[] };
19
-
20
- function version(): string {
21
- const embedded = process.env.DEVTHINK_VERSION?.trim();
22
- if (embedded) return embedded;
23
- try {
24
- const file = new URL("./package.json", import.meta.url);
25
- return String((JSON.parse(readFileSync(file, "utf8")) as { version?: string }).version || "0.0.0");
26
- } catch {
27
- return "0.0.0";
28
- }
29
- }
30
-
31
- function parseArgs(args: string[]): ParsedArgs {
32
- const flags: Record<string, string | boolean> = {};
33
- const positional: string[] = [];
34
- for (let index = 0; index < args.length; index += 1) {
35
- const value = args[index];
36
- if (!value.startsWith("-")) {
37
- positional.push(value);
38
- continue;
39
- }
40
- const key = value.replace(/^-+/, "");
41
- const next = args[index + 1];
42
- if (next && !next.startsWith("-")) {
43
- flags[key] = next;
44
- index += 1;
45
- } else flags[key] = true;
46
- }
47
- return { flags, positional };
48
- }
49
-
50
- function stringFlag(parsed: ParsedArgs, key: string): string | undefined {
51
- const value = parsed.flags[key];
52
- return typeof value === "string" ? value : undefined;
53
- }
54
-
55
- function boolFlag(parsed: ParsedArgs, key: string): boolean {
56
- return parsed.flags[key] === true;
57
- }
58
-
59
- function printHelp(): void {
60
- console.log([
61
- `${colors.cyan}${colors.bold}DevThink CLI${colors.reset} v${version()}`,
62
- "",
63
- "Usage: devthink <command> [options]",
64
- "",
65
- "Commands:",
66
- " chat [message] Stream a model response",
67
- " providers List supported provider transports",
68
- " models --provider <id> List provider models",
69
- " modes List the 20 operational modes",
70
- " config [key] [value] Read or write local configuration",
71
- " config preferences Show shared workbench preferences",
72
- " config settings Show shared identity, pairing, provider and database summary",
73
- " config set <key> <value> Set theme, railMode, or interfaceZoom",
74
- " auth login <provider> Save a user-provided official credential",
75
- " auth status Show redacted configured credential sources",
76
- " auth clear <provider> Remove a stored provider credential",
77
- " auth migrate Move legacy config credentials into auth.json",
78
- " identity [--id <value>] Show or set the local public user identifier",
79
- " pair create Create an automatic one-time link for the web workbench",
80
- " pair revoke Revoke current paired browser sessions",
81
- " sync status Show local snapshot and remote adapter readiness",
82
- " sync export Print a credential-free local sync snapshot",
83
- " gateway status Show the embedded provider gateway state",
84
- " sessions list List saved sessions",
85
- " sessions export <id> Print a session as Markdown or JSON",
86
- " projects List local workspaces shared with the web",
87
- " usage Show local workspace, session, tab and message counts",
88
- " routes List browser and gateway routes",
89
- " serve [--port <n>] Start the local loopback API",
90
- " init Create local DevThink directories",
91
- " interactive Start the terminal chat loop",
92
- "",
93
- "Chat options:",
94
- " --provider <id> openai, zai, anthropic, google, openrouter, qwen, deepseek, groq, mistral, xai, ollama, mimo",
95
- " --model <id> Provider model identifier",
96
- " --mode <id> One of the 20 registered modes",
97
- " --prompt <text> Explicit prompt, useful in scripts",
98
- " --format json Emit one machine-readable result",
99
- " --no-save Do not persist the session",
100
- "",
101
- "Credentials are read from ~/.config/devthink/auth.json or environment variables. Browser-cookie capture and anti-bot bypass are not supported.",
102
- ].join("\n"));
103
- }
104
-
105
- function loadRuntime(): { config: DevThinkConfig; paths: ReturnType<typeof resolvePaths>; memory: ReturnType<typeof createMemoryStore> } {
106
- const paths = ensurePaths(resolvePaths());
107
- const config = readConfig(paths);
108
- return { config, paths, memory: createMemoryStore(paths) };
109
- }
110
-
111
- function printEvents(events: ChatEvent[], json: boolean): void {
112
- if (json) {
113
- console.log(JSON.stringify(events));
114
- return;
115
- }
116
- for (const event of events) {
117
- const rendered = formatEvent(event);
118
- if (rendered) process.stdout.write(rendered);
119
- }
120
- process.stdout.write("\n");
121
- }
122
-
123
- async function runChat(prompt: string, parsed: ParsedArgs, runtime: ReturnType<typeof loadRuntime>, current?: Session, render?: { onEvent?: (event: ChatEvent) => void; silent?: boolean }): Promise<Session | undefined> {
124
- const provider = stringFlag(parsed, "provider") || runtime.config.activeProvider || runtime.config.provider;
125
- const model = stringFlag(parsed, "model") || runtime.config.activeModel || runtime.config.model;
126
- const mode = stringFlag(parsed, "mode") || "chat";
127
- if (!provider || !model) throw new Error("Set provider and model with --provider/--model or devthink config.");
128
- const modeDefinition = resolveMode(mode);
129
- const requestedSession = stringFlag(parsed, "session");
130
- const session = current || (requestedSession ? loadSession(runtime.paths, requestedSession) : undefined) || createSession(runtime.paths, { mode: modeDefinition.id, model, provider, workspaceId: stringFlag(parsed, "workspace"), tabId: stringFlag(parsed, "tab") });
131
- const storedMemory = runtime.memory.resolve({ text: prompt, sessionId: session.id });
132
- const system = [modePrompt(modeDefinition), storedMemory ? `Relevant local memory: ${storedMemory.content}` : memorySummary(runtime.memory)].filter(Boolean).join("\n\n");
133
- const userMessage = { role: "user" as const, content: prompt };
134
- const nextSession = appendMessage(runtime.paths, session, userMessage);
135
- const events: ChatEvent[] = [];
136
- let text = "";
137
- let reasoning = "";
138
- const json = stringFlag(parsed, "format") === "json";
139
- const stream = await streamChat({ provider, model, messages: [{ role: "system", content: system }, ...nextSession.messages.map(({ role, content }) => ({ role, content }))], temperature: runtime.config.temperature, maxTokens: runtime.config.maxTokens }, runtime.config, runtime.paths);
140
- for await (const event of stream) {
141
- events.push(event);
142
- if (event.type === "text") text += event.text;
143
- if (event.type === "reasoning") reasoning += event.text;
144
- render?.onEvent?.(event);
145
- if (!json && !render?.silent) {
146
- const rendered = formatEvent(event);
147
- if (rendered) process.stdout.write(rendered);
148
- }
149
- }
150
- const withAssistant = appendMessage(runtime.paths, nextSession, { role: "assistant", content: text });
151
- runtime.memory.remember({ layer: "session", key: withAssistant.id, content: text.slice(0, 2000) });
152
- if (json) console.log(JSON.stringify({ workspaceId: withAssistant.workspaceId, sessionId: withAssistant.id, tabId: withAssistant.activeTabId, messageId: withAssistant.messages.at(-1)?.id, provider, model, mode: modeDefinition.id, text, reasoning }));
153
- else if (!render?.silent) process.stdout.write("\n");
154
- return withAssistant;
155
- }
156
-
157
- async function handleConfig(parsed: ParsedArgs, runtime: ReturnType<typeof loadRuntime>): Promise<void> {
158
- const action = parsed.positional[1];
159
- if (action === "settings") {
160
- const identity = getIdentity(runtime.paths);
161
- const sessions = listSessions(runtime.paths);
162
- return console.log(JSON.stringify({
163
- identity,
164
- pairing: pairingStatus(runtime.paths),
165
- preferences: Object.fromEntries(Object.entries(readPreferences(runtime.paths)).map(([key, preference]) => [key, preference.value])),
166
- provider: { activeProvider: runtime.config.activeProvider || runtime.config.provider || undefined, activeModel: runtime.config.activeModel || runtime.config.model || undefined },
167
- database: { ownerUserId: identity.userId, local: true, persistence: "cli-owned-sqlite", workspaces: new Set(sessions.map((session) => session.workspaceId)).size, sessions: sessions.length },
168
- }, null, 2));
169
- }
170
- if (action === "preferences") return console.log(JSON.stringify(Object.fromEntries(Object.entries(readPreferences(runtime.paths)).map(([key, preference]) => [key, preference.value])), null, 2));
171
- if (action === "set") {
172
- const key = parsed.positional[2];
173
- const value = parsed.positional[3];
174
- if (!key || !value) throw new Error("Usage: devthink config set <theme|railMode|interfaceZoom> <value>");
175
- const valid = (key === "theme" && ["dark", "light"].includes(value)) || (key === "railMode" && ["always", "auto", "off"].includes(value)) || (key === "interfaceZoom" && /^(?:[8-9][0-9]|1[0-4][0-9]|150)$/.test(value));
176
- if (!valid) throw new Error("Supported values: theme dark|light; railMode always|auto|off; interfaceZoom 80-150.");
177
- savePreference(runtime.paths, key, value);
178
- return console.log(`${key} updated.`);
179
- }
180
- const key = action;
181
- const raw = parsed.positional[2];
182
- if (!key) return console.log(formatConfig(redactConfig(runtime.config)));
183
- if (raw === undefined) return console.log(`${key} = ${JSON.stringify(getConfigValue(runtime.config, key))}`);
184
- saveConfig(setConfigValue(runtime.config, key, parseConfigValue(raw)), runtime.paths);
185
- console.log(`${key} updated.`);
186
- }
187
-
188
- async function handleAuth(parsed: ParsedArgs, runtime: ReturnType<typeof loadRuntime>): Promise<void> {
189
- const action = parsed.positional[1] || "status";
190
- if (action === "status") return console.log(JSON.stringify({ config: redactConfig(runtime.config), auth: redactAuth(readAuth(runtime.paths)), authPath: runtime.paths.auth }, null, 2));
191
- if (action === "login") {
192
- const provider = parsed.positional[2];
193
- const token = stringFlag(parsed, "token") || stringFlag(parsed, "api-key");
194
- const kind = stringFlag(parsed, "kind") || "api-key";
195
- if (!provider || !token) throw new Error("Usage: devthink auth login <provider> --token <user-provided-credential> [--kind api-key|bearer|oauth]");
196
- if (!(["api-key", "bearer", "oauth"] as string[]).includes(kind)) throw new Error("--kind must be api-key, bearer or oauth.");
197
- const expiresAt = stringFlag(parsed, "expires-at");
198
- const refreshToken = stringFlag(parsed, "refresh-token");
199
- const resourceUrl = stringFlag(parsed, "resource-url");
200
- const credential = kind === "oauth" ? { kind: "oauth" as const, accessToken: token, ...(refreshToken ? { refreshToken } : {}), ...(expiresAt ? { expiresAt: Number(expiresAt) } : {}), ...(resourceUrl ? { resourceUrl } : {}) } : { kind: kind as "api-key" | "bearer", value: token };
201
- setAuthCredential(provider, credential, runtime.paths);
202
- saveConfig({ ...runtime.config, activeProvider: runtime.config.activeProvider || provider }, runtime.paths);
203
- return console.log(`${provider} credential saved to ${runtime.paths.auth}.`);
204
- }
205
- if (action === "clear") {
206
- const provider = parsed.positional[2];
207
- if (!provider) throw new Error("Usage: devthink auth clear <provider>");
208
- clearAuthCredential(provider, runtime.paths);
209
- const next = { ...runtime.config };
210
- const providers = { ...(next.providers || {}) };
211
- if (providers[provider]) {
212
- const { auth: _auth, apiKey: _apiKey, ...remaining } = providers[provider];
213
- providers[provider] = remaining;
214
- }
215
- next.providers = providers;
216
- delete (next as Record<string, unknown>)[`${provider}ApiKey`];
217
- saveConfig(next, runtime.paths);
218
- return console.log(`${provider} credential cleared from ${runtime.paths.auth}; legacy config fields were removed if present.`);
219
- }
220
- if (action === "migrate") {
221
- const result = migrateLegacyCredentials(runtime.config, runtime.paths);
222
- return console.log(result.migrated.length ? `Migrated ${result.migrated.join(", ")} to ${runtime.paths.auth}.` : "No legacy credentials needed migration.");
223
- }
224
- throw new Error("Supported auth actions: login, status, clear, migrate.");
225
- }
226
-
227
- function handleIdentity(parsed: ParsedArgs, runtime: ReturnType<typeof loadRuntime>): void {
228
- const requestedUserId = stringFlag(parsed, "id");
229
- const identity = requestedUserId ? setIdentityUserId(runtime.paths, requestedUserId) : getIdentity(runtime.paths);
230
- console.log(JSON.stringify({ identity, pairing: pairingStatus(runtime.paths) }, null, 2));
231
- }
232
-
233
- function handlePairing(parsed: ParsedArgs, runtime: ReturnType<typeof loadRuntime>): void {
234
- const action = parsed.positional[1] || "create";
235
- if (action === "status") return handleIdentity(parsed, runtime);
236
- if (action === "revoke") return console.log(`Revoked ${revokeBrowserSessions(runtime.paths)} paired browser session(s).`);
237
- if (action !== "create") throw new Error("Supported pair actions: create, status, revoke.");
238
- const pairing = createPairing(runtime.paths);
239
- const pagesUrl = stringFlag(parsed, "web-url") || runtime.config.web?.pagesUrl;
240
- const gatewayUrl = stringFlag(parsed, "gateway") || runtime.config.web?.gatewayUrl;
241
- const openUrl = createPairingLink(pagesUrl, gatewayUrl, pairing.pairingId, pairing.code);
242
- console.log(JSON.stringify({ userId: pairing.identity.userId, pairingId: pairing.pairingId, code: pairing.code, expiresAt: pairing.expiresAt, ...(pagesUrl ? { pagesUrl } : {}), ...(gatewayUrl ? { gatewayUrl } : {}), ...(openUrl ? { openUrl } : {}), message: openUrl ? "Open openUrl to prefill and consume this one-time local invitation automatically." : "Set web.pagesUrl and web.gatewayUrl, or pass --web-url and --gateway, to generate an automatic invitation link." }, null, 2));
243
- }
244
-
245
- function handleSync(parsed: ParsedArgs, runtime: ReturnType<typeof loadRuntime>): void {
246
- const action = parsed.positional[1] || "status";
247
- const snapshot = exportLocalSnapshot(runtime.paths);
248
- if (action === "export") return console.log(JSON.stringify(snapshot, null, 2));
249
- if (action === "status") return console.log(JSON.stringify({ userId: snapshot.userId, deviceId: snapshot.deviceId, workspaces: snapshot.workspaces.length, sessions: snapshot.sessions.length, remote: remoteSyncStatus(runtime.config) }, null, 2));
250
- throw new Error("Supported sync actions: status, export.");
251
- }
252
-
253
- function handleGateway(runtime: ReturnType<typeof loadRuntime>): void {
254
- const activeProvider = runtime.config.activeProvider || runtime.config.provider || "not configured";
255
- const endpoint = activeProvider === "not configured" ? undefined : runtime.config.providers?.[activeProvider]?.baseUrl || runtime.config.baseUrl;
256
- console.log(formatConfig({
257
- mode: runtime.config.gateway?.mode || "embedded",
258
- streaming: runtime.config.gateway?.stream ?? true,
259
- activeProvider,
260
- endpoint: endpoint || "provider default",
261
- webEnabled: runtime.config.web?.enabled ?? true,
262
- config: runtime.paths.config,
263
- auth: runtime.paths.auth,
264
- }));
265
- }
266
-
267
- async function handleSessions(parsed: ParsedArgs, runtime: ReturnType<typeof loadRuntime>): Promise<void> {
268
- const action = parsed.positional[1] || "list";
269
- if (action === "list") return console.log(listSessions(runtime.paths).map((session) => `${session.id} ${session.title}`).join("\n") || "No sessions.");
270
- if (action === "export") {
271
- const id = parsed.positional[2];
272
- if (!id) throw new Error("Usage: devthink sessions export <id>");
273
- console.log(exportSession(runtime.paths, id, stringFlag(parsed, "format") === "json" ? "json" : "markdown"));
274
- return;
275
- }
276
- throw new Error("Supported session actions: list, export.");
277
- }
278
-
279
- function handleProjects(runtime: ReturnType<typeof loadRuntime>): void {
280
- const projects = new Map<string, { title: string; sessions: number; updatedAt: string }>();
281
- for (const session of listSessions(runtime.paths)) {
282
- const current = projects.get(session.workspaceId);
283
- projects.set(session.workspaceId, { title: current?.title || session.title.replace(/^Untitled session$/, "Untitled workspace"), sessions: (current?.sessions || 0) + 1, updatedAt: current?.updatedAt && current.updatedAt > session.updatedAt ? current.updatedAt : session.updatedAt });
284
- }
285
- const output = [...projects.entries()].sort((left, right) => right[1].updatedAt.localeCompare(left[1].updatedAt)).map(([id, project]) => `${id} ${project.sessions} session${project.sessions === 1 ? "" : "s"} ${project.title}`);
286
- console.log(output.join("\n") || "No projects.");
287
- }
288
-
289
- function handleUsage(runtime: ReturnType<typeof loadRuntime>): void {
290
- const sessions = listSessions(runtime.paths);
291
- console.log(JSON.stringify({ workspaces: new Set(sessions.map((session) => session.workspaceId)).size, sessions: sessions.length, tabs: sessions.reduce((total, session) => total + session.tabs.length, 0), messages: sessions.reduce((total, session) => total + session.messages.length, 0), providers: listProviders().length }, null, 2));
292
- }
293
-
294
- function handleRoutes(): void {
295
- console.log(["web: /", "web: /providers", "web: /projects", "web: /routes", "web: /usage", "web: /settings", "web: /w/:workspaceId/s/:sessionId/t/:tabId/:sectionId", "gateway: GET /health", "gateway: GET /identity", "gateway: PUT /identity", "gateway: GET /settings", "gateway: GET /providers", "gateway: GET /workspaces", "gateway: GET /usage", "gateway: GET /preferences", "gateway: PATCH /preferences", "gateway: POST /sessions", "gateway: POST /chat"].join("\n"));
296
- }
297
-
298
- async function interactive(runtime: ReturnType<typeof loadRuntime>): Promise<void> {
299
- if (process.stdin.isTTY && process.stdout.isTTY && process.env.DEVTHINK_PLAIN !== "1") {
300
- try {
301
- const { startTerminalWorkspace } = await import("./terminal-ui.tsx");
302
- return await startTerminalWorkspace(runtime, version(), (prompt, current, onEvent) => runChat(prompt, { flags: { mode: runtime.config.mode || "chat" }, positional: [] }, runtime, current, { onEvent, silent: true }));
303
- } catch (error) {
304
- process.stderr.write(`${colors.yellow}Terminal workspace fallback: ${error instanceof Error ? error.message : "renderer unavailable"}${colors.reset}\n`);
305
- }
306
- }
307
- const input = createInterface({ input: stdin, output: stdout, terminal: true });
308
- let session: Session | undefined;
309
- let mode = runtime.config.mode || "chat";
310
- console.log(banner(version()));
311
- console.log(box("status", statusBar(runtime.config.activeProvider || runtime.config.provider || "not configured", runtime.config.activeModel || runtime.config.model || "not configured", mode)));
312
- console.log("Type a message or /help. Use /exit to leave.");
313
- try {
314
- while (true) {
315
- const value = (await input.question("> ")).trim();
316
- if (!value) continue;
317
- if (value === "/exit" || value === "/quit") break;
318
- if (value === "/help") {
319
- console.log("/models /modes /new /sessions /mode <id> /clear /exit");
320
- continue;
321
- }
322
- if (value === "/clear") {
323
- console.clear();
324
- continue;
325
- }
326
- if (value === "/new") {
327
- session = undefined;
328
- console.log("New session ready.");
329
- continue;
330
- }
331
- if (value.startsWith("/mode ")) {
332
- mode = resolveMode(value.slice(6).trim()).id;
333
- console.log(`Mode: ${mode}`);
334
- continue;
335
- }
336
- if (value === "/modes") {
337
- console.log(listModes().map((item) => `${item.id} ${item.purpose}`).join("\n"));
338
- continue;
339
- }
340
- if (value === "/sessions") {
341
- console.log(listSessions(runtime.paths).map((item) => `${item.id} ${item.title}`).join("\n") || "No sessions.");
342
- continue;
343
- }
344
- if (value === "/models") {
345
- const provider = runtime.config.activeProvider || runtime.config.provider;
346
- if (!provider) console.log("Configure a provider first.");
347
- else console.log((await listModels(provider, runtime.config)).map((item) => item.id).join("\n"));
348
- continue;
349
- }
350
- session = await runChat(value, { flags: { mode }, positional: [] }, runtime, session);
351
- }
352
- } finally {
353
- input.close();
354
- }
355
- }
356
-
357
- export async function main(argv = process.argv.slice(2)): Promise<void> {
358
- const parsed = parseArgs(argv);
359
- const runtime = loadRuntime();
360
- const command = parsed.positional[0] || (process.stdin.isTTY ? "interactive" : "help");
361
- if (boolFlag(parsed, "help") || command === "help") return printHelp();
362
- if (boolFlag(parsed, "version") || command === "version") return console.log(version());
363
- if (command === "init") return console.log(`Initialized ${runtime.paths.home}\nConfig: ${runtime.paths.config}`);
364
- if (command === "providers") return console.log(listProviders().map((provider) => `${provider.id} ${provider.protocol} ${provider.env}`).join("\n"));
365
- if (command === "modes") return console.log(listModes().map((mode) => `${mode.id} ${mode.purpose}`).join("\n"));
366
- if (command === "config") return handleConfig(parsed, runtime);
367
- if (command === "auth") return handleAuth(parsed, runtime);
368
- if (command === "identity") return handleIdentity(parsed, runtime);
369
- if (command === "pair") return handlePairing(parsed, runtime);
370
- if (command === "sync") return handleSync(parsed, runtime);
371
- if (command === "gateway") return handleGateway(runtime);
372
- if (command === "sessions") return handleSessions(parsed, runtime);
373
- if (command === "projects") return handleProjects(runtime);
374
- if (command === "usage") return handleUsage(runtime);
375
- if (command === "routes") return handleRoutes();
376
- if (command === "models") {
377
- const provider = stringFlag(parsed, "provider") || runtime.config.activeProvider || runtime.config.provider;
378
- if (!provider) throw new Error("Use --provider or configure provider.");
379
- return console.log((await listModels(provider, runtime.config, runtime.paths)).map((model) => `${model.id} ${model.provider}`).join("\n"));
380
- }
381
- if (command === "serve") {
382
- const server = await startServer({ port: stringFlag(parsed, "port") ? Number(stringFlag(parsed, "port")) : undefined, config: runtime.config, paths: runtime.paths });
383
- console.log(`DevThink API listening at ${server.address}`);
384
- return;
385
- }
386
- if (command === "interactive") return interactive(runtime);
387
- if (command === "chat") {
388
- const prompt = stringFlag(parsed, "prompt") || parsed.positional.slice(1).join(" ");
389
- if (!prompt) throw new Error("Usage: devthink chat --prompt \"your message\"");
390
- return void (await runChat(prompt, parsed, runtime));
391
- }
392
- throw new Error(`Unknown command: ${command}`);
393
- }
394
-
395
- const directEntry = process.argv[1]?.endsWith("devthink.ts") || process.argv[1]?.endsWith("devthink");
396
- const bunEntry = (import.meta as ImportMeta & { main?: boolean }).main === true;
397
- if (directEntry || bunEntry) main().catch((error: unknown) => { console.error(`${colors.red}Error:${colors.reset} ${error instanceof Error ? error.message : "Command failed."}`); process.exitCode = 1; });
package/identity.ts DELETED
@@ -1,156 +0,0 @@
1
- import { createHash, randomInt, timingSafeEqual } from "node:crypto";
2
- import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
3
- import { ensurePaths, type DevThinkPaths } from "./config.ts";
4
- import { createCompactId } from "./ids.ts";
5
-
6
- export type DevThinkIdentity = { version: 1; userId: string; deviceId: string; createdAt: string };
7
- export type PairingRecord = { id: string; userId: string; deviceId: string; codeHash?: string; createdAt: string; expiresAt: number; usedAt?: string; revokedAt?: string };
8
- export type BrowserSession = { id: string; userId: string; deviceId: string; tokenHash: string; createdAt: string; expiresAt: number; revokedAt?: string };
9
- type PairingStore = { version: 1; pairings: PairingRecord[]; sessions: BrowserSession[] };
10
-
11
- const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
12
- const codeLength = 8;
13
- const pairingLifetimeMs = 5 * 60 * 1000;
14
- const browserSessionLifetimeMs = 15 * 60 * 1000;
15
-
16
- export function normalizePublicUserId(value: string): string {
17
- const userId = value.trim().toLowerCase();
18
- if (!/^[a-z][a-z0-9]{9,14}$/.test(userId)) throw new Error("Public user ID must contain 10-15 lowercase letters or numbers and start with a letter.");
19
- return userId;
20
- }
21
-
22
- function digest(value: string): string {
23
- return createHash("sha256").update(value).digest("hex");
24
- }
25
-
26
- function privateWrite(path: string, value: unknown): void {
27
- const temporary = `${path}.${process.pid}.${Date.now()}.tmp`;
28
- writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
29
- renameSync(temporary, path);
30
- }
31
-
32
- function createId(prefix: string): string {
33
- return createCompactId(prefix);
34
- }
35
-
36
- function code(): string {
37
- return Array.from({ length: codeLength }, () => alphabet[randomInt(alphabet.length)]).join("");
38
- }
39
-
40
- export function getIdentity(paths: DevThinkPaths): DevThinkIdentity {
41
- ensurePaths(paths);
42
- try {
43
- if (existsSync(paths.identity)) {
44
- const parsed: unknown = JSON.parse(readFileSync(paths.identity, "utf8"));
45
- if (parsed && typeof parsed === "object" && typeof (parsed as DevThinkIdentity).userId === "string" && typeof (parsed as DevThinkIdentity).deviceId === "string") return parsed as DevThinkIdentity;
46
- }
47
- } catch {}
48
- const identity: DevThinkIdentity = { version: 1, userId: createId("u"), deviceId: createId("d"), createdAt: new Date().toISOString() };
49
- privateWrite(paths.identity, identity);
50
- return identity;
51
- }
52
-
53
- /** Updates the public local-person identifier and migrates active local pairing records. */
54
- export function setIdentityUserId(paths: DevThinkPaths, requestedUserId: string): DevThinkIdentity {
55
- const current = getIdentity(paths);
56
- const userId = normalizePublicUserId(requestedUserId);
57
- if (current.userId === userId) return current;
58
- const next = { ...current, userId };
59
- privateWrite(paths.identity, next);
60
- const store = readStore(paths);
61
- for (const record of store.pairings) if (record.userId === current.userId) record.userId = userId;
62
- for (const session of store.sessions) if (session.userId === current.userId) session.userId = userId;
63
- writeStore(paths, store);
64
- return next;
65
- }
66
-
67
- function readStore(paths: DevThinkPaths): PairingStore {
68
- try {
69
- if (existsSync(paths.pairings)) {
70
- const parsed: unknown = JSON.parse(readFileSync(paths.pairings, "utf8"));
71
- if (parsed && typeof parsed === "object" && Array.isArray((parsed as PairingStore).pairings) && Array.isArray((parsed as PairingStore).sessions)) return parsed as PairingStore;
72
- }
73
- } catch {}
74
- return { version: 1, pairings: [], sessions: [] };
75
- }
76
-
77
- function writeStore(paths: DevThinkPaths, store: PairingStore): void {
78
- ensurePaths(paths);
79
- privateWrite(paths.pairings, store);
80
- }
81
-
82
- function active(store: PairingStore): PairingStore {
83
- const now = Date.now();
84
- return { version: 1, pairings: store.pairings.filter((record) => record.expiresAt > now || Boolean(record.usedAt) || Boolean(record.revokedAt)).slice(-100), sessions: store.sessions.filter((session) => session.expiresAt > now && !session.revokedAt).slice(-100) };
85
- }
86
-
87
- export function createPairing(paths: DevThinkPaths, lifetimeMs = pairingLifetimeMs): { identity: DevThinkIdentity; pairingId: string; code: string; expiresAt: number } {
88
- const identity = getIdentity(paths);
89
- const store = active(readStore(paths));
90
- const pairingId = createId("p");
91
- const oneTimeCode = code();
92
- const expiresAt = Date.now() + lifetimeMs;
93
- store.pairings.push({ id: pairingId, userId: identity.userId, deviceId: identity.deviceId, codeHash: digest(oneTimeCode), createdAt: new Date().toISOString(), expiresAt });
94
- writeStore(paths, store);
95
- return { identity, pairingId, code: oneTimeCode, expiresAt };
96
- }
97
-
98
- /** Builds a one-time workbench invitation without embedding any provider credential. */
99
- export function createPairingLink(pagesUrl: string | undefined, gatewayUrl: string | undefined, pairingId: string, oneTimeCode: string): string | undefined {
100
- if (!pagesUrl || !gatewayUrl) return undefined;
101
- try {
102
- const url = new URL(pagesUrl);
103
- url.searchParams.set("gateway", gatewayUrl);
104
- url.searchParams.set("pair", pairingId);
105
- url.searchParams.set("code", oneTimeCode);
106
- return url.toString();
107
- } catch {
108
- return undefined;
109
- }
110
- }
111
-
112
- export function consumePairing(paths: DevThinkPaths, pairingId: string, oneTimeCode: string, sessionLifetimeMs = browserSessionLifetimeMs): { token: string; identity: DevThinkIdentity; expiresAt: number } | undefined {
113
- const store = active(readStore(paths));
114
- const record = store.pairings.find((candidate) => candidate.id === pairingId && !candidate.usedAt && !candidate.revokedAt && candidate.expiresAt > Date.now());
115
- if (!record?.codeHash) return undefined;
116
- const supplied = Buffer.from(digest(oneTimeCode));
117
- const stored = Buffer.from(record.codeHash);
118
- if (supplied.length !== stored.length || !timingSafeEqual(supplied, stored)) return undefined;
119
- const token = createId("pt");
120
- const expiresAt = Date.now() + sessionLifetimeMs;
121
- record.usedAt = new Date().toISOString();
122
- delete record.codeHash;
123
- store.sessions.push({ id: createId("bs"), userId: record.userId, deviceId: record.deviceId, tokenHash: digest(token), createdAt: new Date().toISOString(), expiresAt });
124
- writeStore(paths, store);
125
- return { token, identity: getIdentity(paths), expiresAt };
126
- }
127
-
128
- export function verifyBrowserSession(paths: DevThinkPaths, token: string | undefined): BrowserSession | undefined {
129
- if (!token) return undefined;
130
- const store = active(readStore(paths));
131
- const tokenHash = digest(token);
132
- const session = store.sessions.find((candidate) => candidate.tokenHash === tokenHash && candidate.expiresAt > Date.now() && !candidate.revokedAt);
133
- writeStore(paths, store);
134
- return session;
135
- }
136
-
137
- export function revokeBrowserSessions(paths: DevThinkPaths): number {
138
- const store = readStore(paths);
139
- const identity = getIdentity(paths);
140
- let count = 0;
141
- for (const session of store.sessions) {
142
- if (session.userId === identity.userId && !session.revokedAt) {
143
- session.revokedAt = new Date().toISOString();
144
- count += 1;
145
- }
146
- }
147
- writeStore(paths, store);
148
- return count;
149
- }
150
-
151
- export function pairingStatus(paths: DevThinkPaths): { identity: DevThinkIdentity; activePairs: number; activeSessions: number } {
152
- const store = active(readStore(paths));
153
- writeStore(paths, store);
154
- const identity = getIdentity(paths);
155
- return { identity, activePairs: store.pairings.filter((record) => record.userId === identity.userId && !record.usedAt && !record.revokedAt && record.expiresAt > Date.now()).length, activeSessions: store.sessions.filter((session) => session.userId === identity.userId && !session.revokedAt && session.expiresAt > Date.now()).length };
156
- }
package/ids.ts DELETED
@@ -1,22 +0,0 @@
1
- /** DevThink identity contract: compact, lowercase, URL-safe identifiers for new local records. */
2
- import { randomBytes } from "node:crypto";
3
-
4
- const alphabet = "0123456789abcdefghjkmnpqrstvwxyz";
5
- const tokenLength = 10;
6
-
7
- /** Generates a 50-bit compact suffix that avoids ambiguous characters. */
8
- export function compactToken(): string {
9
- const bytes = randomBytes(tokenLength);
10
- return Array.from(bytes, (value) => alphabet[value & 31]).join("");
11
- }
12
-
13
- /** Creates a short identifier such as `s_1e9k2m7v3q`; existing longer IDs remain valid inputs. */
14
- export function createCompactId(prefix: string): string {
15
- const normalized = prefix.toLowerCase().replace(/[^a-z0-9]/g, "").slice(0, 4) || "id";
16
- return `${normalized}_${compactToken()}`;
17
- }
18
-
19
- /** Identifies the compact format without rejecting existing legacy identifiers. */
20
- export function isCompactId(value: string): boolean {
21
- return /^[a-z0-9]{1,4}_[0123456789abcdefghjkmnpqrstvwxyz]{10}$/.test(value);
22
- }