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.
- package/LICENSE +21 -0
- package/README.md +335 -0
- package/dist/agentic/compiler.d.ts +21 -0
- package/dist/agentic/compiler.d.ts.map +1 -0
- package/dist/agentic/compiler.js +267 -0
- package/dist/agentic/compiler.js.map +1 -0
- package/dist/agentic/golden.d.ts +25 -0
- package/dist/agentic/golden.d.ts.map +1 -0
- package/dist/agentic/golden.js +255 -0
- package/dist/agentic/golden.js.map +1 -0
- package/dist/agentic/index.d.ts +17 -0
- package/dist/agentic/index.d.ts.map +1 -0
- package/dist/agentic/index.js +153 -0
- package/dist/agentic/index.js.map +1 -0
- package/dist/agentic/linter.d.ts +20 -0
- package/dist/agentic/linter.d.ts.map +1 -0
- package/dist/agentic/linter.js +340 -0
- package/dist/agentic/linter.js.map +1 -0
- package/dist/agentic/renderer.d.ts +17 -0
- package/dist/agentic/renderer.d.ts.map +1 -0
- package/dist/agentic/renderer.js +277 -0
- package/dist/agentic/renderer.js.map +1 -0
- package/dist/agentic/types.d.ts +199 -0
- package/dist/agentic/types.d.ts.map +1 -0
- package/dist/agentic/types.js +8 -0
- package/dist/agentic/types.js.map +1 -0
- package/dist/architectAdrs.d.ts +19 -0
- package/dist/architectAdrs.d.ts.map +1 -0
- package/dist/architectAdrs.js +123 -0
- package/dist/architectAdrs.js.map +1 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +19 -0
- package/dist/config.js.map +1 -0
- package/dist/dataRoot.d.ts +45 -0
- package/dist/dataRoot.d.ts.map +1 -0
- package/dist/dataRoot.js +201 -0
- package/dist/dataRoot.js.map +1 -0
- package/dist/decibelPaths.d.ts +42 -0
- package/dist/decibelPaths.d.ts.map +1 -0
- package/dist/decibelPaths.js +150 -0
- package/dist/decibelPaths.js.map +1 -0
- package/dist/httpServer.d.ts +49 -0
- package/dist/httpServer.d.ts.map +1 -0
- package/dist/httpServer.js +1472 -0
- package/dist/httpServer.js.map +1 -0
- package/dist/lib/benchmark.d.ts +110 -0
- package/dist/lib/benchmark.d.ts.map +1 -0
- package/dist/lib/benchmark.js +338 -0
- package/dist/lib/benchmark.js.map +1 -0
- package/dist/lib/supabase.d.ts +123 -0
- package/dist/lib/supabase.d.ts.map +1 -0
- package/dist/lib/supabase.js +91 -0
- package/dist/lib/supabase.js.map +1 -0
- package/dist/projectPaths.d.ts +27 -0
- package/dist/projectPaths.d.ts.map +1 -0
- package/dist/projectPaths.js +86 -0
- package/dist/projectPaths.js.map +1 -0
- package/dist/projectRegistry.d.ts +97 -0
- package/dist/projectRegistry.d.ts.map +1 -0
- package/dist/projectRegistry.js +362 -0
- package/dist/projectRegistry.js.map +1 -0
- package/dist/sentinelIssues.d.ts +44 -0
- package/dist/sentinelIssues.d.ts.map +1 -0
- package/dist/sentinelIssues.js +213 -0
- package/dist/sentinelIssues.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +93 -0
- package/dist/server.js.map +1 -0
- package/dist/test.d.ts +7 -0
- package/dist/test.d.ts.map +1 -0
- package/dist/test.js +77 -0
- package/dist/test.js.map +1 -0
- package/dist/tools/agentic/index.d.ts +7 -0
- package/dist/tools/agentic/index.d.ts.map +1 -0
- package/dist/tools/agentic/index.js +180 -0
- package/dist/tools/agentic/index.js.map +1 -0
- package/dist/tools/architect/index.d.ts +9 -0
- package/dist/tools/architect/index.d.ts.map +1 -0
- package/dist/tools/architect/index.js +383 -0
- package/dist/tools/architect/index.js.map +1 -0
- package/dist/tools/architect.d.ts +19 -0
- package/dist/tools/architect.d.ts.map +1 -0
- package/dist/tools/architect.js +88 -0
- package/dist/tools/architect.js.map +1 -0
- package/dist/tools/bench.d.ts +89 -0
- package/dist/tools/bench.d.ts.map +1 -0
- package/dist/tools/bench.js +826 -0
- package/dist/tools/bench.js.map +1 -0
- package/dist/tools/context/index.d.ts +11 -0
- package/dist/tools/context/index.d.ts.map +1 -0
- package/dist/tools/context/index.js +439 -0
- package/dist/tools/context/index.js.map +1 -0
- package/dist/tools/context.d.ts +146 -0
- package/dist/tools/context.d.ts.map +1 -0
- package/dist/tools/context.js +481 -0
- package/dist/tools/context.js.map +1 -0
- package/dist/tools/crit.d.ts +63 -0
- package/dist/tools/crit.d.ts.map +1 -0
- package/dist/tools/crit.js +159 -0
- package/dist/tools/crit.js.map +1 -0
- package/dist/tools/data-inspector.d.ts +188 -0
- package/dist/tools/data-inspector.d.ts.map +1 -0
- package/dist/tools/data-inspector.js +650 -0
- package/dist/tools/data-inspector.js.map +1 -0
- package/dist/tools/deck.d.ts +11 -0
- package/dist/tools/deck.d.ts.map +1 -0
- package/dist/tools/deck.js +170 -0
- package/dist/tools/deck.js.map +1 -0
- package/dist/tools/designer/index.d.ts +6 -0
- package/dist/tools/designer/index.d.ts.map +1 -0
- package/dist/tools/designer/index.js +177 -0
- package/dist/tools/designer/index.js.map +1 -0
- package/dist/tools/designer.d.ts +20 -0
- package/dist/tools/designer.d.ts.map +1 -0
- package/dist/tools/designer.js +75 -0
- package/dist/tools/designer.js.map +1 -0
- package/dist/tools/dojo/index.d.ts +13 -0
- package/dist/tools/dojo/index.d.ts.map +1 -0
- package/dist/tools/dojo/index.js +547 -0
- package/dist/tools/dojo/index.js.map +1 -0
- package/dist/tools/dojo.d.ts +254 -0
- package/dist/tools/dojo.d.ts.map +1 -0
- package/dist/tools/dojo.js +933 -0
- package/dist/tools/dojo.js.map +1 -0
- package/dist/tools/dojoBench.d.ts +49 -0
- package/dist/tools/dojoBench.d.ts.map +1 -0
- package/dist/tools/dojoBench.js +205 -0
- package/dist/tools/dojoBench.js.map +1 -0
- package/dist/tools/dojoGraduated.d.ts +50 -0
- package/dist/tools/dojoGraduated.d.ts.map +1 -0
- package/dist/tools/dojoGraduated.js +174 -0
- package/dist/tools/dojoGraduated.js.map +1 -0
- package/dist/tools/dojoPolicy.d.ts +65 -0
- package/dist/tools/dojoPolicy.d.ts.map +1 -0
- package/dist/tools/dojoPolicy.js +263 -0
- package/dist/tools/dojoPolicy.js.map +1 -0
- package/dist/tools/friction/index.d.ts +7 -0
- package/dist/tools/friction/index.d.ts.map +1 -0
- package/dist/tools/friction/index.js +239 -0
- package/dist/tools/friction/index.js.map +1 -0
- package/dist/tools/friction.d.ts +82 -0
- package/dist/tools/friction.d.ts.map +1 -0
- package/dist/tools/friction.js +331 -0
- package/dist/tools/friction.js.map +1 -0
- package/dist/tools/index.d.ts +5 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +52 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/learnings/index.d.ts +5 -0
- package/dist/tools/learnings/index.d.ts.map +1 -0
- package/dist/tools/learnings/index.js +123 -0
- package/dist/tools/learnings/index.js.map +1 -0
- package/dist/tools/learnings.d.ts +41 -0
- package/dist/tools/learnings.d.ts.map +1 -0
- package/dist/tools/learnings.js +149 -0
- package/dist/tools/learnings.js.map +1 -0
- package/dist/tools/oracle/index.d.ts +5 -0
- package/dist/tools/oracle/index.d.ts.map +1 -0
- package/dist/tools/oracle/index.js +97 -0
- package/dist/tools/oracle/index.js.map +1 -0
- package/dist/tools/oracle.d.ts +90 -0
- package/dist/tools/oracle.d.ts.map +1 -0
- package/dist/tools/oracle.js +529 -0
- package/dist/tools/oracle.js.map +1 -0
- package/dist/tools/policy.d.ts +119 -0
- package/dist/tools/policy.d.ts.map +1 -0
- package/dist/tools/policy.js +406 -0
- package/dist/tools/policy.js.map +1 -0
- package/dist/tools/provenance/index.d.ts +4 -0
- package/dist/tools/provenance/index.d.ts.map +1 -0
- package/dist/tools/provenance/index.js +58 -0
- package/dist/tools/provenance/index.js.map +1 -0
- package/dist/tools/provenance.d.ts +75 -0
- package/dist/tools/provenance.d.ts.map +1 -0
- package/dist/tools/provenance.js +197 -0
- package/dist/tools/provenance.js.map +1 -0
- package/dist/tools/rateLimiter.d.ts +45 -0
- package/dist/tools/rateLimiter.d.ts.map +1 -0
- package/dist/tools/rateLimiter.js +91 -0
- package/dist/tools/rateLimiter.js.map +1 -0
- package/dist/tools/registry/index.d.ts +10 -0
- package/dist/tools/registry/index.d.ts.map +1 -0
- package/dist/tools/registry/index.js +467 -0
- package/dist/tools/registry/index.js.map +1 -0
- package/dist/tools/registry.d.ts +3 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +189 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/roadmap/index.d.ts +9 -0
- package/dist/tools/roadmap/index.d.ts.map +1 -0
- package/dist/tools/roadmap/index.js +250 -0
- package/dist/tools/roadmap/index.js.map +1 -0
- package/dist/tools/roadmap.d.ts +88 -0
- package/dist/tools/roadmap.d.ts.map +1 -0
- package/dist/tools/roadmap.js +365 -0
- package/dist/tools/roadmap.js.map +1 -0
- package/dist/tools/sentinel/index.d.ts +19 -0
- package/dist/tools/sentinel/index.d.ts.map +1 -0
- package/dist/tools/sentinel/index.js +832 -0
- package/dist/tools/sentinel/index.js.map +1 -0
- package/dist/tools/sentinel-scan-data.d.ts +90 -0
- package/dist/tools/sentinel-scan-data.d.ts.map +1 -0
- package/dist/tools/sentinel-scan-data.js +122 -0
- package/dist/tools/sentinel-scan-data.js.map +1 -0
- package/dist/tools/sentinel.d.ts +156 -0
- package/dist/tools/sentinel.d.ts.map +1 -0
- package/dist/tools/sentinel.js +603 -0
- package/dist/tools/sentinel.js.map +1 -0
- package/dist/tools/shared/index.d.ts +4 -0
- package/dist/tools/shared/index.d.ts.map +1 -0
- package/dist/tools/shared/index.js +7 -0
- package/dist/tools/shared/index.js.map +1 -0
- package/dist/tools/shared/project.d.ts +17 -0
- package/dist/tools/shared/project.d.ts.map +1 -0
- package/dist/tools/shared/project.js +36 -0
- package/dist/tools/shared/project.js.map +1 -0
- package/dist/tools/shared/response.d.ts +10 -0
- package/dist/tools/shared/response.d.ts.map +1 -0
- package/dist/tools/shared/response.js +36 -0
- package/dist/tools/shared/response.js.map +1 -0
- package/dist/tools/shared/validation.d.ts +10 -0
- package/dist/tools/shared/validation.d.ts.map +1 -0
- package/dist/tools/shared/validation.js +26 -0
- package/dist/tools/shared/validation.js.map +1 -0
- package/dist/tools/studio/cloud-spine.d.ts +27 -0
- package/dist/tools/studio/cloud-spine.d.ts.map +1 -0
- package/dist/tools/studio/cloud-spine.js +773 -0
- package/dist/tools/studio/cloud-spine.js.map +1 -0
- package/dist/tools/studio/index.d.ts +154 -0
- package/dist/tools/studio/index.d.ts.map +1 -0
- package/dist/tools/studio/index.js +525 -0
- package/dist/tools/studio/index.js.map +1 -0
- package/dist/tools/testSpec.d.ts +122 -0
- package/dist/tools/testSpec.d.ts.map +1 -0
- package/dist/tools/testSpec.js +525 -0
- package/dist/tools/testSpec.js.map +1 -0
- package/dist/tools/toolsIndex.d.ts +5 -0
- package/dist/tools/toolsIndex.d.ts.map +1 -0
- package/dist/tools/toolsIndex.js +37 -0
- package/dist/tools/toolsIndex.js.map +1 -0
- package/dist/tools/types.d.ts +30 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +7 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/tools/voice/index.d.ts +8 -0
- package/dist/tools/voice/index.d.ts.map +1 -0
- package/dist/tools/voice/index.js +176 -0
- package/dist/tools/voice/index.js.map +1 -0
- package/dist/tools/voice.d.ts +291 -0
- package/dist/tools/voice.d.ts.map +1 -0
- package/dist/tools/voice.js +734 -0
- package/dist/tools/voice.js.map +1 -0
- 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
|