devglide 0.1.1

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 (252) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +338 -0
  3. package/bin/claude-md-template.js +94 -0
  4. package/bin/devglide.js +387 -0
  5. package/package.json +85 -0
  6. package/pnpm-workspace.yaml +3 -0
  7. package/src/apps/coder/.turbo/turbo-lint.log +5 -0
  8. package/src/apps/coder/package.json +16 -0
  9. package/src/apps/coder/public/favicon.svg +7 -0
  10. package/src/apps/coder/public/page.css +275 -0
  11. package/src/apps/coder/public/page.js +528 -0
  12. package/src/apps/coder/server.js +3 -0
  13. package/src/apps/documentation/public/page.css +597 -0
  14. package/src/apps/documentation/public/page.js +609 -0
  15. package/src/apps/kanban/.turbo/turbo-lint.log +97 -0
  16. package/src/apps/kanban/.turbo/turbo-typecheck.log +5 -0
  17. package/src/apps/kanban/package.json +32 -0
  18. package/src/apps/kanban/public/favicon.svg +7 -0
  19. package/src/apps/kanban/public/page.css +1010 -0
  20. package/src/apps/kanban/public/page.js +1730 -0
  21. package/src/apps/kanban/public/vendor/marked.min.js +6 -0
  22. package/src/apps/kanban/public/vendor/sortable.min.js +2 -0
  23. package/src/apps/kanban/src/db.ts +319 -0
  24. package/src/apps/kanban/src/index.ts +14 -0
  25. package/src/apps/kanban/src/mcp-helpers.test.ts +88 -0
  26. package/src/apps/kanban/src/mcp-helpers.ts +60 -0
  27. package/src/apps/kanban/src/mcp.ts +59 -0
  28. package/src/apps/kanban/src/routes/attachments.ts +161 -0
  29. package/src/apps/kanban/src/routes/features.ts +233 -0
  30. package/src/apps/kanban/src/routes/issues.ts +373 -0
  31. package/src/apps/kanban/src/tools/feature-tools.ts +164 -0
  32. package/src/apps/kanban/src/tools/item-tools.ts +307 -0
  33. package/src/apps/kanban/src/tools/versioned-entry-tools.ts +72 -0
  34. package/src/apps/kanban/tsconfig.check.json +9 -0
  35. package/src/apps/kanban/tsconfig.json +9 -0
  36. package/src/apps/keymap/.turbo/turbo-lint.log +5 -0
  37. package/src/apps/keymap/package.json +16 -0
  38. package/src/apps/keymap/public/page.css +275 -0
  39. package/src/apps/keymap/public/page.js +294 -0
  40. package/src/apps/keymap/server.js +25 -0
  41. package/src/apps/log/.turbo/turbo-build.log +5 -0
  42. package/src/apps/log/.turbo/turbo-lint.log +45 -0
  43. package/src/apps/log/.turbo/turbo-typecheck.log +5 -0
  44. package/src/apps/log/node_modules/.bin/tsc +21 -0
  45. package/src/apps/log/node_modules/.bin/tsserver +21 -0
  46. package/src/apps/log/node_modules/.bin/tsx +21 -0
  47. package/src/apps/log/package.json +36 -0
  48. package/src/apps/log/public/console-sniffer.js +221 -0
  49. package/src/apps/log/public/favicon.svg +7 -0
  50. package/src/apps/log/public/page.css +322 -0
  51. package/src/apps/log/public/page.js +463 -0
  52. package/src/apps/log/src/index.ts +9 -0
  53. package/src/apps/log/src/mcp.ts +122 -0
  54. package/src/apps/log/src/routes/log.ts +333 -0
  55. package/src/apps/log/src/routes/status.ts +25 -0
  56. package/src/apps/log/src/server-sniffer.ts +118 -0
  57. package/src/apps/log/src/services/file-patterns.ts +39 -0
  58. package/src/apps/log/src/services/file-tailer.ts +228 -0
  59. package/src/apps/log/src/services/line-parser.ts +94 -0
  60. package/src/apps/log/src/services/log-writer.ts +39 -0
  61. package/src/apps/log/tsconfig.json +8 -0
  62. package/src/apps/prompts/.turbo/turbo-build.log +5 -0
  63. package/src/apps/prompts/.turbo/turbo-lint.log +24 -0
  64. package/src/apps/prompts/.turbo/turbo-typecheck.log +5 -0
  65. package/src/apps/prompts/mcp.ts +175 -0
  66. package/src/apps/prompts/node_modules/.bin/tsc +21 -0
  67. package/src/apps/prompts/node_modules/.bin/tsserver +21 -0
  68. package/src/apps/prompts/node_modules/.bin/tsx +21 -0
  69. package/src/apps/prompts/package.json +25 -0
  70. package/src/apps/prompts/public/page.css +315 -0
  71. package/src/apps/prompts/public/page.js +541 -0
  72. package/src/apps/prompts/services/prompt-store.ts +212 -0
  73. package/src/apps/prompts/src/index.ts +9 -0
  74. package/src/apps/prompts/tsconfig.json +8 -0
  75. package/src/apps/prompts/types.ts +27 -0
  76. package/src/apps/shell/.turbo/turbo-build.log +5 -0
  77. package/src/apps/shell/.turbo/turbo-lint.log +34 -0
  78. package/src/apps/shell/.turbo/turbo-typecheck.log +5 -0
  79. package/src/apps/shell/package.json +35 -0
  80. package/src/apps/shell/public/favicon.svg +7 -0
  81. package/src/apps/shell/public/page.css +407 -0
  82. package/src/apps/shell/public/page.js +1577 -0
  83. package/src/apps/shell/src/index.ts +150 -0
  84. package/src/apps/shell/src/mcp.ts +398 -0
  85. package/src/apps/shell/src/shell-types.ts +41 -0
  86. package/src/apps/shell/tsconfig.json +8 -0
  87. package/src/apps/test/.turbo/turbo-build.log +5 -0
  88. package/src/apps/test/.turbo/turbo-lint.log +27 -0
  89. package/src/apps/test/.turbo/turbo-typecheck.log +5 -0
  90. package/src/apps/test/node_modules/.bin/tsc +21 -0
  91. package/src/apps/test/node_modules/.bin/tsserver +21 -0
  92. package/src/apps/test/node_modules/.bin/tsx +21 -0
  93. package/src/apps/test/node_modules/.bin/uuid +21 -0
  94. package/src/apps/test/package.json +35 -0
  95. package/src/apps/test/public/favicon.svg +7 -0
  96. package/src/apps/test/public/page.css +499 -0
  97. package/src/apps/test/public/page.js +417 -0
  98. package/src/apps/test/public/scenario-runner.js +450 -0
  99. package/src/apps/test/src/index.ts +9 -0
  100. package/src/apps/test/src/mcp.ts +192 -0
  101. package/src/apps/test/src/routes/trigger.ts +285 -0
  102. package/src/apps/test/src/services/scenario-broadcaster.ts +60 -0
  103. package/src/apps/test/src/services/scenario-manager.ts +361 -0
  104. package/src/apps/test/src/services/scenario-store.ts +145 -0
  105. package/src/apps/test/tsconfig.json +8 -0
  106. package/src/apps/vocabulary/.turbo/turbo-build.log +5 -0
  107. package/src/apps/vocabulary/.turbo/turbo-lint.log +25 -0
  108. package/src/apps/vocabulary/.turbo/turbo-typecheck.log +5 -0
  109. package/src/apps/vocabulary/mcp.ts +173 -0
  110. package/src/apps/vocabulary/node_modules/.bin/tsc +21 -0
  111. package/src/apps/vocabulary/node_modules/.bin/tsserver +21 -0
  112. package/src/apps/vocabulary/node_modules/.bin/tsx +21 -0
  113. package/src/apps/vocabulary/package.json +25 -0
  114. package/src/apps/vocabulary/public/page.css +247 -0
  115. package/src/apps/vocabulary/public/page.js +444 -0
  116. package/src/apps/vocabulary/services/vocabulary-store.ts +179 -0
  117. package/src/apps/vocabulary/src/index.ts +10 -0
  118. package/src/apps/vocabulary/tsconfig.json +8 -0
  119. package/src/apps/vocabulary/types.ts +22 -0
  120. package/src/apps/voice/.turbo/turbo-build.log +5 -0
  121. package/src/apps/voice/.turbo/turbo-lint.log +43 -0
  122. package/src/apps/voice/.turbo/turbo-typecheck.log +5 -0
  123. package/src/apps/voice/node_modules/.bin/openai +21 -0
  124. package/src/apps/voice/node_modules/.bin/tsc +21 -0
  125. package/src/apps/voice/node_modules/.bin/tsserver +21 -0
  126. package/src/apps/voice/node_modules/.bin/tsx +21 -0
  127. package/src/apps/voice/package.json +35 -0
  128. package/src/apps/voice/public/favicon.svg +7 -0
  129. package/src/apps/voice/public/page.css +388 -0
  130. package/src/apps/voice/public/page.js +718 -0
  131. package/src/apps/voice/src/index.ts +10 -0
  132. package/src/apps/voice/src/mcp.ts +70 -0
  133. package/src/apps/voice/src/providers/index.ts +85 -0
  134. package/src/apps/voice/src/providers/openai-compatible.ts +94 -0
  135. package/src/apps/voice/src/providers/types.ts +27 -0
  136. package/src/apps/voice/src/routes/config.ts +118 -0
  137. package/src/apps/voice/src/routes/transcribe.ts +90 -0
  138. package/src/apps/voice/src/services/config-store.ts +129 -0
  139. package/src/apps/voice/src/services/stats.ts +108 -0
  140. package/src/apps/voice/src/transcribe.ts +11 -0
  141. package/src/apps/voice/src/utils/mime.ts +16 -0
  142. package/src/apps/voice/tsconfig.json +8 -0
  143. package/src/apps/workflow/.turbo/turbo-build.log +5 -0
  144. package/src/apps/workflow/.turbo/turbo-lint.log +96 -0
  145. package/src/apps/workflow/.turbo/turbo-typecheck.log +5 -0
  146. package/src/apps/workflow/engine/executors/decision-executor.ts +87 -0
  147. package/src/apps/workflow/engine/executors/file-executor.ts +90 -0
  148. package/src/apps/workflow/engine/executors/git-executor.ts +137 -0
  149. package/src/apps/workflow/engine/executors/http-executor.ts +65 -0
  150. package/src/apps/workflow/engine/executors/index.ts +28 -0
  151. package/src/apps/workflow/engine/executors/kanban-executor.ts +154 -0
  152. package/src/apps/workflow/engine/executors/llm-executor.ts +46 -0
  153. package/src/apps/workflow/engine/executors/log-executor.ts +62 -0
  154. package/src/apps/workflow/engine/executors/loop-executor.ts +14 -0
  155. package/src/apps/workflow/engine/executors/shell-executor.ts +107 -0
  156. package/src/apps/workflow/engine/executors/sub-workflow-executor.ts +61 -0
  157. package/src/apps/workflow/engine/executors/test-executor.ts +73 -0
  158. package/src/apps/workflow/engine/executors/trigger-executor.ts +39 -0
  159. package/src/apps/workflow/engine/expression-evaluator.ts +117 -0
  160. package/src/apps/workflow/engine/graph-runner.ts +438 -0
  161. package/src/apps/workflow/engine/node-executor.ts +104 -0
  162. package/src/apps/workflow/engine/node-registry.ts +15 -0
  163. package/src/apps/workflow/engine/variable-resolver.ts +109 -0
  164. package/src/apps/workflow/mcp.ts +223 -0
  165. package/src/apps/workflow/node_modules/.bin/tsc +21 -0
  166. package/src/apps/workflow/node_modules/.bin/tsserver +21 -0
  167. package/src/apps/workflow/node_modules/.bin/tsx +21 -0
  168. package/src/apps/workflow/package.json +25 -0
  169. package/src/apps/workflow/public/editor/canvas.js +366 -0
  170. package/src/apps/workflow/public/editor/drag-manager.js +326 -0
  171. package/src/apps/workflow/public/editor/edge-renderer.js +235 -0
  172. package/src/apps/workflow/public/editor/history-manager.js +147 -0
  173. package/src/apps/workflow/public/editor/layout-engine.js +159 -0
  174. package/src/apps/workflow/public/editor/node-renderer.js +199 -0
  175. package/src/apps/workflow/public/editor/selection-manager.js +193 -0
  176. package/src/apps/workflow/public/favicon.svg +7 -0
  177. package/src/apps/workflow/public/models/node-types.js +300 -0
  178. package/src/apps/workflow/public/models/workflow-model.js +257 -0
  179. package/src/apps/workflow/public/page.css +406 -0
  180. package/src/apps/workflow/public/page.js +658 -0
  181. package/src/apps/workflow/public/panels/inspector.js +360 -0
  182. package/src/apps/workflow/public/panels/palette.js +106 -0
  183. package/src/apps/workflow/public/panels/run-view.js +275 -0
  184. package/src/apps/workflow/public/panels/toolbar.js +232 -0
  185. package/src/apps/workflow/public/panels/workflow-list.js +237 -0
  186. package/src/apps/workflow/public/state/store.js +47 -0
  187. package/src/apps/workflow/services/custom-node-loader.ts +48 -0
  188. package/src/apps/workflow/services/legacy-converter.ts +72 -0
  189. package/src/apps/workflow/services/run-manager.ts +190 -0
  190. package/src/apps/workflow/services/workflow-store.ts +424 -0
  191. package/src/apps/workflow/services/workflow-validator.test.ts +103 -0
  192. package/src/apps/workflow/services/workflow-validator.ts +98 -0
  193. package/src/apps/workflow/src/index.ts +10 -0
  194. package/src/apps/workflow/templates/ci-pipeline.json +18 -0
  195. package/src/apps/workflow/templates/code-review.json +22 -0
  196. package/src/apps/workflow/templates/kanban-testing.json +24 -0
  197. package/src/apps/workflow/tsconfig.json +8 -0
  198. package/src/apps/workflow/types.ts +268 -0
  199. package/src/packages/auth-middleware.ts +14 -0
  200. package/src/packages/design-tokens/.turbo/turbo-build.log +10 -0
  201. package/src/packages/design-tokens/STYLEGUIDE.md +414 -0
  202. package/src/packages/design-tokens/build.js +413 -0
  203. package/src/packages/design-tokens/demo/index.html +1367 -0
  204. package/src/packages/design-tokens/demo/proposition-a.html +717 -0
  205. package/src/packages/design-tokens/demo/proposition-b.html +1239 -0
  206. package/src/packages/design-tokens/demo/proposition-c.html +1049 -0
  207. package/src/packages/design-tokens/dist/tailwind-preset.js +115 -0
  208. package/src/packages/design-tokens/dist/tokens.css +345 -0
  209. package/src/packages/design-tokens/dist/tokens.d.ts +229 -0
  210. package/src/packages/design-tokens/dist/tokens.js +386 -0
  211. package/src/packages/design-tokens/package.json +25 -0
  212. package/src/packages/design-tokens/tokens.json +228 -0
  213. package/src/packages/devtools-middleware.ts +22 -0
  214. package/src/packages/eslint-config/index.js +63 -0
  215. package/src/packages/eslint-config/node_modules/.bin/eslint +21 -0
  216. package/src/packages/eslint-config/package.json +18 -0
  217. package/src/packages/json-file-store.ts +232 -0
  218. package/src/packages/mcp-utils/.turbo/turbo-build.log +5 -0
  219. package/src/packages/mcp-utils/dist/index.d.ts +33 -0
  220. package/src/packages/mcp-utils/dist/index.d.ts.map +1 -0
  221. package/src/packages/mcp-utils/dist/index.js +126 -0
  222. package/src/packages/mcp-utils/dist/index.js.map +1 -0
  223. package/src/packages/mcp-utils/node_modules/.bin/tsc +21 -0
  224. package/src/packages/mcp-utils/node_modules/.bin/tsserver +21 -0
  225. package/src/packages/mcp-utils/package.json +32 -0
  226. package/src/packages/mcp-utils/src/index.ts +171 -0
  227. package/src/packages/mcp-utils/tsconfig.json +9 -0
  228. package/src/packages/paths.ts +18 -0
  229. package/src/packages/project-context/index.js +55 -0
  230. package/src/packages/project-context/package.json +13 -0
  231. package/src/packages/project-store.ts +127 -0
  232. package/src/packages/server-sniffer.ts +132 -0
  233. package/src/packages/shared-assets/favicon.svg +7 -0
  234. package/src/packages/shared-assets/keymap-registry.js +512 -0
  235. package/src/packages/shared-assets/logo.svg +6 -0
  236. package/src/packages/shared-assets/package.json +11 -0
  237. package/src/packages/shared-assets/ui-utils.js +48 -0
  238. package/src/packages/shared-assets/voice-widget.d.ts +37 -0
  239. package/src/packages/shared-assets/voice-widget.js +695 -0
  240. package/src/packages/shared-types/.turbo/turbo-build.log +5 -0
  241. package/src/packages/shared-types/dist/index.d.ts +39 -0
  242. package/src/packages/shared-types/dist/index.d.ts.map +1 -0
  243. package/src/packages/shared-types/node_modules/.bin/tsc +21 -0
  244. package/src/packages/shared-types/node_modules/.bin/tsserver +21 -0
  245. package/src/packages/shared-types/package.json +25 -0
  246. package/src/packages/shared-types/src/index.ts +41 -0
  247. package/src/packages/shared-types/tsconfig.json +11 -0
  248. package/src/packages/tsconfig/base.json +15 -0
  249. package/src/packages/tsconfig/next.json +14 -0
  250. package/src/packages/tsconfig/node.json +11 -0
  251. package/src/packages/tsconfig/package.json +10 -0
  252. package/turbo.json +25 -0
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import { createVoiceMcpServer } from "./mcp.js";
3
+ import { runStdio } from "@devglide/mcp-utils";
4
+
5
+ // ── Stdio MCP mode ──────────────────────────────────────────────────────────
6
+ if (process.argv.includes("--stdio")) {
7
+ const mcpServer = createVoiceMcpServer();
8
+ await runStdio(mcpServer);
9
+ console.error("Devglide Voice MCP server running on stdio");
10
+ }
@@ -0,0 +1,70 @@
1
+ import { z } from "zod";
2
+ import { createDevglideMcpServer } from "../../../packages/mcp-utils/src/index.js";
3
+ import { transcribe } from "./transcribe.js";
4
+ import { stats } from "./services/stats.js";
5
+ import { mimeFromFilename } from "./utils/mime.js";
6
+ import { configStore } from "./services/config-store.js";
7
+
8
+ export function createVoiceMcpServer() {
9
+ const server = createDevglideMcpServer("devglide-voice", "0.1.0");
10
+
11
+ server.tool(
12
+ "voice_transcribe",
13
+ "Transcribe audio using the configured speech-to-text provider. Accepts base64-encoded audio data.",
14
+ {
15
+ audioBase64: z.string().describe("Base64-encoded audio data"),
16
+ filename: z
17
+ .string()
18
+ .optional()
19
+ .describe("Original filename with extension (e.g. 'recording.webm')"),
20
+ language: z
21
+ .string()
22
+ .optional()
23
+ .describe("BCP 47 language hint (e.g. 'en')"),
24
+ },
25
+ async ({ audioBase64, filename, language }) => {
26
+ const startTime = Date.now();
27
+ try {
28
+ const name = filename || "audio.webm";
29
+ const buffer = Buffer.from(audioBase64, "base64");
30
+ const file = new File([buffer], name, {
31
+ type: mimeFromFilename(name),
32
+ });
33
+
34
+ const cfg = configStore.get();
35
+ const lang = language ?? (cfg.language !== "auto" ? cfg.language : undefined);
36
+ const result = await transcribe(file, { language: lang });
37
+ const durationSec = (Date.now() - startTime) / 1000;
38
+ stats.recordSuccess(result.duration ?? durationSec);
39
+
40
+ return {
41
+ content: [
42
+ { type: "text" as const, text: JSON.stringify(result, null, 2) },
43
+ ],
44
+ };
45
+ } catch (error) {
46
+ stats.recordError();
47
+ throw error;
48
+ }
49
+ }
50
+ );
51
+
52
+ server.tool(
53
+ "voice_status",
54
+ "Check voice service status and transcription statistics",
55
+ {},
56
+ async () => {
57
+ const voiceStats = stats.getStats();
58
+ return {
59
+ content: [
60
+ {
61
+ type: "text" as const,
62
+ text: JSON.stringify(voiceStats, null, 2),
63
+ },
64
+ ],
65
+ };
66
+ }
67
+ );
68
+
69
+ return server;
70
+ }
@@ -0,0 +1,85 @@
1
+ import type { ProviderConfig, TranscriptionProvider } from "./types.js";
2
+ import { OpenAICompatibleProvider } from "./openai-compatible.js";
3
+ import { configStore } from "../services/config-store.js";
4
+
5
+ export type { TranscriptionProvider, ProviderConfig };
6
+
7
+ interface ProviderMeta {
8
+ displayName: string;
9
+ requiresApiKey: boolean;
10
+ defaultBaseURL?: string;
11
+ defaultModel: string;
12
+ }
13
+
14
+ export const PROVIDER_META: Record<string, ProviderMeta> = {
15
+ openai: {
16
+ displayName: "OpenAI Whisper",
17
+ requiresApiKey: true,
18
+ defaultModel: "whisper-1",
19
+ },
20
+ groq: {
21
+ displayName: "Groq",
22
+ requiresApiKey: true,
23
+ defaultBaseURL: "https://api.groq.com/openai/v1",
24
+ defaultModel: "whisper-large-v3-turbo",
25
+ },
26
+ "whisper-cpp": {
27
+ displayName: "whisper.cpp",
28
+ requiresApiKey: false,
29
+ defaultBaseURL: "http://localhost:8080",
30
+ defaultModel: "default",
31
+ },
32
+ "faster-whisper": {
33
+ displayName: "Faster Whisper",
34
+ requiresApiKey: false,
35
+ defaultBaseURL: "http://localhost:8000/v1",
36
+ defaultModel: "Systran/faster-whisper-small",
37
+ },
38
+ vllm: {
39
+ displayName: "vLLM",
40
+ requiresApiKey: false,
41
+ defaultBaseURL: "http://localhost:8000/v1",
42
+ defaultModel: "default",
43
+ },
44
+ "local-ai": {
45
+ displayName: "LocalAI",
46
+ requiresApiKey: false,
47
+ defaultBaseURL: "http://localhost:8080",
48
+ defaultModel: "whisper-1",
49
+ },
50
+ };
51
+
52
+ let cachedProvider: TranscriptionProvider | null = null;
53
+
54
+ export function invalidateProvider(): void {
55
+ cachedProvider = null;
56
+ }
57
+
58
+ function buildConfig(providerName: string): ProviderConfig {
59
+ const meta = PROVIDER_META[providerName];
60
+ if (!meta) {
61
+ throw new Error(
62
+ `Unknown provider "${providerName}". Supported: ${Object.keys(PROVIDER_META).join(", ")}`
63
+ );
64
+ }
65
+ const settings = configStore.getProviderSettings(providerName);
66
+ return {
67
+ name: providerName,
68
+ displayName: meta.displayName,
69
+ apiKey: settings.apiKey,
70
+ baseURL: settings.baseURL ?? meta.defaultBaseURL,
71
+ model: settings.model || meta.defaultModel,
72
+ requiresApiKey: meta.requiresApiKey,
73
+ };
74
+ }
75
+
76
+ export function getProvider(): TranscriptionProvider {
77
+ if (cachedProvider) return cachedProvider;
78
+ const name = configStore.get().provider;
79
+ cachedProvider = new OpenAICompatibleProvider(buildConfig(name));
80
+ return cachedProvider;
81
+ }
82
+
83
+ export function getProviderConfig(): ProviderConfig {
84
+ return buildConfig(configStore.get().provider);
85
+ }
@@ -0,0 +1,94 @@
1
+ import type {
2
+ TranscriptionProvider,
3
+ ProviderConfig,
4
+ TranscribeOptions,
5
+ TranscriptionResult,
6
+ } from "./types.js";
7
+
8
+ // Cached OpenAI constructor and client instances keyed by config signature
9
+ let _OpenAI: any = null;
10
+ const _clientCache = new Map<string, any>();
11
+ const CLIENT_CACHE_MAX = 8;
12
+
13
+ function getClientCacheKey(apiKey: string, baseURL?: string): string {
14
+ return `${apiKey}::${baseURL || ""}`;
15
+ }
16
+
17
+ export class OpenAICompatibleProvider implements TranscriptionProvider {
18
+ readonly name: string;
19
+ readonly displayName: string;
20
+ readonly requiresApiKey: boolean;
21
+ private readonly config: ProviderConfig;
22
+
23
+ constructor(config: ProviderConfig) {
24
+ this.config = config;
25
+ this.name = config.name;
26
+ this.displayName = config.displayName;
27
+ this.requiresApiKey = config.requiresApiKey;
28
+ }
29
+
30
+ async transcribe(
31
+ audio: File,
32
+ options: TranscribeOptions = {}
33
+ ): Promise<TranscriptionResult> {
34
+ if (!_OpenAI) {
35
+ try {
36
+ _OpenAI = (await import("openai")).default;
37
+ } catch {
38
+ throw new Error(
39
+ "@devglide/voice server transcription requires the 'openai' package. Install it with: pnpm add openai"
40
+ );
41
+ }
42
+ }
43
+
44
+ const apiKey = this.config.apiKey || "not-needed";
45
+ const cacheKey = getClientCacheKey(apiKey, this.config.baseURL);
46
+ let client = _clientCache.get(cacheKey);
47
+ if (!client) {
48
+ const clientOptions: Record<string, unknown> = { apiKey };
49
+ if (this.config.baseURL) {
50
+ clientOptions.baseURL = this.config.baseURL;
51
+ }
52
+ client = new _OpenAI(clientOptions);
53
+ if (_clientCache.size >= CLIENT_CACHE_MAX) {
54
+ const oldestKey = _clientCache.keys().next().value!;
55
+ _clientCache.delete(oldestKey);
56
+ }
57
+ _clientCache.set(cacheKey, client);
58
+ }
59
+
60
+ let response;
61
+ try {
62
+ response = await client.audio.transcriptions.create({
63
+ file: audio,
64
+ model: this.config.model,
65
+ language: options.language,
66
+ response_format: options.responseFormat || "verbose_json",
67
+ });
68
+ } catch (err: unknown) {
69
+ if (err != null && typeof err === 'object' && 'status' in err && (err as { status: number }).status === 404) {
70
+ throw new Error(
71
+ `Model "${this.config.model}" not found on ${this.displayName} (${this.config.baseURL || "api.openai.com"}). Check available models or update the voice config.`
72
+ );
73
+ }
74
+ throw err;
75
+ }
76
+
77
+ if (typeof response === "string") {
78
+ return { text: response };
79
+ }
80
+
81
+ return {
82
+ text: response.text,
83
+ language: response.language,
84
+ duration: response.duration,
85
+ };
86
+ }
87
+
88
+ isConfigured(): boolean {
89
+ if (this.requiresApiKey) {
90
+ return !!this.config.apiKey;
91
+ }
92
+ return !!this.config.baseURL;
93
+ }
94
+ }
@@ -0,0 +1,27 @@
1
+ export interface TranscriptionResult {
2
+ text: string;
3
+ language?: string;
4
+ duration?: number;
5
+ }
6
+
7
+ export interface TranscribeOptions {
8
+ language?: string;
9
+ responseFormat?: "json" | "text" | "verbose_json";
10
+ }
11
+
12
+ export interface TranscriptionProvider {
13
+ readonly name: string;
14
+ readonly displayName: string;
15
+ readonly requiresApiKey: boolean;
16
+ transcribe(audio: File, options?: TranscribeOptions): Promise<TranscriptionResult>;
17
+ isConfigured(): boolean;
18
+ }
19
+
20
+ export interface ProviderConfig {
21
+ name: string;
22
+ displayName: string;
23
+ apiKey?: string;
24
+ baseURL?: string;
25
+ model: string;
26
+ requiresApiKey: boolean;
27
+ }
@@ -0,0 +1,118 @@
1
+ import { Router, type Router as RouterType } from "express";
2
+ import {
3
+ getProvider,
4
+ getProviderConfig,
5
+ invalidateProvider,
6
+ PROVIDER_META,
7
+ } from "../providers/index.js";
8
+ import { configStore } from "../services/config-store.js";
9
+ import { stats } from "../services/stats.js";
10
+ import { handleTranscribe } from "./transcribe.js";
11
+
12
+ export const configRouter: RouterType = Router();
13
+
14
+ configRouter.get("/", (_req, res) => {
15
+ const cfg = configStore.get();
16
+ const pc = getProviderConfig();
17
+ const settings = configStore.getProviderSettings(cfg.provider);
18
+ res.json({
19
+ provider: pc.name,
20
+ displayName: pc.displayName,
21
+ model: pc.model,
22
+ baseURL: pc.baseURL ?? null,
23
+ language: cfg.language,
24
+ configured: pc.requiresApiKey ? !!pc.apiKey : !!pc.baseURL,
25
+ requiresApiKey: pc.requiresApiKey,
26
+ apiKeyMasked: settings.apiKey ? `...${settings.apiKey.slice(-4)}` : null,
27
+ });
28
+ });
29
+
30
+ configRouter.get("/providers", (_req, res) => {
31
+ const cfg = configStore.get();
32
+ const providers = Object.entries(PROVIDER_META).map(([id, meta]) => {
33
+ const settings = configStore.getProviderSettings(id);
34
+ return {
35
+ id,
36
+ displayName: meta.displayName,
37
+ requiresApiKey: meta.requiresApiKey,
38
+ defaultBaseURL: meta.defaultBaseURL ?? null,
39
+ defaultModel: meta.defaultModel,
40
+ currentApiKeyMasked: settings.apiKey ? `...${settings.apiKey.slice(-4)}` : null,
41
+ currentBaseURL: settings.baseURL ?? null,
42
+ currentModel: settings.model ?? null,
43
+ };
44
+ });
45
+ res.json({ current: cfg.provider, language: cfg.language, providers });
46
+ });
47
+
48
+ configRouter.put("/", (req, res) => {
49
+ const { provider, language, apiKey, baseURL, model } = req.body as Record<
50
+ string,
51
+ string | undefined
52
+ >;
53
+
54
+ if (provider && !PROVIDER_META[provider]) {
55
+ res.status(400).json({ error: `Unknown provider "${provider}"` });
56
+ return;
57
+ }
58
+
59
+ // Validate language (BCP 47 pattern or "auto")
60
+ if (language !== undefined) {
61
+ if (language !== "auto" && !/^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/.test(language)) {
62
+ res.status(400).json({ error: `Invalid language value "${language}". Use BCP 47 code (e.g. "en", "en-US") or "auto".` });
63
+ return;
64
+ }
65
+ }
66
+
67
+ // Validate model is non-empty if provided
68
+ if (model !== undefined && typeof model === "string" && model.trim() === "") {
69
+ res.status(400).json({ error: "Model must be a non-empty string if provided." });
70
+ return;
71
+ }
72
+
73
+ const targetProvider = provider ?? configStore.get().provider;
74
+ const patch: Parameters<typeof configStore.update>[0] = {};
75
+
76
+ if (provider) patch.provider = provider;
77
+ if (language) patch.language = language;
78
+
79
+ if (apiKey !== undefined || baseURL !== undefined || model !== undefined) {
80
+ const settings: Record<string, string | undefined> = {};
81
+ if (apiKey !== undefined) settings.apiKey = apiKey || undefined;
82
+ if (baseURL !== undefined) settings.baseURL = baseURL || undefined;
83
+ if (model !== undefined) settings.model = model || undefined;
84
+ patch.providerName = targetProvider;
85
+ patch.providerSettings = settings;
86
+ }
87
+
88
+ configStore.update(patch);
89
+ invalidateProvider();
90
+
91
+ const updated = getProviderConfig();
92
+ res.json({ ok: true, provider: updated.name, model: updated.model, baseURL: updated.baseURL ?? null });
93
+ });
94
+
95
+ configRouter.post("/test", (_req, res) => {
96
+ try {
97
+ const provider = getProvider();
98
+ if (!provider.isConfigured()) {
99
+ res.json({ ok: false, reason: "Provider is not configured (missing API key or base URL)" });
100
+ return;
101
+ }
102
+ res.json({ ok: true, provider: provider.name, displayName: provider.displayName });
103
+ } catch (err) {
104
+ res.json({ ok: false, reason: err instanceof Error ? err.message : String(err) });
105
+ }
106
+ });
107
+
108
+ configRouter.delete("/stats", (_req, res) => {
109
+ stats.reset();
110
+ res.json({ ok: true });
111
+ });
112
+
113
+ // Alias for backwards compatibility — delegates to the canonical /api/transcribe handler
114
+ configRouter.post("/test-transcription", handleTranscribe);
115
+
116
+ configRouter.get("/stats", (_req, res) => {
117
+ res.json(stats.getStats());
118
+ });
@@ -0,0 +1,90 @@
1
+ import { Router, type Request, type Response } from "express";
2
+ import { getProvider } from "../providers/index.js";
3
+ import { configStore } from "../services/config-store.js";
4
+ import { stats } from "../services/stats.js";
5
+ import { mimeFromFilename } from "../utils/mime.js";
6
+
7
+ export const transcribeRouter: Router = Router();
8
+
9
+ export async function handleTranscribe(req: Request, res: Response) {
10
+ const { audioBase64, filename, language } = req.body as {
11
+ audioBase64?: string;
12
+ filename?: string;
13
+ language?: string;
14
+ };
15
+
16
+ if (!audioBase64 || !filename) {
17
+ res.status(400).json({ error: "audioBase64 and filename are required" });
18
+ return;
19
+ }
20
+
21
+ // Reject payloads larger than 25MB of base64 (~18.75MB decoded)
22
+ if (audioBase64.length > 25 * 1024 * 1024) {
23
+ res.status(413).json({ error: "audioBase64 exceeds maximum size (25MB)" });
24
+ return;
25
+ }
26
+
27
+ // Validate base64: check length is valid and content uses only base64 chars.
28
+ // Use a chunked regex to avoid catastrophic backtracking on large strings.
29
+ const b64Len = audioBase64.length;
30
+ if (b64Len % 4 !== 0) {
31
+ res.status(400).json({ error: "audioBase64 is not valid base64" });
32
+ return;
33
+ }
34
+ const b64ChunkRe = /^[A-Za-z0-9+/]*$/;
35
+ const CHUNK = 64 * 1024; // validate in 64KB chunks
36
+ let b64Valid = true;
37
+ for (let off = 0; off < b64Len; off += CHUNK) {
38
+ const slice = audioBase64.slice(off, Math.min(off + CHUNK, b64Len));
39
+ // Allow trailing '=' only in the final chunk
40
+ if (off + CHUNK >= b64Len) {
41
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(slice)) { b64Valid = false; break; }
42
+ } else {
43
+ if (!b64ChunkRe.test(slice)) { b64Valid = false; break; }
44
+ }
45
+ }
46
+ if (!b64Valid) {
47
+ res.status(400).json({ error: "audioBase64 is not valid base64" });
48
+ return;
49
+ }
50
+
51
+ // Sanitize filename: strip path components, null bytes, and control characters
52
+ const sanitizedFilename = filename
53
+ .replace(/.*[/\\]/, "") // strip path components
54
+ .replace(/[\x00-\x1f\x7f]/g, ""); // strip null bytes & control chars
55
+
56
+ if (!sanitizedFilename) {
57
+ res.status(400).json({ error: "filename is invalid after sanitization" });
58
+ return;
59
+ }
60
+
61
+ const startTime = Date.now();
62
+
63
+ try {
64
+ let buffer: Buffer;
65
+ try {
66
+ buffer = Buffer.from(audioBase64, "base64");
67
+ } catch {
68
+ res.status(400).json({ error: "audioBase64 could not be decoded" });
69
+ return;
70
+ }
71
+ const file = new File([buffer], sanitizedFilename, {
72
+ type: mimeFromFilename(sanitizedFilename),
73
+ });
74
+ const cfg = configStore.get();
75
+ const lang =
76
+ language ?? (cfg.language !== "auto" ? cfg.language : undefined);
77
+ const provider = getProvider();
78
+ const result = await provider.transcribe(file, { language: lang });
79
+ const durationSec = (Date.now() - startTime) / 1000;
80
+ stats.recordSuccess(result.duration ?? durationSec);
81
+ res.json({ ok: true, ...result });
82
+ } catch (err) {
83
+ stats.recordError();
84
+ res
85
+ .status(500)
86
+ .json({ ok: false, error: err instanceof Error ? err.message : String(err) });
87
+ }
88
+ }
89
+
90
+ transcribeRouter.post("/", handleTranscribe);
@@ -0,0 +1,129 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
2
+ import { join } from "path";
3
+ import { VOICE_DIR } from "../../../../packages/paths.js";
4
+
5
+ const DATA_DIR = VOICE_DIR;
6
+ const CONFIG_FILE = join(DATA_DIR, "config.json");
7
+
8
+ export interface PerProviderConfig {
9
+ apiKey?: string;
10
+ baseURL?: string;
11
+ model?: string;
12
+ }
13
+
14
+ export interface PersistentConfig {
15
+ provider: string;
16
+ language: string;
17
+ providers: Record<string, PerProviderConfig>;
18
+ }
19
+
20
+ function loadFile(): Partial<PersistentConfig> {
21
+ try {
22
+ if (existsSync(CONFIG_FILE)) {
23
+ const raw = readFileSync(CONFIG_FILE, "utf-8");
24
+ try {
25
+ return JSON.parse(raw) as Partial<PersistentConfig>;
26
+ } catch (parseErr) {
27
+ console.error(`[voice] corrupt config file at ${CONFIG_FILE} — ignoring and using defaults. Parse error:`, parseErr);
28
+ return {};
29
+ }
30
+ }
31
+ } catch (e) {
32
+ console.error(`[voice] failed to read config file at ${CONFIG_FILE}:`, e);
33
+ }
34
+ return {};
35
+ }
36
+
37
+ function saveFile(config: PersistentConfig): void {
38
+ mkdirSync(DATA_DIR, { recursive: true, mode: 0o700 });
39
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
40
+ }
41
+
42
+ function fromEnv(): PersistentConfig {
43
+ return {
44
+ provider: process.env.VOICE_PROVIDER || "openai",
45
+ language: process.env.WHISPER_LANGUAGE || "auto",
46
+ providers: {
47
+ openai: {
48
+ apiKey: process.env.OPENAI_API_KEY,
49
+ model: process.env.WHISPER_MODEL || "whisper-1",
50
+ },
51
+ groq: {
52
+ apiKey: process.env.GROQ_API_KEY,
53
+ baseURL: "https://api.groq.com/openai/v1",
54
+ model: process.env.GROQ_WHISPER_MODEL || "whisper-large-v3-turbo",
55
+ },
56
+ "whisper-cpp": {
57
+ baseURL: process.env.WHISPER_CPP_URL || "http://localhost:8080",
58
+ model: process.env.WHISPER_CPP_MODEL || "default",
59
+ },
60
+ "faster-whisper": {
61
+ baseURL: process.env.FASTER_WHISPER_URL || "http://localhost:8000/v1",
62
+ model: process.env.FASTER_WHISPER_MODEL || "default",
63
+ },
64
+ vllm: {
65
+ baseURL: process.env.VLLM_URL || "http://localhost:8000/v1",
66
+ model: process.env.VLLM_WHISPER_MODEL || "default",
67
+ },
68
+ "local-ai": {
69
+ baseURL: process.env.LOCAL_AI_URL || "http://localhost:8080",
70
+ model: process.env.LOCAL_AI_WHISPER_MODEL || "whisper-1",
71
+ },
72
+ },
73
+ };
74
+ }
75
+
76
+ class ConfigStore {
77
+ private static _instance: ConfigStore;
78
+ private config: PersistentConfig;
79
+
80
+ private constructor() {
81
+ const env = fromEnv();
82
+ const file = loadFile();
83
+ this.config = {
84
+ provider: file.provider ?? env.provider,
85
+ language: file.language ?? env.language,
86
+ providers: {},
87
+ };
88
+ for (const key of Object.keys(env.providers)) {
89
+ this.config.providers[key] = {
90
+ ...env.providers[key],
91
+ ...(file.providers?.[key] ?? {}),
92
+ };
93
+ }
94
+ }
95
+
96
+ static getInstance(): ConfigStore {
97
+ if (!ConfigStore._instance) {
98
+ ConfigStore._instance = new ConfigStore();
99
+ }
100
+ return ConfigStore._instance;
101
+ }
102
+
103
+ get(): PersistentConfig {
104
+ return structuredClone(this.config);
105
+ }
106
+
107
+ update(patch: {
108
+ provider?: string;
109
+ language?: string;
110
+ providerName?: string;
111
+ providerSettings?: PerProviderConfig;
112
+ }): void {
113
+ if (patch.provider != null) this.config.provider = patch.provider;
114
+ if (patch.language != null) this.config.language = patch.language;
115
+ if (patch.providerName && patch.providerSettings) {
116
+ this.config.providers[patch.providerName] = {
117
+ ...this.config.providers[patch.providerName],
118
+ ...patch.providerSettings,
119
+ };
120
+ }
121
+ saveFile(this.config);
122
+ }
123
+
124
+ getProviderSettings(name: string): PerProviderConfig {
125
+ return this.config.providers[name] ?? {};
126
+ }
127
+ }
128
+
129
+ export const configStore = ConfigStore.getInstance();