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.
- package/.env.example +108 -0
- package/Dockerfile +84 -0
- package/LICENSE +22 -0
- package/README.md +231 -0
- package/claude-api-server/package-lock.json +832 -0
- package/claude-api-server/package.json +20 -0
- package/claude-api-server/server.js +540 -0
- package/claude-api-server/structured.js +208 -0
- package/cli/README.md +231 -0
- package/cli/bin/check-publish-safety.js +53 -0
- package/cli/bin/claude-phone.js +23 -0
- package/cli/bin/cli-main.js +284 -0
- package/cli/bin/postinstall.js +20 -0
- package/cli/lib/commands/api-server.js +77 -0
- package/cli/lib/commands/backup.js +69 -0
- package/cli/lib/commands/config/path.js +30 -0
- package/cli/lib/commands/config/reset.js +73 -0
- package/cli/lib/commands/config/show.js +86 -0
- package/cli/lib/commands/device/add.js +143 -0
- package/cli/lib/commands/device/list.js +55 -0
- package/cli/lib/commands/device/remove.js +83 -0
- package/cli/lib/commands/doctor.js +422 -0
- package/cli/lib/commands/logs.js +186 -0
- package/cli/lib/commands/restore.js +172 -0
- package/cli/lib/commands/setup.js +1246 -0
- package/cli/lib/commands/start.js +346 -0
- package/cli/lib/commands/status.js +139 -0
- package/cli/lib/commands/stop.js +101 -0
- package/cli/lib/commands/uninstall.js +183 -0
- package/cli/lib/commands/update.js +205 -0
- package/cli/lib/config.js +113 -0
- package/cli/lib/docker.js +384 -0
- package/cli/lib/mcp-register.js +108 -0
- package/cli/lib/network.js +109 -0
- package/cli/lib/platform.js +85 -0
- package/cli/lib/port-check.js +135 -0
- package/cli/lib/prereqs/checks/compose.js +147 -0
- package/cli/lib/prereqs/checks/disk.js +89 -0
- package/cli/lib/prereqs/checks/docker.js +155 -0
- package/cli/lib/prereqs/checks/network.js +78 -0
- package/cli/lib/prereqs/checks/node.js +95 -0
- package/cli/lib/prereqs/installers/docker-desktop.js +248 -0
- package/cli/lib/prereqs/installers/docker.js +229 -0
- package/cli/lib/prereqs/installers/node.js +254 -0
- package/cli/lib/prereqs/platform.js +175 -0
- package/cli/lib/prereqs/utils/execute.js +208 -0
- package/cli/lib/prereqs/utils/rollback.js +223 -0
- package/cli/lib/prereqs/utils/sudo.js +177 -0
- package/cli/lib/prereqs.js +228 -0
- package/cli/lib/prerequisites.js +115 -0
- package/cli/lib/process-manager.js +176 -0
- package/cli/lib/utils.js +78 -0
- package/cli/lib/validators.js +219 -0
- package/cli/lib/voice-downloader.js +79 -0
- package/docker/entrypoint.sh +110 -0
- package/docker/supervisord.conf +78 -0
- package/docker-compose.yml +40 -0
- package/docs/CLAUDE-CODE-SKILL.md +45 -0
- package/docs/LANGUAGES.md +113 -0
- package/docs/MCP-SERVER.md +111 -0
- package/docs/TROUBLESHOOTING.md +288 -0
- package/mcp-server/index.js +159 -0
- package/mcp-server/package-lock.json +1201 -0
- package/mcp-server/package.json +9 -0
- package/package.json +73 -0
- package/stt-local/Dockerfile +12 -0
- package/stt-local/server.py +45 -0
- package/tts-local/Dockerfile +9 -0
- package/tts-local/server.py +83 -0
- package/voice-app/API-QUERY-CONTRACT.md +238 -0
- package/voice-app/DEPLOYMENT.md +176 -0
- package/voice-app/Dockerfile +20 -0
- package/voice-app/README-OUTBOUND.md +316 -0
- package/voice-app/config/devices.json.example +18 -0
- package/voice-app/index.js +273 -0
- package/voice-app/lib/audio-fork.js +465 -0
- package/voice-app/lib/claude-bridge.js +113 -0
- package/voice-app/lib/connection-retry.js +58 -0
- package/voice-app/lib/conversation-loop.js +490 -0
- package/voice-app/lib/device-registry.js +171 -0
- package/voice-app/lib/http-server.js +242 -0
- package/voice-app/lib/logger.js +35 -0
- package/voice-app/lib/multi-registrar.js +133 -0
- package/voice-app/lib/outbound-handler.js +299 -0
- package/voice-app/lib/outbound-routes.js +417 -0
- package/voice-app/lib/outbound-session.js +324 -0
- package/voice-app/lib/query-routes.js +484 -0
- package/voice-app/lib/registrar.js +144 -0
- package/voice-app/lib/sip-handler.js +490 -0
- package/voice-app/lib/tts-service.js +205 -0
- package/voice-app/lib/whisper-client.js +140 -0
- package/voice-app/package.json +38 -0
- package/voice-app/static/gotit-beep.wav +0 -0
- package/voice-app/static/hold-music.wav +0 -0
- package/voice-app/static/ready-beep.wav +0 -0
- 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');
|