decibel-tools-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (255) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +335 -0
  3. package/dist/agentic/compiler.d.ts +21 -0
  4. package/dist/agentic/compiler.d.ts.map +1 -0
  5. package/dist/agentic/compiler.js +267 -0
  6. package/dist/agentic/compiler.js.map +1 -0
  7. package/dist/agentic/golden.d.ts +25 -0
  8. package/dist/agentic/golden.d.ts.map +1 -0
  9. package/dist/agentic/golden.js +255 -0
  10. package/dist/agentic/golden.js.map +1 -0
  11. package/dist/agentic/index.d.ts +17 -0
  12. package/dist/agentic/index.d.ts.map +1 -0
  13. package/dist/agentic/index.js +153 -0
  14. package/dist/agentic/index.js.map +1 -0
  15. package/dist/agentic/linter.d.ts +20 -0
  16. package/dist/agentic/linter.d.ts.map +1 -0
  17. package/dist/agentic/linter.js +340 -0
  18. package/dist/agentic/linter.js.map +1 -0
  19. package/dist/agentic/renderer.d.ts +17 -0
  20. package/dist/agentic/renderer.d.ts.map +1 -0
  21. package/dist/agentic/renderer.js +277 -0
  22. package/dist/agentic/renderer.js.map +1 -0
  23. package/dist/agentic/types.d.ts +199 -0
  24. package/dist/agentic/types.d.ts.map +1 -0
  25. package/dist/agentic/types.js +8 -0
  26. package/dist/agentic/types.js.map +1 -0
  27. package/dist/architectAdrs.d.ts +19 -0
  28. package/dist/architectAdrs.d.ts.map +1 -0
  29. package/dist/architectAdrs.js +123 -0
  30. package/dist/architectAdrs.js.map +1 -0
  31. package/dist/config.d.ts +8 -0
  32. package/dist/config.d.ts.map +1 -0
  33. package/dist/config.js +19 -0
  34. package/dist/config.js.map +1 -0
  35. package/dist/dataRoot.d.ts +45 -0
  36. package/dist/dataRoot.d.ts.map +1 -0
  37. package/dist/dataRoot.js +201 -0
  38. package/dist/dataRoot.js.map +1 -0
  39. package/dist/decibelPaths.d.ts +42 -0
  40. package/dist/decibelPaths.d.ts.map +1 -0
  41. package/dist/decibelPaths.js +150 -0
  42. package/dist/decibelPaths.js.map +1 -0
  43. package/dist/httpServer.d.ts +49 -0
  44. package/dist/httpServer.d.ts.map +1 -0
  45. package/dist/httpServer.js +1472 -0
  46. package/dist/httpServer.js.map +1 -0
  47. package/dist/lib/benchmark.d.ts +110 -0
  48. package/dist/lib/benchmark.d.ts.map +1 -0
  49. package/dist/lib/benchmark.js +338 -0
  50. package/dist/lib/benchmark.js.map +1 -0
  51. package/dist/lib/supabase.d.ts +123 -0
  52. package/dist/lib/supabase.d.ts.map +1 -0
  53. package/dist/lib/supabase.js +91 -0
  54. package/dist/lib/supabase.js.map +1 -0
  55. package/dist/projectPaths.d.ts +27 -0
  56. package/dist/projectPaths.d.ts.map +1 -0
  57. package/dist/projectPaths.js +86 -0
  58. package/dist/projectPaths.js.map +1 -0
  59. package/dist/projectRegistry.d.ts +97 -0
  60. package/dist/projectRegistry.d.ts.map +1 -0
  61. package/dist/projectRegistry.js +362 -0
  62. package/dist/projectRegistry.js.map +1 -0
  63. package/dist/sentinelIssues.d.ts +44 -0
  64. package/dist/sentinelIssues.d.ts.map +1 -0
  65. package/dist/sentinelIssues.js +213 -0
  66. package/dist/sentinelIssues.js.map +1 -0
  67. package/dist/server.d.ts +3 -0
  68. package/dist/server.d.ts.map +1 -0
  69. package/dist/server.js +93 -0
  70. package/dist/server.js.map +1 -0
  71. package/dist/test.d.ts +7 -0
  72. package/dist/test.d.ts.map +1 -0
  73. package/dist/test.js +77 -0
  74. package/dist/test.js.map +1 -0
  75. package/dist/tools/agentic/index.d.ts +7 -0
  76. package/dist/tools/agentic/index.d.ts.map +1 -0
  77. package/dist/tools/agentic/index.js +180 -0
  78. package/dist/tools/agentic/index.js.map +1 -0
  79. package/dist/tools/architect/index.d.ts +9 -0
  80. package/dist/tools/architect/index.d.ts.map +1 -0
  81. package/dist/tools/architect/index.js +383 -0
  82. package/dist/tools/architect/index.js.map +1 -0
  83. package/dist/tools/architect.d.ts +19 -0
  84. package/dist/tools/architect.d.ts.map +1 -0
  85. package/dist/tools/architect.js +88 -0
  86. package/dist/tools/architect.js.map +1 -0
  87. package/dist/tools/bench.d.ts +89 -0
  88. package/dist/tools/bench.d.ts.map +1 -0
  89. package/dist/tools/bench.js +826 -0
  90. package/dist/tools/bench.js.map +1 -0
  91. package/dist/tools/context/index.d.ts +11 -0
  92. package/dist/tools/context/index.d.ts.map +1 -0
  93. package/dist/tools/context/index.js +439 -0
  94. package/dist/tools/context/index.js.map +1 -0
  95. package/dist/tools/context.d.ts +146 -0
  96. package/dist/tools/context.d.ts.map +1 -0
  97. package/dist/tools/context.js +481 -0
  98. package/dist/tools/context.js.map +1 -0
  99. package/dist/tools/crit.d.ts +63 -0
  100. package/dist/tools/crit.d.ts.map +1 -0
  101. package/dist/tools/crit.js +159 -0
  102. package/dist/tools/crit.js.map +1 -0
  103. package/dist/tools/data-inspector.d.ts +188 -0
  104. package/dist/tools/data-inspector.d.ts.map +1 -0
  105. package/dist/tools/data-inspector.js +650 -0
  106. package/dist/tools/data-inspector.js.map +1 -0
  107. package/dist/tools/deck.d.ts +11 -0
  108. package/dist/tools/deck.d.ts.map +1 -0
  109. package/dist/tools/deck.js +170 -0
  110. package/dist/tools/deck.js.map +1 -0
  111. package/dist/tools/designer/index.d.ts +6 -0
  112. package/dist/tools/designer/index.d.ts.map +1 -0
  113. package/dist/tools/designer/index.js +177 -0
  114. package/dist/tools/designer/index.js.map +1 -0
  115. package/dist/tools/designer.d.ts +20 -0
  116. package/dist/tools/designer.d.ts.map +1 -0
  117. package/dist/tools/designer.js +75 -0
  118. package/dist/tools/designer.js.map +1 -0
  119. package/dist/tools/dojo/index.d.ts +13 -0
  120. package/dist/tools/dojo/index.d.ts.map +1 -0
  121. package/dist/tools/dojo/index.js +547 -0
  122. package/dist/tools/dojo/index.js.map +1 -0
  123. package/dist/tools/dojo.d.ts +254 -0
  124. package/dist/tools/dojo.d.ts.map +1 -0
  125. package/dist/tools/dojo.js +933 -0
  126. package/dist/tools/dojo.js.map +1 -0
  127. package/dist/tools/dojoBench.d.ts +49 -0
  128. package/dist/tools/dojoBench.d.ts.map +1 -0
  129. package/dist/tools/dojoBench.js +205 -0
  130. package/dist/tools/dojoBench.js.map +1 -0
  131. package/dist/tools/dojoGraduated.d.ts +50 -0
  132. package/dist/tools/dojoGraduated.d.ts.map +1 -0
  133. package/dist/tools/dojoGraduated.js +174 -0
  134. package/dist/tools/dojoGraduated.js.map +1 -0
  135. package/dist/tools/dojoPolicy.d.ts +65 -0
  136. package/dist/tools/dojoPolicy.d.ts.map +1 -0
  137. package/dist/tools/dojoPolicy.js +263 -0
  138. package/dist/tools/dojoPolicy.js.map +1 -0
  139. package/dist/tools/friction/index.d.ts +7 -0
  140. package/dist/tools/friction/index.d.ts.map +1 -0
  141. package/dist/tools/friction/index.js +239 -0
  142. package/dist/tools/friction/index.js.map +1 -0
  143. package/dist/tools/friction.d.ts +82 -0
  144. package/dist/tools/friction.d.ts.map +1 -0
  145. package/dist/tools/friction.js +331 -0
  146. package/dist/tools/friction.js.map +1 -0
  147. package/dist/tools/index.d.ts +5 -0
  148. package/dist/tools/index.d.ts.map +1 -0
  149. package/dist/tools/index.js +52 -0
  150. package/dist/tools/index.js.map +1 -0
  151. package/dist/tools/learnings/index.d.ts +5 -0
  152. package/dist/tools/learnings/index.d.ts.map +1 -0
  153. package/dist/tools/learnings/index.js +123 -0
  154. package/dist/tools/learnings/index.js.map +1 -0
  155. package/dist/tools/learnings.d.ts +41 -0
  156. package/dist/tools/learnings.d.ts.map +1 -0
  157. package/dist/tools/learnings.js +149 -0
  158. package/dist/tools/learnings.js.map +1 -0
  159. package/dist/tools/oracle/index.d.ts +5 -0
  160. package/dist/tools/oracle/index.d.ts.map +1 -0
  161. package/dist/tools/oracle/index.js +97 -0
  162. package/dist/tools/oracle/index.js.map +1 -0
  163. package/dist/tools/oracle.d.ts +90 -0
  164. package/dist/tools/oracle.d.ts.map +1 -0
  165. package/dist/tools/oracle.js +529 -0
  166. package/dist/tools/oracle.js.map +1 -0
  167. package/dist/tools/policy.d.ts +119 -0
  168. package/dist/tools/policy.d.ts.map +1 -0
  169. package/dist/tools/policy.js +406 -0
  170. package/dist/tools/policy.js.map +1 -0
  171. package/dist/tools/provenance/index.d.ts +4 -0
  172. package/dist/tools/provenance/index.d.ts.map +1 -0
  173. package/dist/tools/provenance/index.js +58 -0
  174. package/dist/tools/provenance/index.js.map +1 -0
  175. package/dist/tools/provenance.d.ts +75 -0
  176. package/dist/tools/provenance.d.ts.map +1 -0
  177. package/dist/tools/provenance.js +197 -0
  178. package/dist/tools/provenance.js.map +1 -0
  179. package/dist/tools/rateLimiter.d.ts +45 -0
  180. package/dist/tools/rateLimiter.d.ts.map +1 -0
  181. package/dist/tools/rateLimiter.js +91 -0
  182. package/dist/tools/rateLimiter.js.map +1 -0
  183. package/dist/tools/registry/index.d.ts +10 -0
  184. package/dist/tools/registry/index.d.ts.map +1 -0
  185. package/dist/tools/registry/index.js +467 -0
  186. package/dist/tools/registry/index.js.map +1 -0
  187. package/dist/tools/registry.d.ts +3 -0
  188. package/dist/tools/registry.d.ts.map +1 -0
  189. package/dist/tools/registry.js +189 -0
  190. package/dist/tools/registry.js.map +1 -0
  191. package/dist/tools/roadmap/index.d.ts +9 -0
  192. package/dist/tools/roadmap/index.d.ts.map +1 -0
  193. package/dist/tools/roadmap/index.js +250 -0
  194. package/dist/tools/roadmap/index.js.map +1 -0
  195. package/dist/tools/roadmap.d.ts +88 -0
  196. package/dist/tools/roadmap.d.ts.map +1 -0
  197. package/dist/tools/roadmap.js +365 -0
  198. package/dist/tools/roadmap.js.map +1 -0
  199. package/dist/tools/sentinel/index.d.ts +19 -0
  200. package/dist/tools/sentinel/index.d.ts.map +1 -0
  201. package/dist/tools/sentinel/index.js +832 -0
  202. package/dist/tools/sentinel/index.js.map +1 -0
  203. package/dist/tools/sentinel-scan-data.d.ts +90 -0
  204. package/dist/tools/sentinel-scan-data.d.ts.map +1 -0
  205. package/dist/tools/sentinel-scan-data.js +122 -0
  206. package/dist/tools/sentinel-scan-data.js.map +1 -0
  207. package/dist/tools/sentinel.d.ts +156 -0
  208. package/dist/tools/sentinel.d.ts.map +1 -0
  209. package/dist/tools/sentinel.js +603 -0
  210. package/dist/tools/sentinel.js.map +1 -0
  211. package/dist/tools/shared/index.d.ts +4 -0
  212. package/dist/tools/shared/index.d.ts.map +1 -0
  213. package/dist/tools/shared/index.js +7 -0
  214. package/dist/tools/shared/index.js.map +1 -0
  215. package/dist/tools/shared/project.d.ts +17 -0
  216. package/dist/tools/shared/project.d.ts.map +1 -0
  217. package/dist/tools/shared/project.js +36 -0
  218. package/dist/tools/shared/project.js.map +1 -0
  219. package/dist/tools/shared/response.d.ts +10 -0
  220. package/dist/tools/shared/response.d.ts.map +1 -0
  221. package/dist/tools/shared/response.js +36 -0
  222. package/dist/tools/shared/response.js.map +1 -0
  223. package/dist/tools/shared/validation.d.ts +10 -0
  224. package/dist/tools/shared/validation.d.ts.map +1 -0
  225. package/dist/tools/shared/validation.js +26 -0
  226. package/dist/tools/shared/validation.js.map +1 -0
  227. package/dist/tools/studio/cloud-spine.d.ts +27 -0
  228. package/dist/tools/studio/cloud-spine.d.ts.map +1 -0
  229. package/dist/tools/studio/cloud-spine.js +773 -0
  230. package/dist/tools/studio/cloud-spine.js.map +1 -0
  231. package/dist/tools/studio/index.d.ts +154 -0
  232. package/dist/tools/studio/index.d.ts.map +1 -0
  233. package/dist/tools/studio/index.js +525 -0
  234. package/dist/tools/studio/index.js.map +1 -0
  235. package/dist/tools/testSpec.d.ts +122 -0
  236. package/dist/tools/testSpec.d.ts.map +1 -0
  237. package/dist/tools/testSpec.js +525 -0
  238. package/dist/tools/testSpec.js.map +1 -0
  239. package/dist/tools/toolsIndex.d.ts +5 -0
  240. package/dist/tools/toolsIndex.d.ts.map +1 -0
  241. package/dist/tools/toolsIndex.js +37 -0
  242. package/dist/tools/toolsIndex.js.map +1 -0
  243. package/dist/tools/types.d.ts +30 -0
  244. package/dist/tools/types.d.ts.map +1 -0
  245. package/dist/tools/types.js +7 -0
  246. package/dist/tools/types.js.map +1 -0
  247. package/dist/tools/voice/index.d.ts +8 -0
  248. package/dist/tools/voice/index.d.ts.map +1 -0
  249. package/dist/tools/voice/index.js +176 -0
  250. package/dist/tools/voice/index.js.map +1 -0
  251. package/dist/tools/voice.d.ts +291 -0
  252. package/dist/tools/voice.d.ts.map +1 -0
  253. package/dist/tools/voice.js +734 -0
  254. package/dist/tools/voice.js.map +1 -0
  255. package/package.json +54 -0
@@ -0,0 +1,734 @@
1
+ /**
2
+ * Voice MCP Tools - Voice Input for Decibel Commands
3
+ *
4
+ * Provides MCP tools for:
5
+ * - Managing a Voice Inbox (queued transcripts)
6
+ * - Processing voice commands via AI intent parsing
7
+ * - Routing to existing Decibel tools based on intent
8
+ *
9
+ * DOJO-EXP-0001: Voice Input for Decibel Commands
10
+ * Status: Ungraduated experiment - feature flagged
11
+ */
12
+ import fs from 'fs/promises';
13
+ import path from 'path';
14
+ import YAML from 'yaml';
15
+ import { log } from '../config.js';
16
+ import { ensureDir } from '../dataRoot.js';
17
+ import { resolveProjectRoot } from '../projectPaths.js';
18
+ import { getDefaultProject } from '../projectRegistry.js';
19
+ import { getSupabaseServiceClient, isSupabaseConfigured } from '../lib/supabase.js';
20
+ async function resolveVoiceRoot(projectId) {
21
+ const targetProjectId = projectId || getDefaultProject()?.id;
22
+ if (!targetProjectId) {
23
+ // No project specified and no default - use Supabase with 'default' project
24
+ if (isSupabaseConfigured()) {
25
+ log('voice: No project specified, using Supabase with project_id="default"');
26
+ return { projectId: 'default', voiceRoot: null, isRemote: true };
27
+ }
28
+ throw new Error('No project specified and no default project found.');
29
+ }
30
+ // Try to resolve local project
31
+ try {
32
+ const project = await resolveProjectRoot(targetProjectId);
33
+ const voiceRoot = path.join(project.root, '.decibel', 'voice');
34
+ return { projectId: targetProjectId, voiceRoot, isRemote: false };
35
+ }
36
+ catch (err) {
37
+ // Local project not found - use Supabase if configured
38
+ if (isSupabaseConfigured()) {
39
+ log(`voice: Project "${targetProjectId}" not found locally, using Supabase`);
40
+ return { projectId: targetProjectId, voiceRoot: null, isRemote: true };
41
+ }
42
+ // Re-throw original error if Supabase not available
43
+ throw err;
44
+ }
45
+ }
46
+ // Write inbox item to Supabase
47
+ async function writeToSupabase(item) {
48
+ const supabase = getSupabaseServiceClient();
49
+ const { error } = await supabase.from('voice_inbox').insert({
50
+ id: item.id,
51
+ project_id: item.project_id,
52
+ transcript: item.transcript,
53
+ source: item.source,
54
+ intent: item.intent || 'unknown',
55
+ intent_confidence: item.intent_confidence || 0,
56
+ parsed_params: item.parsed_params || {},
57
+ status: item.status,
58
+ device: item.tags?.find(t => t.startsWith('device:'))?.replace('device:', '') || null,
59
+ tags: item.tags || [],
60
+ created_at: item.created_at,
61
+ });
62
+ if (error) {
63
+ log(`voice: Supabase insert error: ${error.message}`);
64
+ throw new Error(`Failed to write to Supabase: ${error.message}`);
65
+ }
66
+ log(`voice: Wrote inbox item ${item.id} to Supabase for project "${item.project_id}"`);
67
+ }
68
+ function generateInboxId() {
69
+ const now = new Date();
70
+ const timestamp = now.toISOString().replace(/[-:]/g, '').slice(0, 15).replace('T', '-');
71
+ const random = Math.random().toString(36).substring(2, 6);
72
+ return `voice-${timestamp}-${random}`;
73
+ }
74
+ // ============================================================================
75
+ // Intent Parsing
76
+ // ============================================================================
77
+ /**
78
+ * Parse intent from a voice transcript.
79
+ * Uses pattern matching for now - can be upgraded to AI-powered later.
80
+ */
81
+ export function parseIntent(transcript) {
82
+ const lower = transcript.toLowerCase().trim();
83
+ // Wish patterns
84
+ if (lower.match(/^(add|create|log|make)\s+(a\s+)?wish\s+(for|about|to)\s+/i) ||
85
+ lower.match(/^i\s+wish\s+(we\s+)?(had|could|can)\s+/i) ||
86
+ lower.match(/^wish\s*:\s*/i)) {
87
+ const content = transcript
88
+ .replace(/^(add|create|log|make)\s+(a\s+)?wish\s+(for|about|to)\s+/i, '')
89
+ .replace(/^i\s+wish\s+(we\s+)?(had|could|can)\s+/i, '')
90
+ .replace(/^wish\s*:\s*/i, '')
91
+ .trim();
92
+ return {
93
+ intent: 'add_wish',
94
+ confidence: 0.9,
95
+ params: { capability: content, reason: 'Voice-captured wish' }
96
+ };
97
+ }
98
+ // Issue patterns
99
+ if (lower.match(/^(log|create|add|file)\s+(an?\s+)?issue\s+(about|for|with)\s+/i) ||
100
+ lower.match(/^there\s+(is\s+)?(a\s+)?(bug|problem|issue)\s+(with|in)\s+/i) ||
101
+ lower.match(/^(there'?s?\s+)?(a\s+)?(bug|problem|issue)\s+(with|in)\s+/i) ||
102
+ lower.match(/^issue\s*:\s*/i)) {
103
+ const content = transcript
104
+ .replace(/^(log|create|add|file)\s+(an?\s+)?issue\s+(about|for|with)\s+/i, '')
105
+ .replace(/^(there'?s?\s+)?(a\s+)?(bug|problem|issue)\s+(with|in)\s+/i, '')
106
+ .replace(/^issue\s*:\s*/i, '')
107
+ .trim();
108
+ return {
109
+ intent: 'log_issue',
110
+ confidence: 0.85,
111
+ params: { title: content, description: `Voice-logged: ${content}` }
112
+ };
113
+ }
114
+ // Search patterns
115
+ if (lower.match(/^(find|search|look\s+for|where\s+is|what\s+is|show\s+me)\s+/i) ||
116
+ lower.match(/\?$/)) {
117
+ const query = transcript
118
+ .replace(/^(find|search|look\s+for|where\s+is|what\s+is|show\s+me)\s+/i, '')
119
+ .replace(/\?$/, '')
120
+ .trim();
121
+ return {
122
+ intent: 'search',
123
+ confidence: 0.8,
124
+ params: { query }
125
+ };
126
+ }
127
+ // Oracle/roadmap patterns
128
+ if (lower.match(/^(what'?s?\s+)?(the\s+)?(roadmap|status|progress|health)/i) ||
129
+ lower.match(/^(how\s+are\s+we\s+doing|project\s+status)/i)) {
130
+ return {
131
+ intent: 'ask_oracle',
132
+ confidence: 0.85,
133
+ params: { query: transcript }
134
+ };
135
+ }
136
+ // Crit patterns
137
+ if (lower.match(/^(crit|critique|feedback|observation)\s*:\s*/i) ||
138
+ lower.match(/^i\s+(noticed|think|feel|observe)/i)) {
139
+ const content = transcript
140
+ .replace(/^(crit|critique|feedback|observation)\s*:\s*/i, '')
141
+ .replace(/^i\s+(noticed|think|feel|observe)\s+/i, '')
142
+ .trim();
143
+ return {
144
+ intent: 'log_crit',
145
+ confidence: 0.75,
146
+ params: { observation: content, area: 'voice-captured' }
147
+ };
148
+ }
149
+ // Friction patterns
150
+ if (lower.match(/^(friction|pain\s+point|annoying|frustrating)\s*:\s*/i) ||
151
+ lower.match(/^it\s+is\s+(really\s+)?(annoying|frustrating|painful)\s+(that|when)/i) ||
152
+ lower.match(/^(it'?s?\s+)?(really\s+)?(annoying|frustrating|painful)\s+(that|when)/i)) {
153
+ const content = transcript
154
+ .replace(/^(friction|pain\s+point|annoying|frustrating)\s*:\s*/i, '')
155
+ .replace(/^(it'?s?\s+)?(really\s+)?(annoying|frustrating|painful)\s+(that|when)\s*/i, '')
156
+ .trim();
157
+ return {
158
+ intent: 'log_friction',
159
+ confidence: 0.8,
160
+ params: { description: content, context: 'voice-captured' }
161
+ };
162
+ }
163
+ // Learning patterns
164
+ if (lower.match(/^(learned|til|today\s+i\s+learned|lesson)\s*:\s*/i) ||
165
+ lower.match(/^i\s+(just\s+)?(learned|discovered|figured\s+out)/i)) {
166
+ const content = transcript
167
+ .replace(/^(learned|til|today\s+i\s+learned|lesson)\s*:\s*/i, '')
168
+ .replace(/^i\s+(just\s+)?(learned|discovered|figured\s+out)\s+(that\s+)?/i, '')
169
+ .trim();
170
+ return {
171
+ intent: 'record_learning',
172
+ confidence: 0.75,
173
+ params: { title: content.slice(0, 50), content, category: 'other' }
174
+ };
175
+ }
176
+ // Unknown - but still capture it
177
+ return {
178
+ intent: 'unknown',
179
+ confidence: 0.3,
180
+ params: { raw_transcript: transcript }
181
+ };
182
+ }
183
+ export async function voiceInboxAdd(input) {
184
+ const { projectId, voiceRoot, isRemote } = await resolveVoiceRoot(input.project_id);
185
+ const inboxId = generateInboxId();
186
+ const timestamp = new Date().toISOString();
187
+ // Use explicit intent if provided (human-labeled from button tap), otherwise parse
188
+ let intent;
189
+ let confidence;
190
+ let params;
191
+ if (input.explicit_intent) {
192
+ // Human-labeled intent - use as-is with 100% confidence
193
+ intent = input.explicit_intent;
194
+ confidence = 1.0;
195
+ params = { raw_transcript: input.transcript };
196
+ log(`voice: Using explicit intent "${intent}" (human-labeled)`);
197
+ }
198
+ else {
199
+ // Parse intent from transcript patterns
200
+ const parsed = parseIntent(input.transcript);
201
+ intent = parsed.intent;
202
+ confidence = parsed.confidence;
203
+ params = parsed.params;
204
+ }
205
+ const item = {
206
+ id: inboxId,
207
+ transcript: input.transcript,
208
+ source: input.source || 'text_input',
209
+ created_at: timestamp,
210
+ status: 'queued',
211
+ intent,
212
+ intent_confidence: confidence,
213
+ parsed_params: params,
214
+ tags: input.tags,
215
+ project_id: projectId,
216
+ };
217
+ // Write to storage (Supabase for remote, local files otherwise)
218
+ if (isRemote) {
219
+ await writeToSupabase(item);
220
+ }
221
+ else {
222
+ const inboxDir = path.join(voiceRoot, 'inbox');
223
+ ensureDir(inboxDir);
224
+ const itemPath = path.join(inboxDir, `${inboxId}.yaml`);
225
+ await fs.writeFile(itemPath, YAML.stringify(item), 'utf-8');
226
+ log(`voice: Added inbox item ${inboxId} with intent "${intent}" (${(confidence * 100).toFixed(0)}%)`);
227
+ }
228
+ // Process immediately if requested, confidence is high enough, and running locally
229
+ // (Remote mode skips processing - messages are just stored for later sync)
230
+ let immediateResult;
231
+ if (input.process_immediately && confidence >= 0.7 && !isRemote) {
232
+ const inboxDir = path.join(voiceRoot, 'inbox');
233
+ const itemPath = path.join(inboxDir, `${inboxId}.yaml`);
234
+ try {
235
+ immediateResult = await processVoiceItem(item, voiceRoot);
236
+ item.status = 'completed';
237
+ item.result = {
238
+ tool_called: intent,
239
+ tool_result: immediateResult,
240
+ completed_at: new Date().toISOString(),
241
+ };
242
+ await fs.writeFile(itemPath, YAML.stringify(item), 'utf-8');
243
+ }
244
+ catch (err) {
245
+ item.status = 'failed';
246
+ item.error = err instanceof Error ? err.message : String(err);
247
+ await fs.writeFile(itemPath, YAML.stringify(item), 'utf-8');
248
+ }
249
+ }
250
+ return {
251
+ inbox_id: inboxId,
252
+ transcript: input.transcript,
253
+ intent,
254
+ intent_confidence: confidence,
255
+ status: item.status,
256
+ immediate_result: immediateResult,
257
+ // Include remote flag in output so caller knows message was stored remotely
258
+ ...(isRemote && { stored_in: 'supabase' }),
259
+ };
260
+ }
261
+ export async function voiceInboxList(input) {
262
+ const { projectId, voiceRoot, isRemote } = await resolveVoiceRoot(input.project_id);
263
+ const items = [];
264
+ const byStatus = {
265
+ queued: 0,
266
+ processing: 0,
267
+ completed: 0,
268
+ failed: 0,
269
+ };
270
+ if (isRemote) {
271
+ // Query Supabase for remote mode
272
+ const supabase = getSupabaseServiceClient();
273
+ let query = supabase
274
+ .from('voice_inbox')
275
+ .select('*')
276
+ .eq('project_id', projectId)
277
+ .order('created_at', { ascending: false });
278
+ if (input.status) {
279
+ query = query.eq('status', input.status);
280
+ }
281
+ if (input.limit) {
282
+ query = query.limit(input.limit);
283
+ }
284
+ const { data, error } = await query;
285
+ if (error) {
286
+ throw new Error(`Failed to query Supabase: ${error.message}`);
287
+ }
288
+ for (const row of data || []) {
289
+ byStatus[row.status]++;
290
+ items.push({
291
+ id: row.id,
292
+ transcript: row.transcript,
293
+ source: row.source,
294
+ created_at: row.created_at,
295
+ status: row.status,
296
+ intent: row.intent,
297
+ intent_confidence: row.intent_confidence,
298
+ parsed_params: row.parsed_params || {},
299
+ result: row.result || undefined,
300
+ error: row.error || undefined,
301
+ tags: row.tags || [],
302
+ project_id: row.project_id,
303
+ });
304
+ }
305
+ }
306
+ else {
307
+ // Read from local filesystem
308
+ const inboxDir = path.join(voiceRoot, 'inbox');
309
+ try {
310
+ const files = await fs.readdir(inboxDir);
311
+ for (const file of files) {
312
+ if (!file.endsWith('.yaml'))
313
+ continue;
314
+ try {
315
+ const content = await fs.readFile(path.join(inboxDir, file), 'utf-8');
316
+ const item = YAML.parse(content);
317
+ byStatus[item.status]++;
318
+ // Apply filters
319
+ if (input.status && item.status !== input.status)
320
+ continue;
321
+ items.push(item);
322
+ }
323
+ catch {
324
+ // Skip malformed files
325
+ }
326
+ }
327
+ }
328
+ catch {
329
+ // Directory doesn't exist yet
330
+ }
331
+ // Sort by created_at descending (newest first)
332
+ items.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
333
+ // Apply limit
334
+ if (input.limit) {
335
+ items.splice(input.limit);
336
+ }
337
+ }
338
+ return {
339
+ items,
340
+ total: items.length,
341
+ by_status: byStatus,
342
+ };
343
+ }
344
+ /**
345
+ * Process a voice inbox item by routing to the appropriate tool
346
+ */
347
+ async function processVoiceItem(item, voiceRoot) {
348
+ const intent = item.intent || 'unknown';
349
+ const params = item.parsed_params || {};
350
+ log(`voice: Processing item ${item.id} with intent "${intent}"`);
351
+ // Import tools dynamically to avoid circular dependencies
352
+ switch (intent) {
353
+ case 'add_wish': {
354
+ const { addWish } = await import('./dojo.js');
355
+ const result = await addWish({
356
+ project_id: item.project_id,
357
+ capability: params.capability || item.transcript,
358
+ reason: params.reason || 'Voice-captured wish',
359
+ inputs: ['voice_transcript'],
360
+ outputs: { captured_from: 'voice' },
361
+ });
362
+ return result;
363
+ }
364
+ case 'log_issue': {
365
+ const { createIssue } = await import('./sentinel.js');
366
+ const result = await createIssue({
367
+ projectId: item.project_id,
368
+ severity: 'med',
369
+ title: params.title || item.transcript.slice(0, 80),
370
+ details: params.description || `Voice-logged: ${item.transcript}`,
371
+ });
372
+ return result;
373
+ }
374
+ case 'log_crit': {
375
+ const { logCrit } = await import('./crit.js');
376
+ const result = await logCrit({
377
+ projectId: item.project_id,
378
+ area: params.area || 'general',
379
+ observation: params.observation || item.transcript,
380
+ sentiment: 'neutral',
381
+ context: 'Voice-captured crit',
382
+ });
383
+ return result;
384
+ }
385
+ case 'log_friction': {
386
+ const { logFriction } = await import('./friction.js');
387
+ const result = await logFriction({
388
+ projectId: item.project_id,
389
+ context: params.context || 'voice-captured',
390
+ description: params.description || item.transcript,
391
+ source: 'human',
392
+ });
393
+ return result;
394
+ }
395
+ case 'record_learning': {
396
+ const { appendLearning } = await import('./learnings.js');
397
+ const result = await appendLearning({
398
+ projectId: item.project_id,
399
+ category: 'other',
400
+ title: params.title || item.transcript.slice(0, 50),
401
+ content: params.content || item.transcript,
402
+ tags: ['voice-captured'],
403
+ });
404
+ return result;
405
+ }
406
+ case 'search':
407
+ case 'ask_oracle':
408
+ // These require more complex handling - queue for now
409
+ return {
410
+ status: 'queued_for_ai',
411
+ message: 'Search and oracle queries require AI processing - queued for later',
412
+ query: params.query || item.transcript,
413
+ };
414
+ case 'unknown':
415
+ default:
416
+ return {
417
+ status: 'needs_clarification',
418
+ message: 'Could not determine intent from transcript',
419
+ transcript: item.transcript,
420
+ suggestion: 'Try starting with "wish:", "issue:", or "crit:" for clearer intent',
421
+ };
422
+ }
423
+ }
424
+ export async function voiceInboxProcess(input) {
425
+ const { voiceRoot, isRemote } = await resolveVoiceRoot(input.project_id);
426
+ // Processing requires local project - sync first if running remotely
427
+ if (isRemote) {
428
+ throw new Error('Cannot process voice items in remote mode. Use voice_inbox_sync to pull messages to local first.');
429
+ }
430
+ const inboxDir = path.join(voiceRoot, 'inbox');
431
+ const itemPath = path.join(inboxDir, `${input.inbox_id}.yaml`);
432
+ // Read item
433
+ let item;
434
+ try {
435
+ const content = await fs.readFile(itemPath, 'utf-8');
436
+ item = YAML.parse(content);
437
+ }
438
+ catch {
439
+ throw new Error(`Inbox item not found: ${input.inbox_id}`);
440
+ }
441
+ // Apply overrides
442
+ if (input.override_intent) {
443
+ item.intent = input.override_intent;
444
+ }
445
+ if (input.override_params) {
446
+ item.parsed_params = { ...item.parsed_params, ...input.override_params };
447
+ }
448
+ // Mark as processing
449
+ item.status = 'processing';
450
+ await fs.writeFile(itemPath, YAML.stringify(item), 'utf-8');
451
+ try {
452
+ const result = await processVoiceItem(item, voiceRoot);
453
+ // Mark as completed
454
+ item.status = 'completed';
455
+ item.result = {
456
+ tool_called: item.intent || 'unknown',
457
+ tool_result: result,
458
+ completed_at: new Date().toISOString(),
459
+ };
460
+ await fs.writeFile(itemPath, YAML.stringify(item), 'utf-8');
461
+ return {
462
+ inbox_id: input.inbox_id,
463
+ intent: item.intent || 'unknown',
464
+ tool_called: item.intent || 'unknown',
465
+ result,
466
+ success: true,
467
+ };
468
+ }
469
+ catch (err) {
470
+ // Mark as failed
471
+ item.status = 'failed';
472
+ item.error = err instanceof Error ? err.message : String(err);
473
+ await fs.writeFile(itemPath, YAML.stringify(item), 'utf-8');
474
+ return {
475
+ inbox_id: input.inbox_id,
476
+ intent: item.intent || 'unknown',
477
+ tool_called: item.intent || 'unknown',
478
+ result: { error: item.error },
479
+ success: false,
480
+ };
481
+ }
482
+ }
483
+ /**
484
+ * Process a voice command directly without storing in inbox.
485
+ * Use for real-time voice interactions.
486
+ */
487
+ export async function voiceCommand(input) {
488
+ const { projectId, voiceRoot, isRemote } = await resolveVoiceRoot(input.project_id);
489
+ // Direct processing requires local project
490
+ if (isRemote) {
491
+ throw new Error('Cannot process voice commands in remote mode. Use voice_inbox_add to queue, then sync locally.');
492
+ }
493
+ // Parse intent
494
+ const { intent, confidence, params } = parseIntent(input.transcript);
495
+ log(`voice: Direct command with intent "${intent}" (${(confidence * 100).toFixed(0)}%)`);
496
+ // Create ephemeral item for processing
497
+ const item = {
498
+ id: 'ephemeral',
499
+ transcript: input.transcript,
500
+ source: 'text_input',
501
+ created_at: new Date().toISOString(),
502
+ status: 'processing',
503
+ intent,
504
+ intent_confidence: confidence,
505
+ parsed_params: params,
506
+ project_id: projectId,
507
+ };
508
+ try {
509
+ const result = await processVoiceItem(item, voiceRoot);
510
+ return {
511
+ transcript: input.transcript,
512
+ intent,
513
+ intent_confidence: confidence,
514
+ tool_called: intent,
515
+ result,
516
+ success: true,
517
+ };
518
+ }
519
+ catch (err) {
520
+ return {
521
+ transcript: input.transcript,
522
+ intent,
523
+ intent_confidence: confidence,
524
+ tool_called: intent,
525
+ result: { error: err instanceof Error ? err.message : String(err) },
526
+ success: false,
527
+ };
528
+ }
529
+ }
530
+ /**
531
+ * Sync voice inbox messages from Supabase to local project.
532
+ * This is the "pull" operation - projects can call this to get pending messages.
533
+ */
534
+ export async function voiceInboxSync(input) {
535
+ if (!isSupabaseConfigured()) {
536
+ throw new Error('Supabase is not configured. Cannot sync from remote.');
537
+ }
538
+ // Must have a local project to sync to
539
+ const targetProjectId = input.project_id || getDefaultProject()?.id;
540
+ if (!targetProjectId) {
541
+ throw new Error('No project specified and no default project found.');
542
+ }
543
+ // Verify local project exists
544
+ let voiceRoot;
545
+ try {
546
+ const project = await resolveProjectRoot(targetProjectId);
547
+ voiceRoot = path.join(project.root, '.decibel', 'voice');
548
+ }
549
+ catch (err) {
550
+ throw new Error(`Cannot sync: local project "${targetProjectId}" not found.`);
551
+ }
552
+ const supabase = getSupabaseServiceClient();
553
+ const unsyncedOnly = input.unsynced_only !== false; // default true
554
+ const limit = input.limit || 50;
555
+ // Query Supabase for messages
556
+ let query = supabase
557
+ .from('voice_inbox')
558
+ .select('*')
559
+ .eq('project_id', targetProjectId)
560
+ .order('created_at', { ascending: true })
561
+ .limit(limit);
562
+ if (unsyncedOnly) {
563
+ query = query.is('synced_at', null);
564
+ }
565
+ const { data: messages, error: queryError } = await query;
566
+ if (queryError) {
567
+ throw new Error(`Failed to query Supabase: ${queryError.message}`);
568
+ }
569
+ if (!messages || messages.length === 0) {
570
+ log(`voice: No messages to sync for project "${targetProjectId}"`);
571
+ return { synced: 0, skipped: 0, errors: 0, items: [] };
572
+ }
573
+ log(`voice: Found ${messages.length} messages to sync for project "${targetProjectId}"`);
574
+ const inboxDir = path.join(voiceRoot, 'inbox');
575
+ ensureDir(inboxDir);
576
+ const results = [];
577
+ const syncedIds = [];
578
+ for (const msg of messages) {
579
+ const itemPath = path.join(inboxDir, `${msg.id}.yaml`);
580
+ // Check if already exists locally
581
+ try {
582
+ await fs.access(itemPath);
583
+ // File exists - skip
584
+ results.push({
585
+ id: msg.id,
586
+ transcript: msg.transcript,
587
+ intent: msg.intent,
588
+ status: 'already_exists',
589
+ });
590
+ continue;
591
+ }
592
+ catch {
593
+ // File doesn't exist - good, we'll create it
594
+ }
595
+ // Convert Supabase row to VoiceInboxItem
596
+ const item = {
597
+ id: msg.id,
598
+ transcript: msg.transcript,
599
+ source: msg.source,
600
+ created_at: msg.created_at,
601
+ status: msg.status,
602
+ intent: msg.intent,
603
+ intent_confidence: msg.intent_confidence,
604
+ parsed_params: msg.parsed_params || {},
605
+ result: msg.result || undefined,
606
+ error: msg.error || undefined,
607
+ tags: msg.tags || [],
608
+ project_id: msg.project_id,
609
+ };
610
+ try {
611
+ await fs.writeFile(itemPath, YAML.stringify(item), 'utf-8');
612
+ syncedIds.push(msg.id);
613
+ results.push({
614
+ id: msg.id,
615
+ transcript: msg.transcript,
616
+ intent: msg.intent,
617
+ status: 'synced',
618
+ });
619
+ log(`voice: Synced message ${msg.id}`);
620
+ }
621
+ catch (err) {
622
+ results.push({
623
+ id: msg.id,
624
+ transcript: msg.transcript,
625
+ intent: msg.intent,
626
+ status: 'error',
627
+ error: err instanceof Error ? err.message : String(err),
628
+ });
629
+ }
630
+ }
631
+ // Update synced_at in Supabase for successfully synced messages
632
+ if (syncedIds.length > 0) {
633
+ const { error: updateError } = await supabase
634
+ .from('voice_inbox')
635
+ .update({ synced_at: new Date().toISOString() })
636
+ .in('id', syncedIds);
637
+ if (updateError) {
638
+ log(`voice: Warning - failed to update synced_at: ${updateError.message}`);
639
+ }
640
+ }
641
+ const summary = {
642
+ synced: results.filter(r => r.status === 'synced').length,
643
+ skipped: results.filter(r => r.status === 'already_exists').length,
644
+ errors: results.filter(r => r.status === 'error').length,
645
+ items: results,
646
+ };
647
+ log(`voice: Sync complete - ${summary.synced} synced, ${summary.skipped} skipped, ${summary.errors} errors`);
648
+ return summary;
649
+ }
650
+ // ============================================================================
651
+ // Exported Tool Definitions for MCP Registration
652
+ // ============================================================================
653
+ export const voiceToolDefinitions = [
654
+ {
655
+ name: 'voice_inbox_add',
656
+ description: 'Add a voice transcript to the inbox for processing. Parses intent automatically. Use process_immediately=true for instant execution.',
657
+ inputSchema: {
658
+ type: 'object',
659
+ properties: {
660
+ project_id: { type: 'string', description: 'Project ID (optional, uses default)' },
661
+ transcript: { type: 'string', description: 'The voice transcript text' },
662
+ source: {
663
+ type: 'string',
664
+ enum: ['voice_cli', 'text_input', 'share_extension', 'api'],
665
+ description: 'Source of the transcript (default: text_input)'
666
+ },
667
+ tags: { type: 'array', items: { type: 'string' }, description: 'Optional tags' },
668
+ process_immediately: { type: 'boolean', description: 'Process now instead of queuing (default: false)' },
669
+ },
670
+ required: ['transcript'],
671
+ },
672
+ },
673
+ {
674
+ name: 'voice_inbox_list',
675
+ description: 'List voice inbox items, optionally filtered by status.',
676
+ inputSchema: {
677
+ type: 'object',
678
+ properties: {
679
+ project_id: { type: 'string', description: 'Project ID (optional, uses default)' },
680
+ status: {
681
+ type: 'string',
682
+ enum: ['queued', 'processing', 'completed', 'failed'],
683
+ description: 'Filter by status'
684
+ },
685
+ limit: { type: 'number', description: 'Max items to return' },
686
+ },
687
+ },
688
+ },
689
+ {
690
+ name: 'voice_inbox_process',
691
+ description: 'Process a queued voice inbox item by routing to the appropriate tool.',
692
+ inputSchema: {
693
+ type: 'object',
694
+ properties: {
695
+ project_id: { type: 'string', description: 'Project ID (optional, uses default)' },
696
+ inbox_id: { type: 'string', description: 'The inbox item ID to process' },
697
+ override_intent: {
698
+ type: 'string',
699
+ enum: ['add_wish', 'log_issue', 'create_epic', 'search', 'ask_oracle', 'log_crit', 'log_friction', 'record_learning'],
700
+ description: 'Override the auto-detected intent'
701
+ },
702
+ override_params: { type: 'object', description: 'Override parsed parameters' },
703
+ },
704
+ required: ['inbox_id'],
705
+ },
706
+ },
707
+ {
708
+ name: 'voice_command',
709
+ description: 'Process a voice command directly without storing in inbox. For real-time interactions.',
710
+ inputSchema: {
711
+ type: 'object',
712
+ properties: {
713
+ project_id: { type: 'string', description: 'Project ID (optional, uses default)' },
714
+ transcript: { type: 'string', description: 'The voice transcript to process' },
715
+ },
716
+ required: ['transcript'],
717
+ },
718
+ },
719
+ {
720
+ name: 'voice_inbox_sync',
721
+ description: 'Sync voice inbox messages from Supabase to local project. Call this to pull down pending messages that were captured remotely (e.g., from iOS app). Like checking email.',
722
+ inputSchema: {
723
+ type: 'object',
724
+ properties: {
725
+ project_id: { type: 'string', description: 'Project ID to sync messages for (required)' },
726
+ unsynced_only: { type: 'boolean', description: 'Only sync messages not yet synced (default: true)' },
727
+ limit: { type: 'number', description: 'Maximum messages to sync (default: 50)' },
728
+ process_after_sync: { type: 'boolean', description: 'Process synced messages immediately (default: false)' },
729
+ },
730
+ required: ['project_id'],
731
+ },
732
+ },
733
+ ];
734
+ //# sourceMappingURL=voice.js.map