claude-phone-local 2.0.6 → 2.0.7

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.
@@ -938,6 +938,36 @@ async function setupAPIKeys(config) {
938
938
  * @returns {Promise<object>} Updated config
939
939
  */
940
940
  async function setupSIP(config) {
941
+ // Detect a native 3CX SBC service on this machine (checks the process and
942
+ // port 5060) so drachtio doesn't try to bind the same port and fail
943
+ // silently at startup - the exact bug this setup wizard used to leave
944
+ // standard ("both"/all-in-one) installs exposed to, since this detection
945
+ // previously only ran in the separate Pi-mode setup path.
946
+ const sbcSpinner = ora('Checking for a local 3CX SBC service...').start();
947
+ let sbcDetected = false;
948
+ try {
949
+ sbcDetected = await detect3cxSbc();
950
+ sbcSpinner.succeed(sbcDetected
951
+ ? '3CX SBC detected on this PC (port 5060 in use)'
952
+ : 'No local 3CX SBC detected');
953
+ } catch (err) {
954
+ sbcSpinner.warn('3CX SBC detection failed: ' + err.message);
955
+ }
956
+
957
+ const { sbcOnThisPc } = await inquirer.prompt([
958
+ {
959
+ type: 'confirm',
960
+ name: 'sbcOnThisPc',
961
+ message: 'Is the 3CX SBC service running on this same PC?',
962
+ default: sbcDetected || config.sip.registrar === 'auto' || config.sip.registrar === config.server?.externalIp
963
+ }
964
+ ]);
965
+
966
+ config.sip.drachtioSipPort = sbcOnThisPc ? 5070 : 5060;
967
+ if (sbcOnThisPc) {
968
+ console.log(chalk.green('✓ Will use port 5070 for drachtio (avoid conflict with the SBC on port 5060)\n'));
969
+ }
970
+
941
971
  const answers = await inquirer.prompt([
942
972
  {
943
973
  type: 'input',
@@ -954,11 +984,11 @@ async function setupSIP(config) {
954
984
  return true;
955
985
  }
956
986
  },
957
- {
987
+ ...(sbcOnThisPc ? [] : [{
958
988
  type: 'input',
959
989
  name: 'registrar',
960
990
  message: '3CX registrar IP (e.g., 192.168.1.100):',
961
- default: config.sip.registrar,
991
+ default: config.sip.registrar === 'auto' ? '' : config.sip.registrar,
962
992
  validate: (input) => {
963
993
  if (!input || input.trim() === '') {
964
994
  return 'SIP registrar IP is required';
@@ -968,11 +998,14 @@ async function setupSIP(config) {
968
998
  }
969
999
  return true;
970
1000
  }
971
- }
1001
+ }])
972
1002
  ]);
973
1003
 
974
1004
  config.sip.domain = answers.domain;
975
- config.sip.registrar = answers.registrar;
1005
+ // 'auto' tracks this machine's current LAN IP on every `claude-phone
1006
+ // start`, same as server.externalIp - the SBC and this app running on the
1007
+ // same PC means both need to move together whenever the network changes.
1008
+ config.sip.registrar = sbcOnThisPc ? 'auto' : answers.registrar;
976
1009
 
977
1010
  return config;
978
1011
  }
package/cli/lib/docker.js CHANGED
@@ -1,11 +1,13 @@
1
1
  import { spawn, execSync } from 'child_process';
2
2
  import crypto from 'crypto';
3
3
  import fs from 'fs';
4
+ import path from 'path';
4
5
  import {
5
6
  getDockerComposePath,
6
7
  getEnvPath,
7
8
  getConfigDir
8
9
  } from './config.js';
10
+ import { getLocalIP } from './utils.js';
9
11
 
10
12
  /**
11
13
  * Detect which docker compose command to use
@@ -86,18 +88,32 @@ export async function checkDocker() {
86
88
  * @returns {string} Docker compose YAML content
87
89
  */
88
90
  export function generateDockerCompose(config) {
89
- const externalIp = config.server.externalIp === 'auto' ? '${EXTERNAL_IP}' : config.server.externalIp;
90
- const drachtioPort = config.deployment?.pi?.drachtioPort || 5070;
91
+ const drachtioPort = config.deployment?.pi?.drachtioPort || config.sip?.drachtioSipPort || 5070;
91
92
  const rtpStart = config.server.rtpStart || 30000;
92
93
  const rtpEnd = config.server.rtpEnd || 30100;
93
94
 
95
+ // This file is written to ~/.claude-phone/ (getDockerComposePath()), which
96
+ // has no Dockerfile or source of its own - those live in the installed npm
97
+ // package directory (config.paths.voiceApp's parent). `build: .` would try
98
+ // to build from ~/.claude-phone and fail (or silently reuse a stale cached
99
+ // image tag from an unrelated build, masking the failure) on any machine
100
+ // without that image already present.
101
+ const projectRoot = config.paths?.voiceApp
102
+ ? path.dirname(config.paths.voiceApp)
103
+ : '.';
104
+
94
105
  // Single image: drachtio + FreeSWITCH + voice-app + STT + TTS under supervisord.
95
106
  // They talk over 127.0.0.1, so there is no container DNS to misconfigure.
96
107
  // Everything that must outlive a restart is on the ./data bind mount.
108
+ //
109
+ // EXTERNAL_IP is intentionally NOT set under `environment:` here - it comes
110
+ // from `.env` via env_file below. An explicit `environment:` entry would
111
+ // override env_file, so editing .env after a network change (a new LAN IP,
112
+ // a different office) would silently do nothing.
97
113
  return `# Generated by \`claude-phone setup\` - edit .env, not this file.
98
114
  services:
99
115
  claude-phone:
100
- build: .
116
+ build: "${projectRoot.replace(/\\/g, '/')}"
101
117
  image: claude-phone-local:latest
102
118
  container_name: claude-phone
103
119
  restart: unless-stopped
@@ -109,7 +125,6 @@ services:
109
125
  - STT_LOCAL_URL=http://127.0.0.1:9001
110
126
  - TTS_LOCAL_URL=http://127.0.0.1:9002
111
127
  - AUDIO_BASE_URL=http://127.0.0.1:3000
112
- - EXTERNAL_IP=${externalIp}
113
128
  - RTP_START=${rtpStart}
114
129
  - RTP_END=${rtpEnd}
115
130
  ports:
@@ -136,6 +151,15 @@ export function generateEnvFile(config) {
136
151
  };
137
152
  }
138
153
 
154
+ // 'auto' means re-detect the current LAN IP every time this is written
155
+ // (i.e. every `claude-phone start`), instead of using whatever IP was
156
+ // current the last time setup ran. A saved concrete IP goes stale the
157
+ // moment the machine moves networks (home <-> office, DHCP renewal) and
158
+ // nothing here writes to it again until setup is manually re-run.
159
+ const resolvedExternalIp = config.server.externalIp === 'auto'
160
+ ? getLocalIP()
161
+ : config.server.externalIp;
162
+
139
163
  // Determine Claude API URL based on deployment mode
140
164
  let claudeApiUrl;
141
165
  if (config.deployment && config.deployment.mode === 'pi-split' && config.deployment.pi && config.deployment.pi.macIp) {
@@ -162,14 +186,14 @@ export function generateEnvFile(config) {
162
186
  '# ====================================',
163
187
  '',
164
188
  '# Network Configuration',
165
- `EXTERNAL_IP=${config.server.externalIp === 'auto' ? 'auto' : config.server.externalIp}`,
189
+ `EXTERNAL_IP=${resolvedExternalIp}`,
166
190
  '',
167
191
  '# Drachtio Configuration',
168
192
  'DRACHTIO_HOST=drachtio',
169
193
  'DRACHTIO_PORT=9022',
170
194
  `DRACHTIO_SECRET=${config.secrets.drachtio}`,
171
195
  // SIP port for Contact header (5070 when 3CX SBC is present, 5060 otherwise)
172
- `DRACHTIO_SIP_PORT=${config.deployment?.pi?.drachtioPort || 5060}`,
196
+ `DRACHTIO_SIP_PORT=${config.sip?.drachtioSipPort || config.deployment?.pi?.drachtioPort || 5070}`,
173
197
  '',
174
198
  '# FreeSWITCH Configuration',
175
199
  'FREESWITCH_HOST=freeswitch',
@@ -179,7 +203,10 @@ export function generateEnvFile(config) {
179
203
  '',
180
204
  '# 3CX / SIP Configuration',
181
205
  `SIP_DOMAIN=${config.sip.domain}`,
182
- `SIP_REGISTRAR=${config.sip.registrar}`,
206
+ // 'auto' means the 3CX SBC runs on this same PC, so its address is
207
+ // whatever this machine's current LAN IP is - re-resolved every start,
208
+ // same reasoning as EXTERNAL_IP above.
209
+ `SIP_REGISTRAR=${config.sip.registrar === 'auto' ? resolvedExternalIp : config.sip.registrar}`,
183
210
  '',
184
211
  '# Default extension (primary device)',
185
212
  `SIP_EXTENSION=${config.devices[0].extension}`,
@@ -196,7 +223,7 @@ export function generateEnvFile(config) {
196
223
  `STT_LOCAL_URL=${config.api.local?.sttUrl || 'http://stt-local:9001'}`,
197
224
  `TTS_LOCAL_URL=${config.api.local?.ttsUrl || 'http://tts-local:9002'}`,
198
225
  `AUDIO_BASE_URL=${config.api.local?.audioBaseUrl || 'http://voice-app:3000'}`,
199
- `PIPER_VOICE=${config.devices[0].voiceId || config.api.local?.voice || 'en_US-lessac-medium'}`,
226
+ `PIPER_VOICE=${config.devices[0].voice || config.api.local?.voice || 'en_US-lessac-medium'}`,
200
227
  `WHISPER_MODEL=${config.api.local?.whisperModel || 'small'}`,
201
228
  ''
202
229
  ] : [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-phone-local",
3
- "version": "2.0.6",
3
+ "version": "2.0.7",
4
4
  "description": "Local/offline fork of NetworkChuck's claude-phone: talk to Claude Code over 3CX/SIP with faster-whisper STT + Piper TTS in one Docker container.",
5
5
  "type": "module",
6
6
  "bin": {