claude-phone-local 2.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 (96) hide show
  1. package/.env.example +108 -0
  2. package/Dockerfile +84 -0
  3. package/LICENSE +22 -0
  4. package/README.md +231 -0
  5. package/claude-api-server/package-lock.json +832 -0
  6. package/claude-api-server/package.json +20 -0
  7. package/claude-api-server/server.js +540 -0
  8. package/claude-api-server/structured.js +208 -0
  9. package/cli/README.md +231 -0
  10. package/cli/bin/check-publish-safety.js +53 -0
  11. package/cli/bin/claude-phone.js +23 -0
  12. package/cli/bin/cli-main.js +284 -0
  13. package/cli/bin/postinstall.js +20 -0
  14. package/cli/lib/commands/api-server.js +77 -0
  15. package/cli/lib/commands/backup.js +69 -0
  16. package/cli/lib/commands/config/path.js +30 -0
  17. package/cli/lib/commands/config/reset.js +73 -0
  18. package/cli/lib/commands/config/show.js +86 -0
  19. package/cli/lib/commands/device/add.js +143 -0
  20. package/cli/lib/commands/device/list.js +55 -0
  21. package/cli/lib/commands/device/remove.js +83 -0
  22. package/cli/lib/commands/doctor.js +422 -0
  23. package/cli/lib/commands/logs.js +186 -0
  24. package/cli/lib/commands/restore.js +172 -0
  25. package/cli/lib/commands/setup.js +1246 -0
  26. package/cli/lib/commands/start.js +346 -0
  27. package/cli/lib/commands/status.js +139 -0
  28. package/cli/lib/commands/stop.js +101 -0
  29. package/cli/lib/commands/uninstall.js +183 -0
  30. package/cli/lib/commands/update.js +205 -0
  31. package/cli/lib/config.js +113 -0
  32. package/cli/lib/docker.js +384 -0
  33. package/cli/lib/mcp-register.js +108 -0
  34. package/cli/lib/network.js +109 -0
  35. package/cli/lib/platform.js +85 -0
  36. package/cli/lib/port-check.js +135 -0
  37. package/cli/lib/prereqs/checks/compose.js +147 -0
  38. package/cli/lib/prereqs/checks/disk.js +89 -0
  39. package/cli/lib/prereqs/checks/docker.js +155 -0
  40. package/cli/lib/prereqs/checks/network.js +78 -0
  41. package/cli/lib/prereqs/checks/node.js +95 -0
  42. package/cli/lib/prereqs/installers/docker-desktop.js +248 -0
  43. package/cli/lib/prereqs/installers/docker.js +229 -0
  44. package/cli/lib/prereqs/installers/node.js +254 -0
  45. package/cli/lib/prereqs/platform.js +175 -0
  46. package/cli/lib/prereqs/utils/execute.js +208 -0
  47. package/cli/lib/prereqs/utils/rollback.js +223 -0
  48. package/cli/lib/prereqs/utils/sudo.js +177 -0
  49. package/cli/lib/prereqs.js +228 -0
  50. package/cli/lib/prerequisites.js +115 -0
  51. package/cli/lib/process-manager.js +176 -0
  52. package/cli/lib/utils.js +78 -0
  53. package/cli/lib/validators.js +219 -0
  54. package/cli/lib/voice-downloader.js +79 -0
  55. package/docker/entrypoint.sh +110 -0
  56. package/docker/supervisord.conf +78 -0
  57. package/docker-compose.yml +40 -0
  58. package/docs/CLAUDE-CODE-SKILL.md +45 -0
  59. package/docs/LANGUAGES.md +113 -0
  60. package/docs/MCP-SERVER.md +111 -0
  61. package/docs/TROUBLESHOOTING.md +288 -0
  62. package/mcp-server/index.js +159 -0
  63. package/mcp-server/package-lock.json +1201 -0
  64. package/mcp-server/package.json +9 -0
  65. package/package.json +73 -0
  66. package/stt-local/Dockerfile +12 -0
  67. package/stt-local/server.py +45 -0
  68. package/tts-local/Dockerfile +9 -0
  69. package/tts-local/server.py +83 -0
  70. package/voice-app/API-QUERY-CONTRACT.md +238 -0
  71. package/voice-app/DEPLOYMENT.md +176 -0
  72. package/voice-app/Dockerfile +20 -0
  73. package/voice-app/README-OUTBOUND.md +316 -0
  74. package/voice-app/config/devices.json.example +18 -0
  75. package/voice-app/index.js +273 -0
  76. package/voice-app/lib/audio-fork.js +465 -0
  77. package/voice-app/lib/claude-bridge.js +113 -0
  78. package/voice-app/lib/connection-retry.js +58 -0
  79. package/voice-app/lib/conversation-loop.js +490 -0
  80. package/voice-app/lib/device-registry.js +171 -0
  81. package/voice-app/lib/http-server.js +242 -0
  82. package/voice-app/lib/logger.js +35 -0
  83. package/voice-app/lib/multi-registrar.js +133 -0
  84. package/voice-app/lib/outbound-handler.js +299 -0
  85. package/voice-app/lib/outbound-routes.js +417 -0
  86. package/voice-app/lib/outbound-session.js +324 -0
  87. package/voice-app/lib/query-routes.js +484 -0
  88. package/voice-app/lib/registrar.js +144 -0
  89. package/voice-app/lib/sip-handler.js +490 -0
  90. package/voice-app/lib/tts-service.js +205 -0
  91. package/voice-app/lib/whisper-client.js +140 -0
  92. package/voice-app/package.json +38 -0
  93. package/voice-app/static/gotit-beep.wav +0 -0
  94. package/voice-app/static/hold-music.wav +0 -0
  95. package/voice-app/static/ready-beep.wav +0 -0
  96. package/voice-app/test/freeswitch-retry.test.js +100 -0
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Structured JSON Output helpers for Claude responses.
3
+ *
4
+ * Goal: make Claude usable from automation (n8n) by:
5
+ * - Prompting for a single JSON object
6
+ * - Extracting JSON from mixed prose/markdown responses
7
+ * - Validating required fields
8
+ * - Retrying once with a "repair" prompt when invalid
9
+ */
10
+
11
+ function buildQueryContext({
12
+ queryType,
13
+ requiredFields,
14
+ fieldGuidance,
15
+ allowExtraFields = true,
16
+ example,
17
+ }) {
18
+ const required = Array.isArray(requiredFields) ? requiredFields : [];
19
+ const guidanceLines = fieldGuidance && typeof fieldGuidance === 'object'
20
+ ? Object.entries(fieldGuidance).map(([key, value]) => `- ${key}: ${String(value)}`)
21
+ : [];
22
+
23
+ const exampleBlock = example ? `\nExample JSON (shape only):\n${JSON.stringify(example, null, 2)}\n` : '';
24
+
25
+ return `[STRUCTURED QUERY CONTEXT]
26
+ You are responding to an automation system (n8n). Your output must be machine-parseable.
27
+
28
+ Return EXACTLY ONE valid JSON object.
29
+ - No markdown
30
+ - No code fences
31
+ - No backticks
32
+ - No explanations
33
+ - No leading/trailing text
34
+
35
+ Query type: ${queryType || 'generic'}
36
+ Required fields (must be present as keys): ${JSON.stringify(required)}
37
+ Missing/unknown values: use null (not "unknown", not empty string).
38
+ ${allowExtraFields ? 'Extra fields allowed if useful.' : 'Do not include extra fields.'}
39
+
40
+ Field guidance:
41
+ ${guidanceLines.length ? guidanceLines.join('\n') : '- (none)'}
42
+ ${exampleBlock}[END STRUCTURED QUERY CONTEXT]
43
+
44
+ `;
45
+ }
46
+
47
+ function buildStructuredPrompt({
48
+ devicePrompt,
49
+ queryContext,
50
+ userPrompt,
51
+ }) {
52
+ let fullPrompt = '';
53
+
54
+ if (devicePrompt) {
55
+ fullPrompt += `[DEVICE IDENTITY]\n${devicePrompt}\n[END DEVICE IDENTITY]\n\n`;
56
+ }
57
+
58
+ fullPrompt += queryContext;
59
+ fullPrompt += String(userPrompt || '');
60
+ return fullPrompt;
61
+ }
62
+
63
+ function stripBom(text) {
64
+ return text && text.charCodeAt(0) === 0xFEFF ? text.slice(1) : text;
65
+ }
66
+
67
+ function extractJsonCandidates(text) {
68
+ const input = stripBom(String(text || ''));
69
+ const candidates = [];
70
+
71
+ // 1) Prefer fenced ```json blocks if present.
72
+ const fenceRegex = /```(?:json)?\s*([\s\S]*?)\s*```/gi;
73
+ let match;
74
+ while ((match = fenceRegex.exec(input)) !== null) {
75
+ const inside = match[1].trim();
76
+ if (inside) candidates.push(inside);
77
+ }
78
+
79
+ // 2) Scan for balanced { ... } / [ ... ] blocks (handles prose around JSON).
80
+ const starts = [];
81
+ for (let i = 0; i < input.length; i++) {
82
+ const ch = input[i];
83
+ if (ch === '{' || ch === '[') starts.push(i);
84
+ }
85
+
86
+ for (const startIndex of starts) {
87
+ const startChar = input[startIndex];
88
+ const endChar = startChar === '{' ? '}' : ']';
89
+
90
+ let depth = 0;
91
+ let inString = false;
92
+ let escape = false;
93
+
94
+ for (let i = startIndex; i < input.length; i++) {
95
+ const ch = input[i];
96
+
97
+ if (inString) {
98
+ if (escape) {
99
+ escape = false;
100
+ } else if (ch === '\\\\') {
101
+ escape = true;
102
+ } else if (ch === '"') {
103
+ inString = false;
104
+ }
105
+ continue;
106
+ }
107
+
108
+ if (ch === '"') {
109
+ inString = true;
110
+ continue;
111
+ }
112
+
113
+ if (ch === startChar) depth++;
114
+ if (ch === endChar) depth--;
115
+
116
+ if (depth === 0) {
117
+ const candidate = input.slice(startIndex, i + 1).trim();
118
+ if (candidate) candidates.push(candidate);
119
+ break;
120
+ }
121
+ }
122
+ }
123
+
124
+ // De-dupe, preserve order.
125
+ return [...new Set(candidates)];
126
+ }
127
+
128
+ function tryParseJsonFromText(text) {
129
+ const candidates = extractJsonCandidates(text);
130
+
131
+ for (const candidate of candidates) {
132
+ try {
133
+ const parsed = JSON.parse(candidate);
134
+ return { ok: true, jsonText: candidate, data: parsed, candidatesTried: candidates.length };
135
+ } catch {
136
+ // keep trying
137
+ }
138
+ }
139
+
140
+ return {
141
+ ok: false,
142
+ error: 'No valid JSON found in response',
143
+ candidatesTried: candidates.length,
144
+ };
145
+ }
146
+
147
+ function getByPath(obj, path) {
148
+ const parts = String(path).split('.').filter(Boolean);
149
+ let cur = obj;
150
+ for (const part of parts) {
151
+ if (cur && typeof cur === 'object' && part in cur) cur = cur[part];
152
+ else return undefined;
153
+ }
154
+ return cur;
155
+ }
156
+
157
+ function validateRequiredFields(data, requiredFields) {
158
+ const required = Array.isArray(requiredFields) ? requiredFields : [];
159
+ const missing = [];
160
+
161
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
162
+ return { ok: false, missing: required, error: 'Parsed JSON is not an object' };
163
+ }
164
+
165
+ for (const field of required) {
166
+ const value = getByPath(data, field);
167
+ if (value === undefined) missing.push(field);
168
+ }
169
+
170
+ if (missing.length) {
171
+ return { ok: false, missing, error: `Missing required fields: ${missing.join(', ')}` };
172
+ }
173
+
174
+ return { ok: true };
175
+ }
176
+
177
+ function buildRepairPrompt({
178
+ queryType,
179
+ requiredFields,
180
+ fieldGuidance,
181
+ allowExtraFields = true,
182
+ originalUserPrompt,
183
+ invalidAssistantOutput,
184
+ example,
185
+ }) {
186
+ const queryContext = buildQueryContext({ queryType, requiredFields, fieldGuidance, allowExtraFields, example });
187
+
188
+ return `${queryContext}[REPAIR TASK]
189
+ The previous assistant output was not valid JSON or did not include required fields.
190
+ Reformat and return ONLY the corrected JSON object that answers the user's request.
191
+
192
+ User request:
193
+ """${String(originalUserPrompt || '').trim()}"""
194
+
195
+ Invalid assistant output:
196
+ """${String(invalidAssistantOutput || '').trim()}"""
197
+ [END REPAIR TASK]
198
+ `;
199
+ }
200
+
201
+ module.exports = {
202
+ buildQueryContext,
203
+ buildStructuredPrompt,
204
+ tryParseJsonFromText,
205
+ validateRequiredFields,
206
+ buildRepairPrompt,
207
+ };
208
+
package/cli/README.md ADDED
@@ -0,0 +1,231 @@
1
+ # Claude Phone CLI
2
+
3
+ > **Note:** since v2 all services run in a **single container**, so commands
4
+ > that used to target one service now act on the whole stack. Speech models and
5
+ > config live in `./data` and are provisioned automatically on first run --
6
+ > there are no manual download steps. See [../README.md](../README.md).
7
+
8
+ Command-line interface for Claude Phone. Single-command setup and management.
9
+
10
+ ## Installation
11
+
12
+ ### One-Line Install
13
+
14
+ ```bash
15
+ npm install -g claude-phone-local
16
+ ```
17
+
18
+ ### Manual Install
19
+
20
+ ```bash
21
+ git clone https://github.com/masterdeepak15/claude-phone-local.git
22
+ cd claude-phone/cli
23
+ npm install
24
+ npm link
25
+ ```
26
+
27
+ ## Setup Wizard
28
+
29
+ ```bash
30
+ claude-phone setup
31
+ ```
32
+
33
+ The wizard guides you through configuration based on your deployment type:
34
+
35
+ ### Voice Server
36
+
37
+ Select this when setting up a Raspberry Pi or dedicated voice box that connects to a remote API server.
38
+
39
+ **What it asks for:**
40
+ 1. 3CX SIP domain and registrar
41
+ 2. API server IP and port (where claude-api-server runs)
42
+ 3. Local or cloud speech (local = faster-whisper + Piper, no API keys; cloud = ElevenLabs + OpenAI)
43
+ 4. Device configuration (name, extension, auth, voice, prompt)
44
+ 5. Server LAN IP (for RTP audio routing)
45
+
46
+ **What `claude-phone start` does:**
47
+ - Starts the `claude-phone` container (drachtio, FreeSWITCH, voice-app, STT, TTS under supervisord)
48
+ - Connects to the remote API server you specified
49
+
50
+ ### API Server
51
+
52
+ Select this when setting up the Claude API wrapper on a machine with Claude Code CLI.
53
+
54
+ **What it asks for:**
55
+ - API server port (default: 3333)
56
+
57
+ **What `claude-phone start` does:**
58
+ - Starts claude-api-server on the configured port
59
+
60
+ **Note:** You can also just run `claude-phone api-server` without setup - it defaults to port 3333.
61
+
62
+ ### Both (All-in-One)
63
+
64
+ Select this for a single machine running everything.
65
+
66
+ **What it asks for:**
67
+ 1. Local or cloud speech (local = faster-whisper + Piper, no API keys; cloud = ElevenLabs + OpenAI)
68
+ 2. 3CX SIP domain and registrar
69
+ 3. Device configuration
70
+ 4. Server LAN IP, API port, and HTTP port
71
+
72
+ **What `claude-phone start` does:**
73
+ - Starts the `claude-phone` container (drachtio, FreeSWITCH, voice-app, STT, TTS under supervisord)
74
+ - Starts claude-api-server
75
+
76
+ ### Pi Auto-Detection
77
+
78
+ On Raspberry Pi, the setup wizard:
79
+ - Recommends "Voice Server" mode if you select "Both"
80
+ - Checks for 3CX SBC on port 5060 and auto-configures drachtio to use 5070 to avoid conflicts
81
+ - Uses optimized settings for Pi hardware
82
+
83
+ ## Commands
84
+
85
+ ### Setup & Configuration
86
+
87
+ ```bash
88
+ claude-phone setup # Interactive configuration wizard
89
+ claude-phone setup --skip-prereqs # Skip prerequisite checks
90
+ claude-phone config show # Display config (secrets redacted)
91
+ claude-phone config path # Show config file location (~/.claude-phone/config.json)
92
+ claude-phone config reset # Reset config (creates backup first)
93
+ ```
94
+
95
+ ### Service Management
96
+
97
+ ```bash
98
+ claude-phone start # Start services based on installation type
99
+ claude-phone stop # Stop all services
100
+ claude-phone status # Show service status
101
+ claude-phone doctor # Health check for dependencies and services
102
+ claude-phone api-server # Start API server standalone (default port 3333)
103
+ claude-phone api-server -p 4000 # Start on custom port
104
+ ```
105
+
106
+ ### Device Management
107
+
108
+ ```bash
109
+ claude-phone device add # Add a new device/extension
110
+ claude-phone device list # List configured devices
111
+ claude-phone device remove <name> # Remove a device by name
112
+ ```
113
+
114
+ ### Logs
115
+
116
+ ```bash
117
+ claude-phone logs # Tail all service logs
118
+ claude-phone logs # all services (one container now)
119
+ claude-phone logs drachtio # SIP server only
120
+ claude-phone logs freeswitch # Media server only
121
+ ```
122
+
123
+ ### Backup & Recovery
124
+
125
+ ```bash
126
+ claude-phone backup # Create timestamped backup
127
+ claude-phone restore # Restore from backup (interactive)
128
+ ```
129
+
130
+ ### Maintenance
131
+
132
+ ```bash
133
+ claude-phone update # Update Claude Phone to latest
134
+ claude-phone uninstall # Complete removal
135
+ ```
136
+
137
+ ## Configuration Files
138
+
139
+ All configuration is stored in `~/.claude-phone/`:
140
+
141
+ ```
142
+ ~/.claude-phone/
143
+ ├── config.json # Main configuration (chmod 600)
144
+ ├── docker-compose.yml # Generated: one service + ./data volume
145
+ ├── .env # Generated environment file
146
+ ├── server.pid # API server process ID
147
+ └── backups/ # Configuration backups
148
+ ```
149
+
150
+ ### Config Structure
151
+
152
+ ```json
153
+ {
154
+ "version": "1.0.0",
155
+ "installationType": "both",
156
+ "api": {
157
+ "elevenlabs": { "apiKey": "...", "defaultVoiceId": "...", "validated": true },
158
+ "openai": { "apiKey": "...", "validated": true }
159
+ },
160
+ "sip": {
161
+ "domain": "your-3cx.3cx.us",
162
+ "registrar": "192.168.1.100",
163
+ "transport": "udp"
164
+ },
165
+ "server": {
166
+ "claudeApiPort": 3333,
167
+ "httpPort": 3000,
168
+ "externalIp": "192.168.1.50"
169
+ },
170
+ "devices": [{
171
+ "name": "Morpheus",
172
+ "extension": "9000",
173
+ "authId": "9000",
174
+ "password": "***",
175
+ "voiceId": "elevenlabs-voice-id",
176
+ "prompt": "You are Morpheus..."
177
+ }],
178
+ "deployment": {
179
+ "mode": "both"
180
+ }
181
+ }
182
+ ```
183
+
184
+ ## Split Deployment Example
185
+
186
+ ### On Raspberry Pi (Voice Server)
187
+
188
+ ```bash
189
+ # Install
190
+ npm install -g claude-phone-local
191
+
192
+ # Setup - select "Voice Server"
193
+ # Enter your Mac's IP when prompted for API server
194
+ claude-phone setup
195
+
196
+ # Start voice services
197
+ claude-phone start
198
+ ```
199
+
200
+ ### On Mac (API Server)
201
+
202
+ ```bash
203
+ # Install (if not already)
204
+ npm install -g claude-phone-local
205
+
206
+ # Start API server (no setup needed)
207
+ claude-phone api-server
208
+
209
+ # Or on a custom port
210
+ claude-phone api-server --port 4000
211
+ ```
212
+
213
+ ## Requirements
214
+
215
+ - **Node.js 18+** - Required for CLI
216
+ - **Docker** - Required for Voice Server or Both modes
217
+ - **Claude Code CLI** - Required for API Server or Both modes
218
+
219
+ ## Development
220
+
221
+ ```bash
222
+ # Run tests
223
+ npm test
224
+
225
+ # Lint
226
+ npm run lint
227
+ ```
228
+
229
+ ## License
230
+
231
+ MIT
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * prepublishOnly guard.
4
+ *
5
+ * `npm publish` is irreversible - a version number can be deprecated but its
6
+ * contents stay downloadable. This aborts the publish if the tarball would
7
+ * contain anything secret or absurdly large, rather than trusting .npmignore
8
+ * to have been maintained correctly.
9
+ */
10
+ import { execSync } from 'node:child_process';
11
+
12
+ const FORBIDDEN = [
13
+ { pattern: /(^|\/)\.env$/, reason: 'environment file with credentials' },
14
+ { pattern: /devices\.json$/, reason: 'SIP credentials' },
15
+ { pattern: /\.onnx$/, reason: 'voice model (downloaded at runtime)' },
16
+ { pattern: /(^|\/)data\//, reason: 'runtime data directory' },
17
+ { pattern: /\.(pem|key|p12|pfx)$/, reason: 'private key' },
18
+ ];
19
+
20
+ const MAX_TARBALL_MB = 25;
21
+
22
+ let files, sizeMB;
23
+ try {
24
+ const out = execSync('npm pack --dry-run --json', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
25
+ const meta = JSON.parse(out)[0];
26
+ files = meta.files.map((f) => f.path);
27
+ sizeMB = meta.unpackedSize / (1024 * 1024);
28
+ } catch (err) {
29
+ console.error('publish-safety: could not inspect the tarball -', err.message);
30
+ process.exit(1);
31
+ }
32
+
33
+ const offenders = [];
34
+ for (const file of files) {
35
+ for (const { pattern, reason } of FORBIDDEN) {
36
+ if (pattern.test(file)) offenders.push(` ${file} <- ${reason}`);
37
+ }
38
+ }
39
+
40
+ if (offenders.length) {
41
+ console.error('\npublish BLOCKED - these files must not be published:\n');
42
+ console.error(offenders.join('\n'));
43
+ console.error('\nAdd them to .npmignore, then try again.\n');
44
+ process.exit(1);
45
+ }
46
+
47
+ if (sizeMB > MAX_TARBALL_MB) {
48
+ console.error(`\npublish BLOCKED - unpacked size ${sizeMB.toFixed(1)} MB exceeds ${MAX_TARBALL_MB} MB.`);
49
+ console.error('Models are meant to download at runtime, not ship in the package.\n');
50
+ process.exit(1);
51
+ }
52
+
53
+ console.log(`publish-safety OK - ${files.length} files, ${sizeMB.toFixed(1)} MB unpacked`);
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Early Node.js version check - ES5 compatible for old Node versions
4
+ // This MUST run before any ES module imports to catch version issues
5
+ var nodeMajor = parseInt(process.versions.node.split('.')[0], 10);
6
+ var requiredMajor = 18;
7
+
8
+ if (nodeMajor < requiredMajor) {
9
+ console.error('');
10
+ console.error('\x1b[31m' + 'ERROR: Node.js version too old' + '\x1b[0m');
11
+ console.error(' Current: v' + process.versions.node);
12
+ console.error(' Required: v' + requiredMajor + '.0.0 or higher');
13
+ console.error('');
14
+ console.error('\x1b[33m' + 'To install Node.js ' + requiredMajor + '+:' + '\x1b[0m');
15
+ console.error(' macOS/Linux: curl -fsSL https://fnm.vercel.app/install | bash');
16
+ console.error(' fnm install 20 && fnm default 20');
17
+ console.error(' Or visit: https://nodejs.org/');
18
+ console.error('');
19
+ process.exit(1);
20
+ }
21
+
22
+ // Node version OK - load the actual CLI
23
+ import('./cli-main.js');