@clawchatsai/connector 0.0.87 → 0.0.88

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/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 });
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@clawchatsai/connector",
3
- "version": "0.0.87",
3
+ "version": "0.0.88",
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';
package/server/index.js CHANGED
@@ -9,7 +9,8 @@ import { Database, requestDbStore } from './bootstrap/native.js';
9
9
  import { GATEWAY_WS_URL, AUTH_TOKEN, getSessionsDirForAgent } from './config.js';
10
10
  import { DebugLogger } from './debug.js';
11
11
  import { GatewayClient } from './gateway.js';
12
- import { discoverMemoryConfig, createMemoryProvider } from './providers/memory.js';
12
+ import { discoverMemoryConfig } from './providers/memory-config.js';
13
+ import { createMemoryProvider } from './providers/memory.js';
13
14
  import { WorkspaceController } from './controllers/workspaces.js';
14
15
  import { ThreadController } from './controllers/threads.js';
15
16
  import { MessageController } from './controllers/messages.js';
@@ -17,26 +18,33 @@ import { FileController } from './controllers/files.js';
17
18
  import { MemoryController } from './controllers/memory.js';
18
19
  import { handleServeFile, handleWorkspaceList, handleWorkspaceFileRead, handleWorkspaceFileWrite, handleWorkspaceFileDelete, handleWorkspaceUpload } from './controllers/filesystem.js';
19
20
  import { handleTranscribe } from './controllers/transcribe.js';
21
+ import { handleStatic } from './controllers/static.js';
22
+ import { handleAgents } from './controllers/agents.js';
23
+ import { createSettingsHandlers } from './controllers/settings.js';
24
+ import { createWorkspaceStore } from './store/workspace-store.js';
20
25
  import { parseSessionKey } from './util/helpers.js';
21
26
  import { send, sendError, parseBody, uuid, matchRoute, setCors } from './util/http.js';
22
27
 
23
28
  const HOME = os.homedir();
24
- const PORT = parseInt(process.env.PORT || '3001', 10);
29
+ // PORT is passed via createApp(config.port); env var is read by plugin host (src/index.ts).
30
+ const DEFAULT_PORT = 3001;
25
31
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
26
32
 
27
33
  // Resolve the plugin directory (parent of server/) for static file serving
28
34
  const PLUGIN_DIR = path.resolve(__dirname, '..');
29
35
 
30
36
  export function createApp(config = {}) {
31
- const DATA_DIR = config.dataDir || path.join(PLUGIN_DIR, 'data');
37
+ const PORT = config.port || DEFAULT_PORT;
38
+ const DATA_DIR = config.dataDir || path.join(PLUGIN_DIR, 'data');
32
39
  const UPLOADS_DIR = config.uploadsDir || path.join(PLUGIN_DIR, 'uploads');
33
40
  const WORKSPACES_FILE = path.join(DATA_DIR, 'workspaces.json');
34
41
  const SETTINGS_FILE = path.join(DATA_DIR, 'settings.json');
35
42
  const INTELLIGENCE_DIR = path.join(DATA_DIR, 'intelligence');
36
43
 
37
- const authToken = config.authToken !== undefined ? config.authToken : AUTH_TOKEN;
38
- const gatewayToken = config.gatewayToken !== undefined ? config.gatewayToken : authToken;
39
- const gatewayUrl = config.gatewayUrl || GATEWAY_WS_URL;
44
+ const authToken = config.authToken !== undefined ? config.authToken : AUTH_TOKEN;
45
+ const gatewayToken = config.gatewayToken !== undefined ? config.gatewayToken : authToken;
46
+ const gatewayUrl = config.gatewayUrl || GATEWAY_WS_URL;
47
+ const openaiApiKey = config.openaiApiKey || null;
40
48
 
41
49
  fs.mkdirSync(DATA_DIR, { recursive: true });
42
50
  fs.mkdirSync(UPLOADS_DIR, { recursive: true });
@@ -69,21 +77,13 @@ export function createApp(config = {}) {
69
77
  close() { if (_globalDb) { _globalDb.close(); _globalDb = null; } }
70
78
  };
71
79
 
72
- // Workspace config (JSON sidecar)
73
- let workspacesConfig = null;
74
- function getWorkspaces() {
75
- if (!workspacesConfig) {
76
- try { workspacesConfig = JSON.parse(fs.readFileSync(WORKSPACES_FILE, 'utf8')); }
77
- catch { workspacesConfig = { active: 'default', workspaces: { default: { name: 'default', label: 'Default', createdAt: Date.now() } } }; fs.writeFileSync(WORKSPACES_FILE, JSON.stringify(workspacesConfig, null, 2)); }
78
- }
79
- return workspacesConfig;
80
- }
81
- function setWorkspaces(data) { workspacesConfig = data; fs.writeFileSync(WORKSPACES_FILE, JSON.stringify(data, null, 2)); }
80
+ // Workspace config (JSON sidecar) — file I/O lives in workspace-store.js
81
+ const { getWorkspaces, setWorkspaces } = createWorkspaceStore(WORKSPACES_FILE);
82
82
 
83
83
  const debugLogger = new DebugLogger(DATA_DIR);
84
84
  const mediaStash = new Map();
85
85
 
86
- const memoryConfig = discoverMemoryConfig();
86
+ const memoryConfig = discoverMemoryConfig(config.memoryEnv || {});
87
87
  const memoryProvider = createMemoryProvider(memoryConfig);
88
88
  memoryProvider.init().catch(err => console.error('[createApp] Memory provider init error:', err.message));
89
89
  const MEMORY_FILES_DIR = path.join(memoryConfig.workspaceDir, 'memory');
@@ -99,17 +99,8 @@ export function createApp(config = {}) {
99
99
  const files = new FileController({ getActiveDb, getWorkspaces, uploadsDir: UPLOADS_DIR, intelligenceDir: INTELLIGENCE_DIR });
100
100
  const memory = new MemoryController({ memoryProvider, memoryFilesDir: MEMORY_FILES_DIR, memoryConfig });
101
101
 
102
- // Settings (simple key-value file store, no class needed)
103
- function handleGetSettings(req, res) {
104
- try { send(res, 200, fs.existsSync(SETTINGS_FILE) ? JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8')) : {}); }
105
- catch { send(res, 200, {}); }
106
- }
107
- async function handleSaveSettings(req, res) {
108
- const body = await parseBody(req);
109
- fs.mkdirSync(path.dirname(SETTINGS_FILE), { recursive: true });
110
- fs.writeFileSync(SETTINGS_FILE, JSON.stringify(body, null, 2));
111
- send(res, 200, { ok: true });
112
- }
102
+ // Settings file I/O lives in settings.js
103
+ const { handleGetSettings, handleSaveSettings } = createSettingsHandlers(SETTINGS_FILE);
113
104
 
114
105
  // Auth middleware
115
106
  function checkAuth(req, res) {
@@ -136,21 +127,9 @@ export function createApp(config = {}) {
136
127
 
137
128
  if (method === 'OPTIONS') { setCors(res); res.writeHead(204); return res.end(); }
138
129
 
139
- // Static file serving
130
+ // Static file serving — file I/O lives in static.js
140
131
  if (method === 'GET' && !urlPath.startsWith('/api/')) {
141
- const STATIC = { '/': 'index.html', '/index.html': 'index.html', '/app.js': 'app.js', '/style.css': 'style.css', '/error-handler.js': 'error-handler.js', '/manifest.json': 'manifest.json', '/favicon.ico': 'favicon.ico' };
142
- const fileName = STATIC[urlPath];
143
- const isAllowed = fileName || urlPath.startsWith('/icons/') || urlPath.startsWith('/lib/') || urlPath.startsWith('/frontend/') || urlPath.startsWith('/emoji/') || urlPath === '/config.js';
144
- if (isAllowed) {
145
- const staticPath = path.join(PLUGIN_DIR, fileName || urlPath.slice(1));
146
- if (fs.existsSync(staticPath) && fs.statSync(staticPath).isFile()) {
147
- const MIME = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.json': 'application/json', '.ico': 'image/x-icon', '.png': 'image/png', '.svg': 'image/svg+xml', '.gif': 'image/gif', '.webp': 'image/webp' };
148
- const ext = path.extname(staticPath).toLowerCase();
149
- const stat = fs.statSync(staticPath);
150
- res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream', 'Content-Length': stat.size, 'Cache-Control': ext === '.html' ? 'no-cache' : 'public, max-age=3600' });
151
- return fs.createReadStream(staticPath).pipe(res);
152
- }
153
- }
132
+ if (handleStatic(req, res, PLUGIN_DIR)) return;
154
133
  }
155
134
 
156
135
  // Unauthenticated routes
@@ -215,14 +194,10 @@ export function createApp(config = {}) {
215
194
 
216
195
  // Settings & misc
217
196
  if (method === 'GET' && urlPath === '/api/settings') return handleGetSettings(req, res);
218
- if (method === 'PUT' && urlPath === '/api/settings') return await handleSaveSettings(req, res);
219
- if (method === 'POST' && urlPath === '/api/transcribe') return await handleTranscribe(req, res);
197
+ if (method === 'PUT' && urlPath === '/api/settings') return await handleSaveSettings(req, res, parseBody);
198
+ if (method === 'POST' && urlPath === '/api/transcribe') return await handleTranscribe(req, res, { openaiApiKey });
220
199
  if (method === 'GET' && urlPath === '/api/health') return send(res, 200, { ok: true, workspace: getWorkspaces().active, uptime: process.uptime() });
221
- if (method === 'GET' && urlPath === '/api/agents') {
222
- try { send(res, 200, { agents: fs.readdirSync(path.join(HOME, '.openclaw', 'agents'), { withFileTypes: true }).filter(e => e.isDirectory()).map(e => e.name) }); }
223
- catch { send(res, 200, { agents: ['main'] }); }
224
- return;
225
- }
200
+ if (method === 'GET' && urlPath === '/api/agents') return handleAgents(req, res);
226
201
 
227
202
  // Workspaces
228
203
  if (method === 'GET' && urlPath === '/api/workspaces') return workspaces.getAll(req, res);
@@ -0,0 +1,52 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+
5
+ /**
6
+ * Discover memory backend configuration from OpenClaw config + optional env overrides.
7
+ * env vars (MEMORY_PROVIDER, QDRANT_HOST, etc.) are read by the plugin host (src/index.ts)
8
+ * and passed in via envOverrides to keep env vars out of the server bundle.
9
+ *
10
+ * This file is intentionally network-free: it only reads local config files.
11
+ * The actual provider implementations (Qdrant, Postgres) live in memory.js.
12
+ */
13
+ export function discoverMemoryConfig(envOverrides = {}) {
14
+ const defaults = { provider: 'qdrant', host: 'localhost', port: 6333, collection: null };
15
+ let oc = null;
16
+ for (const cfgPath of [path.join(os.homedir(), '.openclaw', 'openclaw.json'), '/etc/openclaw/openclaw.json']) {
17
+ try { oc = JSON.parse(fs.readFileSync(cfgPath, 'utf8')); break; } catch { /* try next */ }
18
+ }
19
+
20
+ let cfg = { ...defaults };
21
+ if (oc) {
22
+ const vs = oc.plugins?.slots?.memory
23
+ ? oc.plugins?.entries?.[oc.plugins.slots.memory]?.config?.oss?.vectorStore
24
+ : null;
25
+ if (vs) {
26
+ if (vs.provider) cfg.provider = vs.provider;
27
+ if (vs.config?.host) cfg.host = vs.config.host;
28
+ if (vs.config?.port) cfg.port = vs.config.port;
29
+ if (vs.config?.collectionName) cfg.collection = vs.config.collectionName;
30
+ if (vs.config?.user) cfg.pgUser = vs.config.user;
31
+ if (vs.config?.password) cfg.pgPassword = vs.config.password;
32
+ if (vs.config?.dbname) cfg.pgDbName = vs.config.dbname;
33
+ }
34
+ const wsDir = oc.agents?.defaults?.workspace;
35
+ if (wsDir) cfg.workspaceDir = wsDir;
36
+ }
37
+
38
+ if (envOverrides.provider) cfg.provider = envOverrides.provider;
39
+ if (envOverrides.host) cfg.host = envOverrides.host;
40
+ if (envOverrides.port) cfg.port = parseInt(envOverrides.port, 10);
41
+ if (envOverrides.collection) cfg.collection = envOverrides.collection;
42
+ if (envOverrides.pgUrl) cfg.pgUrl = envOverrides.pgUrl;
43
+ if (envOverrides.qdrantUrl && !envOverrides.host) {
44
+ try {
45
+ const u = new URL(envOverrides.qdrantUrl);
46
+ cfg.host = u.hostname;
47
+ if (u.port) cfg.port = parseInt(u.port, 10);
48
+ } catch { /* ignore */ }
49
+ }
50
+ if (!cfg.workspaceDir) cfg.workspaceDir = path.join(os.homedir(), '.openclaw', 'workspace');
51
+ return cfg;
52
+ }