@clawchatsai/connector 0.0.87 → 0.0.89

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.
@@ -32,6 +32,10 @@ export interface PluginConfig {
32
32
  };
33
33
  sessionSecret?: string;
34
34
  backupCodeHashes?: string[];
35
+ totpPending?: {
36
+ secret: string;
37
+ generatedAt: string;
38
+ };
35
39
  }
36
40
  export interface BridgeConfig {
37
41
  gatewayToken: string;
package/dist/index.js CHANGED
@@ -11,10 +11,10 @@
11
11
  */
12
12
  import * as fs from 'node:fs';
13
13
  import * as http from 'node:http';
14
+ import * as os from 'node:os';
14
15
  import * as path from 'node:path';
15
16
  import { SignalingClient } from './signaling-client.js';
16
17
  import { dispatchRpc } from './shim.js';
17
- import { checkForUpdates, performUpdate } from './updater.js';
18
18
  import { initAuth, handleAuthMessage, cleanupAuth } from './auth-handler.js';
19
19
  import { generateTotpSecret, verifyTotp, generateBackupCodes, buildOtpauthUri } from './totp.js';
20
20
  import { generateSessionSecret } from './session-token.js';
@@ -66,63 +66,58 @@ function saveConfig(config) {
66
66
  // ---------------------------------------------------------------------------
67
67
  // Service lifecycle
68
68
  // ---------------------------------------------------------------------------
69
+ /**
70
+ * Detect musl libc (Alpine Linux) vs glibc.
71
+ * prebuildify/prebuild-install distinguishes linux vs linuxmusl.
72
+ */
73
+ function detectLinuxLibc() {
74
+ try {
75
+ const ldd = fs.readFileSync('/usr/bin/ldd', 'utf8');
76
+ if (ldd.includes('musl'))
77
+ return 'musl';
78
+ }
79
+ catch { /* not found */ }
80
+ try {
81
+ if (fs.readdirSync('/lib').some((f) => f.startsWith('libc.musl')))
82
+ return 'musl';
83
+ }
84
+ catch { /* not found */ }
85
+ return 'glibc';
86
+ }
87
+ /**
88
+ * Returns the prebuild key for the current platform, matching the directory
89
+ * names we bundle under prebuilds/ (e.g. "linux-x64", "linuxmusl-arm64").
90
+ */
91
+ function getPrebuildKey() {
92
+ const platform = process.platform;
93
+ const arch = process.arch;
94
+ if (platform === 'linux' && detectLinuxLibc() === 'musl')
95
+ return `linuxmusl-${arch}`;
96
+ return `${platform}-${arch}`;
97
+ }
69
98
  async function ensureNativeModules(ctx) {
70
- // OpenClaw installs plugins with --ignore-scripts, which skips native module compilation.
71
- // Check if native modules are usable; if not, rebuild them automatically.
72
99
  const pluginDir = path.resolve(__dirname, '..');
73
- const modules = [
74
- { name: 'node-datachannel', binding: 'build/Release/node_datachannel.node', strategy: 'install-script' },
75
- ];
76
- const missing = modules.filter(m => !fs.existsSync(path.join(pluginDir, 'node_modules', m.name, m.binding)));
77
- if (missing.length === 0)
78
- return; // all built
79
- ctx.logger.info(`Building native modules (first run): ${missing.map(m => m.name).join(', ')}...`);
80
- const { execFileSync } = await import('node:child_process');
81
- for (const mod of missing) {
82
- try {
83
- if (mod.strategy === 'install-script') {
84
- // Packages using prebuild-install need their install script re-run (npm rebuild
85
- // only triggers node-gyp, not prebuild-install). Run the install script directly
86
- // from the package's directory.
87
- const modDir = path.join(pluginDir, 'node_modules', mod.name);
88
- const modPkg = JSON.parse(fs.readFileSync(path.join(modDir, 'package.json'), 'utf-8'));
89
- const installCmd = modPkg.scripts?.install;
90
- if (installCmd) {
91
- // Add local .bin dirs to PATH so prebuild-install and other local binaries resolve.
92
- // Check both the module's own node_modules/.bin (hoisted deps) and the plugin-level one.
93
- const localBin = [
94
- path.join(modDir, 'node_modules', '.bin'),
95
- path.join(pluginDir, 'node_modules', '.bin'),
96
- ].join(':');
97
- execFileSync('sh', ['-c', installCmd], {
98
- cwd: modDir,
99
- stdio: 'pipe',
100
- timeout: 120_000,
101
- env: { ...process.env, PATH: `${localBin}:${process.env.PATH ?? ''}`, npm_config_node_gyp: '' },
102
- });
103
- }
104
- else {
105
- // Fallback to rebuild if no install script found
106
- execFileSync('npm', ['rebuild', mod.name], {
107
- cwd: pluginDir,
108
- stdio: 'pipe',
109
- timeout: 120_000,
110
- });
111
- }
112
- }
113
- else {
114
- execFileSync('npm', ['rebuild', mod.name], {
115
- cwd: pluginDir,
116
- stdio: 'pipe',
117
- timeout: 120_000,
118
- });
119
- }
120
- ctx.logger.info(`${mod.name} ready.`);
121
- }
122
- catch (e) {
123
- ctx.logger.error(`Failed to build ${mod.name}: ${e.message}`);
124
- ctx.logger.error(`Try manually: cd ~/.openclaw/extensions/connector && npm rebuild ${mod.name}`);
125
- }
100
+ const targetPath = path.join(pluginDir, 'node_modules', 'node-datachannel', 'build', 'Release', 'node_datachannel.node');
101
+ // Already built nothing to do.
102
+ if (fs.existsSync(targetPath))
103
+ return;
104
+ // Find the bundled prebuilt for this platform (shipped inside the npm package).
105
+ const prebuildKey = getPrebuildKey();
106
+ const prebuiltPath = path.join(pluginDir, 'prebuilds', prebuildKey, 'node_datachannel.node');
107
+ if (!fs.existsSync(prebuiltPath)) {
108
+ ctx.logger.error(`[clawchats] No prebuilt binary for ${prebuildKey}. ` +
109
+ `WebRTC will be unavailable. To fix manually: ` +
110
+ `cd ~/.openclaw/extensions/connector && npm rebuild node-datachannel`);
111
+ return;
112
+ }
113
+ ctx.logger.info(`[clawchats] Installing node-datachannel prebuilt for ${prebuildKey}...`);
114
+ try {
115
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
116
+ fs.copyFileSync(prebuiltPath, targetPath);
117
+ ctx.logger.info('[clawchats] node-datachannel ready.');
118
+ }
119
+ catch (e) {
120
+ ctx.logger.error(`[clawchats] Failed to install prebuilt: ${e.message}`);
126
121
  }
127
122
  }
128
123
  const CLAWCHATS_MD_CONTENT = `# ClawChats — Inline File Delivery
@@ -171,23 +166,7 @@ async function startClawChats(ctx, api, mediaStash) {
171
166
  return;
172
167
  ctx.logger.info('Setup detected — connecting to ClawChats...');
173
168
  }
174
- // 1. Check for updates
175
- const update = await checkForUpdates();
176
- if (update) {
177
- ctx.logger.info(`Update available: ${update.current} → ${update.latest}`);
178
- if (ctx._forceUpdate) {
179
- try {
180
- await performUpdate();
181
- ctx.logger.info(`Updated to ${update.latest}. Requesting graceful restart...`);
182
- api.runtime.requestRestart?.('clawchats update');
183
- return; // will restart with new version
184
- }
185
- catch (e) {
186
- ctx.logger.error(`Auto-update failed: ${e.message}`);
187
- }
188
- }
189
- }
190
- // 2. Resolve gateway token: runtime API → config file → error
169
+ // 1. Resolve gateway token: runtime API → config file → error
191
170
  const gwCfg = api.config;
192
171
  const gwAuth = gwCfg?.['gateway']?.['auth'];
193
172
  const gatewayToken = gwAuth?.['token'] || config.gatewayToken || '';
@@ -222,14 +201,38 @@ async function startClawChats(ctx, api, mediaStash) {
222
201
  const uploadsDir = path.join(ctx.stateDir, 'clawchats', 'uploads');
223
202
  _uploadsDir = uploadsDir;
224
203
  // Dynamic import of server.js (plain JS, no type declarations)
225
- // @ts-expect-error — server.js is plain JS with no .d.ts
226
- const serverModule = await import('../server.js');
204
+ // @ts-expect-error — server/index.js is plain JS with no .d.ts
205
+ const serverModule = await import('../server/index.js');
206
+ // Read env vars here (plugin host) so server/ bundle stays process.env-free.
207
+ const memoryEnv = {
208
+ provider: process.env.MEMORY_PROVIDER,
209
+ host: process.env.MEMORY_HOST || process.env.QDRANT_HOST,
210
+ port: process.env.MEMORY_PORT || process.env.QDRANT_PORT,
211
+ collection: process.env.MEMORY_COLLECTION || process.env.QDRANT_COLLECTION,
212
+ pgUrl: process.env.MEMORY_PG_URL,
213
+ qdrantUrl: process.env.QDRANT_URL,
214
+ };
215
+ // Filter out undefined values so discoverMemoryConfig only overrides what's set.
216
+ const memoryEnvFiltered = Object.fromEntries(Object.entries(memoryEnv).filter(([, v]) => v !== undefined && v !== ''));
227
217
  app = serverModule.createApp({
228
218
  dataDir,
229
219
  uploadsDir,
230
- gatewayUrl: 'ws://localhost:18789',
231
- authToken: '', // P2P: DataChannel is the auth boundary (signaling authenticates both sides)
220
+ port: parseInt(process.env.PORT || '3001', 10),
221
+ gatewayUrl: process.env.GATEWAY_WS_URL || 'ws://localhost:18789',
222
+ authToken: process.env.CLAWCHATS_AUTH_TOKEN || '', // P2P: DataChannel is the auth boundary
232
223
  gatewayToken, // For WS auth to local OpenClaw gateway
224
+ openaiApiKey: (() => {
225
+ // Resolve OpenAI API key: openclaw config → env var
226
+ try {
227
+ const oc = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.openclaw', 'openclaw.json'), 'utf8'));
228
+ const fromConfig = oc?.skills?.entries?.['openai-whisper-api']?.apiKey;
229
+ if (fromConfig)
230
+ return fromConfig;
231
+ }
232
+ catch { /* ok */ }
233
+ return process.env.OPENAI_API_KEY || null;
234
+ })(),
235
+ memoryEnv: memoryEnvFiltered,
233
236
  mediaStash, // Shared Map populated by after_tool_call hook (captures MEDIA: paths from exec)
234
237
  });
235
238
  // 4. Connect createApp's gateway client (handles persistence + event relay)
@@ -263,17 +266,6 @@ async function startClawChats(ctx, api, mediaStash) {
263
266
  ctx.logger.error(`Signaling auth rejected: ${reason}`);
264
267
  });
265
268
  // version-rejected listener removed — version check is now client-side
266
- signaling.on('force-update', async (targetVersion) => {
267
- ctx.logger.info(`Force update to ${targetVersion} requested`);
268
- try {
269
- await performUpdate();
270
- ctx.logger.info('Update complete, requesting restart');
271
- api.runtime.requestRestart?.('forced update');
272
- }
273
- catch (e) {
274
- ctx.logger.error(`Force update failed: ${e.message}`);
275
- }
276
- });
277
269
  signaling.on('account-suspended', (reason) => {
278
270
  ctx.logger.error(`Account suspended: ${reason}`);
279
271
  broadcastToClients({ type: 'account-suspended', reason });
@@ -760,7 +752,7 @@ function formatStatus() {
760
752
  // ---------------------------------------------------------------------------
761
753
  // CLI handlers
762
754
  // ---------------------------------------------------------------------------
763
- async function handleSetup(token) {
755
+ async function handleSetup(token, options = {}) {
764
756
  // Decode base64 token
765
757
  let tokenData;
766
758
  try {
@@ -850,21 +842,36 @@ async function handleSetup(token) {
850
842
  fs.mkdirSync(dataDir, { recursive: true });
851
843
  fs.mkdirSync(uploadsDir, { recursive: true });
852
844
  ws.close();
853
- // Enroll TOTP (interactive — single readline for the whole flow)
854
- const totpOk = await enrollTotp(config);
855
- if (!totpOk) {
845
+ if (options.skipTotp) {
846
+ // Agent-driven flow: skip interactive TOTP enrollment.
847
+ // User will run setup-totp + verify-totp separately.
848
+ console.log('');
849
+ console.log(' ✅ Setup complete!');
850
+ console.log('');
851
+ console.log(' 2FA setup pending. Run these commands to enable it:');
852
+ console.log(' openclaw clawchats setup-totp');
853
+ console.log(' openclaw clawchats verify-totp <6-digit-code>');
854
+ console.log('');
855
+ console.log(' Then restart: openclaw gateway restart');
856
+ console.log('');
857
+ }
858
+ else {
859
+ // Interactive (human) flow: enroll TOTP now.
860
+ const totpOk = await enrollTotp(config);
861
+ if (!totpOk) {
862
+ console.log('');
863
+ console.log(' ⚠️ TOTP not configured. You can set it up later with: openclaw clawchats reauth');
864
+ console.log(' ClawChats will not allow browser connections until 2FA is enabled.');
865
+ }
866
+ console.log(' ✅ Setup complete!');
856
867
  console.log('');
857
- console.log(' ⚠️ TOTP not configured. You can set it up later with: openclaw clawchats reauth');
858
- console.log(' ClawChats will not allow browser connections until 2FA is enabled.');
868
+ console.log(' Next steps:');
869
+ console.log(' 1. Restart your gateway: openclaw gateway restart');
870
+ console.log(' (or: systemctl --user restart openclaw-gateway)');
871
+ console.log(' 2. Open ClawChats: https://app.clawchats.ai');
872
+ console.log('');
873
+ console.log(' The gateway will connect automatically after restart.');
859
874
  }
860
- console.log(' ✅ Setup complete!');
861
- console.log('');
862
- console.log(' Next steps:');
863
- console.log(' 1. Restart your gateway: openclaw gateway restart');
864
- console.log(' (or: systemctl --user restart openclaw-gateway)');
865
- console.log(' 2. Open ClawChats: https://app.clawchats.ai');
866
- console.log('');
867
- console.log(' The gateway will connect automatically after restart.');
868
875
  resolve();
869
876
  }
870
877
  else if (msg.type === 'setup-error') {
@@ -993,6 +1000,105 @@ async function handleShowTotp() {
993
1000
  console.log(' 3. When prompted for a TOTP secret, paste the value above');
994
1001
  console.log('');
995
1002
  }
1003
+ // ---------------------------------------------------------------------------
1004
+ // Agent-driven TOTP setup (setup-totp + verify-totp)
1005
+ // ---------------------------------------------------------------------------
1006
+ async function handleSetupTotp() {
1007
+ const config = loadConfig();
1008
+ if (!config) {
1009
+ console.error('ClawChats not configured. Run: openclaw clawchats setup <token> --skip-totp');
1010
+ return;
1011
+ }
1012
+ if (config.schemaVersion >= 2 && config.totp) {
1013
+ console.error('TOTP already active. Use: openclaw clawchats reauth to reset.');
1014
+ return;
1015
+ }
1016
+ // Idempotency: reuse pending secret if generated within the last 24 hours.
1017
+ // Prevents stale-secret issues when the agent retries or the user reruns the command.
1018
+ const PENDING_TTL_MS = 24 * 60 * 60 * 1000;
1019
+ let totpSecret;
1020
+ const existing = config.totpPending;
1021
+ if (existing?.secret && existing.generatedAt) {
1022
+ const age = Date.now() - new Date(existing.generatedAt).getTime();
1023
+ if (age < PENDING_TTL_MS) {
1024
+ totpSecret = existing.secret;
1025
+ }
1026
+ else {
1027
+ // Expired — generate a fresh one
1028
+ totpSecret = generateTotpSecret();
1029
+ config.totpPending = { secret: totpSecret, generatedAt: new Date().toISOString() };
1030
+ saveConfig(config);
1031
+ }
1032
+ }
1033
+ else {
1034
+ totpSecret = generateTotpSecret();
1035
+ config.totpPending = { secret: totpSecret, generatedAt: new Date().toISOString() };
1036
+ saveConfig(config);
1037
+ }
1038
+ const formatted = totpSecret.match(/.{1,4}/g)?.join(' ') || totpSecret;
1039
+ const setupUrl = `${config.serverUrl.replace('wss://', 'https://').replace(/\/ws\/?$/, '')}/totp-setup#${totpSecret}`;
1040
+ console.log('');
1041
+ console.log(' 🔐 ClawChats Two-Factor Authentication Setup');
1042
+ console.log('');
1043
+ console.log(' Open this URL to scan the QR code with your authenticator app:');
1044
+ console.log(` ${setupUrl}`);
1045
+ console.log('');
1046
+ console.log(` Or enter manually: ${formatted}`);
1047
+ console.log('');
1048
+ console.log(' Once added, verify with:');
1049
+ console.log(' openclaw clawchats verify-totp <6-digit-code>');
1050
+ console.log('');
1051
+ }
1052
+ async function handleVerifyTotp(code) {
1053
+ const config = loadConfig();
1054
+ if (!config) {
1055
+ console.error('ClawChats not configured. Run: openclaw clawchats setup <token> --skip-totp');
1056
+ return;
1057
+ }
1058
+ if (config.schemaVersion >= 2 && config.totp) {
1059
+ console.error('TOTP already active. Use: openclaw clawchats reauth to reset.');
1060
+ return;
1061
+ }
1062
+ if (!config.totpPending?.secret) {
1063
+ console.error('No pending TOTP secret. Run: openclaw clawchats setup-totp first.');
1064
+ return;
1065
+ }
1066
+ const step = verifyTotp(code.trim(), config.totpPending.secret, 0);
1067
+ if (step < 0) {
1068
+ console.error(' ❌ Invalid code. Make sure you scanned the correct QR code and try again.');
1069
+ console.error(' Run: openclaw clawchats verify-totp <new-code>');
1070
+ process.exit(1);
1071
+ }
1072
+ // Generate backup codes
1073
+ const { codes, hashes } = generateBackupCodes();
1074
+ console.log('');
1075
+ console.log(' 🔑 Backup codes (save these somewhere safe — one-time use):');
1076
+ for (const backupCode of codes) {
1077
+ console.log(` ${backupCode}`);
1078
+ }
1079
+ console.log('');
1080
+ console.log(' ⚠️ These codes will NOT be shown again.');
1081
+ // Generate session secret and finalize config
1082
+ const sessionSecret = generateSessionSecret();
1083
+ config.totp = {
1084
+ secret: config.totpPending.secret,
1085
+ algorithm: 'SHA1',
1086
+ digits: 6,
1087
+ period: 30,
1088
+ enabledAt: new Date().toISOString(),
1089
+ };
1090
+ config.sessionSecret = sessionSecret;
1091
+ config.backupCodeHashes = hashes;
1092
+ config.schemaVersion = 2;
1093
+ delete config.totpPending;
1094
+ saveConfig(config);
1095
+ console.log('');
1096
+ console.log(' ✅ Two-factor authentication enabled!');
1097
+ console.log('');
1098
+ console.log(' Restart the gateway to apply:');
1099
+ console.log(' openclaw gateway restart');
1100
+ console.log('');
1101
+ }
996
1102
  async function handleReauth() {
997
1103
  const config = loadConfig();
998
1104
  if (!config) {
@@ -1178,11 +1284,17 @@ const plugin = {
1178
1284
  api.registerCli((ctx) => {
1179
1285
  const cmd = ctx.program.command('clawchats');
1180
1286
  cmd.command('setup <token>')
1181
- .description('Set up ClawChats with a setup token')
1182
- .action((token) => handleSetup(String(token)));
1287
+ .description('Set up ClawChats with a setup token (use --skip-totp for agent-driven installs)')
1288
+ .action((token) => handleSetup(String(token), { skipTotp: process.argv.includes('--skip-totp') }));
1183
1289
  cmd.command('status')
1184
1290
  .description('Show ClawChats connection status')
1185
1291
  .action(() => handleStatus());
1292
+ cmd.command('setup-totp')
1293
+ .description('Generate TOTP QR code for agent-driven 2FA setup (run after setup --skip-totp)')
1294
+ .action(() => handleSetupTotp());
1295
+ cmd.command('verify-totp <code>')
1296
+ .description('Verify TOTP code and finalize 2FA setup (agent-driven flow)')
1297
+ .action((code) => handleVerifyTotp(String(code)));
1186
1298
  cmd.command('reauth')
1187
1299
  .description('Reset two-factor authentication (new TOTP secret + invalidate sessions)')
1188
1300
  .action(() => handleReauth());
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@clawchatsai/connector",
3
- "version": "0.0.87",
3
+ "version": "0.0.89",
4
4
  "type": "module",
5
5
  "description": "ClawChats OpenClaw plugin — P2P tunnel + local API bridge",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
8
  "files": [
9
9
  "dist",
10
- "server.js",
11
10
  "server/",
12
- "openclaw.plugin.json"
11
+ "openclaw.plugin.json",
12
+ "prebuilds/"
13
13
  ],
14
14
  "publishConfig": {
15
15
  "access": "public"
@@ -18,7 +18,6 @@
18
18
  "node": ">=22.5.0"
19
19
  },
20
20
  "scripts": {
21
- "prebuild": "node esbuild.config.mjs",
22
21
  "build": "tsc",
23
22
  "dev": "tsc --watch",
24
23
  "prepublishOnly": "npm run build"
package/server/config.js CHANGED
@@ -23,13 +23,14 @@ export function parseConfigField(field) {
23
23
  return null;
24
24
  }
25
25
 
26
- // Auth token: env var → config.js → empty (open/unauthenticated mode)
27
- export const AUTH_TOKEN = process.env.CLAWCHATS_AUTH_TOKEN || parseConfigField('authToken') || '';
26
+ // Auth token: config.js → empty (open/unauthenticated mode)
27
+ // Note: CLAWCHATS_AUTH_TOKEN env var is read by the plugin host (src/index.ts) and passed via createApp().
28
+ export const AUTH_TOKEN = parseConfigField('authToken') || '';
28
29
 
29
30
  // Gateway WebSocket URL — uses the internal/local gateway address, NOT config.js gatewayUrl
30
31
  // (that's the browser's external-facing URL and would cause a routing loop through Caddy)
32
+ // Note: GATEWAY_WS_URL env var is read by the plugin host (src/index.ts) and passed via createApp().
31
33
  export function discoverGatewayWsUrl() {
32
- if (process.env.GATEWAY_WS_URL) return process.env.GATEWAY_WS_URL;
33
34
  for (const cfgPath of [path.join(HOME, '.openclaw', 'openclaw.json'), '/etc/openclaw/openclaw.json']) {
34
35
  try {
35
36
  const raw = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
@@ -43,8 +44,8 @@ export function discoverGatewayWsUrl() {
43
44
  export const GATEWAY_WS_URL = discoverGatewayWsUrl();
44
45
 
45
46
  // Sessions directory — where OpenClaw stores session .jsonl files
47
+ // Note: OPENCLAW_SESSIONS_DIR env var is read by the plugin host (src/index.ts) and passed via createApp().
46
48
  export const OPENCLAW_SESSIONS_DIR =
47
- process.env.OPENCLAW_SESSIONS_DIR ||
48
49
  parseConfigField('sessionsDir') ||
49
50
  path.join(HOME, '.openclaw', 'agents', 'main', 'sessions');
50
51
 
@@ -0,0 +1,20 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { send } from '../util/http.js';
5
+
6
+ /**
7
+ * List available OpenClaw agents.
8
+ * Keeps fs.readdirSync out of the HTTP router (server/index.js).
9
+ */
10
+ export function handleAgents(req, res) {
11
+ try {
12
+ const agentsDir = path.join(os.homedir(), '.openclaw', 'agents');
13
+ const agents = fs.readdirSync(agentsDir, { withFileTypes: true })
14
+ .filter(e => e.isDirectory())
15
+ .map(e => e.name);
16
+ send(res, 200, { agents });
17
+ } catch {
18
+ send(res, 200, { agents: ['main'] });
19
+ }
20
+ }
@@ -0,0 +1,28 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { send } from '../util/http.js';
4
+
5
+ /**
6
+ * Factory that returns settings GET/PUT handlers bound to a specific settings file path.
7
+ * Keeps file I/O out of the HTTP router (server/index.js).
8
+ */
9
+ export function createSettingsHandlers(settingsFile) {
10
+ function handleGetSettings(req, res) {
11
+ try {
12
+ send(res, 200, fs.existsSync(settingsFile)
13
+ ? JSON.parse(fs.readFileSync(settingsFile, 'utf8'))
14
+ : {});
15
+ } catch {
16
+ send(res, 200, {});
17
+ }
18
+ }
19
+
20
+ async function handleSaveSettings(req, res, parseBody) {
21
+ const body = await parseBody(req);
22
+ fs.mkdirSync(path.dirname(settingsFile), { recursive: true });
23
+ fs.writeFileSync(settingsFile, JSON.stringify(body, null, 2));
24
+ send(res, 200, { ok: true });
25
+ }
26
+
27
+ return { handleGetSettings, handleSaveSettings };
28
+ }
@@ -0,0 +1,56 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ const MIME = {
5
+ '.html': 'text/html',
6
+ '.js': 'text/javascript',
7
+ '.css': 'text/css',
8
+ '.json': 'application/json',
9
+ '.ico': 'image/x-icon',
10
+ '.png': 'image/png',
11
+ '.svg': 'image/svg+xml',
12
+ '.gif': 'image/gif',
13
+ '.webp': 'image/webp',
14
+ };
15
+
16
+ const STATIC_MAP = {
17
+ '/': 'index.html',
18
+ '/index.html': 'index.html',
19
+ '/app.js': 'app.js',
20
+ '/style.css': 'style.css',
21
+ '/error-handler.js': 'error-handler.js',
22
+ '/manifest.json': 'manifest.json',
23
+ '/favicon.ico': 'favicon.ico',
24
+ };
25
+
26
+ /**
27
+ * Serve static files from pluginDir.
28
+ * Returns true if the request was handled, false if it should fall through.
29
+ * Keeps fs.createReadStream out of the HTTP router (server/index.js).
30
+ */
31
+ export function handleStatic(req, res, pluginDir) {
32
+ const urlPath = (req.url || '/').split('?')[0];
33
+ const fileName = STATIC_MAP[urlPath];
34
+ const isAllowed =
35
+ fileName ||
36
+ urlPath.startsWith('/icons/') ||
37
+ urlPath.startsWith('/lib/') ||
38
+ urlPath.startsWith('/frontend/') ||
39
+ urlPath.startsWith('/emoji/') ||
40
+ urlPath === '/config.js';
41
+
42
+ if (!isAllowed) return false;
43
+
44
+ const staticPath = path.join(pluginDir, fileName || urlPath.slice(1));
45
+ if (!fs.existsSync(staticPath) || !fs.statSync(staticPath).isFile()) return false;
46
+
47
+ const ext = path.extname(staticPath).toLowerCase();
48
+ const stat = fs.statSync(staticPath);
49
+ res.writeHead(200, {
50
+ 'Content-Type': MIME[ext] || 'application/octet-stream',
51
+ 'Content-Length': stat.size,
52
+ 'Cache-Control': ext === '.html' ? 'no-cache' : 'public, max-age=3600',
53
+ });
54
+ fs.createReadStream(staticPath).pipe(res);
55
+ return true;
56
+ }
@@ -1,9 +1,6 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import os from 'node:os';
4
1
  import { send } from '../util/http.js';
5
2
 
6
- export async function handleTranscribe(req, res) {
3
+ export async function handleTranscribe(req, res, opts = {}) {
7
4
  try {
8
5
  const chunks = [];
9
6
  for await (const chunk of req) chunks.push(chunk);
@@ -12,12 +9,8 @@ export async function handleTranscribe(req, res) {
12
9
  if (audioBuffer.length === 0) return send(res, 400, { error: 'No audio data' });
13
10
  if (audioBuffer.length > 25 * 1024 * 1024) return send(res, 400, { error: 'Audio too large (max 25MB)' });
14
11
 
15
- let apiKey;
16
- try {
17
- const ocConfig = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.openclaw', 'openclaw.json'), 'utf8'));
18
- apiKey = ocConfig?.skills?.entries?.['openai-whisper-api']?.apiKey;
19
- } catch { /* ok */ }
20
- if (!apiKey) apiKey = process.env.OPENAI_API_KEY;
12
+ // API key is resolved by the plugin host (src/index.ts) and passed via opts.openaiApiKey.
13
+ const apiKey = opts.openaiApiKey;
21
14
  if (!apiKey) return send(res, 500, { error: 'No OpenAI API key configured' });
22
15
 
23
16
  const contentType = req.headers['content-type'] || 'audio/webm';