@clawchatsai/connector 0.0.86 → 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.
Files changed (43) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +67 -13
  3. package/dist/index.js +80 -89
  4. package/openclaw.plugin.json +1 -1
  5. package/package.json +11 -8
  6. package/prebuilds/darwin-arm64/node_datachannel.node +0 -0
  7. package/prebuilds/darwin-x64/node_datachannel.node +0 -0
  8. package/prebuilds/linux-arm/node_datachannel.node +0 -0
  9. package/prebuilds/linux-arm64/node_datachannel.node +0 -0
  10. package/prebuilds/linux-x64/node_datachannel.node +0 -0
  11. package/prebuilds/linuxmusl-arm64/node_datachannel.node +0 -0
  12. package/prebuilds/linuxmusl-x64/node_datachannel.node +0 -0
  13. package/prebuilds/win32-arm64/node_datachannel.node +0 -0
  14. package/prebuilds/win32-x64/node_datachannel.node +0 -0
  15. package/server/bootstrap/identity.js +47 -0
  16. package/server/bootstrap/native.js +9 -0
  17. package/server/config.js +63 -0
  18. package/server/controllers/agents.js +20 -0
  19. package/server/controllers/files.js +64 -0
  20. package/server/controllers/filesystem.js +139 -0
  21. package/server/controllers/memory.js +86 -0
  22. package/server/controllers/messages.js +128 -0
  23. package/server/controllers/settings.js +28 -0
  24. package/server/controllers/static.js +56 -0
  25. package/server/controllers/threads.js +113 -0
  26. package/server/controllers/transcribe.js +44 -0
  27. package/server/controllers/workspaces.js +102 -0
  28. package/server/debug.js +56 -0
  29. package/server/gateway-cleanup.js +47 -0
  30. package/server/gateway.js +331 -0
  31. package/server/index.js +397 -0
  32. package/server/providers/memory-config.js +52 -0
  33. package/server/providers/memory.js +108 -0
  34. package/server/store/workspace-store.js +31 -0
  35. package/server/util/context.js +49 -0
  36. package/server/util/helpers.js +111 -0
  37. package/server/util/http.js +57 -0
  38. package/server/util/multipart.js +46 -0
  39. package/dist/migrate.d.ts +0 -16
  40. package/dist/migrate.js +0 -114
  41. package/dist/updater.d.ts +0 -21
  42. package/dist/updater.js +0 -64
  43. package/server.js +0 -2459
package/README.md CHANGED
@@ -1,30 +1,84 @@
1
1
  # @clawchatsai/connector
2
2
 
3
- OpenClaw plugin for [ClawChats](https://clawchats.ai) connects your local gateway to the ClawChats web app via WebRTC P2P.
3
+ OpenClaw plugin that bridges the [ClawChats](https://clawchats.ai) web app to your local OpenClaw gateway.
4
4
 
5
- ## Install
5
+ ## What it does
6
+
7
+ When installed, this plugin runs as a background service alongside your OpenClaw gateway. It:
8
+
9
+ - Connects to the OpenClaw gateway WebSocket and persists conversation history to a local SQLite database
10
+ - Connects to the ClawChats signaling server (`wss://login.clawchats.ai`) to establish a P2P WebRTC session with your browser
11
+ - Once the P2P connection is established, your browser communicates directly with this plugin — no data passes through any external server
12
+ - Exposes a local HTTP/WebSocket API for threads, messages, workspaces, file management, and memory
13
+
14
+ ## How the connection works
15
+
16
+ ```
17
+ app.clawchats.ai ──── signaling server ──── connector plugin (your machine)
18
+ (browser UI) (handshake only) (OpenClaw gateway)
19
+ │ │
20
+ └──────── WebRTC DataChannel (P2P) ────────────┘
21
+ encrypted, direct, no relay
22
+ ```
23
+
24
+ 1. You open [app.clawchats.ai](https://app.clawchats.ai) in your browser
25
+ 2. The browser authenticates with `login.clawchats.ai` and gets a session token
26
+ 3. The signaling server coordinates a WebRTC handshake between your browser and this plugin
27
+ 4. Your browser connects directly to your gateway via an encrypted P2P DataChannel
28
+ 5. All conversation data — messages, files, memory — travels over that direct connection
29
+
30
+ The signaling server only facilitates the handshake. After step 4, it is out of the picture.
31
+
32
+ ## Installation
6
33
 
7
34
  ```bash
8
35
  openclaw plugins install @clawchatsai/connector
36
+ openclaw gateway restart
9
37
  ```
10
38
 
11
- ## Setup
39
+ Then open [app.clawchats.ai](https://app.clawchats.ai) and follow the setup flow.
12
40
 
13
- After installing, run the setup command from your OpenClaw session:
41
+ ## Architecture
14
42
 
15
43
  ```
16
- /shellchat setup <token>
44
+ server/ # Local backend server (Node.js, plain ESM)
45
+ index.js # createApp() factory — HTTP API + WebSocket relay to OpenClaw
46
+ gateway.js # GatewayClient — maintains connection to local OpenClaw gateway
47
+ config.js # Config discovery (env vars → openclaw.json → defaults)
48
+ debug.js # Debug session logger
49
+ gateway-cleanup.js # Session cleanup on thread/workspace delete
50
+ bootstrap/
51
+ native.js # node:sqlite initialisation (built into Node ≥22.5, no compilation)
52
+ identity.js # ed25519 device signing for OpenClaw ≥2.15
53
+ controllers/ # HTTP route handlers (threads, messages, workspaces, files, memory)
54
+ providers/ # Memory backends (Qdrant, Postgres)
55
+ util/ # HTTP helpers, multipart parser, context builder, misc
56
+ src/ # OpenClaw plugin wrapper (TypeScript)
57
+ index.ts # Plugin entry point — registers with OpenClaw, manages lifecycle
58
+ signaling-client.ts # Connects to ClawChats signaling server for P2P handshake
59
+ webrtc-peer.ts # WebRTC DataChannel peer — direct browser ↔ gateway connection
60
+ auth-handler.ts # TOTP + Google session auth for DataChannel connections
17
61
  ```
18
62
 
19
- Get your token at [clawchats.ai](https://clawchats.ai)
63
+ ## Security & permissions
20
64
 
21
- ## Update
65
+ **Filesystem access:** The plugin reads/writes under `~/.openclaw/` (gateway data) and the user's home directory for workspace file browsing. File serving is restricted to an explicit allowlist (`HOME`, `/tmp`). Write access is limited to within `HOME`.
22
66
 
23
- ```bash
24
- openclaw plugins update @clawchatsai/connector
25
- ```
67
+ **Session cleanup:** When a thread or workspace is deleted, the plugin removes the associated OpenClaw session files (`.jsonl`) to prevent stale context. These are files created by the gateway itself during that session.
68
+
69
+ **Auth:** The DataChannel connection is authenticated with TOTP + Google session token before any data is processed. The HTTP API uses a local bearer token (set via `CLAWCHATS_AUTH_TOKEN` or `config.js`). No credentials are sent to external servers.
70
+
71
+ **No shell execution:** The plugin contains no `exec`, `spawn`, or shell calls. SQLite is handled by Node's built-in `node:sqlite` module — no native binary compilation required.
72
+
73
+ ## Configuration
74
+
75
+ Config is read in priority order:
76
+ 1. Environment variables (`CLAWCHATS_AUTH_TOKEN`, `GATEWAY_WS_URL`, `OPENCLAW_SESSIONS_DIR`)
77
+ 2. `~/.openclaw/openclaw.json` (OpenClaw gateway config)
78
+ 3. `config.js` in the plugin directory (local override, gitignored)
79
+
80
+ ## License
26
81
 
27
- ## Links
82
+ [AGPL-3.0-only](LICENSE) — source is open for audit and contribution.
28
83
 
29
- - [Website](https://clawchats.ai)
30
- - [npm](https://www.npmjs.com/package/@clawchatsai/connector)
84
+ For commercial licensing, contact [clawchats.ai](https://clawchats.ai).
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,64 +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: 'better-sqlite3', binding: 'build/Release/better_sqlite3.node', strategy: 'rebuild' },
75
- { name: 'node-datachannel', binding: 'build/Release/node_datachannel.node', strategy: 'install-script' },
76
- ];
77
- const missing = modules.filter(m => !fs.existsSync(path.join(pluginDir, 'node_modules', m.name, m.binding)));
78
- if (missing.length === 0)
79
- return; // all built
80
- ctx.logger.info(`Building native modules (first run): ${missing.map(m => m.name).join(', ')}...`);
81
- const { execFileSync } = await import('node:child_process');
82
- for (const mod of missing) {
83
- try {
84
- if (mod.strategy === 'install-script') {
85
- // Packages using prebuild-install need their install script re-run (npm rebuild
86
- // only triggers node-gyp, not prebuild-install). Run the install script directly
87
- // from the package's directory.
88
- const modDir = path.join(pluginDir, 'node_modules', mod.name);
89
- const modPkg = JSON.parse(fs.readFileSync(path.join(modDir, 'package.json'), 'utf-8'));
90
- const installCmd = modPkg.scripts?.install;
91
- if (installCmd) {
92
- // Add local .bin dirs to PATH so prebuild-install and other local binaries resolve.
93
- // Check both the module's own node_modules/.bin (hoisted deps) and the plugin-level one.
94
- const localBin = [
95
- path.join(modDir, 'node_modules', '.bin'),
96
- path.join(pluginDir, 'node_modules', '.bin'),
97
- ].join(':');
98
- execFileSync('sh', ['-c', installCmd], {
99
- cwd: modDir,
100
- stdio: 'pipe',
101
- timeout: 120_000,
102
- env: { ...process.env, PATH: `${localBin}:${process.env.PATH ?? ''}`, npm_config_node_gyp: '' },
103
- });
104
- }
105
- else {
106
- // Fallback to rebuild if no install script found
107
- execFileSync('npm', ['rebuild', mod.name], {
108
- cwd: pluginDir,
109
- stdio: 'pipe',
110
- timeout: 120_000,
111
- });
112
- }
113
- }
114
- else {
115
- execFileSync('npm', ['rebuild', mod.name], {
116
- cwd: pluginDir,
117
- stdio: 'pipe',
118
- timeout: 120_000,
119
- });
120
- }
121
- ctx.logger.info(`${mod.name} ready.`);
122
- }
123
- catch (e) {
124
- ctx.logger.error(`Failed to build ${mod.name}: ${e.message}`);
125
- ctx.logger.error(`Try manually: cd ~/.openclaw/extensions/connector && npm rebuild ${mod.name}`);
126
- }
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}`);
127
121
  }
128
122
  }
129
123
  const CLAWCHATS_MD_CONTENT = `# ClawChats — Inline File Delivery
@@ -172,23 +166,7 @@ async function startClawChats(ctx, api, mediaStash) {
172
166
  return;
173
167
  ctx.logger.info('Setup detected — connecting to ClawChats...');
174
168
  }
175
- // 1. Check for updates
176
- const update = await checkForUpdates();
177
- if (update) {
178
- ctx.logger.info(`Update available: ${update.current} → ${update.latest}`);
179
- if (ctx._forceUpdate) {
180
- try {
181
- await performUpdate();
182
- ctx.logger.info(`Updated to ${update.latest}. Requesting graceful restart...`);
183
- api.runtime.requestRestart?.('clawchats update');
184
- return; // will restart with new version
185
- }
186
- catch (e) {
187
- ctx.logger.error(`Auto-update failed: ${e.message}`);
188
- }
189
- }
190
- }
191
- // 2. Resolve gateway token: runtime API → config file → error
169
+ // 1. Resolve gateway token: runtime API → config file → error
192
170
  const gwCfg = api.config;
193
171
  const gwAuth = gwCfg?.['gateway']?.['auth'];
194
172
  const gatewayToken = gwAuth?.['token'] || config.gatewayToken || '';
@@ -223,14 +201,38 @@ async function startClawChats(ctx, api, mediaStash) {
223
201
  const uploadsDir = path.join(ctx.stateDir, 'clawchats', 'uploads');
224
202
  _uploadsDir = uploadsDir;
225
203
  // Dynamic import of server.js (plain JS, no type declarations)
226
- // @ts-expect-error — server.js is plain JS with no .d.ts
227
- 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 !== ''));
228
217
  app = serverModule.createApp({
229
218
  dataDir,
230
219
  uploadsDir,
231
- gatewayUrl: 'ws://localhost:18789',
232
- 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
233
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,
234
236
  mediaStash, // Shared Map populated by after_tool_call hook (captures MEDIA: paths from exec)
235
237
  });
236
238
  // 4. Connect createApp's gateway client (handles persistence + event relay)
@@ -264,17 +266,6 @@ async function startClawChats(ctx, api, mediaStash) {
264
266
  ctx.logger.error(`Signaling auth rejected: ${reason}`);
265
267
  });
266
268
  // version-rejected listener removed — version check is now client-side
267
- signaling.on('force-update', async (targetVersion) => {
268
- ctx.logger.info(`Force update to ${targetVersion} requested`);
269
- try {
270
- await performUpdate();
271
- ctx.logger.info('Update complete, requesting restart');
272
- api.runtime.requestRestart?.('forced update');
273
- }
274
- catch (e) {
275
- ctx.logger.error(`Force update failed: ${e.message}`);
276
- }
277
- });
278
269
  signaling.on('account-suspended', (reason) => {
279
270
  ctx.logger.error(`Account suspended: ${reason}`);
280
271
  broadcastToClients({ type: 'account-suspended', reason });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "connector",
3
3
  "name": "ClawChats",
4
- "description": "Connects your OpenClaw gateway to ClawChats via WebRTC P2P",
4
+ "description": "Local chat backend and API bridge for OpenClaw.",
5
5
  "kind": "integration",
6
6
  "configSchema": {
7
7
  "type": "object",
package/package.json CHANGED
@@ -1,30 +1,28 @@
1
1
  {
2
2
  "name": "@clawchatsai/connector",
3
- "version": "0.0.86",
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
- "openclaw.plugin.json"
10
+ "server/",
11
+ "openclaw.plugin.json",
12
+ "prebuilds/"
12
13
  ],
13
14
  "publishConfig": {
14
15
  "access": "public"
15
16
  },
16
17
  "engines": {
17
- "node": ">=18"
18
+ "node": ">=22.5.0"
18
19
  },
19
20
  "scripts": {
20
- "prebuild": "node esbuild.config.mjs",
21
21
  "build": "tsc",
22
22
  "dev": "tsc --watch",
23
- "prepublishOnly": "npm run build",
24
- "postinstall": "npm rebuild better-sqlite3 2>/dev/null || true"
23
+ "prepublishOnly": "npm run build"
25
24
  },
26
25
  "dependencies": {
27
- "better-sqlite3": ">=9.0.0",
28
26
  "jose": "^5.10.0",
29
27
  "node-datachannel": "^0.32.1",
30
28
  "ws": "^8.0.0"
@@ -38,5 +36,10 @@
38
36
  "@types/ws": "^8.0.0",
39
37
  "esbuild": "^0.27.4",
40
38
  "typescript": "^5.4.0"
39
+ },
40
+ "license": "AGPL-3.0-only",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "https://github.com/clawchatsai/connector"
41
44
  }
42
45
  }
@@ -0,0 +1,47 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ const ED25519_SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex');
6
+
7
+ function derivePublicKeyRaw(publicKeyPem) {
8
+ const spki = crypto.createPublicKey(publicKeyPem).export({ type: 'spki', format: 'der' });
9
+ if (spki.length === ED25519_SPKI_PREFIX.length + 32 &&
10
+ spki.subarray(0, ED25519_SPKI_PREFIX.length).equals(ED25519_SPKI_PREFIX)) {
11
+ return spki.subarray(ED25519_SPKI_PREFIX.length);
12
+ }
13
+ return spki;
14
+ }
15
+
16
+ function fingerprintPublicKey(publicKeyPem) {
17
+ return crypto.createHash('sha256').update(derivePublicKeyRaw(publicKeyPem)).digest('hex');
18
+ }
19
+
20
+ function base64UrlEncode(buf) {
21
+ return buf.toString('base64').replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/g, '');
22
+ }
23
+
24
+ export function loadOrCreateDeviceIdentity(identityPath) {
25
+ try {
26
+ if (fs.existsSync(identityPath)) {
27
+ const parsed = JSON.parse(fs.readFileSync(identityPath, 'utf8'));
28
+ if (parsed?.version === 1 && parsed.deviceId && parsed.publicKeyPem && parsed.privateKeyPem) return parsed;
29
+ }
30
+ } catch { /* regenerate */ }
31
+ const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
32
+ const publicKeyPem = publicKey.export({ type: 'spki', format: 'pem' }).toString();
33
+ const privateKeyPem = privateKey.export({ type: 'pkcs8', format: 'pem' }).toString();
34
+ const identity = { version: 1, deviceId: fingerprintPublicKey(publicKeyPem), publicKeyPem, privateKeyPem, createdAtMs: Date.now() };
35
+ fs.mkdirSync(path.dirname(identityPath), { recursive: true });
36
+ fs.writeFileSync(identityPath, JSON.stringify(identity, null, 2) + '\n', { mode: 0o600 });
37
+ return identity;
38
+ }
39
+
40
+ export function buildDeviceAuth(identity, { clientId, clientMode, role, scopes, token, nonce }) {
41
+ const signedAt = Date.now();
42
+ const payload = ['v2', identity.deviceId, clientId, clientMode, role, scopes.join(','), String(signedAt), token || '', nonce].join('|');
43
+ const privateKey = crypto.createPrivateKey(identity.privateKeyPem);
44
+ const signature = base64UrlEncode(crypto.sign(null, Buffer.from(payload, 'utf8'), privateKey));
45
+ const publicKeyB64Url = base64UrlEncode(derivePublicKeyRaw(identity.publicKeyPem));
46
+ return { id: identity.deviceId, publicKey: publicKeyB64Url, signature, signedAt, nonce };
47
+ }
@@ -0,0 +1,9 @@
1
+ import { DatabaseSync as Database } from 'node:sqlite';
2
+ import { AsyncLocalStorage } from 'node:async_hooks';
3
+
4
+ // Per-request workspace DB — isolates concurrent clients on different workspaces.
5
+ export const requestDbStore = new AsyncLocalStorage();
6
+
7
+ // node:sqlite is a built-in Node.js module (available since Node 22.5).
8
+ // No native compilation required — SQLite is bundled directly into Node.
9
+ export { Database };
@@ -0,0 +1,63 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ export const HOME = os.homedir();
7
+ export const MAX_PREAMBLE_CHARS = 50000;
8
+
9
+ // Resolve __dirname for ESM (esbuild inlines this correctly)
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
12
+
13
+ export function parseConfigField(field) {
14
+ // Try both plugin root and parent of server/ — handles bundled and standalone modes
15
+ const candidates = [path.join(__dirname, 'config.js'), path.join(__dirname, '..', 'config.js')];
16
+ for (const configPath of candidates) {
17
+ try {
18
+ const configText = fs.readFileSync(configPath, 'utf8');
19
+ const match = configText.match(new RegExp(`${field}:\\s*['"]([^'"]+)['"]`));
20
+ if (match) return match[1];
21
+ } catch { /* try next */ }
22
+ }
23
+ return null;
24
+ }
25
+
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') || '';
29
+
30
+ // Gateway WebSocket URL — uses the internal/local gateway address, NOT config.js gatewayUrl
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().
33
+ export function discoverGatewayWsUrl() {
34
+ for (const cfgPath of [path.join(HOME, '.openclaw', 'openclaw.json'), '/etc/openclaw/openclaw.json']) {
35
+ try {
36
+ const raw = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
37
+ const port = raw.gateway?.port || raw.port;
38
+ const host = raw.gateway?.host || raw.host || 'localhost';
39
+ if (port) return `ws://${host}:${port}`;
40
+ } catch { /* try next */ }
41
+ }
42
+ return 'ws://localhost:18789';
43
+ }
44
+ export const GATEWAY_WS_URL = discoverGatewayWsUrl();
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().
48
+ export const OPENCLAW_SESSIONS_DIR =
49
+ parseConfigField('sessionsDir') ||
50
+ path.join(HOME, '.openclaw', 'agents', 'main', 'sessions');
51
+
52
+ export function getSessionsDirForAgent(agentId) {
53
+ if (!agentId || agentId === 'main') return OPENCLAW_SESSIONS_DIR;
54
+ return path.join(HOME, '.openclaw', 'agents', agentId, 'sessions');
55
+ }
56
+
57
+ export function validateAgent(agentId) {
58
+ if (!agentId) return 'main';
59
+ if (!/^[a-zA-Z0-9_-]+$/.test(agentId)) throw new Error('Invalid agent ID');
60
+ const agentDir = path.join(HOME, '.openclaw', 'agents', agentId);
61
+ if (!fs.existsSync(agentDir)) throw new Error(`Agent not found: ${agentId}`);
62
+ return agentId;
63
+ }
@@ -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,64 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { send, sendError, parseBody, uuid } from '../util/http.js';
4
+ import { parseMultipart } from '../util/multipart.js';
5
+
6
+ export class FileController {
7
+ constructor({ getActiveDb, getWorkspaces, uploadsDir, intelligenceDir }) {
8
+ this.getActiveDb = getActiveDb;
9
+ this.getWorkspaces = getWorkspaces;
10
+ this.uploadsDir = uploadsDir;
11
+ this.intelligenceDir = intelligenceDir;
12
+ }
13
+
14
+ async upload(req, res, params) {
15
+ if (!this.getActiveDb().prepare('SELECT id FROM threads WHERE id = ?').get(params.id)) return sendError(res, 404, 'Thread not found');
16
+ const files = await parseMultipart(req);
17
+ const dir = path.join(this.uploadsDir, params.id);
18
+ fs.mkdirSync(dir, { recursive: true });
19
+ const savedFiles = [];
20
+ for (const file of files) {
21
+ const fileId = uuid();
22
+ const ext = path.extname(file.filename) || '';
23
+ fs.writeFileSync(path.join(dir, fileId + ext), file.data);
24
+ savedFiles.push({ id: fileId, filename: file.filename, path: `/api/uploads/${params.id}/${fileId}${ext}`, mimeType: file.mimeType, size: file.data.length });
25
+ }
26
+ send(res, 200, { files: savedFiles });
27
+ }
28
+
29
+ serveUpload(req, res, params) {
30
+ const base = path.join(this.uploadsDir, params.threadId, params.fileId);
31
+ let resolved = base;
32
+ if (!fs.existsSync(resolved)) {
33
+ try {
34
+ const match = fs.readdirSync(path.join(this.uploadsDir, params.threadId)).find(e => e.startsWith(params.fileId.replace(/\.[^.]+$/, '')));
35
+ if (match) resolved = path.join(this.uploadsDir, params.threadId, match);
36
+ } catch { /* ok */ }
37
+ }
38
+ if (!fs.existsSync(resolved)) return sendError(res, 404, 'File not found');
39
+ const MIME = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp', '.pdf': 'application/pdf', '.txt': 'text/plain', '.json': 'application/json' };
40
+ const stat = fs.statSync(resolved);
41
+ res.writeHead(200, { 'Content-Type': MIME[path.extname(resolved).toLowerCase()] || 'application/octet-stream', 'Content-Length': stat.size, 'Cache-Control': 'public, max-age=86400', 'Access-Control-Allow-Origin': '*' });
42
+ fs.createReadStream(resolved).pipe(res);
43
+ }
44
+
45
+ _intelligencePath(threadId) {
46
+ return path.join(this.intelligenceDir, this.getWorkspaces().active, `${threadId}.json`);
47
+ }
48
+
49
+ getIntelligence(req, res, params) {
50
+ const filePath = this._intelligencePath(params.id);
51
+ if (!fs.existsSync(filePath)) return send(res, 200, { versions: [], currentVersion: -1 });
52
+ try { return send(res, 200, JSON.parse(fs.readFileSync(filePath, 'utf8'))); }
53
+ catch { return send(res, 200, { versions: [], currentVersion: -1 }); }
54
+ }
55
+
56
+ async saveIntelligence(req, res, params) {
57
+ const body = await parseBody(req);
58
+ const filePath = this._intelligencePath(params.id);
59
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
60
+ const data = { versions: body.versions || [], currentVersion: body.currentVersion ?? -1, updatedAt: Date.now() };
61
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
62
+ send(res, 200, data);
63
+ }
64
+ }