squad-openclaw 1.0.0 → 2026.2.27

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
@@ -1,803 +1,714 @@
1
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
- }) : x)(function(x) {
4
- if (typeof require !== "undefined") return require.apply(this, arguments);
5
- throw Error('Dynamic require of "' + x + '" is not supported');
6
- });
1
+ // src/agents.ts
2
+ import { execSync } from "child_process";
3
+ import path3 from "path";
7
4
 
8
- // src/entities.ts
9
- import { Type as T } from "@sinclair/typebox";
10
- import Database from "better-sqlite3";
5
+ // src/paths.ts
11
6
  import path from "path";
7
+ import os from "os";
12
8
  import fs from "fs";
13
- var DB_NAME = "squad.db";
14
- var EMBEDDING_DIMENSIONS = 1536;
15
- var EMBEDDING_MODEL = "google/gemini-embedding-001";
16
- var SYNC_INTERVAL_MS = 6e4;
17
- var EntityType = T.Union([
18
- T.Literal("agent"),
19
- T.Literal("skill"),
20
- T.Literal("tool"),
21
- T.Literal("session"),
22
- T.Literal("file"),
23
- T.Literal("directory"),
24
- T.Literal("url"),
25
- T.Literal("memory"),
26
- T.Literal("asset")
27
- ]);
28
- var db;
29
- var vecEnabled = false;
30
- function getDb(configDir) {
31
- if (db) return db;
32
- const pluginDir = path.join(configDir, "extensions", "squad-app");
33
- const dataDir = path.join(pluginDir, "data");
34
- fs.mkdirSync(dataDir, { recursive: true });
35
- const dbPath = path.join(dataDir, DB_NAME);
36
- db = new Database(dbPath);
37
- db.pragma("journal_mode = WAL");
38
- db.pragma("foreign_keys = ON");
39
- try {
40
- const sqliteVec = __require("sqlite-vec");
41
- sqliteVec.load(db);
42
- vecEnabled = true;
43
- } catch {
9
+ function getOpenclawStateDir() {
10
+ if (process.env.OPENCLAW_STATE_DIR) {
11
+ return process.env.OPENCLAW_STATE_DIR;
44
12
  }
45
- runMigrations(db);
46
- return db;
47
- }
48
- function runMigrations(db2) {
49
- db2.exec(`
50
- CREATE TABLE IF NOT EXISTS _schema_version (
51
- version INTEGER PRIMARY KEY
52
- )
53
- `);
54
- const currentVersion = db2.prepare("SELECT MAX(version) as v FROM _schema_version").get()?.v ?? 0;
55
- const migrations = [
56
- {
57
- version: 1,
58
- up: () => {
59
- db2.exec(`
60
- CREATE TABLE IF NOT EXISTS entities (
61
- id TEXT PRIMARY KEY,
62
- type TEXT NOT NULL,
63
- name TEXT NOT NULL,
64
- title TEXT,
65
- description TEXT,
66
- metadata TEXT,
67
- source TEXT NOT NULL DEFAULT 'manual',
68
- source_key TEXT,
69
- created_at INTEGER NOT NULL,
70
- updated_at INTEGER NOT NULL
71
- );
72
- CREATE INDEX IF NOT EXISTS idx_entities_type ON entities(type);
73
- CREATE INDEX IF NOT EXISTS idx_entities_name ON entities(name);
74
- `);
13
+ if (process.env.OPENCLAW_CONFIG_PATH) {
14
+ return path.dirname(process.env.OPENCLAW_CONFIG_PATH);
15
+ }
16
+ const legacyDir = process.env.OPENCLAW_DIR;
17
+ if (legacyDir) {
18
+ const resolvedLegacyDir = path.resolve(legacyDir);
19
+ const configPath = path.join(resolvedLegacyDir, "openclaw.json");
20
+ const hasStateMarkers = fs.existsSync(configPath) || fs.existsSync(path.join(resolvedLegacyDir, "agents")) || fs.existsSync(path.join(resolvedLegacyDir, "workspace"));
21
+ const looksLikeStateDir = resolvedLegacyDir.endsWith(`${path.sep}.openclaw`);
22
+ if (hasStateMarkers || looksLikeStateDir) {
23
+ return resolvedLegacyDir;
24
+ }
25
+ }
26
+ return path.join(os.homedir(), ".openclaw");
27
+ }
28
+
29
+ // src/auth-profiles.ts
30
+ import fs2 from "fs";
31
+ import path2 from "path";
32
+ function getMainAuthProfilesPath(stateDir) {
33
+ return path2.join(stateDir, "agents", "main", "agent", "auth-profiles.json");
34
+ }
35
+ function getAgentAuthProfilesPath(stateDir, agentId) {
36
+ return path2.join(stateDir, "agents", agentId, "agent", "auth-profiles.json");
37
+ }
38
+ function ensureAgentAuthProfiles(agentId) {
39
+ const normalizedAgentId = agentId.trim();
40
+ if (!normalizedAgentId || normalizedAgentId === "main") return false;
41
+ const stateDir = getOpenclawStateDir();
42
+ const sourcePath = getMainAuthProfilesPath(stateDir);
43
+ const targetPath = getAgentAuthProfilesPath(stateDir, normalizedAgentId);
44
+ if (!fs2.existsSync(sourcePath) || fs2.existsSync(targetPath)) return false;
45
+ fs2.mkdirSync(path2.dirname(targetPath), { recursive: true });
46
+ fs2.copyFileSync(sourcePath, targetPath);
47
+ return true;
48
+ }
49
+ function backfillAgentAuthProfiles() {
50
+ const stateDir = getOpenclawStateDir();
51
+ const agentsDir = path2.join(stateDir, "agents");
52
+ if (!fs2.existsSync(agentsDir)) return [];
53
+ const copied = [];
54
+ for (const entry of fs2.readdirSync(agentsDir, { withFileTypes: true })) {
55
+ if (!entry.isDirectory()) continue;
56
+ const agentId = entry.name;
57
+ if (ensureAgentAuthProfiles(agentId)) {
58
+ copied.push(agentId);
59
+ }
60
+ }
61
+ return copied;
62
+ }
63
+
64
+ // src/agents.ts
65
+ function deriveAgentIdFromName(name) {
66
+ const normalized = name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
67
+ return normalized || "agent";
68
+ }
69
+ function registerAgentMethods(api) {
70
+ const callGateway = async (ctx, method, params = {}) => {
71
+ const ctxRequest = ctx.request;
72
+ if (typeof ctxRequest === "function") return ctxRequest(method, params);
73
+ const apiRequest = api?.request;
74
+ if (typeof apiRequest === "function") return apiRequest(method, params);
75
+ const apiCallGatewayMethod = api?.callGatewayMethod;
76
+ if (typeof apiCallGatewayMethod === "function") return apiCallGatewayMethod(method, params);
77
+ throw new Error("Gateway method invocation API unavailable in plugin context");
78
+ };
79
+ api.registerGatewayMethod(
80
+ "squad.agents.add",
81
+ async ({ params, respond }) => {
82
+ const name = params?.name;
83
+ const agentId = params?.agentId;
84
+ const workspace = params?.workspace;
85
+ const model = params?.model;
86
+ if (!name || typeof name !== "string" || !name.trim()) {
87
+ respond(false, { error: "Missing or empty 'name' parameter" });
88
+ return;
75
89
  }
76
- },
77
- {
78
- version: 3,
79
- up: () => {
80
- db2.exec(`
81
- CREATE VIRTUAL TABLE IF NOT EXISTS search_fts USING fts5(
82
- entity_id UNINDEXED,
83
- entity_type UNINDEXED,
84
- name,
85
- title,
86
- description,
87
- content,
88
- tokenize='porter unicode61'
89
- );
90
- `);
90
+ const safeName = name.trim();
91
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9 _-]*$/.test(safeName)) {
92
+ respond(false, { error: "Agent name must start with a letter/number and contain only letters, numbers, spaces, hyphens, or underscores" });
93
+ return;
91
94
  }
92
- },
93
- {
94
- version: 4,
95
- up: () => {
96
- if (!vecEnabled) return;
97
- db2.exec(`
98
- CREATE VIRTUAL TABLE IF NOT EXISTS search_vec USING vec0(
99
- entity_id TEXT PRIMARY KEY,
100
- embedding float[${EMBEDDING_DIMENSIONS}]
101
- );
102
- `);
95
+ const providedAgentId = typeof agentId === "string" ? agentId.trim() : "";
96
+ if (providedAgentId && !/^[a-z0-9][a-z0-9-]*$/.test(providedAgentId)) {
97
+ respond(false, { error: "Invalid agentId format" });
98
+ return;
103
99
  }
104
- }
105
- ];
106
- const pending = migrations.filter((m) => m.version > currentVersion);
107
- if (pending.length === 0) return;
108
- const migrate = db2.transaction(() => {
109
- for (const m of pending) {
110
- m.up();
111
- db2.prepare("INSERT INTO _schema_version (version) VALUES (?)").run(
112
- m.version
100
+ const effectiveAgentId = providedAgentId || deriveAgentIdFromName(safeName);
101
+ const defaultWorkspace = path3.join(
102
+ getOpenclawStateDir(),
103
+ effectiveAgentId === "main" ? "workspace" : `workspace-${effectiveAgentId}`
113
104
  );
105
+ const workspacePath = typeof workspace === "string" && workspace.trim() ? workspace.trim() : defaultWorkspace;
106
+ try {
107
+ let cmd = `openclaw agents add ${JSON.stringify(safeName)} --non-interactive --workspace ${JSON.stringify(workspacePath)}`;
108
+ if (model) {
109
+ cmd += ` --model ${JSON.stringify(model)}`;
110
+ }
111
+ const output = execSync(cmd, {
112
+ timeout: 3e4,
113
+ encoding: "utf-8",
114
+ stdio: ["pipe", "pipe", "pipe"]
115
+ });
116
+ try {
117
+ ensureAgentAuthProfiles(effectiveAgentId);
118
+ } catch (authErr) {
119
+ console.warn("[squad.agents.add] failed to copy main auth profile to agent:", authErr);
120
+ }
121
+ respond(true, { ok: true, output: output.slice(0, 1e3) });
122
+ } catch (err2) {
123
+ const msg = err2 instanceof Error ? err2.message : String(err2);
124
+ const stderr = err2?.stderr;
125
+ respond(false, {
126
+ error: `Failed to add agent: ${stderr || msg}`.slice(0, 500)
127
+ });
128
+ }
114
129
  }
115
- });
116
- migrate();
130
+ );
131
+ api.registerGatewayMethod(
132
+ "squad.agents.delete",
133
+ async ({ params, respond }) => {
134
+ const agentId = params?.agentId;
135
+ if (!agentId || typeof agentId !== "string" || !agentId.trim()) {
136
+ respond(false, { error: "Missing or empty 'agentId' parameter" });
137
+ return;
138
+ }
139
+ if (agentId === "main") {
140
+ respond(false, { error: "Cannot delete the main agent" });
141
+ return;
142
+ }
143
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(agentId)) {
144
+ respond(false, { error: "Invalid agent ID format" });
145
+ return;
146
+ }
147
+ try {
148
+ const output = execSync(
149
+ `openclaw agents delete ${JSON.stringify(agentId)} --non-interactive 2>&1`,
150
+ { timeout: 3e4, encoding: "utf-8" }
151
+ );
152
+ respond(true, { ok: true, output: output.slice(0, 1e3) });
153
+ } catch (err2) {
154
+ const msg = err2 instanceof Error ? err2.message : String(err2);
155
+ const stderr = err2?.stderr;
156
+ respond(false, {
157
+ error: `Failed to delete agent: ${stderr || msg}`.slice(0, 500)
158
+ });
159
+ }
160
+ }
161
+ );
162
+ api.registerGatewayMethod(
163
+ "squad.agents.set-identity",
164
+ async (ctx) => {
165
+ const { params, respond } = ctx;
166
+ const agentId = params?.agentId;
167
+ const name = params?.name;
168
+ const emoji = params?.emoji;
169
+ const theme = params?.theme;
170
+ if (!agentId || typeof agentId !== "string") {
171
+ respond(false, { error: "Missing 'agentId' parameter" });
172
+ return;
173
+ }
174
+ const identity = {};
175
+ const trimmedName = typeof name === "string" ? name.trim() : "";
176
+ const trimmedEmoji = typeof emoji === "string" ? emoji.trim() : "";
177
+ const trimmedTheme = typeof theme === "string" ? theme.trim() : "";
178
+ if (trimmedName) identity.name = trimmedName;
179
+ if (trimmedEmoji) identity.emoji = trimmedEmoji;
180
+ if (trimmedTheme) identity.theme = trimmedTheme;
181
+ if (Object.keys(identity).length === 0) {
182
+ respond(false, { error: "No identity fields provided (name, emoji, or theme)" });
183
+ return;
184
+ }
185
+ try {
186
+ const doPatch = async (baseHash) => {
187
+ await callGateway(ctx, "config.patch", {
188
+ ...baseHash ? { baseHash } : {},
189
+ raw: JSON.stringify({
190
+ agents: {
191
+ list: [{ id: agentId, identity }]
192
+ }
193
+ })
194
+ });
195
+ };
196
+ let snapshot = await callGateway(ctx, "config.get", {});
197
+ try {
198
+ await doPatch(snapshot?.hash);
199
+ } catch (firstErr) {
200
+ const msg = firstErr instanceof Error ? firstErr.message : String(firstErr);
201
+ if (!/config changed since last load/i.test(msg)) throw firstErr;
202
+ snapshot = await callGateway(ctx, "config.get", {});
203
+ await doPatch(snapshot?.hash);
204
+ }
205
+ respond(true, { ok: true, identity });
206
+ } catch (err2) {
207
+ const msg = err2 instanceof Error ? err2.message : String(err2);
208
+ respond(false, {
209
+ error: `Failed to set identity: ${msg}`.slice(0, 500)
210
+ });
211
+ }
212
+ }
213
+ );
214
+ api.registerGatewayMethod(
215
+ "squad.agents.patch-config",
216
+ async (ctx) => {
217
+ const { params, respond } = ctx;
218
+ const agentId = params?.agentId;
219
+ const fields = params?.fields ?? {};
220
+ if (!agentId || typeof agentId !== "string") {
221
+ respond(false, { error: "Missing 'agentId' parameter" });
222
+ return;
223
+ }
224
+ const allowedFieldNames = /* @__PURE__ */ new Set(["tools", "skills", "default", "model"]);
225
+ const filteredFields = {};
226
+ for (const [k, v] of Object.entries(fields)) {
227
+ if (allowedFieldNames.has(k) && v !== void 0) filteredFields[k] = v;
228
+ }
229
+ if (Object.keys(filteredFields).length === 0) {
230
+ respond(false, { error: "No patchable fields provided (tools, skills, default, model)" });
231
+ return;
232
+ }
233
+ try {
234
+ const doPatch = async (baseHash) => {
235
+ await callGateway(ctx, "config.patch", {
236
+ ...baseHash ? { baseHash } : {},
237
+ raw: JSON.stringify({
238
+ agents: {
239
+ list: [{ id: agentId, ...filteredFields }]
240
+ }
241
+ })
242
+ });
243
+ };
244
+ let snapshot = await callGateway(ctx, "config.get", {});
245
+ try {
246
+ await doPatch(snapshot?.hash);
247
+ } catch (firstErr) {
248
+ const msg = firstErr instanceof Error ? firstErr.message : String(firstErr);
249
+ if (!/config changed since last load/i.test(msg)) throw firstErr;
250
+ snapshot = await callGateway(ctx, "config.get", {});
251
+ await doPatch(snapshot?.hash);
252
+ }
253
+ respond(true, { ok: true, fields: filteredFields });
254
+ } catch (err2) {
255
+ const msg = err2 instanceof Error ? err2.message : String(err2);
256
+ respond(false, {
257
+ error: `Failed to patch agent config: ${msg}`.slice(0, 500)
258
+ });
259
+ }
260
+ }
261
+ );
117
262
  }
118
- function entityRow(row) {
119
- return {
120
- ...row,
121
- metadata: row.metadata ? JSON.parse(row.metadata) : {}
122
- };
263
+
264
+ // src/entities.ts
265
+ import { Type as T } from "@sinclair/typebox";
266
+ import path7 from "path";
267
+ import fs6 from "fs";
268
+
269
+ // src/watcher.ts
270
+ import path4 from "path";
271
+ import fs3 from "fs";
272
+ import chokidar from "chokidar";
273
+ var debounceTimers = /* @__PURE__ */ new Map();
274
+ var DEBOUNCE_MS = 500;
275
+ function debounced(key, fn) {
276
+ const existing = debounceTimers.get(key);
277
+ if (existing) clearTimeout(existing);
278
+ debounceTimers.set(
279
+ key,
280
+ setTimeout(() => {
281
+ debounceTimers.delete(key);
282
+ fn();
283
+ }, DEBOUNCE_MS)
284
+ );
123
285
  }
124
- function ftsUpsert(db2, entityId, entityType, name, title, description, content) {
125
- ftsDelete(db2, entityId);
126
- db2.prepare(
127
- `INSERT INTO search_fts (entity_id, entity_type, name, title, description, content)
128
- VALUES (?, ?, ?, ?, ?, ?)`
129
- ).run(entityId, entityType, name, title ?? "", description ?? "", content);
130
- }
131
- function ftsDelete(db2, entityId) {
132
- const existing = db2.prepare("SELECT * FROM search_fts WHERE entity_id = ?").get(entityId);
133
- if (existing) {
134
- db2.prepare(
135
- `INSERT INTO search_fts (search_fts, entity_id, entity_type, name, title, description, content)
136
- VALUES ('delete', ?, ?, ?, ?, ?, ?)`
137
- ).run(
138
- existing.entity_id,
139
- existing.entity_type,
140
- existing.name,
141
- existing.title,
142
- existing.description,
143
- existing.content
144
- );
145
- }
286
+ var fsDebounceTimers = /* @__PURE__ */ new Map();
287
+ var FS_DEBOUNCE_MS = 300;
288
+ function debouncedFs(relPath, action, fn) {
289
+ const key = `fs:${action}:${relPath}`;
290
+ const existing = fsDebounceTimers.get(key);
291
+ if (existing) clearTimeout(existing);
292
+ fsDebounceTimers.set(
293
+ key,
294
+ setTimeout(() => {
295
+ fsDebounceTimers.delete(key);
296
+ fn();
297
+ }, FS_DEBOUNCE_MS)
298
+ );
299
+ }
300
+ function isWorkspaceIdentity(filePath, configDir) {
301
+ const rel = path4.relative(configDir, filePath);
302
+ const match = rel.match(/^(workspace(?:-([^/]+))?)\/IDENTITY\.md$/);
303
+ if (!match) return null;
304
+ const dirName = match[1];
305
+ const agentId = match[2] ?? "main";
306
+ return { agentId, workspacePath: path4.join(configDir, dirName) };
307
+ }
308
+ function isWorkspaceAgentJson(filePath, configDir) {
309
+ const rel = path4.relative(configDir, filePath);
310
+ const match = rel.match(/^(workspace(?:-([^/]+))?)\/agent\.json$/);
311
+ if (!match) return null;
312
+ const dirName = match[1];
313
+ const agentId = match[2] ?? "main";
314
+ return { agentId, workspacePath: path4.join(configDir, dirName) };
315
+ }
316
+ function isGlobalSkillDir(filePath, configDir) {
317
+ const rel = path4.relative(configDir, filePath);
318
+ const match = rel.match(/^skills\/([^/]+)\/?$/);
319
+ if (!match) return null;
320
+ return { skillKey: match[1] };
146
321
  }
147
- function ftsSearch(db2, query, entityType, limit = 20) {
148
- const safeQuery = query.replace(/[*"(){}[\]^~\\:]/g, " ").trim().split(/\s+/).filter(Boolean).map((term) => `"${term}"*`).join(" OR ");
149
- if (!safeQuery) return [];
150
- let sql = `
151
- SELECT e.*, search_fts.rank
152
- FROM search_fts
153
- JOIN entities e ON e.id = search_fts.entity_id
154
- WHERE search_fts MATCH ?
155
- `;
156
- const params = [safeQuery];
157
- if (entityType) {
158
- sql += ` AND search_fts.entity_type = ?`;
159
- params.push(entityType);
160
- }
161
- sql += ` ORDER BY search_fts.rank LIMIT ?`;
162
- params.push(limit);
163
- return db2.prepare(sql).all(...params).map(entityRow);
164
- }
165
- function vecUpsert(db2, entityId, embedding) {
166
- if (!vecEnabled) return;
167
- db2.prepare("DELETE FROM search_vec WHERE entity_id = ?").run(entityId);
168
- const buffer = embedding instanceof Float32Array ? Buffer.from(embedding.buffer) : Buffer.from(new Float32Array(embedding).buffer);
169
- db2.prepare("INSERT INTO search_vec (entity_id, embedding) VALUES (?, ?)").run(
170
- entityId,
171
- buffer
322
+ function isWorkspaceSkillDir(filePath, configDir) {
323
+ const rel = path4.relative(configDir, filePath);
324
+ const match = rel.match(
325
+ /^workspace(?:-([^/]+))?\/skills\/([^/]+)\/?$/
172
326
  );
327
+ if (!match) return null;
328
+ return { agentId: match[1] ?? "main", skillKey: match[2] };
173
329
  }
174
- function vecSearch(db2, embedding, limit = 20) {
175
- if (!vecEnabled) return [];
176
- const buffer = embedding instanceof Float32Array ? Buffer.from(embedding.buffer) : Buffer.from(new Float32Array(embedding).buffer);
177
- const rows = db2.prepare(
178
- `SELECT entity_id, distance
179
- FROM search_vec
180
- WHERE embedding MATCH ?
181
- ORDER BY distance
182
- LIMIT ?`
183
- ).all(buffer, limit);
184
- if (rows.length === 0) return [];
185
- const ids = rows.map((r) => r.entity_id);
186
- const placeholders = ids.map(() => "?").join(",");
187
- const entities = db2.prepare(`SELECT * FROM entities WHERE id IN (${placeholders})`).all(...ids).map(entityRow);
188
- const entityMap = new Map(entities.map((e) => [e.id, e]));
189
- return rows.map((r) => {
190
- const entity = entityMap.get(r.entity_id);
191
- if (!entity) return null;
192
- return { ...entity, _distance: r.distance };
193
- }).filter(Boolean);
194
- }
195
- async function generateEmbedding(text, configDir) {
196
- const config = readEmbeddingsConfig(configDir);
197
- if (!config) return null;
198
- const response = await fetch(`${config.apiUrl}/embeddings`, {
199
- method: "POST",
200
- headers: {
201
- "Content-Type": "application/json",
202
- Authorization: `Bearer ${config.apiKey}`
203
- },
204
- body: JSON.stringify({
205
- model: config.model,
206
- input: text,
207
- dimensions: config.dimensions
208
- })
209
- });
210
- if (!response.ok) {
211
- const err2 = await response.text();
212
- throw new Error(`Embedding API error (${response.status}): ${err2}`);
213
- }
214
- const data = await response.json();
215
- return new Float32Array(data.data[0].embedding);
330
+ function isPluginManifest(filePath, configDir) {
331
+ const rel = path4.relative(configDir, filePath);
332
+ const match = rel.match(/^extensions\/([^/]+)\/openclaw\.plugin\.json$/);
333
+ if (!match) return null;
334
+ return { pluginDirName: match[1] };
335
+ }
336
+ function isOpenClawConfig(filePath, configDir) {
337
+ return path4.relative(configDir, filePath) === "openclaw.json";
216
338
  }
217
- function readEmbeddingsConfig(configDir) {
339
+ function updateAgent(agentId, workspacePath) {
340
+ const now = Date.now();
341
+ let name = agentId;
342
+ const metadata = { workspacePath };
218
343
  try {
219
- const raw = fs.readFileSync(
220
- path.join(configDir, "openclaw.json"),
344
+ const content = fs3.readFileSync(
345
+ path4.join(workspacePath, "IDENTITY.md"),
221
346
  "utf-8"
222
347
  );
223
- const config = JSON.parse(raw);
224
- const emb = config.embeddings;
225
- if (!emb?.apiUrl || !emb?.apiKey) return null;
226
- return {
227
- apiUrl: emb.apiUrl,
228
- apiKey: emb.apiKey,
229
- model: emb.model ?? EMBEDDING_MODEL,
230
- dimensions: emb.dimensions ?? EMBEDDING_DIMENSIONS
231
- };
348
+ const parsed = parseIdentityName(content);
349
+ if (parsed) name = parsed;
232
350
  } catch {
233
- return null;
234
351
  }
352
+ if (name === agentId) {
353
+ try {
354
+ const raw = fs3.readFileSync(
355
+ path4.join(workspacePath, "agent.json"),
356
+ "utf-8"
357
+ );
358
+ const config = JSON.parse(raw);
359
+ if (config.displayName) name = config.displayName;
360
+ if (config.model) metadata.model = config.model;
361
+ } catch {
362
+ }
363
+ }
364
+ registrySet({
365
+ id: agentId,
366
+ type: "agent",
367
+ name,
368
+ title: name,
369
+ description: null,
370
+ metadata,
371
+ source: "filesystem",
372
+ source_key: workspacePath,
373
+ created_at: now,
374
+ updated_at: now
375
+ });
235
376
  }
236
- function buildEmbeddingText(entity) {
237
- const parts = [entity.type + ":", entity.name];
238
- if (entity.title) parts.push(entity.title);
239
- if (entity.description) parts.push(entity.description);
240
- return parts.join(" ");
241
- }
242
- function syncEntitiesFromFilesystem(db2, configDir) {
243
- let synced = 0;
244
- let removed = 0;
377
+ function updatePlugin(pluginDirName, configDir) {
245
378
  const now = Date.now();
246
- const upsert = db2.prepare(`
247
- INSERT INTO entities (id, type, name, title, description, metadata, source, source_key, created_at, updated_at)
248
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
249
- ON CONFLICT(id) DO UPDATE SET
250
- name = excluded.name,
251
- title = excluded.title,
252
- description = excluded.description,
253
- metadata = excluded.metadata,
254
- updated_at = excluded.updated_at
255
- `);
256
- const agentsDir = path.join(configDir, "agents");
257
- const seenAgentIds = /* @__PURE__ */ new Set();
258
- if (fs.existsSync(agentsDir)) {
259
- const entries = fs.readdirSync(agentsDir, { withFileTypes: true });
260
- for (const entry of entries) {
261
- if (!entry.isDirectory()) continue;
262
- const agentId = entry.name;
263
- seenAgentIds.add(agentId);
264
- let name = agentId;
265
- let description = "";
266
- const metadata = {};
267
- const soulPath = path.join(agentsDir, agentId, "SOUL.md");
268
- if (fs.existsSync(soulPath)) {
269
- try {
270
- const content = fs.readFileSync(soulPath, "utf-8");
271
- const firstLine = content.split("\n")[0]?.replace(/^#\s*/, "").trim();
272
- if (firstLine) name = firstLine;
273
- description = content.slice(0, 500);
274
- metadata.soul_content = content;
275
- } catch {
276
- }
277
- }
278
- upsert.run(
279
- agentId,
280
- "agent",
281
- name,
282
- name,
283
- description,
284
- JSON.stringify(metadata),
285
- "filesystem",
286
- soulPath,
287
- now,
288
- now
379
+ const manifestPath = path4.join(
380
+ configDir,
381
+ "extensions",
382
+ pluginDirName,
383
+ "openclaw.plugin.json"
384
+ );
385
+ try {
386
+ const raw = fs3.readFileSync(manifestPath, "utf-8");
387
+ const manifest = JSON.parse(raw);
388
+ const pluginId = manifest.id || pluginDirName;
389
+ const name = manifest.name || pluginId;
390
+ registrySet({
391
+ id: `plugin:${pluginId}`,
392
+ type: "plugin",
393
+ name,
394
+ title: name,
395
+ description: manifest.description || null,
396
+ metadata: { pluginId, pluginDir: path4.dirname(manifestPath) },
397
+ source: "filesystem",
398
+ source_key: manifestPath,
399
+ created_at: now,
400
+ updated_at: now
401
+ });
402
+ } catch {
403
+ registryDelete(`plugin:${pluginDirName}`);
404
+ }
405
+ }
406
+ function startWatcher(configDir, onFsChange) {
407
+ const watcher = chokidar.watch(configDir, {
408
+ persistent: true,
409
+ usePolling: false,
410
+ ignoreInitial: true,
411
+ awaitWriteFinish: { stabilityThreshold: 300 },
412
+ depth: 4,
413
+ ignored: [
414
+ // Ignore heavy directories that aren't relevant
415
+ "**/node_modules/**",
416
+ "**/dist/**",
417
+ "**/.git/**",
418
+ "**/data/**"
419
+ ]
420
+ });
421
+ const emitFsChange = (action, filePath) => {
422
+ if (!onFsChange) return;
423
+ const rel = path4.relative(configDir, filePath);
424
+ debouncedFs(rel, action, () => {
425
+ onFsChange({ action, path: rel });
426
+ });
427
+ };
428
+ const handleChange = (filePath, action) => {
429
+ emitFsChange(action, filePath);
430
+ const identity = isWorkspaceIdentity(filePath, configDir);
431
+ if (identity) {
432
+ debounced(
433
+ `agent:${identity.agentId}`,
434
+ () => updateAgent(identity.agentId, identity.workspacePath)
289
435
  );
290
- ftsUpsert(db2, agentId, "agent", name, name, description, "");
291
- synced++;
436
+ return;
292
437
  }
293
- }
294
- const staleAgents = db2.prepare(
295
- "SELECT id FROM entities WHERE type = 'agent' AND source = 'filesystem' AND id NOT IN (" + Array.from(seenAgentIds).map(() => "?").join(",") + ")"
296
- ).all(...Array.from(seenAgentIds));
297
- if (seenAgentIds.size > 0) {
298
- for (const stale of staleAgents) {
299
- db2.prepare("DELETE FROM entities WHERE id = ?").run(stale.id);
300
- ftsDelete(db2, stale.id);
301
- removed++;
438
+ const agentJson = isWorkspaceAgentJson(filePath, configDir);
439
+ if (agentJson) {
440
+ debounced(
441
+ `agent:${agentJson.agentId}`,
442
+ () => updateAgent(agentJson.agentId, agentJson.workspacePath)
443
+ );
444
+ return;
302
445
  }
303
- }
304
- try {
305
- const raw = fs.readFileSync(
306
- path.join(configDir, "openclaw.json"),
307
- "utf-8"
308
- );
309
- const config = JSON.parse(raw);
310
- const allowedTools = config?.tools?.allow ?? [];
311
- for (const toolName of allowedTools) {
312
- const toolId = `tool:${toolName}`;
313
- upsert.run(
314
- toolId,
315
- "tool",
316
- toolName,
317
- toolName,
318
- null,
319
- JSON.stringify({ tool_name: toolName }),
320
- "filesystem",
321
- "openclaw.json:tools.allow",
322
- now,
323
- now
446
+ const plugin = isPluginManifest(filePath, configDir);
447
+ if (plugin) {
448
+ debounced(
449
+ `plugin:${plugin.pluginDirName}`,
450
+ () => updatePlugin(plugin.pluginDirName, configDir)
324
451
  );
325
- ftsUpsert(db2, toolId, "tool", toolName, toolName, "", "");
326
- synced++;
452
+ return;
453
+ }
454
+ if (isOpenClawConfig(filePath, configDir)) {
455
+ debounced("tools", () => scanTools(configDir));
456
+ return;
457
+ }
458
+ };
459
+ const handleAddDir = (dirPath) => {
460
+ emitFsChange("addDir", dirPath);
461
+ const globalSkill = isGlobalSkillDir(dirPath, configDir);
462
+ if (globalSkill) {
463
+ debounced(
464
+ `skill:${globalSkill.skillKey}`,
465
+ () => scanSkills(configDir)
466
+ );
467
+ return;
468
+ }
469
+ const wsSkill = isWorkspaceSkillDir(dirPath, configDir);
470
+ if (wsSkill) {
471
+ debounced(
472
+ `skill:${wsSkill.agentId}:${wsSkill.skillKey}`,
473
+ () => scanSkills(configDir)
474
+ );
475
+ return;
476
+ }
477
+ const rel = path4.relative(configDir, dirPath);
478
+ if (/^workspace(-[^/]+)?$/.test(rel)) {
479
+ debounced("agents", () => scanAgents(configDir));
480
+ return;
481
+ }
482
+ };
483
+ const handleUnlinkDir = (dirPath) => {
484
+ emitFsChange("unlinkDir", dirPath);
485
+ const rel = path4.relative(configDir, dirPath);
486
+ const wsMatch = rel.match(/^workspace(?:-([^/]+))?$/);
487
+ if (wsMatch) {
488
+ const agentId = wsMatch[1] ?? "main";
489
+ registryDelete(agentId);
490
+ return;
491
+ }
492
+ const globalSkill = isGlobalSkillDir(dirPath, configDir);
493
+ if (globalSkill) {
494
+ registryDelete(`skill:${globalSkill.skillKey}`);
495
+ return;
496
+ }
497
+ const wsSkill = isWorkspaceSkillDir(dirPath, configDir);
498
+ if (wsSkill) {
499
+ registryDelete(`skill:${wsSkill.agentId}:${wsSkill.skillKey}`);
500
+ return;
501
+ }
502
+ };
503
+ watcher.on("add", (fp) => handleChange(fp, "add"));
504
+ watcher.on("change", (fp) => handleChange(fp, "change"));
505
+ watcher.on("unlink", (fp) => handleChange(fp, "unlink"));
506
+ watcher.on("addDir", handleAddDir);
507
+ watcher.on("unlinkDir", handleUnlinkDir);
508
+ return () => {
509
+ for (const timer of debounceTimers.values()) {
510
+ clearTimeout(timer);
511
+ }
512
+ debounceTimers.clear();
513
+ for (const timer of fsDebounceTimers.values()) {
514
+ clearTimeout(timer);
327
515
  }
516
+ fsDebounceTimers.clear();
517
+ watcher.close();
518
+ };
519
+ }
520
+
521
+ // src/filesystem.ts
522
+ import fs5 from "fs";
523
+ import path6 from "path";
524
+
525
+ // src/layout.ts
526
+ import fs4 from "fs";
527
+ import path5 from "path";
528
+ function resolveMaybeRelativePath(stateDir, p) {
529
+ if (path5.isAbsolute(p)) return path5.resolve(p);
530
+ return path5.resolve(stateDir, p);
531
+ }
532
+ function listWorkspaceFallbacks(stateDir) {
533
+ let entries;
534
+ try {
535
+ entries = fs4.readdirSync(stateDir, { withFileTypes: true });
328
536
  } catch {
537
+ return [];
329
538
  }
330
- const seedPath = path.join(
331
- configDir,
332
- "extensions",
333
- "squad-app",
334
- "data",
335
- "search-seed.json"
336
- );
539
+ return entries.filter((entry) => entry.isDirectory() && (entry.name === "workspace" || entry.name.startsWith("workspace-"))).map((entry) => {
540
+ const agentId = entry.name === "workspace" ? "main" : entry.name.replace("workspace-", "");
541
+ const workspacePath = path5.join(stateDir, entry.name);
542
+ return {
543
+ agentId,
544
+ path: workspacePath,
545
+ source: "filesystem",
546
+ exists: true
547
+ };
548
+ });
549
+ }
550
+ function readOpenclawConfig(configPath) {
337
551
  try {
338
- if (fs.existsSync(seedPath)) {
339
- const seedRaw = fs.readFileSync(seedPath, "utf-8");
340
- const seedEntities = JSON.parse(seedRaw);
341
- for (const entity of seedEntities) {
342
- upsert.run(
343
- entity.id,
344
- entity.type,
345
- entity.name,
346
- entity.title,
347
- entity.description,
348
- JSON.stringify(entity.metadata ?? {}),
349
- entity.source ?? "gateway",
350
- entity.sourceKey ?? null,
351
- now,
352
- now
353
- );
354
- ftsUpsert(
355
- db2,
356
- entity.id,
357
- entity.type,
358
- entity.name,
359
- entity.title,
360
- entity.description,
361
- ""
362
- );
363
- synced++;
364
- }
365
- }
552
+ const raw = fs4.readFileSync(configPath, "utf-8");
553
+ return JSON.parse(raw);
366
554
  } catch {
555
+ return null;
367
556
  }
368
- return { synced, removed };
369
557
  }
370
- function hybridSearch(db2, ftsResults, vecResults, limit) {
371
- const k = 60;
372
- const scores = /* @__PURE__ */ new Map();
373
- for (let i = 0; i < ftsResults.length; i++) {
374
- const entity = ftsResults[i];
375
- const rrf = 1 / (k + i + 1);
376
- const existing = scores.get(entity.id);
377
- if (existing) {
378
- existing.score += rrf;
379
- } else {
380
- scores.set(entity.id, { score: rrf, entity });
558
+ function resolveGatewayLayout() {
559
+ const stateDir = getOpenclawStateDir();
560
+ const configPath = path5.join(stateDir, "openclaw.json");
561
+ const config = readOpenclawConfig(configPath);
562
+ const workspaces = [];
563
+ if (config?.agents?.main?.workspace || config?.agents?.main?.workspacePath) {
564
+ const rawPath = config.agents.main.workspace ?? config.agents.main.workspacePath;
565
+ if (rawPath) {
566
+ const resolvedPath = resolveMaybeRelativePath(stateDir, rawPath);
567
+ workspaces.push({
568
+ agentId: "main",
569
+ path: resolvedPath,
570
+ source: "config",
571
+ exists: fs4.existsSync(resolvedPath)
572
+ });
381
573
  }
382
574
  }
383
- for (let i = 0; i < vecResults.length; i++) {
384
- const entity = vecResults[i];
385
- const rrf = 1 / (k + i + 1);
386
- const existing = scores.get(entity.id);
387
- if (existing) {
388
- existing.score += rrf;
389
- } else {
390
- scores.set(entity.id, { score: rrf, entity });
575
+ for (const agent of config?.agents?.list ?? []) {
576
+ const agentId = typeof agent.id === "string" && agent.id.trim() ? agent.id : null;
577
+ const rawPath = agent.workspace ?? agent.workspacePath;
578
+ if (!agentId || !rawPath) continue;
579
+ const resolvedPath = resolveMaybeRelativePath(stateDir, rawPath);
580
+ workspaces.push({
581
+ agentId,
582
+ path: resolvedPath,
583
+ source: "config",
584
+ exists: fs4.existsSync(resolvedPath)
585
+ });
586
+ }
587
+ const deduped = /* @__PURE__ */ new Map();
588
+ for (const ws of [...workspaces, ...listWorkspaceFallbacks(stateDir)]) {
589
+ if (!deduped.has(ws.agentId)) {
590
+ deduped.set(ws.agentId, ws);
391
591
  }
392
592
  }
393
- return Array.from(scores.values()).sort((a, b) => b.score - a.score).slice(0, limit).map((s) => ({ ...s.entity, _rrf_score: s.score }));
593
+ const resolvedWorkspaces = Array.from(deduped.values());
594
+ const defaultFileBrowserRoot = stateDir;
595
+ return {
596
+ stateDir,
597
+ configPath,
598
+ mediaDir: path5.join(stateDir, "media"),
599
+ skillsDir: path5.join(stateDir, "skills"),
600
+ extensionsDir: path5.join(stateDir, "extensions"),
601
+ defaultFileBrowserRoot,
602
+ workspaces: resolvedWorkspaces
603
+ };
394
604
  }
395
- function registerEntityTools(api) {
396
- let syncTimer = null;
397
- api.registerTool({
398
- name: "entity_upsert",
399
- description: "Register or update an entity in the unified entity registry. Makes the entity searchable and resolvable via {{type:id}} references.",
400
- parameters: T.Object({
401
- id: T.String({ description: "Unique entity ID" }),
402
- type: EntityType,
403
- name: T.String({ description: "Primary display name" }),
404
- title: T.Optional(T.String({ description: "Longer title" })),
405
- description: T.Optional(T.String({ description: "Description text" })),
406
- metadata: T.Optional(
407
- T.Any({ description: "JSON object with type-specific data" })
408
- ),
409
- source: T.Optional(
410
- T.String({
411
- description: "Origin: gateway, filesystem, plugin, manual"
412
- })
413
- ),
414
- sourceKey: T.Optional(
415
- T.String({ description: "Source-specific identifier" })
416
- ),
417
- content: T.Optional(
418
- T.String({
419
- description: "Additional searchable text content (e.g., file contents, memory text)"
420
- })
421
- )
422
- }),
423
- async execute(_id, params, _ctx) {
424
- const configDir2 = _ctx?.configDir ?? process.env.HOME + "/.openclaw";
425
- const database = getDb(configDir2);
426
- const now = Date.now();
427
- const existing = database.prepare("SELECT created_at FROM entities WHERE id = ?").get(params.id);
428
- const createdAt = existing?.created_at ?? now;
429
- database.prepare(
430
- `INSERT INTO entities (id, type, name, title, description, metadata, source, source_key, created_at, updated_at)
431
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
432
- ON CONFLICT(id) DO UPDATE SET
433
- type = excluded.type,
434
- name = excluded.name,
435
- title = excluded.title,
436
- description = excluded.description,
437
- metadata = excluded.metadata,
438
- source = excluded.source,
439
- source_key = excluded.source_key,
440
- updated_at = excluded.updated_at`
441
- ).run(
442
- params.id,
443
- params.type,
444
- params.name,
445
- params.title ?? null,
446
- params.description ?? null,
447
- params.metadata ? JSON.stringify(params.metadata) : null,
448
- params.source ?? "manual",
449
- params.sourceKey ?? null,
450
- createdAt,
451
- now
452
- );
453
- ftsUpsert(
454
- database,
455
- params.id,
456
- params.type,
457
- params.name,
458
- params.title ?? null,
459
- params.description ?? null,
460
- params.content ?? ""
461
- );
462
- const entity = database.prepare("SELECT * FROM entities WHERE id = ?").get(params.id);
463
- return {
464
- content: [{ type: "text", text: JSON.stringify(entityRow(entity)) }]
465
- };
605
+
606
+ // src/filesystem.ts
607
+ var HOME_DIR = process.env.HOME ?? "/root";
608
+ var OPENCLAW_DIR = getOpenclawStateDir();
609
+ var SENSITIVE_BLOCKED_DIRS = [
610
+ path6.join(OPENCLAW_DIR, "credentials"),
611
+ path6.join(OPENCLAW_DIR, "devices"),
612
+ path6.join(OPENCLAW_DIR, "identity")
613
+ ];
614
+ var SENSITIVE_BLOCKED_FILES = [
615
+ path6.join(OPENCLAW_DIR, "squad-ceo-data", "relay", "squad-relay.json")
616
+ ];
617
+ function isSensitivePath(resolvedPath) {
618
+ for (const blocked of SENSITIVE_BLOCKED_DIRS) {
619
+ if (resolvedPath === blocked || resolvedPath.startsWith(blocked + path6.sep)) {
620
+ return true;
466
621
  }
467
- });
468
- api.registerTool({
469
- name: "entity_get",
470
- description: "Get a single entity by ID with full details. Used for reference resolution.",
471
- parameters: T.Object({
472
- entityId: T.String({ description: "The entity ID to retrieve" })
473
- }),
474
- async execute(_id, params, _ctx) {
475
- const configDir2 = _ctx?.configDir ?? process.env.HOME + "/.openclaw";
476
- const database = getDb(configDir2);
477
- const entity = database.prepare("SELECT * FROM entities WHERE id = ?").get(params.entityId);
478
- if (!entity) {
479
- return {
480
- content: [
481
- {
482
- type: "text",
483
- text: JSON.stringify({ error: "Entity not found" })
484
- }
485
- ],
486
- isError: true
487
- };
622
+ }
623
+ for (const blocked of SENSITIVE_BLOCKED_FILES) {
624
+ if (resolvedPath === blocked) {
625
+ return true;
626
+ }
627
+ }
628
+ if (path6.dirname(resolvedPath) === OPENCLAW_DIR && resolvedPath.endsWith(".bak")) {
629
+ return true;
630
+ }
631
+ return false;
632
+ }
633
+ var OPENCLAW_JSON_FILENAME = "openclaw.json";
634
+ function redactOpenclawJson(rawContent) {
635
+ let config;
636
+ try {
637
+ config = JSON.parse(rawContent);
638
+ } catch {
639
+ return rawContent;
640
+ }
641
+ let redactedCount = 0;
642
+ const channels = config.channels;
643
+ if (channels && typeof channels === "object") {
644
+ for (const channelKey of Object.keys(channels)) {
645
+ const channel = channels[channelKey];
646
+ if (channel && typeof channel === "object" && "botToken" in channel) {
647
+ channel.botToken = "[REDACTED]";
648
+ redactedCount++;
488
649
  }
489
- return {
490
- content: [{ type: "text", text: JSON.stringify(entityRow(entity)) }]
491
- };
492
650
  }
493
- });
494
- api.registerTool({
495
- name: "entity_list",
496
- description: "List entities with optional type and source filters.",
497
- parameters: T.Object({
498
- type: T.Optional(EntityType),
499
- source: T.Optional(T.String({ description: "Filter by source" })),
500
- limit: T.Optional(
501
- T.Number({ description: "Max results (default 100)" })
502
- )
503
- }),
504
- async execute(_id, params, _ctx) {
505
- const configDir2 = _ctx?.configDir ?? process.env.HOME + "/.openclaw";
506
- const database = getDb(configDir2);
507
- const conditions = [];
508
- const values = [];
509
- if (params.type) {
510
- conditions.push("type = ?");
511
- values.push(params.type);
512
- }
513
- if (params.source) {
514
- conditions.push("source = ?");
515
- values.push(params.source);
516
- }
517
- const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
518
- const limit = params.limit ?? 100;
519
- const entities = database.prepare(
520
- `SELECT * FROM entities ${where} ORDER BY updated_at DESC LIMIT ?`
521
- ).all(...values, limit);
522
- return {
523
- content: [
524
- { type: "text", text: JSON.stringify(entities.map(entityRow)) }
525
- ]
526
- };
651
+ }
652
+ const gateway = config.gateway;
653
+ if (gateway && typeof gateway === "object") {
654
+ if (gateway.auth && typeof gateway.auth === "object") {
655
+ const auth = gateway.auth;
656
+ for (const key of Object.keys(auth)) {
657
+ auth[key] = "[REDACTED]";
658
+ redactedCount++;
659
+ }
660
+ }
661
+ if ("token" in gateway) {
662
+ gateway.token = "[REDACTED]";
663
+ redactedCount++;
527
664
  }
665
+ const remote = gateway.remote;
666
+ if (remote && typeof remote === "object" && "token" in remote) {
667
+ remote.token = "[REDACTED]";
668
+ redactedCount++;
669
+ }
670
+ }
671
+ if (redactedCount > 0) {
672
+ console.log(`[security] Redacted ${redactedCount} sensitive field(s) from openclaw.json before returning to client`);
673
+ }
674
+ return JSON.stringify(config, null, 2);
675
+ }
676
+ function isOpenclawJson(resolvedPath) {
677
+ return path6.basename(resolvedPath) === OPENCLAW_JSON_FILENAME && resolvedPath.startsWith(OPENCLAW_DIR);
678
+ }
679
+ function expandHome(p) {
680
+ if (p.startsWith("~/") || p === "~") {
681
+ return path6.join(HOME_DIR, p.slice(1));
682
+ }
683
+ return p;
684
+ }
685
+ function validatePath(p, allowedRoots) {
686
+ const resolved = path6.resolve(expandHome(p));
687
+ if (!allowedRoots || allowedRoots.length === 0) return resolved;
688
+ const allowed = allowedRoots.some((root) => {
689
+ const resolvedRoot = path6.resolve(expandHome(root));
690
+ return resolved === resolvedRoot || resolved.startsWith(resolvedRoot + path6.sep);
528
691
  });
529
- api.registerTool({
530
- name: "entity_delete",
531
- description: "Delete an entity from the registry by ID. Also removes FTS and vector data.",
532
- parameters: T.Object({
533
- entityId: T.String({ description: "The entity ID to delete" })
534
- }),
535
- async execute(_id, params, _ctx) {
536
- const configDir2 = _ctx?.configDir ?? process.env.HOME + "/.openclaw";
537
- const database = getDb(configDir2);
538
- const del = database.transaction(() => {
539
- const result2 = database.prepare("DELETE FROM entities WHERE id = ?").run(params.entityId);
540
- ftsDelete(database, params.entityId);
541
- if (vecEnabled) {
542
- database.prepare("DELETE FROM search_vec WHERE entity_id = ?").run(params.entityId);
543
- }
544
- return result2;
545
- });
546
- const result = del();
547
- if (result.changes === 0) {
548
- return {
549
- content: [
550
- {
551
- type: "text",
552
- text: JSON.stringify({ error: "Entity not found" })
553
- }
554
- ],
555
- isError: true
556
- };
557
- }
558
- return {
559
- content: [
560
- {
561
- type: "text",
562
- text: JSON.stringify({ deleted: params.entityId })
563
- }
564
- ]
565
- };
566
- }
567
- });
568
- api.registerTool({
569
- name: "entity_search",
570
- description: "Search entities using full-text search, vector search, or hybrid. Returns ranked results.",
571
- parameters: T.Object({
572
- query: T.String({ description: "Search query text" }),
573
- type: T.Optional(
574
- T.String({ description: "Filter results by entity type" })
575
- ),
576
- mode: T.Optional(
577
- T.Union(
578
- [T.Literal("fts"), T.Literal("vector"), T.Literal("hybrid")],
579
- {
580
- description: "Search mode: fts (full-text, default), vector (semantic), hybrid (both merged via RRF)"
581
- }
582
- )
583
- ),
584
- limit: T.Optional(
585
- T.Number({ description: "Max results (default 20)" })
586
- )
587
- }),
588
- async execute(_id, params, _ctx) {
589
- const configDir2 = _ctx?.configDir ?? process.env.HOME + "/.openclaw";
590
- const database = getDb(configDir2);
591
- const mode = params.mode ?? "fts";
592
- const limit = params.limit ?? 20;
593
- let results;
594
- if (mode === "fts") {
595
- results = ftsSearch(database, params.query, params.type, limit);
596
- } else if (mode === "vector") {
597
- if (!vecEnabled) {
598
- return {
599
- content: [
600
- {
601
- type: "text",
602
- text: JSON.stringify({
603
- error: "Vector search unavailable \u2014 sqlite-vec extension not loaded"
604
- })
605
- }
606
- ],
607
- isError: true
608
- };
609
- }
610
- const embedding = await generateEmbedding(params.query, configDir2);
611
- if (!embedding) {
612
- return {
613
- content: [
614
- {
615
- type: "text",
616
- text: JSON.stringify({
617
- error: "Embedding generation failed \u2014 check embeddings config in openclaw.json"
618
- })
619
- }
620
- ],
621
- isError: true
622
- };
623
- }
624
- results = vecSearch(database, embedding, limit);
625
- if (params.type) {
626
- results = results.filter((r) => r.type === params.type);
627
- }
628
- } else {
629
- const ftsResults = ftsSearch(
630
- database,
631
- params.query,
632
- params.type,
633
- limit
634
- );
635
- let vecResults = [];
636
- if (vecEnabled) {
637
- const embedding = await generateEmbedding(params.query, configDir2);
638
- if (embedding) {
639
- vecResults = vecSearch(database, embedding, limit);
640
- if (params.type) {
641
- vecResults = vecResults.filter(
642
- (r) => r.type === params.type
643
- );
644
- }
645
- }
646
- }
647
- results = hybridSearch(database, ftsResults, vecResults, limit);
648
- }
649
- return {
650
- content: [{ type: "text", text: JSON.stringify(results) }]
651
- };
652
- }
653
- });
654
- api.registerTool({
655
- name: "entity_batch_resolve",
656
- description: "Resolve multiple entity IDs in a single call. Returns a map of ID to entity data. Missing entities are omitted.",
657
- parameters: T.Object({
658
- entityIds: T.Array(T.String(), {
659
- description: "Array of entity IDs to resolve"
660
- })
661
- }),
662
- async execute(_id, params, _ctx) {
663
- const configDir2 = _ctx?.configDir ?? process.env.HOME + "/.openclaw";
664
- const database = getDb(configDir2);
665
- if (!params.entityIds || params.entityIds.length === 0) {
666
- return {
667
- content: [{ type: "text", text: JSON.stringify({}) }]
668
- };
669
- }
670
- const placeholders = params.entityIds.map(() => "?").join(",");
671
- const entities = database.prepare(`SELECT * FROM entities WHERE id IN (${placeholders})`).all(...params.entityIds);
672
- const result = {};
673
- for (const entity of entities) {
674
- const parsed = entityRow(entity);
675
- result[parsed.id] = parsed;
676
- }
677
- return {
678
- content: [{ type: "text", text: JSON.stringify(result) }]
679
- };
680
- }
681
- });
682
- api.registerTool({
683
- name: "entity_embed",
684
- description: "Generate and store a vector embedding for an entity using OpenRouter (google/gemini-embedding-001). Requires embeddings config in openclaw.json.",
685
- parameters: T.Object({
686
- entityId: T.String({
687
- description: "The entity ID to generate an embedding for"
688
- })
689
- }),
690
- async execute(_id, params, _ctx) {
691
- const configDir2 = _ctx?.configDir ?? process.env.HOME + "/.openclaw";
692
- const database = getDb(configDir2);
693
- if (!vecEnabled) {
694
- return {
695
- content: [
696
- {
697
- type: "text",
698
- text: JSON.stringify({
699
- error: "Vector search unavailable \u2014 sqlite-vec extension not loaded"
700
- })
701
- }
702
- ],
703
- isError: true
704
- };
705
- }
706
- const entity = database.prepare("SELECT * FROM entities WHERE id = ?").get(params.entityId);
707
- if (!entity) {
708
- return {
709
- content: [
710
- {
711
- type: "text",
712
- text: JSON.stringify({ error: "Entity not found" })
713
- }
714
- ],
715
- isError: true
716
- };
717
- }
718
- const text = buildEmbeddingText(entity);
719
- const embedding = await generateEmbedding(text, configDir2);
720
- if (!embedding) {
721
- return {
722
- content: [
723
- {
724
- type: "text",
725
- text: JSON.stringify({
726
- error: "Embedding generation failed \u2014 check embeddings config in openclaw.json"
727
- })
728
- }
729
- ],
730
- isError: true
731
- };
732
- }
733
- vecUpsert(database, params.entityId, embedding);
734
- return {
735
- content: [
736
- {
737
- type: "text",
738
- text: JSON.stringify({
739
- entityId: params.entityId,
740
- dimensions: embedding.length,
741
- embedded: true
742
- })
743
- }
744
- ]
745
- };
746
- }
747
- });
748
- api.registerTool({
749
- name: "entity_sync",
750
- description: "Reconcile the entity registry with data on the filesystem (agents, tools from openclaw.json). Call this after configuration changes to ensure the registry is up to date.",
751
- parameters: T.Object({
752
- sources: T.Optional(
753
- T.Array(T.String(), {
754
- description: "Which sources to sync: 'agents', 'tools'. Default: all."
755
- })
756
- )
757
- }),
758
- async execute(_id, _params, _ctx) {
759
- const configDir2 = _ctx?.configDir ?? process.env.HOME + "/.openclaw";
760
- const database = getDb(configDir2);
761
- const result = syncEntitiesFromFilesystem(database, configDir2);
762
- return {
763
- content: [{ type: "text", text: JSON.stringify(result) }]
764
- };
765
- }
766
- });
767
- const configDir = process.env.HOME + "/.openclaw";
768
- try {
769
- const database = getDb(configDir);
770
- syncEntitiesFromFilesystem(database, configDir);
771
- } catch {
692
+ if (!allowed) {
693
+ throw new Error(`Path "${p}" is outside allowed roots`);
772
694
  }
773
- syncTimer = setInterval(() => {
774
- try {
775
- if (db) {
776
- syncEntitiesFromFilesystem(db, configDir);
777
- }
778
- } catch {
779
- }
780
- }, SYNC_INTERVAL_MS);
695
+ return resolved;
781
696
  }
782
-
783
- // src/filesystem.ts
784
- import fs2 from "fs";
785
- import path2 from "path";
786
- function expandHome(p) {
787
- if (p.startsWith("~/") || p === "~") {
788
- return path2.join(process.env.HOME ?? "/root", p.slice(1));
697
+ function validateAndBlockSensitive(p, allowedRoots) {
698
+ const resolved = validatePath(p, allowedRoots);
699
+ if (isSensitivePath(resolved)) {
700
+ throw new Error(
701
+ `Access denied: path "${p}" is inside a protected directory (credentials/devices/identity)`
702
+ );
789
703
  }
790
- return p;
704
+ return resolved;
791
705
  }
792
- function validatePath(p, allowedRoots) {
793
- const resolved = path2.resolve(expandHome(p));
794
- if (!allowedRoots || allowedRoots.length === 0) return resolved;
795
- const allowed = allowedRoots.some((root) => {
796
- const resolvedRoot = path2.resolve(expandHome(root));
797
- return resolved === resolvedRoot || resolved.startsWith(resolvedRoot + path2.sep);
798
- });
799
- if (!allowed) {
800
- throw new Error(`Path "${p}" is outside allowed roots`);
706
+ function validateWritePath(p, allowedRoots) {
707
+ const resolved = validateAndBlockSensitive(p, allowedRoots);
708
+ if (isOpenclawJson(resolved)) {
709
+ throw new Error(
710
+ `Write denied: "${p}" is a protected configuration file (openclaw.json)`
711
+ );
801
712
  }
802
713
  return resolved;
803
714
  }
@@ -813,18 +724,18 @@ function err(message) {
813
724
  };
814
725
  }
815
726
  function listDir(dirPath, opts) {
816
- const dirents = fs2.readdirSync(dirPath, { withFileTypes: true });
727
+ const dirents = fs5.readdirSync(dirPath, { withFileTypes: true });
817
728
  const results = [];
818
729
  for (const dirent of dirents) {
819
730
  if (!opts.includeHidden && dirent.name.startsWith(".")) continue;
820
- const entryPath = path2.join(dirPath, dirent.name);
731
+ const entryPath = path6.join(dirPath, dirent.name);
821
732
  let type = "other";
822
733
  if (dirent.isFile()) type = "file";
823
734
  else if (dirent.isDirectory()) type = "directory";
824
735
  else if (dirent.isSymbolicLink()) type = "symlink";
825
736
  const entry = { name: dirent.name, path: entryPath, type };
826
737
  try {
827
- const stat = fs2.statSync(entryPath);
738
+ const stat = fs5.statSync(entryPath);
828
739
  entry.size = stat.size;
829
740
  entry.modified = stat.mtime.toISOString();
830
741
  } catch {
@@ -839,12 +750,25 @@ function listDir(dirPath, opts) {
839
750
  }
840
751
  return results;
841
752
  }
753
+ function filterSensitiveEntries(entries) {
754
+ return entries.filter((entry) => !isSensitivePath(entry.path)).map((entry) => {
755
+ if (entry.children) {
756
+ return { ...entry, children: filterSensitiveEntries(entry.children) };
757
+ }
758
+ return entry;
759
+ });
760
+ }
842
761
  function registerFilesystemTools(api) {
843
- const allowedRoots = api.pluginConfig?.["fs.allowedRoots"] ?? [];
762
+ const layout = resolveGatewayLayout();
763
+ const DEFAULT_ALLOWED_ROOTS = Array.from(/* @__PURE__ */ new Set([
764
+ OPENCLAW_DIR,
765
+ ...layout.workspaces.map((ws) => ws.path)
766
+ ]));
767
+ const allowedRoots = api.pluginConfig?.["fs.allowedRoots"] ?? DEFAULT_ALLOWED_ROOTS;
844
768
  api.registerTool({
845
769
  name: "fs_read",
846
770
  label: "Read File",
847
- description: "Read a file from the server filesystem. Returns the file contents as text. Supports ~ for home directory expansion.",
771
+ description: "Read a file from the server filesystem. Returns the file contents as text. Supports ~ for home directory expansion. Sensitive directories (credentials, devices, identity) are blocked. Config files are returned with auth tokens redacted.",
848
772
  parameters: {
849
773
  type: "object",
850
774
  properties: {
@@ -862,10 +786,13 @@ function registerFilesystemTools(api) {
862
786
  },
863
787
  async execute(_id, params) {
864
788
  try {
865
- const filePath = validatePath(params.path, allowedRoots);
789
+ const filePath = validateAndBlockSensitive(params.path, allowedRoots);
866
790
  const encoding = params.encoding ?? "utf-8";
867
- const content = fs2.readFileSync(filePath, encoding);
868
- const stat = fs2.statSync(filePath);
791
+ let content = fs5.readFileSync(filePath, encoding);
792
+ const stat = fs5.statSync(filePath);
793
+ if (isOpenclawJson(filePath) && encoding === "utf-8") {
794
+ content = redactOpenclawJson(content);
795
+ }
869
796
  return ok({
870
797
  path: filePath,
871
798
  content,
@@ -881,7 +808,7 @@ function registerFilesystemTools(api) {
881
808
  api.registerTool({
882
809
  name: "fs_write",
883
810
  label: "Write File",
884
- description: "Write content to a file on the server filesystem. Creates parent directories if they don't exist. Supports ~ for home directory expansion.",
811
+ description: "Write content to a file on the server filesystem. Creates parent directories if they don't exist. Supports ~ for home directory expansion. Writes to protected directories (credentials, devices, identity) and config files (openclaw.json) are denied.",
885
812
  parameters: {
886
813
  type: "object",
887
814
  properties: {
@@ -907,15 +834,15 @@ function registerFilesystemTools(api) {
907
834
  },
908
835
  async execute(_id, params) {
909
836
  try {
910
- const filePath = validatePath(params.path, allowedRoots);
837
+ const filePath = validateWritePath(params.path, allowedRoots);
911
838
  const content = params.content;
912
839
  const encoding = params.encoding ?? "utf-8";
913
840
  const mkdir = params.mkdir !== false;
914
841
  if (mkdir) {
915
- fs2.mkdirSync(path2.dirname(filePath), { recursive: true });
842
+ fs5.mkdirSync(path6.dirname(filePath), { recursive: true });
916
843
  }
917
- fs2.writeFileSync(filePath, content, encoding);
918
- const stat = fs2.statSync(filePath);
844
+ fs5.writeFileSync(filePath, content, encoding);
845
+ const stat = fs5.statSync(filePath);
919
846
  return ok({
920
847
  path: filePath,
921
848
  size: stat.size,
@@ -930,7 +857,7 @@ function registerFilesystemTools(api) {
930
857
  api.registerTool({
931
858
  name: "fs_list",
932
859
  label: "List Directory",
933
- description: "List contents of a directory on the server filesystem. Returns file metadata including name, type, size, and modification time. Supports ~ for home directory expansion.",
860
+ description: "List contents of a directory on the server filesystem. Returns file metadata including name, type, size, and modification time. Supports ~ for home directory expansion. Protected directories (credentials, devices, identity) are excluded from results.",
934
861
  parameters: {
935
862
  type: "object",
936
863
  properties: {
@@ -951,10 +878,11 @@ function registerFilesystemTools(api) {
951
878
  },
952
879
  async execute(_id, params) {
953
880
  try {
954
- const dirPath = validatePath(params.path, allowedRoots);
881
+ const dirPath = validateAndBlockSensitive(params.path, allowedRoots);
955
882
  const recursive = params.recursive === true;
956
883
  const includeHidden = params.includeHidden === true;
957
- const entries = listDir(dirPath, { recursive, includeHidden, depth: 0, maxDepth: 3 });
884
+ let entries = listDir(dirPath, { recursive, includeHidden, depth: 0, maxDepth: 3 });
885
+ entries = filterSensitiveEntries(entries);
958
886
  return ok({
959
887
  path: dirPath,
960
888
  count: entries.length,
@@ -966,115 +894,2513 @@ function registerFilesystemTools(api) {
966
894
  }
967
895
  }
968
896
  });
969
- }
970
-
971
- // src/version.ts
972
- import { execSync } from "child_process";
973
- import fs3 from "fs";
974
- import path3 from "path";
975
- import { fileURLToPath } from "url";
976
- var PACKAGE_NAME = "squad-openclaw";
977
- function getCurrentVersion() {
978
- const thisFile = fileURLToPath(import.meta.url);
979
- const pkgPath = path3.resolve(path3.dirname(thisFile), "..", "package.json");
980
- try {
981
- const pkg = JSON.parse(fs3.readFileSync(pkgPath, "utf-8"));
982
- return pkg.version ?? "0.0.0";
983
- } catch {
984
- return "0.0.0";
985
- }
986
- }
987
- async function fetchLatestVersion() {
988
- const controller = new AbortController();
989
- const timeout = setTimeout(() => controller.abort(), 1e4);
990
- try {
991
- const res = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}`, {
992
- signal: controller.signal
993
- });
994
- if (!res.ok) throw new Error(`npm registry returned ${res.status}`);
995
- const data = await res.json();
996
- return data["dist-tags"]?.latest ?? "0.0.0";
997
- } finally {
998
- clearTimeout(timeout);
999
- }
1000
- }
1001
- function registerVersionMethods(api) {
1002
- api.registerGatewayMethod(
1003
- "squad.version.check",
1004
- async ({ respond }) => {
897
+ api.registerTool({
898
+ name: "fs_mkdir",
899
+ label: "Create Directory",
900
+ description: "Create a directory on the server filesystem. Creates parent directories as needed. Supports ~ for home directory expansion. Cannot create directories inside protected paths (credentials, devices, identity).",
901
+ parameters: {
902
+ type: "object",
903
+ properties: {
904
+ path: {
905
+ type: "string",
906
+ description: "Absolute or ~-prefixed path of the directory to create"
907
+ }
908
+ },
909
+ required: ["path"]
910
+ },
911
+ async execute(_id, params) {
1005
912
  try {
1006
- const current = getCurrentVersion();
1007
- let latest;
1008
- try {
1009
- latest = await fetchLatestVersion();
1010
- } catch {
1011
- respond(true, {
1012
- current,
1013
- latest: null,
1014
- updateAvailable: false,
1015
- registryError: "Could not reach npm registry"
1016
- });
1017
- return;
913
+ const targetPath = validateWritePath(params.path, allowedRoots);
914
+ fs5.mkdirSync(targetPath, { recursive: true });
915
+ return ok({
916
+ path: targetPath,
917
+ created: true
918
+ });
919
+ } catch (e) {
920
+ const msg = e instanceof Error ? e.message : String(e);
921
+ return err(`fs_mkdir failed: ${msg}`);
922
+ }
923
+ }
924
+ });
925
+ api.registerTool({
926
+ name: "fs_rename",
927
+ label: "Rename / Move",
928
+ description: "Rename or move a file or directory on the server filesystem. Supports ~ for home directory expansion. Cannot move files into or out of protected directories.",
929
+ parameters: {
930
+ type: "object",
931
+ properties: {
932
+ oldPath: {
933
+ type: "string",
934
+ description: "Current absolute or ~-prefixed path"
935
+ },
936
+ newPath: {
937
+ type: "string",
938
+ description: "New absolute or ~-prefixed path"
1018
939
  }
1019
- respond(true, {
1020
- current,
1021
- latest,
1022
- updateAvailable: latest !== current && latest !== "0.0.0"
940
+ },
941
+ required: ["oldPath", "newPath"]
942
+ },
943
+ async execute(_id, params) {
944
+ try {
945
+ const resolvedOld = validateWritePath(params.oldPath, allowedRoots);
946
+ const resolvedNew = validateWritePath(params.newPath, allowedRoots);
947
+ fs5.renameSync(resolvedOld, resolvedNew);
948
+ return ok({
949
+ oldPath: resolvedOld,
950
+ newPath: resolvedNew,
951
+ renamed: true
1023
952
  });
1024
953
  } catch (e) {
1025
954
  const msg = e instanceof Error ? e.message : String(e);
1026
- respond(false, { error: msg });
955
+ return err(`fs_rename failed: ${msg}`);
1027
956
  }
1028
957
  }
1029
- );
1030
- api.registerGatewayMethod(
1031
- "squad.version.update",
1032
- async ({ respond }) => {
958
+ });
959
+ api.registerTool({
960
+ name: "fs_delete",
961
+ label: "Delete File or Directory",
962
+ description: "Delete a file or directory from the server filesystem. For directories, removes recursively. Supports ~ for home directory expansion. Cannot delete protected directories or config files.",
963
+ parameters: {
964
+ type: "object",
965
+ properties: {
966
+ path: {
967
+ type: "string",
968
+ description: "Absolute or ~-prefixed path to the file or directory to delete"
969
+ }
970
+ },
971
+ required: ["path"]
972
+ },
973
+ async execute(_id, params) {
1033
974
  try {
1034
- const before = getCurrentVersion();
1035
- let updateOutput = "";
1036
- try {
1037
- updateOutput = execSync(
1038
- `openclaw plugins update ${PACKAGE_NAME} 2>&1`,
1039
- { timeout: 12e4, encoding: "utf-8" }
1040
- );
1041
- } catch {
1042
- try {
1043
- updateOutput = execSync(
1044
- `npm install -g ${PACKAGE_NAME}@latest 2>&1`,
1045
- { timeout: 12e4, encoding: "utf-8" }
1046
- );
1047
- } catch (npmErr) {
1048
- const msg = npmErr instanceof Error ? npmErr.message : String(npmErr);
1049
- respond(false, {
1050
- error: `Update failed: ${msg}`,
1051
- output: updateOutput
1052
- });
1053
- return;
1054
- }
975
+ const targetPath = validateWritePath(params.path, allowedRoots);
976
+ const stat = fs5.statSync(targetPath);
977
+ const wasDirectory = stat.isDirectory();
978
+ if (wasDirectory) {
979
+ fs5.rmSync(targetPath, { recursive: true });
980
+ } else {
981
+ fs5.unlinkSync(targetPath);
1055
982
  }
1056
- const after = getCurrentVersion();
1057
- const updated = before !== after;
1058
- respond(true, {
1059
- previousVersion: before,
1060
- currentVersion: after,
1061
- updated,
1062
- restartRequired: updated,
1063
- output: updateOutput.slice(0, 500)
983
+ return ok({
984
+ path: targetPath,
985
+ deleted: true,
986
+ type: wasDirectory ? "directory" : "file"
1064
987
  });
1065
988
  } catch (e) {
1066
989
  const msg = e instanceof Error ? e.message : String(e);
1067
- respond(false, { error: msg });
990
+ return err(`fs_delete failed: ${msg}`);
1068
991
  }
1069
992
  }
1070
- );
993
+ });
1071
994
  }
1072
995
 
1073
- // src/index.ts
1074
- function squadAppPlugin(api) {
1075
- registerEntityTools(api);
1076
- registerFilesystemTools(api);
1077
- registerVersionMethods(api);
996
+ // src/entities.ts
997
+ var EntityType = T.Union([
998
+ T.Literal("agent"),
999
+ T.Literal("skill"),
1000
+ T.Literal("tool"),
1001
+ T.Literal("plugin"),
1002
+ T.Literal("session"),
1003
+ T.Literal("file"),
1004
+ T.Literal("directory"),
1005
+ T.Literal("url"),
1006
+ T.Literal("memory"),
1007
+ T.Literal("asset")
1008
+ ]);
1009
+ var registry = /* @__PURE__ */ new Map();
1010
+ function registrySet(entity) {
1011
+ registry.set(entity.id, entity);
1012
+ }
1013
+ function registryDelete(id) {
1014
+ registry.delete(id);
1015
+ }
1016
+ function registryList(type) {
1017
+ const all = Array.from(registry.values());
1018
+ if (!type) return all;
1019
+ return all.filter((e) => e.type === type);
1020
+ }
1021
+ var IDENTITY_NAME_RE = /\*\*Name:\*\*\s*\n?\s*(.+?)(?=\n|$)/;
1022
+ function parseIdentityName(content) {
1023
+ const match = content.match(IDENTITY_NAME_RE);
1024
+ const name = match?.[1]?.trim();
1025
+ if (!name) return null;
1026
+ if (/^_\(.+\)_$/.test(name)) return null;
1027
+ return name;
1028
+ }
1029
+ function scanAgents(configDir) {
1030
+ const now = Date.now();
1031
+ let entries;
1032
+ try {
1033
+ entries = fs6.readdirSync(configDir, { withFileTypes: true });
1034
+ } catch {
1035
+ return;
1036
+ }
1037
+ const workspaceDirs = entries.filter(
1038
+ (e) => e.isDirectory() && (e.name === "workspace" || e.name.startsWith("workspace-"))
1039
+ );
1040
+ for (const dir of workspaceDirs) {
1041
+ const agentId = dir.name === "workspace" ? "main" : dir.name.replace("workspace-", "");
1042
+ const workspacePath = path7.join(configDir, dir.name);
1043
+ let name = agentId;
1044
+ const metadata = { workspacePath };
1045
+ const identityPath = path7.join(workspacePath, "IDENTITY.md");
1046
+ try {
1047
+ const content = fs6.readFileSync(identityPath, "utf-8");
1048
+ const parsed = parseIdentityName(content);
1049
+ if (parsed) name = parsed;
1050
+ } catch {
1051
+ }
1052
+ if (name === agentId) {
1053
+ const agentJsonPath = path7.join(workspacePath, "agent.json");
1054
+ try {
1055
+ const raw = fs6.readFileSync(agentJsonPath, "utf-8");
1056
+ const config = JSON.parse(raw);
1057
+ if (config.displayName) name = config.displayName;
1058
+ if (config.model) metadata.model = config.model;
1059
+ if (config.tools) metadata.tools = config.tools;
1060
+ if (config.skills) metadata.skills = config.skills;
1061
+ } catch {
1062
+ }
1063
+ }
1064
+ registrySet({
1065
+ id: agentId,
1066
+ type: "agent",
1067
+ name,
1068
+ title: name,
1069
+ description: null,
1070
+ metadata,
1071
+ source: "filesystem",
1072
+ source_key: workspacePath,
1073
+ created_at: now,
1074
+ updated_at: now
1075
+ });
1076
+ }
1077
+ }
1078
+ function scanSkills(configDir) {
1079
+ const now = Date.now();
1080
+ const globalSkillsDir = path7.join(configDir, "skills");
1081
+ scanSkillsDir(globalSkillsDir, "global", now);
1082
+ let entries;
1083
+ try {
1084
+ entries = fs6.readdirSync(configDir, { withFileTypes: true });
1085
+ } catch {
1086
+ return;
1087
+ }
1088
+ for (const dir of entries) {
1089
+ if (!dir.isDirectory() || !(dir.name === "workspace" || dir.name.startsWith("workspace-"))) {
1090
+ continue;
1091
+ }
1092
+ const agentId = dir.name === "workspace" ? "main" : dir.name.replace("workspace-", "");
1093
+ const agentSkillsDir = path7.join(configDir, dir.name, "skills");
1094
+ scanSkillsDir(agentSkillsDir, agentId, now);
1095
+ }
1096
+ }
1097
+ function scanSkillsDir(skillsDir, scope, now) {
1098
+ let entries;
1099
+ try {
1100
+ entries = fs6.readdirSync(skillsDir, { withFileTypes: true });
1101
+ } catch {
1102
+ return;
1103
+ }
1104
+ for (const entry of entries) {
1105
+ if (!entry.isDirectory()) continue;
1106
+ const skillKey = entry.name;
1107
+ const skillPath = path7.join(skillsDir, skillKey);
1108
+ let name = skillKey;
1109
+ for (const manifestName of ["manifest.json", "package.json"]) {
1110
+ try {
1111
+ const raw = fs6.readFileSync(
1112
+ path7.join(skillPath, manifestName),
1113
+ "utf-8"
1114
+ );
1115
+ const manifest = JSON.parse(raw);
1116
+ if (manifest.name) name = manifest.name;
1117
+ break;
1118
+ } catch {
1119
+ continue;
1120
+ }
1121
+ }
1122
+ const entityId = scope === "global" ? `skill:${skillKey}` : `skill:${scope}:${skillKey}`;
1123
+ registrySet({
1124
+ id: entityId,
1125
+ type: "skill",
1126
+ name,
1127
+ title: name,
1128
+ description: null,
1129
+ metadata: { skillKey, scope, skillPath },
1130
+ source: "filesystem",
1131
+ source_key: skillPath,
1132
+ created_at: now,
1133
+ updated_at: now
1134
+ });
1135
+ }
1136
+ }
1137
+ function scanPlugins2(configDir) {
1138
+ const now = Date.now();
1139
+ const extensionsDir = path7.join(configDir, "extensions");
1140
+ let entries;
1141
+ try {
1142
+ entries = fs6.readdirSync(extensionsDir, { withFileTypes: true });
1143
+ } catch {
1144
+ return;
1145
+ }
1146
+ for (const dir of entries) {
1147
+ if (!dir.isDirectory()) continue;
1148
+ const pluginDir = path7.join(extensionsDir, dir.name);
1149
+ const manifestPath = path7.join(pluginDir, "openclaw.plugin.json");
1150
+ try {
1151
+ const raw = fs6.readFileSync(manifestPath, "utf-8");
1152
+ const manifest = JSON.parse(raw);
1153
+ const pluginId = manifest.id || dir.name;
1154
+ const name = manifest.name || pluginId;
1155
+ registrySet({
1156
+ id: `plugin:${pluginId}`,
1157
+ type: "plugin",
1158
+ name,
1159
+ title: name,
1160
+ description: manifest.description || null,
1161
+ metadata: { pluginId, pluginDir },
1162
+ source: "filesystem",
1163
+ source_key: manifestPath,
1164
+ created_at: now,
1165
+ updated_at: now
1166
+ });
1167
+ } catch {
1168
+ }
1169
+ }
1170
+ }
1171
+ function scanTools(configDir) {
1172
+ const now = Date.now();
1173
+ try {
1174
+ const raw = fs6.readFileSync(
1175
+ path7.join(configDir, "openclaw.json"),
1176
+ "utf-8"
1177
+ );
1178
+ const config = JSON.parse(raw);
1179
+ const allowedTools = config?.tools?.allow ?? [];
1180
+ for (const toolName of allowedTools) {
1181
+ registrySet({
1182
+ id: `tool:${toolName}`,
1183
+ type: "tool",
1184
+ name: toolName,
1185
+ title: toolName,
1186
+ description: null,
1187
+ metadata: { tool_name: toolName },
1188
+ source: "filesystem",
1189
+ source_key: "openclaw.json:tools.allow",
1190
+ created_at: now,
1191
+ updated_at: now
1192
+ });
1193
+ }
1194
+ } catch {
1195
+ }
1196
+ }
1197
+ var MIME_MAP = {
1198
+ ".png": "image/png",
1199
+ ".jpg": "image/jpeg",
1200
+ ".jpeg": "image/jpeg",
1201
+ ".gif": "image/gif",
1202
+ ".webp": "image/webp",
1203
+ ".svg": "image/svg+xml",
1204
+ ".bmp": "image/bmp",
1205
+ ".ico": "image/x-icon",
1206
+ ".mp4": "video/mp4",
1207
+ ".webm": "video/webm",
1208
+ ".mov": "video/quicktime",
1209
+ ".avi": "video/x-msvideo",
1210
+ ".mkv": "video/x-matroska",
1211
+ ".mp3": "audio/mpeg",
1212
+ ".wav": "audio/wav",
1213
+ ".ogg": "audio/ogg",
1214
+ ".flac": "audio/flac",
1215
+ ".aac": "audio/aac",
1216
+ ".pdf": "application/pdf",
1217
+ ".json": "application/json",
1218
+ ".txt": "text/plain",
1219
+ ".md": "text/markdown",
1220
+ ".csv": "text/csv",
1221
+ ".zip": "application/zip",
1222
+ ".tar": "application/x-tar",
1223
+ ".gz": "application/gzip"
1224
+ };
1225
+ function getMimeType(filename) {
1226
+ const ext = path7.extname(filename).toLowerCase();
1227
+ return MIME_MAP[ext] ?? "application/octet-stream";
1228
+ }
1229
+ function scanMedia(configDir) {
1230
+ const now = Date.now();
1231
+ const mediaDir = path7.join(configDir, "media");
1232
+ scanMediaDir(mediaDir, now);
1233
+ }
1234
+ function scanMediaDir(dirPath, now) {
1235
+ let entries;
1236
+ try {
1237
+ entries = fs6.readdirSync(dirPath, { withFileTypes: true });
1238
+ } catch {
1239
+ return;
1240
+ }
1241
+ for (const entry of entries) {
1242
+ if (entry.name.startsWith(".")) continue;
1243
+ const entryPath = path7.join(dirPath, entry.name);
1244
+ if (isSensitivePath(entryPath)) continue;
1245
+ if (entry.isDirectory()) {
1246
+ registrySet({
1247
+ id: entryPath,
1248
+ type: "directory",
1249
+ name: entry.name,
1250
+ title: entry.name,
1251
+ description: null,
1252
+ metadata: { path: entryPath },
1253
+ source: "filesystem",
1254
+ source_key: entryPath,
1255
+ created_at: now,
1256
+ updated_at: now
1257
+ });
1258
+ scanMediaDir(entryPath, now);
1259
+ } else if (entry.isFile()) {
1260
+ const mimeType = getMimeType(entry.name);
1261
+ let size;
1262
+ let mtime = now;
1263
+ try {
1264
+ const stat = fs6.statSync(entryPath);
1265
+ size = stat.size;
1266
+ mtime = stat.mtimeMs;
1267
+ } catch {
1268
+ }
1269
+ registrySet({
1270
+ id: entryPath,
1271
+ type: "asset",
1272
+ name: entry.name,
1273
+ title: entry.name,
1274
+ description: null,
1275
+ metadata: { path: entryPath, size, mime_type: mimeType, original_name: entry.name },
1276
+ source: "filesystem",
1277
+ source_key: entryPath,
1278
+ created_at: mtime,
1279
+ updated_at: mtime
1280
+ });
1281
+ }
1282
+ }
1283
+ }
1284
+ function fullScan(configDir) {
1285
+ registry.clear();
1286
+ scanAgents(configDir);
1287
+ scanSkills(configDir);
1288
+ scanPlugins2(configDir);
1289
+ scanTools(configDir);
1290
+ scanMedia(configDir);
1291
+ }
1292
+ function registerEntityTools(api, onFsChange) {
1293
+ const configDir = getOpenclawStateDir();
1294
+ api.registerTool({
1295
+ name: "entity_list",
1296
+ description: "List all entities in the registry, optionally filtered by type. Returns lightweight entity data for @mention autocomplete.",
1297
+ parameters: T.Object({
1298
+ type: T.Optional(EntityType),
1299
+ limit: T.Optional(
1300
+ T.Number({ description: "Max results (default 500)" })
1301
+ )
1302
+ }),
1303
+ async execute(_id, params, _ctx) {
1304
+ const results = registryList(params.type);
1305
+ const limit = params.limit ?? 500;
1306
+ return {
1307
+ content: [
1308
+ { type: "text", text: JSON.stringify(results.slice(0, limit)) }
1309
+ ]
1310
+ };
1311
+ }
1312
+ });
1313
+ api.registerTool({
1314
+ name: "entity_search",
1315
+ description: "Search entities by name/title substring match for @mention autocomplete.",
1316
+ parameters: T.Object({
1317
+ query: T.String({ description: "Search query text" }),
1318
+ type: T.Optional(
1319
+ T.String({ description: "Filter results by entity type" })
1320
+ ),
1321
+ limit: T.Optional(
1322
+ T.Number({ description: "Max results (default 20)" })
1323
+ )
1324
+ }),
1325
+ async execute(_id, params, _ctx) {
1326
+ const q = (params.query ?? "").toLowerCase();
1327
+ const limit = params.limit ?? 20;
1328
+ let results = Array.from(registry.values());
1329
+ if (params.type) {
1330
+ results = results.filter((e) => e.type === params.type);
1331
+ }
1332
+ if (q) {
1333
+ results = results.filter(
1334
+ (e) => e.name.toLowerCase().includes(q) || (e.title ?? "").toLowerCase().includes(q)
1335
+ );
1336
+ }
1337
+ return {
1338
+ content: [
1339
+ { type: "text", text: JSON.stringify(results.slice(0, limit)) }
1340
+ ]
1341
+ };
1342
+ }
1343
+ });
1344
+ api.registerTool({
1345
+ name: "entity_sync",
1346
+ description: "Re-scan the filesystem to refresh the entity registry. Call after configuration changes for immediate updates.",
1347
+ parameters: T.Object({}),
1348
+ async execute(_id, _params, _ctx) {
1349
+ const before = registry.size;
1350
+ fullScan(configDir);
1351
+ return {
1352
+ content: [
1353
+ {
1354
+ type: "text",
1355
+ text: JSON.stringify({ synced: registry.size, previous: before })
1356
+ }
1357
+ ]
1358
+ };
1359
+ }
1360
+ });
1361
+ try {
1362
+ fullScan(configDir);
1363
+ } catch (err2) {
1364
+ console.error("[squad-openclaw] Initial scan failed:", err2);
1365
+ }
1366
+ let stopWatcher = null;
1367
+ try {
1368
+ stopWatcher = startWatcher(configDir, onFsChange);
1369
+ } catch (err2) {
1370
+ console.error("[squad-openclaw] Watcher failed to start:", err2);
1371
+ }
1372
+ const cleanup = () => {
1373
+ stopWatcher?.();
1374
+ };
1375
+ process.on("SIGTERM", cleanup);
1376
+ process.on("SIGINT", cleanup);
1377
+ }
1378
+
1379
+ // src/sql.ts
1380
+ import { execFile } from "child_process";
1381
+ import path8 from "path";
1382
+ import fs7 from "fs";
1383
+ import { Type as T2 } from "@sinclair/typebox";
1384
+ var HOME_DIR2 = process.env.HOME ?? "/root";
1385
+ var ALLOWED_DATA_DIR = path8.join(getOpenclawStateDir(), "squad-ceo-data");
1386
+ function validateDbPath(dbPath) {
1387
+ let expanded = dbPath;
1388
+ if (expanded.startsWith("~/") || expanded === "~") {
1389
+ expanded = path8.join(HOME_DIR2, expanded.slice(1));
1390
+ }
1391
+ const resolved = path8.resolve(expanded);
1392
+ if (resolved !== ALLOWED_DATA_DIR && !resolved.startsWith(ALLOWED_DATA_DIR + path8.sep)) {
1393
+ throw new Error(
1394
+ `Access denied: database path must be within ~/.openclaw/squad-ceo-data/`
1395
+ );
1396
+ }
1397
+ try {
1398
+ const stat = fs7.statSync(resolved);
1399
+ if (!stat.isFile()) {
1400
+ throw new Error(`Not a file: ${dbPath}`);
1401
+ }
1402
+ } catch (e) {
1403
+ if (e.code === "ENOENT") {
1404
+ throw new Error(`Database file not found: ${dbPath}`);
1405
+ }
1406
+ throw e;
1407
+ }
1408
+ return resolved;
1409
+ }
1410
+ function runSqlite3(dbPath, args) {
1411
+ return new Promise((resolve, reject) => {
1412
+ execFile(
1413
+ "sqlite3",
1414
+ [dbPath, ...args],
1415
+ { timeout: 3e4, maxBuffer: 10 * 1024 * 1024 },
1416
+ (error, stdout, stderr) => {
1417
+ if (error) {
1418
+ reject(new Error(stderr || error.message));
1419
+ return;
1420
+ }
1421
+ resolve(stdout);
1422
+ }
1423
+ );
1424
+ });
1425
+ }
1426
+ function registerSqlTools(api) {
1427
+ api.registerTool({
1428
+ name: "sql_query",
1429
+ label: "SQL Query",
1430
+ description: "Execute a sqlite3 query on a database file within ~/.openclaw/squad-ceo-data/. Only sqlite3 is allowed \u2014 no arbitrary shell commands. Use jsonOutput: true for structured JSON results.",
1431
+ parameters: T2.Object({
1432
+ dbPath: T2.String({
1433
+ description: "Path to the SQLite database file (must be within ~/.openclaw/squad-ceo-data/)"
1434
+ }),
1435
+ query: T2.String({ description: "SQL query to execute" }),
1436
+ jsonOutput: T2.Optional(
1437
+ T2.Boolean({
1438
+ description: "Return results as JSON (sqlite3 -json flag)"
1439
+ })
1440
+ )
1441
+ }),
1442
+ async execute(_id, params) {
1443
+ try {
1444
+ const resolvedDb = validateDbPath(params.dbPath);
1445
+ const args = [];
1446
+ if (params.jsonOutput) args.push("-json");
1447
+ args.push(params.query);
1448
+ const output = await runSqlite3(resolvedDb, args);
1449
+ return {
1450
+ content: [{ type: "text", text: output }]
1451
+ };
1452
+ } catch (e) {
1453
+ const msg = e instanceof Error ? e.message : String(e);
1454
+ return {
1455
+ content: [{ type: "text", text: JSON.stringify({ error: msg }) }],
1456
+ isError: true
1457
+ };
1458
+ }
1459
+ }
1460
+ });
1461
+ }
1462
+
1463
+ // src/version.ts
1464
+ import { spawn } from "child_process";
1465
+ import fs8 from "fs";
1466
+ import path9 from "path";
1467
+ import { fileURLToPath } from "url";
1468
+ var PACKAGE_NAME = "squad-openclaw";
1469
+ var CONFIG_PATH = path9.join(getOpenclawStateDir(), "openclaw.json");
1470
+ var updateInProgress = false;
1471
+ var VERIFY_TIMEOUT_MS = 2e4;
1472
+ var VERIFY_INTERVAL_MS = 500;
1473
+ var RESTART_BUFFER_MS = 5e3;
1474
+ var REGISTRY_TIMEOUT_MS = 2500;
1475
+ var COMMAND_OUTPUT_LIMIT = 8e3;
1476
+ var COMMAND_KILL_GRACE_MS = 2e3;
1477
+ function readInstalledVersionFromConfig() {
1478
+ try {
1479
+ const raw = fs8.readFileSync(CONFIG_PATH, "utf-8");
1480
+ const cfg = JSON.parse(raw);
1481
+ const v = cfg?.plugins?.installs?.[PACKAGE_NAME]?.version;
1482
+ return typeof v === "string" ? v : null;
1483
+ } catch {
1484
+ return null;
1485
+ }
1486
+ }
1487
+ function reconcileInstallMetadata(verification) {
1488
+ if (!verification.installPath || !verification.packageVersion) return;
1489
+ try {
1490
+ const raw = fs8.readFileSync(CONFIG_PATH, "utf-8");
1491
+ const config = JSON.parse(raw);
1492
+ if (!config.plugins || typeof config.plugins !== "object") config.plugins = {};
1493
+ if (!config.plugins.installs || typeof config.plugins.installs !== "object") {
1494
+ config.plugins.installs = {};
1495
+ }
1496
+ if (!config.plugins.entries || typeof config.plugins.entries !== "object") {
1497
+ config.plugins.entries = {};
1498
+ }
1499
+ const current = config.plugins.installs[PACKAGE_NAME] ?? {};
1500
+ config.plugins.installs[PACKAGE_NAME] = {
1501
+ ...current,
1502
+ source: "npm",
1503
+ spec: PACKAGE_NAME,
1504
+ installPath: verification.installPath,
1505
+ version: verification.packageVersion,
1506
+ installedAt: (/* @__PURE__ */ new Date()).toISOString()
1507
+ };
1508
+ const entry = config.plugins.entries[PACKAGE_NAME] ?? {};
1509
+ config.plugins.entries[PACKAGE_NAME] = {
1510
+ ...entry,
1511
+ enabled: true
1512
+ };
1513
+ fs8.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
1514
+ } catch {
1515
+ }
1516
+ }
1517
+ function getCurrentVersion() {
1518
+ const thisFile = fileURLToPath(import.meta.url);
1519
+ const pkgPath = path9.resolve(path9.dirname(thisFile), "..", "package.json");
1520
+ try {
1521
+ const pkg = JSON.parse(fs8.readFileSync(pkgPath, "utf-8"));
1522
+ return pkg.version ?? "0.0.0";
1523
+ } catch {
1524
+ return "0.0.0";
1525
+ }
1526
+ }
1527
+ async function fetchLatestVersion(timeoutMs = REGISTRY_TIMEOUT_MS) {
1528
+ const controller = new AbortController();
1529
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
1530
+ try {
1531
+ const res = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}`, {
1532
+ signal: controller.signal
1533
+ });
1534
+ if (!res.ok) throw new Error(`npm registry returned ${res.status}`);
1535
+ const data = await res.json();
1536
+ return data["dist-tags"]?.latest ?? "0.0.0";
1537
+ } finally {
1538
+ clearTimeout(timeout);
1539
+ }
1540
+ }
1541
+ async function runCommand(args, timeoutMs) {
1542
+ return new Promise((resolve) => {
1543
+ const child = spawn("openclaw", args, {
1544
+ stdio: ["ignore", "pipe", "pipe"],
1545
+ detached: true
1546
+ });
1547
+ let output = "";
1548
+ let timedOut = false;
1549
+ let resolved = false;
1550
+ let killGraceTimer = null;
1551
+ const append = (chunk) => {
1552
+ output += chunk.toString();
1553
+ if (output.length > COMMAND_OUTPUT_LIMIT) {
1554
+ output = output.slice(output.length - COMMAND_OUTPUT_LIMIT);
1555
+ }
1556
+ };
1557
+ const finalize = (result) => {
1558
+ if (resolved) return;
1559
+ resolved = true;
1560
+ if (killGraceTimer) clearTimeout(killGraceTimer);
1561
+ clearTimeout(timeout);
1562
+ resolve(result);
1563
+ };
1564
+ child.stdout?.on("data", append);
1565
+ child.stderr?.on("data", append);
1566
+ child.on("error", (err2) => {
1567
+ append(String(err2));
1568
+ finalize({
1569
+ ok: false,
1570
+ timedOut,
1571
+ exitCode: null,
1572
+ signal: null,
1573
+ output
1574
+ });
1575
+ });
1576
+ child.on("close", (code, signal) => {
1577
+ finalize({
1578
+ ok: !timedOut && code === 0,
1579
+ timedOut,
1580
+ exitCode: code,
1581
+ signal,
1582
+ output
1583
+ });
1584
+ });
1585
+ const timeout = setTimeout(() => {
1586
+ timedOut = true;
1587
+ try {
1588
+ process.kill(-child.pid, "SIGTERM");
1589
+ } catch {
1590
+ }
1591
+ killGraceTimer = setTimeout(() => {
1592
+ try {
1593
+ process.kill(-child.pid, "SIGKILL");
1594
+ } catch {
1595
+ }
1596
+ }, COMMAND_KILL_GRACE_MS);
1597
+ }, timeoutMs);
1598
+ });
1599
+ }
1600
+ function sleep(ms) {
1601
+ return new Promise((resolve) => setTimeout(resolve, ms));
1602
+ }
1603
+ function compareVersions(a, b) {
1604
+ const pa = a.split(".").map((x) => Number(x) || 0);
1605
+ const pb = b.split(".").map((x) => Number(x) || 0);
1606
+ const len = Math.max(pa.length, pb.length);
1607
+ for (let i = 0; i < len; i++) {
1608
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
1609
+ if (d !== 0) return d;
1610
+ }
1611
+ return 0;
1612
+ }
1613
+ function verifyInstalledPluginState() {
1614
+ let configRaw;
1615
+ try {
1616
+ configRaw = fs8.readFileSync(CONFIG_PATH, "utf-8");
1617
+ } catch (err2) {
1618
+ const msg = err2 instanceof Error ? err2.message : String(err2);
1619
+ return {
1620
+ ok: false,
1621
+ reason: `Could not read openclaw.json: ${msg}`,
1622
+ installPath: null,
1623
+ configVersion: null,
1624
+ packageVersion: null,
1625
+ requiredFilesMissing: []
1626
+ };
1627
+ }
1628
+ let config;
1629
+ try {
1630
+ config = JSON.parse(configRaw);
1631
+ } catch (err2) {
1632
+ const msg = err2 instanceof Error ? err2.message : String(err2);
1633
+ return {
1634
+ ok: false,
1635
+ reason: `Could not parse openclaw.json: ${msg}`,
1636
+ installPath: null,
1637
+ configVersion: null,
1638
+ packageVersion: null,
1639
+ requiredFilesMissing: []
1640
+ };
1641
+ }
1642
+ const installMeta = config?.plugins?.installs?.[PACKAGE_NAME];
1643
+ const installPath = typeof installMeta?.installPath === "string" ? installMeta.installPath : null;
1644
+ const configVersion = typeof installMeta?.version === "string" ? installMeta.version : null;
1645
+ if (!installPath) {
1646
+ return {
1647
+ ok: false,
1648
+ reason: "Missing plugins.installs entry or installPath for squad-openclaw",
1649
+ installPath: null,
1650
+ configVersion,
1651
+ packageVersion: null,
1652
+ requiredFilesMissing: []
1653
+ };
1654
+ }
1655
+ const requiredFiles = [
1656
+ path9.join(installPath, "package.json"),
1657
+ path9.join(installPath, "openclaw.plugin.json"),
1658
+ path9.join(installPath, "dist", "index.js")
1659
+ ];
1660
+ const requiredFilesMissing = requiredFiles.filter((p) => !fs8.existsSync(p));
1661
+ if (requiredFilesMissing.length > 0) {
1662
+ return {
1663
+ ok: false,
1664
+ reason: "Missing required installed plugin files",
1665
+ installPath,
1666
+ configVersion,
1667
+ packageVersion: null,
1668
+ requiredFilesMissing
1669
+ };
1670
+ }
1671
+ let installedPackage;
1672
+ try {
1673
+ installedPackage = JSON.parse(
1674
+ fs8.readFileSync(path9.join(installPath, "package.json"), "utf-8")
1675
+ );
1676
+ } catch (err2) {
1677
+ const msg = err2 instanceof Error ? err2.message : String(err2);
1678
+ return {
1679
+ ok: false,
1680
+ reason: `Could not parse installed package.json: ${msg}`,
1681
+ installPath,
1682
+ configVersion,
1683
+ packageVersion: null,
1684
+ requiredFilesMissing: []
1685
+ };
1686
+ }
1687
+ try {
1688
+ JSON.parse(
1689
+ fs8.readFileSync(path9.join(installPath, "openclaw.plugin.json"), "utf-8")
1690
+ );
1691
+ } catch (err2) {
1692
+ const msg = err2 instanceof Error ? err2.message : String(err2);
1693
+ return {
1694
+ ok: false,
1695
+ reason: `Could not parse installed openclaw.plugin.json: ${msg}`,
1696
+ installPath,
1697
+ configVersion,
1698
+ packageVersion: null,
1699
+ requiredFilesMissing: []
1700
+ };
1701
+ }
1702
+ const packageVersion = typeof installedPackage?.version === "string" ? installedPackage.version : null;
1703
+ if (!packageVersion) {
1704
+ return {
1705
+ ok: false,
1706
+ reason: "Installed package.json missing version",
1707
+ installPath,
1708
+ configVersion,
1709
+ packageVersion,
1710
+ requiredFilesMissing: []
1711
+ };
1712
+ }
1713
+ return {
1714
+ ok: true,
1715
+ installPath,
1716
+ configVersion,
1717
+ packageVersion,
1718
+ requiredFilesMissing: []
1719
+ };
1720
+ }
1721
+ async function waitForVerifiedInstall() {
1722
+ const deadline = Date.now() + VERIFY_TIMEOUT_MS;
1723
+ let last = verifyInstalledPluginState();
1724
+ while (!last.ok && Date.now() < deadline) {
1725
+ await sleep(VERIFY_INTERVAL_MS);
1726
+ last = verifyInstalledPluginState();
1727
+ }
1728
+ return last;
1729
+ }
1730
+ function registerVersionMethods(api) {
1731
+ api.registerGatewayMethod(
1732
+ "squad.version.check",
1733
+ async ({ respond, params }) => {
1734
+ try {
1735
+ const checkParams = params ?? {};
1736
+ const current = getCurrentVersion();
1737
+ if (checkParams.skipRegistry) {
1738
+ console.log("[version_check_local] returning local version only");
1739
+ respond(true, {
1740
+ current,
1741
+ latest: null,
1742
+ updateAvailable: false,
1743
+ registrySkipped: true
1744
+ });
1745
+ return;
1746
+ }
1747
+ let latest;
1748
+ try {
1749
+ latest = await fetchLatestVersion();
1750
+ } catch {
1751
+ console.log("[version_check_registry_timeout] npm registry unavailable");
1752
+ respond(true, {
1753
+ current,
1754
+ latest: null,
1755
+ updateAvailable: false,
1756
+ registryError: "Could not reach npm registry"
1757
+ });
1758
+ return;
1759
+ }
1760
+ respond(true, {
1761
+ current,
1762
+ latest,
1763
+ updateAvailable: latest !== current && latest !== "0.0.0"
1764
+ });
1765
+ } catch (e) {
1766
+ const msg = e instanceof Error ? e.message : String(e);
1767
+ respond(false, { error: msg });
1768
+ }
1769
+ }
1770
+ );
1771
+ api.registerGatewayMethod(
1772
+ "squad.version.update",
1773
+ async ({ respond }) => {
1774
+ if (updateInProgress) {
1775
+ respond(false, { error: "Update already in progress" });
1776
+ return;
1777
+ }
1778
+ updateInProgress = true;
1779
+ const updateAttemptId = `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
1780
+ try {
1781
+ const before = getCurrentVersion();
1782
+ const beforeInstalledVersion = readInstalledVersionFromConfig();
1783
+ let latestVersion = null;
1784
+ try {
1785
+ latestVersion = await fetchLatestVersion();
1786
+ } catch {
1787
+ latestVersion = null;
1788
+ }
1789
+ let updateOutput = "";
1790
+ let configBackup = null;
1791
+ try {
1792
+ configBackup = fs8.readFileSync(CONFIG_PATH, "utf-8");
1793
+ } catch {
1794
+ }
1795
+ await runCommand(["doctor", "--fix"], 3e4);
1796
+ try {
1797
+ const first = await runCommand(["plugins", "update", PACKAGE_NAME], 12e4);
1798
+ updateOutput = first.output;
1799
+ if (!first.ok) {
1800
+ throw new Error(
1801
+ first.timedOut ? "UPDATE_TIMEOUT: plugins update timed out" : `UPDATE_FAILED: plugins update exited with code ${String(first.exitCode)}`
1802
+ );
1803
+ }
1804
+ } catch (firstErr) {
1805
+ await runCommand(["doctor", "--fix"], 3e4);
1806
+ try {
1807
+ const retry = await runCommand(["plugins", "update", PACKAGE_NAME], 12e4);
1808
+ updateOutput = retry.output;
1809
+ if (!retry.ok) {
1810
+ throw new Error(
1811
+ retry.timedOut ? "UPDATE_TIMEOUT: plugins update timed out after retry" : `UPDATE_FAILED: plugins update retry exited with code ${String(retry.exitCode)}`
1812
+ );
1813
+ }
1814
+ } catch (installErr) {
1815
+ if (configBackup) {
1816
+ try {
1817
+ fs8.writeFileSync(CONFIG_PATH, configBackup, "utf-8");
1818
+ } catch {
1819
+ }
1820
+ }
1821
+ const firstMsg = firstErr instanceof Error ? firstErr.message : String(firstErr);
1822
+ const retryMsg = installErr instanceof Error ? installErr.message : String(installErr);
1823
+ respond(false, {
1824
+ code: /UPDATE_TIMEOUT/.test(retryMsg) ? "UPDATE_TIMEOUT" : "UPDATE_FAILED",
1825
+ error: `Update failed after doctor fix retry: ${retryMsg}`,
1826
+ output: updateOutput,
1827
+ firstError: firstMsg,
1828
+ updateAttemptId
1829
+ });
1830
+ return;
1831
+ }
1832
+ }
1833
+ const verification = await waitForVerifiedInstall();
1834
+ if (!verification.ok) {
1835
+ if (configBackup) {
1836
+ try {
1837
+ fs8.writeFileSync(CONFIG_PATH, configBackup, "utf-8");
1838
+ } catch {
1839
+ }
1840
+ }
1841
+ respond(false, {
1842
+ code: "VERIFY_FAILED",
1843
+ error: `Update verification failed: ${verification.reason ?? "unknown error"}`,
1844
+ output: updateOutput.slice(0, 500),
1845
+ verification,
1846
+ updateAttemptId
1847
+ });
1848
+ return;
1849
+ }
1850
+ reconcileInstallMetadata(verification);
1851
+ const verificationAfterReconcile = verifyInstalledPluginState();
1852
+ if (beforeInstalledVersion && verificationAfterReconcile.packageVersion && beforeInstalledVersion === verificationAfterReconcile.packageVersion) {
1853
+ const alreadyLatest = !!latestVersion && compareVersions(verificationAfterReconcile.packageVersion, latestVersion) >= 0;
1854
+ respond(false, {
1855
+ code: "UPDATE_FAILED",
1856
+ error: alreadyLatest ? `Already at latest version (${verificationAfterReconcile.packageVersion}).` : `Update command completed but installed version did not change (${verificationAfterReconcile.packageVersion}).`,
1857
+ output: updateOutput.slice(0, 500),
1858
+ verification: verificationAfterReconcile,
1859
+ latestVersion,
1860
+ updateAttemptId
1861
+ });
1862
+ return;
1863
+ }
1864
+ const after = getCurrentVersion();
1865
+ respond(true, {
1866
+ previousVersion: before,
1867
+ currentVersion: after,
1868
+ updated: true,
1869
+ restartRequired: true,
1870
+ restartInMs: RESTART_BUFFER_MS,
1871
+ verification: verificationAfterReconcile,
1872
+ latestVersion,
1873
+ output: updateOutput.slice(0, 500),
1874
+ updateAttemptId
1875
+ });
1876
+ await sleep(RESTART_BUFFER_MS);
1877
+ console.log(
1878
+ `[version] Plugin update verified (was ${before}), restarting gateway...`
1879
+ );
1880
+ const restartResult = await runCommand(["gateway", "restart"], 3e4);
1881
+ if (!restartResult.ok && !restartResult.timedOut) {
1882
+ console.log("[update_cmd_timeout] restart command failed unexpectedly", {
1883
+ updateAttemptId,
1884
+ exitCode: restartResult.exitCode,
1885
+ signal: restartResult.signal
1886
+ });
1887
+ }
1888
+ } catch (e) {
1889
+ const msg = e instanceof Error ? e.message : String(e);
1890
+ respond(false, {
1891
+ code: /VERIFY_FAILED/.test(msg) ? "VERIFY_FAILED" : /UPDATE_TIMEOUT/.test(msg) ? "UPDATE_TIMEOUT" : "UPDATE_FAILED",
1892
+ error: msg
1893
+ });
1894
+ } finally {
1895
+ updateInProgress = false;
1896
+ }
1897
+ }
1898
+ );
1899
+ }
1900
+
1901
+ // src/questions.ts
1902
+ var MARKER = "[HUMAN_INPUT_REQUIRED]";
1903
+ function normalizeEnvelope(raw) {
1904
+ if (raw.blocking !== true) return null;
1905
+ if (typeof raw.qid !== "string" || !raw.qid.trim()) return null;
1906
+ if (typeof raw.sessionKey !== "string" || !raw.sessionKey.trim()) return null;
1907
+ if (typeof raw.title !== "string" || !raw.title.trim()) return null;
1908
+ if (typeof raw.question !== "string" || !raw.question.trim()) return null;
1909
+ const agentId = typeof raw.agentId === "string" && raw.agentId.trim() ? raw.agentId.trim() : void 0;
1910
+ return {
1911
+ qid: raw.qid.trim(),
1912
+ sessionKey: raw.sessionKey.trim(),
1913
+ agentId,
1914
+ title: raw.title.trim(),
1915
+ question: raw.question.trim(),
1916
+ blocking: true
1917
+ };
1918
+ }
1919
+ function validateHumanQuestionEnvelope(text, options) {
1920
+ const markerIndex = text.indexOf(MARKER);
1921
+ if (markerIndex < 0) return { valid: false, markerFound: false };
1922
+ const tail = text.slice(markerIndex + MARKER.length).trimStart();
1923
+ const firstLine = tail.split("\n")[0]?.trim() ?? "";
1924
+ if (!firstLine.startsWith("{")) {
1925
+ return { valid: false, markerFound: true, errorCode: "missing_json_line" };
1926
+ }
1927
+ let parsed = null;
1928
+ try {
1929
+ parsed = JSON.parse(firstLine);
1930
+ } catch {
1931
+ return { valid: false, markerFound: true, errorCode: "invalid_json" };
1932
+ }
1933
+ const normalized = normalizeEnvelope(parsed);
1934
+ if (!normalized) {
1935
+ return { valid: false, markerFound: true, errorCode: "schema_invalid" };
1936
+ }
1937
+ const expectedSessionKey = options?.expectedSessionKey?.trim();
1938
+ const sessionKeyMismatch = !!(expectedSessionKey && normalized.sessionKey !== expectedSessionKey);
1939
+ return {
1940
+ valid: true,
1941
+ markerFound: true,
1942
+ normalizedEnvelope: normalized,
1943
+ sessionKeyMismatch
1944
+ };
1945
+ }
1946
+ function registerQuestionMethods(api) {
1947
+ api.registerGatewayMethod(
1948
+ "squad.questions.validate-envelope",
1949
+ async ({ params, respond }) => {
1950
+ const text = typeof params?.text === "string" ? params.text : "";
1951
+ const expectedSessionKey = typeof params?.expectedSessionKey === "string" ? params.expectedSessionKey : void 0;
1952
+ if (!text) {
1953
+ respond(false, { error: "Missing 'text' parameter" });
1954
+ return;
1955
+ }
1956
+ const result = validateHumanQuestionEnvelope(text, { expectedSessionKey });
1957
+ respond(true, result);
1958
+ }
1959
+ );
1960
+ }
1961
+
1962
+ // src/sessions.ts
1963
+ import fs9 from "fs";
1964
+ import path10 from "path";
1965
+ import crypto from "crypto";
1966
+
1967
+ // src/gateway-invoke.ts
1968
+ function asRecord(value) {
1969
+ return value && typeof value === "object" ? value : null;
1970
+ }
1971
+ function isInvoker(fn) {
1972
+ return typeof fn === "function";
1973
+ }
1974
+ function isUnknownGatewayMethodError(message) {
1975
+ return /unknown method|method .* unavailable|not found|invalid[_ ]request|does not exist/i.test(
1976
+ message
1977
+ );
1978
+ }
1979
+ async function callGatewayAny(ctx, api, method, params) {
1980
+ const ctxGateway = asRecord(ctx.gateway);
1981
+ const apiGateway = asRecord(api?.gateway);
1982
+ const candidates = [
1983
+ ctx.request,
1984
+ ctx.callGatewayMethod,
1985
+ ctx.gatewayRequest,
1986
+ ctx.invokeGatewayMethod,
1987
+ ctxGateway?.request,
1988
+ ctxGateway?.callGatewayMethod,
1989
+ api?.request,
1990
+ api?.callGatewayMethod,
1991
+ api?.gatewayRequest,
1992
+ api?.invokeGatewayMethod,
1993
+ apiGateway?.request,
1994
+ apiGateway?.callGatewayMethod
1995
+ ];
1996
+ let lastErr = null;
1997
+ for (const candidate of candidates) {
1998
+ if (!isInvoker(candidate)) continue;
1999
+ try {
2000
+ return await candidate(method, params);
2001
+ } catch (err2) {
2002
+ lastErr = err2;
2003
+ const msg = err2 instanceof Error ? err2.message : String(err2);
2004
+ if (isUnknownGatewayMethodError(msg)) {
2005
+ continue;
2006
+ }
2007
+ throw err2;
2008
+ }
2009
+ }
2010
+ if (lastErr) throw lastErr;
2011
+ throw new Error("Gateway method invocation API unavailable in plugin context");
2012
+ }
2013
+
2014
+ // src/sessions.ts
2015
+ function asRecord2(value) {
2016
+ return value && typeof value === "object" ? value : null;
2017
+ }
2018
+ function extractSessionKey(value) {
2019
+ const record = asRecord2(value);
2020
+ if (!record) return null;
2021
+ if (typeof record.key === "string" && record.key.trim()) {
2022
+ return record.key.trim();
2023
+ }
2024
+ const nestedKey = asRecord2(record.key);
2025
+ if (nestedKey && typeof nestedKey.key === "string" && nestedKey.key.trim()) {
2026
+ return nestedKey.key.trim();
2027
+ }
2028
+ if (typeof record.sessionKey === "string" && record.sessionKey.trim()) {
2029
+ return record.sessionKey.trim();
2030
+ }
2031
+ const nestedSession = asRecord2(record.session);
2032
+ if (nestedSession && typeof nestedSession.key === "string" && nestedSession.key.trim()) {
2033
+ return nestedSession.key.trim();
2034
+ }
2035
+ return null;
2036
+ }
2037
+ function parseAgentIdFromSessionKey(sessionKey) {
2038
+ const m = sessionKey.match(/^agent:([^:]+):/);
2039
+ return m?.[1] ?? null;
2040
+ }
2041
+ function ensureDir(dirPath) {
2042
+ fs9.mkdirSync(dirPath, { recursive: true });
2043
+ }
2044
+ function readSessionsMap(sessionsJsonPath) {
2045
+ try {
2046
+ const raw = fs9.readFileSync(sessionsJsonPath, "utf-8");
2047
+ const parsed = JSON.parse(raw);
2048
+ if (parsed && typeof parsed === "object") {
2049
+ return parsed;
2050
+ }
2051
+ } catch {
2052
+ }
2053
+ return {};
2054
+ }
2055
+ function writeSessionsMap(sessionsJsonPath, sessionsMap) {
2056
+ fs9.writeFileSync(sessionsJsonPath, JSON.stringify(sessionsMap, null, 2), "utf-8");
2057
+ }
2058
+ function createSessionOnDisk(input) {
2059
+ const requestedSessionKey = input.requestedSessionKey?.trim();
2060
+ const requestedAgentId = input.requestedAgentId?.trim();
2061
+ const derivedAgentId = requestedAgentId || (requestedSessionKey ? parseAgentIdFromSessionKey(requestedSessionKey) : null) || "main";
2062
+ const sessionKey = requestedSessionKey || `agent:${derivedAgentId}:${crypto.randomUUID()}`;
2063
+ const stateDir = getOpenclawStateDir();
2064
+ const sessionsDir = path10.join(stateDir, "agents", derivedAgentId, "sessions");
2065
+ const sessionsJsonPath = path10.join(sessionsDir, "sessions.json");
2066
+ ensureDir(sessionsDir);
2067
+ const now = Date.now();
2068
+ const sessionsMap = readSessionsMap(sessionsJsonPath);
2069
+ const existing = sessionsMap[sessionKey];
2070
+ const sessionId = typeof existing?.sessionId === "string" && existing.sessionId.trim() ? existing.sessionId : crypto.randomUUID();
2071
+ const jsonlPath = path10.join(sessionsDir, `${sessionId}.jsonl`);
2072
+ sessionsMap[sessionKey] = {
2073
+ ...existing ?? {},
2074
+ sessionId,
2075
+ updatedAt: now,
2076
+ label: typeof existing?.label === "string" && existing.label.trim() ? existing.label : "New Session",
2077
+ totalTokens: typeof existing?.totalTokens === "number" ? existing.totalTokens : 0,
2078
+ lastMessageRole: existing?.lastMessageRole ?? "assistant",
2079
+ lastAssistantHasText: typeof existing?.lastAssistantHasText === "boolean" ? existing.lastAssistantHasText : true
2080
+ };
2081
+ writeSessionsMap(sessionsJsonPath, sessionsMap);
2082
+ if (!fs9.existsSync(jsonlPath)) {
2083
+ fs9.writeFileSync(jsonlPath, "", "utf-8");
2084
+ }
2085
+ return {
2086
+ key: sessionKey,
2087
+ sessionKey,
2088
+ sessionId,
2089
+ agentId: derivedAgentId,
2090
+ displayName: sessionKey,
2091
+ status: "active",
2092
+ createdAt: now
2093
+ };
2094
+ }
2095
+ function registerSessionMethods(api) {
2096
+ api.registerGatewayMethod(
2097
+ "squad.sessions.create",
2098
+ async (ctx) => {
2099
+ const { params, respond } = ctx;
2100
+ const sessionKey = params?.sessionKey;
2101
+ const agentId = params?.agentId;
2102
+ if (sessionKey !== void 0) {
2103
+ if (typeof sessionKey !== "string" || !sessionKey.trim()) {
2104
+ respond(false, { error: "Invalid 'sessionKey' parameter (must be a non-empty string when provided)" });
2105
+ return;
2106
+ }
2107
+ }
2108
+ if (agentId !== void 0) {
2109
+ if (typeof agentId !== "string" || !agentId.trim()) {
2110
+ respond(false, { error: "Invalid 'agentId' parameter (must be a non-empty string when provided)" });
2111
+ return;
2112
+ }
2113
+ }
2114
+ const createParams = {
2115
+ ...typeof sessionKey === "string" ? { sessionKey: sessionKey.trim() } : {},
2116
+ ...typeof agentId === "string" ? { agentId: agentId.trim() } : {}
2117
+ };
2118
+ try {
2119
+ const nativeResult = await callGatewayAny(
2120
+ ctx,
2121
+ api,
2122
+ "sessions.create",
2123
+ createParams
2124
+ );
2125
+ const normalizedKey = extractSessionKey(nativeResult);
2126
+ if (!normalizedKey) {
2127
+ const local = createSessionOnDisk({
2128
+ requestedSessionKey: typeof sessionKey === "string" ? sessionKey : void 0,
2129
+ requestedAgentId: typeof agentId === "string" ? agentId : void 0
2130
+ });
2131
+ respond(true, local);
2132
+ return;
2133
+ }
2134
+ const nativeRecord = asRecord2(nativeResult);
2135
+ respond(true, {
2136
+ ...nativeRecord ?? {},
2137
+ key: normalizedKey
2138
+ });
2139
+ } catch (err2) {
2140
+ const msg = err2 instanceof Error ? err2.message : String(err2);
2141
+ if (/gateway method invocation api unavailable in plugin context/i.test(msg)) {
2142
+ try {
2143
+ const local = createSessionOnDisk({
2144
+ requestedSessionKey: typeof sessionKey === "string" ? sessionKey : void 0,
2145
+ requestedAgentId: typeof agentId === "string" ? agentId : void 0
2146
+ });
2147
+ respond(true, local);
2148
+ return;
2149
+ } catch (fallbackErr) {
2150
+ const fallbackMsg = fallbackErr instanceof Error ? fallbackErr.message : String(fallbackErr);
2151
+ respond(false, {
2152
+ error: `Failed to create session (local fallback): ${fallbackMsg}`.slice(0, 500)
2153
+ });
2154
+ return;
2155
+ }
2156
+ }
2157
+ respond(false, {
2158
+ error: `Failed to create session: ${msg}`.slice(0, 500)
2159
+ });
2160
+ }
2161
+ }
2162
+ );
2163
+ }
2164
+
2165
+ // src/agent-send.ts
2166
+ import fs10 from "fs";
2167
+ import path11 from "path";
2168
+ import { execFile as execFile2 } from "child_process";
2169
+ import { promisify } from "util";
2170
+ var ASSET_REFERENCE_RE = /\{\{asset:([^}]+)\}\}/g;
2171
+ var RETRY_INTERVAL_MS = 1e3;
2172
+ var RETRY_BUDGET_MS = 2e4;
2173
+ var CLI_AGENT_TIMEOUT_MS = 3e4;
2174
+ var execFileAsync = promisify(execFile2);
2175
+ var IMAGE_MIME_BY_EXT = {
2176
+ ".png": "image/png",
2177
+ ".jpg": "image/jpeg",
2178
+ ".jpeg": "image/jpeg",
2179
+ ".gif": "image/gif",
2180
+ ".webp": "image/webp",
2181
+ ".bmp": "image/bmp",
2182
+ ".svg": "image/svg+xml",
2183
+ ".ico": "image/x-icon",
2184
+ ".tif": "image/tiff",
2185
+ ".tiff": "image/tiff",
2186
+ ".avif": "image/avif"
2187
+ };
2188
+ function asRecord3(value) {
2189
+ return value && typeof value === "object" ? value : null;
2190
+ }
2191
+ function sleep2(ms) {
2192
+ return new Promise((resolve) => setTimeout(resolve, ms));
2193
+ }
2194
+ function pathWithin(root, candidate) {
2195
+ return candidate === root || candidate.startsWith(`${root}${path11.sep}`);
2196
+ }
2197
+ function parseAssetPathsFromMessage(message) {
2198
+ const result = [];
2199
+ const re = new RegExp(ASSET_REFERENCE_RE.source, "g");
2200
+ let match;
2201
+ while ((match = re.exec(message)) !== null) {
2202
+ const rawPath = String(match[1] ?? "").trim();
2203
+ if (rawPath) result.push(rawPath);
2204
+ }
2205
+ return result;
2206
+ }
2207
+ function parseAssetPathsFromContext(params) {
2208
+ const context = asRecord3(params.context);
2209
+ const assets = context?.assets;
2210
+ if (!Array.isArray(assets)) return [];
2211
+ const result = [];
2212
+ for (const entry of assets) {
2213
+ if (typeof entry === "string") {
2214
+ const trimmed = entry.trim();
2215
+ if (trimmed) result.push(trimmed);
2216
+ continue;
2217
+ }
2218
+ const record = asRecord3(entry);
2219
+ if (!record) continue;
2220
+ const assetPath = typeof record.path === "string" ? record.path.trim() : "";
2221
+ if (assetPath) result.push(assetPath);
2222
+ }
2223
+ return result;
2224
+ }
2225
+ function resolveOpenClawPath(rawPath, stateDir) {
2226
+ const trimmed = rawPath.trim();
2227
+ if (!trimmed) return "";
2228
+ if (trimmed === "~/.openclaw") return stateDir;
2229
+ if (trimmed.startsWith("~/.openclaw/")) {
2230
+ return path11.join(stateDir, trimmed.slice("~/.openclaw/".length));
2231
+ }
2232
+ return trimmed;
2233
+ }
2234
+ function resolveMediaAssetCandidate(rawPath, stateDir) {
2235
+ const mapped = resolveOpenClawPath(rawPath, stateDir);
2236
+ if (!mapped) return null;
2237
+ const mediaRoot = path11.resolve(path11.join(stateDir, "media"));
2238
+ const resolved = path11.resolve(mapped);
2239
+ if (!pathWithin(mediaRoot, resolved)) return null;
2240
+ return {
2241
+ sourcePath: rawPath,
2242
+ resolvedPath: resolved
2243
+ };
2244
+ }
2245
+ function dedupeAssetCandidates(candidates) {
2246
+ const seen = /* @__PURE__ */ new Set();
2247
+ const deduped = [];
2248
+ for (const candidate of candidates) {
2249
+ if (seen.has(candidate.resolvedPath)) continue;
2250
+ seen.add(candidate.resolvedPath);
2251
+ deduped.push(candidate);
2252
+ }
2253
+ return deduped;
2254
+ }
2255
+ async function waitForAvailableAssets(candidates) {
2256
+ if (candidates.length === 0) return [];
2257
+ const pending = new Map(
2258
+ candidates.map((candidate) => [candidate.resolvedPath, candidate])
2259
+ );
2260
+ const deadline = Date.now() + RETRY_BUDGET_MS;
2261
+ while (pending.size > 0) {
2262
+ for (const [resolvedPath, candidate] of pending.entries()) {
2263
+ try {
2264
+ const stat = await fs10.promises.stat(resolvedPath);
2265
+ if (stat.isFile()) {
2266
+ pending.delete(resolvedPath);
2267
+ }
2268
+ } catch {
2269
+ }
2270
+ }
2271
+ if (pending.size === 0) break;
2272
+ if (Date.now() >= deadline) break;
2273
+ await sleep2(RETRY_INTERVAL_MS);
2274
+ }
2275
+ return Array.from(pending.values());
2276
+ }
2277
+ function resolveImageMimeType(filePath) {
2278
+ const ext = path11.extname(filePath).toLowerCase();
2279
+ return IMAGE_MIME_BY_EXT[ext] ?? "application/octet-stream";
2280
+ }
2281
+ function extractGatewayToken(config) {
2282
+ const gateway = asRecord3(config.gateway);
2283
+ const auth = asRecord3(gateway?.auth);
2284
+ const remote = asRecord3(gateway?.remote);
2285
+ const fromAuth = typeof auth?.token === "string" ? auth.token.trim() : "";
2286
+ if (fromAuth) return fromAuth;
2287
+ const fromRemote = typeof remote?.token === "string" ? remote.token.trim() : "";
2288
+ if (fromRemote) return fromRemote;
2289
+ const fromGateway = typeof gateway?.token === "string" ? gateway.token.trim() : "";
2290
+ if (fromGateway) return fromGateway;
2291
+ return void 0;
2292
+ }
2293
+ function resolveGatewayUrlFromConfig(config) {
2294
+ const gateway = asRecord3(config.gateway);
2295
+ const host = typeof gateway?.host === "string" && gateway.host.trim() ? gateway.host.trim() : "127.0.0.1";
2296
+ const port = typeof gateway?.port === "number" && Number.isFinite(gateway.port) ? gateway.port : 18789;
2297
+ const tlsRaw = gateway?.tls;
2298
+ const tls = typeof tlsRaw === "boolean" ? tlsRaw : Boolean(asRecord3(tlsRaw)?.enabled);
2299
+ return `${tls ? "wss" : "ws"}://${host}:${port}`;
2300
+ }
2301
+ function extractCliErrorMessage(payload) {
2302
+ const payloadRecord = asRecord3(payload);
2303
+ if (typeof payloadRecord?.error === "string") return payloadRecord.error;
2304
+ const errorRecord = asRecord3(payloadRecord?.error);
2305
+ if (typeof errorRecord?.message === "string") return errorRecord.message;
2306
+ const nestedPayload = asRecord3(payloadRecord?.payload);
2307
+ if (typeof nestedPayload?.error === "string") return nestedPayload.error;
2308
+ return "";
2309
+ }
2310
+ function parseCliJson(raw) {
2311
+ const trimmed = raw.trim();
2312
+ if (!trimmed) return null;
2313
+ try {
2314
+ const parsed = JSON.parse(trimmed);
2315
+ return asRecord3(parsed);
2316
+ } catch {
2317
+ return null;
2318
+ }
2319
+ }
2320
+ function isGatewayCallUnavailableError(message) {
2321
+ return /gateway method invocation api unavailable in plugin context/i.test(message);
2322
+ }
2323
+ async function callAgentViaCli(stateDir, params) {
2324
+ const configPath = path11.join(stateDir, "openclaw.json");
2325
+ const configRaw = fs10.readFileSync(configPath, "utf-8");
2326
+ const parsedConfig = JSON.parse(configRaw);
2327
+ const token = extractGatewayToken(parsedConfig);
2328
+ const url = resolveGatewayUrlFromConfig(parsedConfig);
2329
+ const args = [
2330
+ "gateway",
2331
+ "call",
2332
+ "agent",
2333
+ "--json",
2334
+ "--timeout",
2335
+ String(CLI_AGENT_TIMEOUT_MS),
2336
+ "--params",
2337
+ JSON.stringify(params),
2338
+ "--url",
2339
+ url
2340
+ ];
2341
+ if (token) {
2342
+ args.push("--token", token);
2343
+ }
2344
+ try {
2345
+ const { stdout } = await execFileAsync("openclaw", args, {
2346
+ env: process.env,
2347
+ maxBuffer: 10 * 1024 * 1024
2348
+ });
2349
+ const json2 = parseCliJson(stdout);
2350
+ if (!json2) {
2351
+ throw new Error("openclaw gateway call agent returned non-JSON output");
2352
+ }
2353
+ if (json2.ok === false) {
2354
+ const msg = extractCliErrorMessage(json2) || JSON.stringify(json2.error ?? json2);
2355
+ throw new Error(msg);
2356
+ }
2357
+ if ("payload" in json2) return json2.payload;
2358
+ return json2;
2359
+ } catch (err2) {
2360
+ const error = err2;
2361
+ const stdoutJson = parseCliJson(typeof error.stdout === "string" ? error.stdout : "");
2362
+ if (stdoutJson) {
2363
+ if (stdoutJson.ok === false) {
2364
+ const msg = extractCliErrorMessage(stdoutJson) || JSON.stringify(stdoutJson.error ?? stdoutJson);
2365
+ throw new Error(msg);
2366
+ }
2367
+ if ("payload" in stdoutJson) return stdoutJson.payload;
2368
+ return stdoutJson;
2369
+ }
2370
+ const stderr = typeof error.stderr === "string" ? error.stderr.trim() : "";
2371
+ const message = stderr || (typeof error.message === "string" ? error.message : String(err2));
2372
+ throw new Error(message);
2373
+ }
2374
+ }
2375
+ async function buildImageAttachments(candidates) {
2376
+ const attachments = [];
2377
+ for (const candidate of candidates) {
2378
+ const buffer = await fs10.promises.readFile(candidate.resolvedPath);
2379
+ attachments.push({
2380
+ type: "image",
2381
+ mimeType: resolveImageMimeType(candidate.resolvedPath),
2382
+ fileName: path11.basename(candidate.resolvedPath),
2383
+ content: buffer.toString("base64")
2384
+ });
2385
+ }
2386
+ return attachments;
2387
+ }
2388
+ function registerAgentSendMethod(api) {
2389
+ api.registerGatewayMethod(
2390
+ "squad.agent.send",
2391
+ async (ctx) => {
2392
+ const { params, respond } = ctx;
2393
+ const message = typeof params?.message === "string" ? params.message : "";
2394
+ if (!message.trim()) {
2395
+ respond(false, { error: "Invalid 'message' parameter (must be a non-empty string)" });
2396
+ return;
2397
+ }
2398
+ const stateDir = getOpenclawStateDir();
2399
+ const rawAssetPaths = [
2400
+ ...parseAssetPathsFromMessage(message),
2401
+ ...parseAssetPathsFromContext(params)
2402
+ ];
2403
+ const candidates = dedupeAssetCandidates(
2404
+ rawAssetPaths.map((rawPath) => resolveMediaAssetCandidate(rawPath, stateDir)).filter((candidate) => candidate !== null)
2405
+ );
2406
+ if (rawAssetPaths.length > 0 && candidates.length === 0) {
2407
+ respond(false, {
2408
+ error: "Referenced image assets must be under ~/.openclaw/media/"
2409
+ });
2410
+ return;
2411
+ }
2412
+ if (candidates.length > 0) {
2413
+ const missing = await waitForAvailableAssets(candidates);
2414
+ if (missing.length > 0) {
2415
+ const missingPaths = missing.map((entry) => entry.sourcePath).join(", ");
2416
+ respond(false, {
2417
+ error: `Referenced image assets not available after ${RETRY_BUDGET_MS}ms: ${missingPaths}`
2418
+ });
2419
+ return;
2420
+ }
2421
+ }
2422
+ try {
2423
+ const imageAttachments = await buildImageAttachments(candidates);
2424
+ const existingAttachments = Array.isArray(params.attachments) ? params.attachments : [];
2425
+ const forwardedParams = {
2426
+ ...params,
2427
+ attachments: [...existingAttachments, ...imageAttachments]
2428
+ };
2429
+ let nativeResult;
2430
+ try {
2431
+ nativeResult = await callGatewayAny(
2432
+ ctx,
2433
+ api,
2434
+ "agent",
2435
+ forwardedParams
2436
+ );
2437
+ } catch (err2) {
2438
+ const msg = err2 instanceof Error ? err2.message : String(err2);
2439
+ if (!isGatewayCallUnavailableError(msg)) {
2440
+ throw err2;
2441
+ }
2442
+ nativeResult = await callAgentViaCli(stateDir, forwardedParams);
2443
+ }
2444
+ respond(true, nativeResult);
2445
+ } catch (err2) {
2446
+ const msg = err2 instanceof Error ? err2.message : String(err2);
2447
+ respond(false, { error: msg });
2448
+ }
2449
+ }
2450
+ );
2451
+ }
2452
+
2453
+ // src/shared-api.ts
2454
+ var CORE_TOOLS = [
2455
+ "exec",
2456
+ "bash",
2457
+ "process",
2458
+ "read",
2459
+ "write",
2460
+ "edit",
2461
+ "apply_patch",
2462
+ "web_search",
2463
+ "web_fetch",
2464
+ "browser",
2465
+ "canvas",
2466
+ "nodes",
2467
+ "image",
2468
+ "message",
2469
+ "cron",
2470
+ "gateway",
2471
+ "sessions_list",
2472
+ "sessions_history",
2473
+ "sessions_send",
2474
+ "sessions_spawn",
2475
+ "session_status",
2476
+ "agents_list",
2477
+ "memory_search"
2478
+ ];
2479
+ var CORE_TOOL_GROUPS = [
2480
+ "group:fs",
2481
+ "group:runtime",
2482
+ "group:sessions",
2483
+ "group:memory",
2484
+ "group:web",
2485
+ "group:ui",
2486
+ "group:automation",
2487
+ "group:messaging",
2488
+ "group:nodes"
2489
+ ];
2490
+ function registerSquadSharedApi(api, onFsChange) {
2491
+ const toolExecutors = /* @__PURE__ */ new Map();
2492
+ const origRegisterTool = api.registerTool.bind(api);
2493
+ api.registerTool = (toolDef) => {
2494
+ if (typeof toolDef.name === "string" && typeof toolDef.execute === "function") {
2495
+ toolExecutors.set(toolDef.name, toolDef.execute);
2496
+ }
2497
+ return origRegisterTool(toolDef);
2498
+ };
2499
+ registerEntityTools(api, onFsChange);
2500
+ registerFilesystemTools(api);
2501
+ registerSqlTools(api);
2502
+ registerVersionMethods(api);
2503
+ registerAgentMethods(api);
2504
+ registerQuestionMethods(api);
2505
+ registerSessionMethods(api);
2506
+ registerAgentSendMethod(api);
2507
+ const invokeTool = async (tool, args) => {
2508
+ const executeFn = toolExecutors.get(tool);
2509
+ if (!executeFn) {
2510
+ throw new Error(`Unknown tool: ${tool}`);
2511
+ }
2512
+ return executeFn(`internal-${Date.now()}`, args);
2513
+ };
2514
+ const listTools = () => [...CORE_TOOLS, ...CORE_TOOL_GROUPS, ...Array.from(toolExecutors.keys())];
2515
+ const registerCoreGatewayMethods = () => {
2516
+ api.registerGatewayMethod(
2517
+ "tools.invoke",
2518
+ async ({ params, respond }) => {
2519
+ const tool = params?.tool;
2520
+ const args = params?.args ?? {};
2521
+ if (!tool) {
2522
+ respond(false, { errorMessage: "Missing 'tool' parameter" });
2523
+ return;
2524
+ }
2525
+ try {
2526
+ const result = await invokeTool(tool, args);
2527
+ respond(true, result);
2528
+ } catch (err2) {
2529
+ const msg = err2 instanceof Error ? err2.message : String(err2);
2530
+ respond(false, { errorMessage: msg });
2531
+ }
2532
+ }
2533
+ );
2534
+ api.registerGatewayMethod(
2535
+ "tools.list",
2536
+ async ({ respond }) => {
2537
+ respond(true, { tools: listTools() });
2538
+ }
2539
+ );
2540
+ api.registerGatewayMethod(
2541
+ "squad.layout.get",
2542
+ async ({ respond }) => {
2543
+ try {
2544
+ const layout = resolveGatewayLayout();
2545
+ respond(true, layout);
2546
+ } catch (err2) {
2547
+ const msg = err2 instanceof Error ? err2.message : String(err2);
2548
+ respond(false, { errorMessage: msg });
2549
+ }
2550
+ }
2551
+ );
2552
+ };
2553
+ return {
2554
+ invokeTool,
2555
+ listTools,
2556
+ registerCoreGatewayMethods
2557
+ };
2558
+ }
2559
+
2560
+ // src/migrations/runner.ts
2561
+ import fs12 from "fs";
2562
+ import path13 from "path";
2563
+
2564
+ // src/migrations/001-enable-main-subagent-access.ts
2565
+ import fs11 from "fs";
2566
+ import path12 from "path";
2567
+ function asRecord4(value) {
2568
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
2569
+ return value;
2570
+ }
2571
+ function mergeStringArrayWithWildcard(value) {
2572
+ const next = /* @__PURE__ */ new Set();
2573
+ if (Array.isArray(value)) {
2574
+ for (const entry of value) {
2575
+ if (typeof entry === "string" && entry.trim()) next.add(entry.trim());
2576
+ }
2577
+ }
2578
+ next.add("*");
2579
+ return Array.from(next);
2580
+ }
2581
+ function patchConfigOnDisk() {
2582
+ const configPath = path12.join(getOpenclawStateDir(), "openclaw.json");
2583
+ const raw = fs11.readFileSync(configPath, "utf-8");
2584
+ const parsed = JSON.parse(raw);
2585
+ const agents = asRecord4(parsed.agents) ?? {};
2586
+ const defaults = asRecord4(agents.defaults) ?? {};
2587
+ const subagentsDefaults = asRecord4(defaults.subagents) ?? {};
2588
+ defaults.maxConcurrent = 4;
2589
+ defaults.subagents = {
2590
+ ...subagentsDefaults,
2591
+ maxConcurrent: 8
2592
+ };
2593
+ const listRaw = Array.isArray(agents.list) ? agents.list : [];
2594
+ const list = listRaw.map((entry) => asRecord4(entry)).filter((entry) => Boolean(entry));
2595
+ const mainIndex = list.findIndex((entry) => entry.id === "main");
2596
+ const existingMain = mainIndex >= 0 ? list[mainIndex] : {};
2597
+ const existingIdentity = asRecord4(existingMain.identity) ?? {};
2598
+ const existingTools = asRecord4(existingMain.tools) ?? {};
2599
+ const existingSubagents = asRecord4(existingMain.subagents) ?? {};
2600
+ const nextMain = {
2601
+ ...existingMain,
2602
+ id: "main",
2603
+ identity: {
2604
+ ...existingIdentity,
2605
+ name: typeof existingIdentity.name === "string" && existingIdentity.name.trim() ? existingIdentity.name : "Pepper"
2606
+ },
2607
+ tools: {
2608
+ ...existingTools,
2609
+ allow: mergeStringArrayWithWildcard(existingTools.allow)
2610
+ },
2611
+ subagents: {
2612
+ ...existingSubagents,
2613
+ allowAgents: mergeStringArrayWithWildcard(existingSubagents.allowAgents)
2614
+ }
2615
+ };
2616
+ if (mainIndex >= 0) list[mainIndex] = nextMain;
2617
+ else list.push(nextMain);
2618
+ parsed.agents = {
2619
+ ...agents,
2620
+ defaults,
2621
+ list
2622
+ };
2623
+ fs11.writeFileSync(configPath, `${JSON.stringify(parsed, null, 2)}
2624
+ `, "utf-8");
2625
+ }
2626
+ var migration = {
2627
+ id: "001-enable-main-subagent-access",
2628
+ description: "Enable full main-agent tool and subagent spawning defaults",
2629
+ run: async ({ gatewayCall }) => {
2630
+ const isGatewayApiUnavailableError = (error) => {
2631
+ const msg = error instanceof Error ? error.message : String(error);
2632
+ return /gateway method invocation api unavailable in plugin context/i.test(msg);
2633
+ };
2634
+ const doPatch = async (baseHash) => {
2635
+ await gatewayCall("config.patch", {
2636
+ ...baseHash ? { baseHash } : {},
2637
+ raw: JSON.stringify({
2638
+ agents: {
2639
+ defaults: {
2640
+ maxConcurrent: 4,
2641
+ subagents: {
2642
+ maxConcurrent: 8
2643
+ }
2644
+ },
2645
+ list: [
2646
+ {
2647
+ id: "main",
2648
+ identity: {
2649
+ name: "Pepper"
2650
+ },
2651
+ tools: {
2652
+ allow: ["*"]
2653
+ },
2654
+ subagents: {
2655
+ allowAgents: ["*"]
2656
+ }
2657
+ }
2658
+ ]
2659
+ }
2660
+ })
2661
+ });
2662
+ };
2663
+ let snapshot;
2664
+ try {
2665
+ snapshot = await gatewayCall("config.get", {});
2666
+ } catch (error) {
2667
+ if (isGatewayApiUnavailableError(error)) {
2668
+ patchConfigOnDisk();
2669
+ return;
2670
+ }
2671
+ throw error;
2672
+ }
2673
+ try {
2674
+ await doPatch(snapshot?.hash);
2675
+ } catch (firstErr) {
2676
+ const msg = firstErr instanceof Error ? firstErr.message : String(firstErr);
2677
+ if (isGatewayApiUnavailableError(firstErr)) {
2678
+ patchConfigOnDisk();
2679
+ return;
2680
+ }
2681
+ if (!/config changed since last load/i.test(msg)) throw firstErr;
2682
+ try {
2683
+ snapshot = await gatewayCall("config.get", {});
2684
+ } catch (secondErr) {
2685
+ if (isGatewayApiUnavailableError(secondErr)) {
2686
+ patchConfigOnDisk();
2687
+ return;
2688
+ }
2689
+ throw secondErr;
2690
+ }
2691
+ await doPatch(snapshot?.hash);
2692
+ }
2693
+ }
2694
+ };
2695
+ var enable_main_subagent_access_default = migration;
2696
+
2697
+ // src/migrations/002-backfill-agent-auth-profiles.ts
2698
+ var migration2 = {
2699
+ id: "002-backfill-agent-auth-profiles",
2700
+ description: "Ensure non-main agents have auth-profiles copied from main when missing",
2701
+ run: async () => {
2702
+ const copied = backfillAgentAuthProfiles();
2703
+ if (copied.length > 0) {
2704
+ console.log(`[startup-migrations] auth profile backfill copied: ${copied.join(", ")}`);
2705
+ }
2706
+ }
2707
+ };
2708
+ var backfill_agent_auth_profiles_default = migration2;
2709
+
2710
+ // src/migrations/index.ts
2711
+ var STARTUP_MIGRATIONS = [
2712
+ enable_main_subagent_access_default,
2713
+ backfill_agent_auth_profiles_default
2714
+ // Append new startup migrations here.
2715
+ ];
2716
+
2717
+ // src/migrations/runner.ts
2718
+ var MIGRATIONS_DIR = path13.join(getOpenclawStateDir(), "squad-ceo-data");
2719
+ var MIGRATIONS_PATH = path13.join(MIGRATIONS_DIR, "migrations.json");
2720
+ function defaultState() {
2721
+ return {
2722
+ version: 1,
2723
+ completed: []
2724
+ };
2725
+ }
2726
+ function readState() {
2727
+ try {
2728
+ const raw = fs12.readFileSync(MIGRATIONS_PATH, "utf-8");
2729
+ const parsed = JSON.parse(raw);
2730
+ if (!Array.isArray(parsed.completed)) return defaultState();
2731
+ return {
2732
+ version: 1,
2733
+ completed: parsed.completed.filter((row) => row && typeof row.id === "string" && typeof row.completedAt === "string").map((row) => ({ id: row.id, completedAt: row.completedAt }))
2734
+ };
2735
+ } catch {
2736
+ return defaultState();
2737
+ }
2738
+ }
2739
+ function writeState(state) {
2740
+ fs12.mkdirSync(MIGRATIONS_DIR, { recursive: true });
2741
+ fs12.writeFileSync(MIGRATIONS_PATH, JSON.stringify(state, null, 2), "utf-8");
2742
+ }
2743
+ function makeGatewayCaller(api) {
2744
+ return async (method, params = {}) => {
2745
+ const apiRequest = api?.request;
2746
+ if (typeof apiRequest === "function") {
2747
+ return apiRequest(method, params);
2748
+ }
2749
+ const apiCallGatewayMethod = api?.callGatewayMethod;
2750
+ if (typeof apiCallGatewayMethod === "function") {
2751
+ return apiCallGatewayMethod(method, params);
2752
+ }
2753
+ throw new Error("Gateway method invocation API unavailable in plugin context");
2754
+ };
2755
+ }
2756
+ async function runMigration(state, migration3, gatewayCall) {
2757
+ if (state.completed.some((row) => row.id === migration3.id)) {
2758
+ return { state, applied: false };
2759
+ }
2760
+ await migration3.run({ gatewayCall });
2761
+ return {
2762
+ applied: true,
2763
+ state: {
2764
+ ...state,
2765
+ completed: [
2766
+ ...state.completed,
2767
+ {
2768
+ id: migration3.id,
2769
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
2770
+ }
2771
+ ]
2772
+ }
2773
+ };
2774
+ }
2775
+ async function runStartupMigrations(api) {
2776
+ const gatewayCall = makeGatewayCaller(api);
2777
+ let state = readState();
2778
+ for (const migration3 of STARTUP_MIGRATIONS) {
2779
+ try {
2780
+ const result = await runMigration(state, migration3, gatewayCall);
2781
+ state = result.state;
2782
+ if (result.applied) {
2783
+ writeState(state);
2784
+ console.log(`[startup-migrations] applied: ${migration3.id}`);
2785
+ }
2786
+ } catch (err2) {
2787
+ const msg = err2 instanceof Error ? err2.message : String(err2);
2788
+ console.warn(`[startup-migrations] failed: ${migration3.id}: ${msg}`);
2789
+ break;
2790
+ }
2791
+ }
2792
+ }
2793
+
2794
+ // src/http-routes.ts
2795
+ import crypto2 from "crypto";
2796
+ var DEFAULT_ALLOWED_ORIGINS = [
2797
+ "https://squad.ceo",
2798
+ "https://www.squad.ceo",
2799
+ "https://squad.pages.dev",
2800
+ "http://localhost:5174",
2801
+ "http://localhost:5800"
2802
+ ];
2803
+ var PAIRING_REQUEST_METHODS = [
2804
+ "node.pair.request",
2805
+ "devices.pair.request",
2806
+ "device.pair.request"
2807
+ ];
2808
+ var PAIRING_STATUS_METHODS = [
2809
+ "node.pair.status",
2810
+ "devices.pair.status",
2811
+ "device.pair.status",
2812
+ "node.pair.get"
2813
+ ];
2814
+ var PROOF_MAX_SKEW_MS = 5 * 60 * 1e3;
2815
+ var DEFAULT_PAIRING_TTL_MS = 15 * 60 * 1e3;
2816
+ var NONCE_TTL_MS = 10 * 60 * 1e3;
2817
+ var LOCATOR_REFRESH_INTERVAL_MS = 15 * 60 * 1e3;
2818
+ function asRecord5(value) {
2819
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
2820
+ return value;
2821
+ }
2822
+ function pickString(value) {
2823
+ if (typeof value !== "string") return null;
2824
+ const trimmed = value.trim();
2825
+ return trimmed.length > 0 ? trimmed : null;
2826
+ }
2827
+ function pickNumber(value) {
2828
+ if (typeof value !== "number" || !Number.isFinite(value)) return null;
2829
+ return value;
2830
+ }
2831
+ function json(body, status = 200) {
2832
+ return new Response(JSON.stringify(body), {
2833
+ status,
2834
+ headers: {
2835
+ "Content-Type": "application/json",
2836
+ "Cache-Control": "no-store"
2837
+ }
2838
+ });
2839
+ }
2840
+ function withCors(request, response, allowMethods = "GET, POST, OPTIONS") {
2841
+ const headers = new Headers(response.headers);
2842
+ const origin = request.headers.get("origin");
2843
+ const allowedOrigin = resolveAllowedOrigin(origin);
2844
+ if (allowedOrigin) {
2845
+ headers.set("Access-Control-Allow-Origin", allowedOrigin);
2846
+ headers.set("Access-Control-Allow-Methods", allowMethods);
2847
+ headers.set("Access-Control-Allow-Headers", "Content-Type");
2848
+ headers.set("Vary", "Origin");
2849
+ }
2850
+ return new Response(response.body, {
2851
+ status: response.status,
2852
+ statusText: response.statusText,
2853
+ headers
2854
+ });
2855
+ }
2856
+ function jsonError(request, code, message, status = 500, extra = {}) {
2857
+ return withCors(request, json({ ok: false, code, error: message, ...extra }, status));
2858
+ }
2859
+ function resolveAllowedOrigin(origin) {
2860
+ if (!origin) return null;
2861
+ const configured = process.env.SQUAD_ALLOWED_ORIGINS ? process.env.SQUAD_ALLOWED_ORIGINS.split(",").map((item) => item.trim()).filter(Boolean) : [];
2862
+ const allowed = configured.length > 0 ? configured : DEFAULT_ALLOWED_ORIGINS;
2863
+ return allowed.includes(origin) ? origin : null;
2864
+ }
2865
+ function isTailnetContext(request) {
2866
+ if (/^(1|true)$/i.test(process.env.SQUAD_ALLOW_NON_TAILNET_INTERNAL ?? "")) {
2867
+ return true;
2868
+ }
2869
+ const hasTailnetHeader = !!request.headers.get("x-tailscale-user-login") || !!request.headers.get("x-tailscale-user-name") || !!request.headers.get("x-tailscale-user-email") || !!request.headers.get("x-tailscale-node-name") || !!request.headers.get("x-tailscale-tailnet");
2870
+ if (hasTailnetHeader) return true;
2871
+ try {
2872
+ const url = new URL(request.url);
2873
+ if (url.hostname.toLowerCase().endsWith(".ts.net")) return true;
2874
+ } catch {
2875
+ }
2876
+ const forwardedHost = request.headers.get("x-forwarded-host")?.toLowerCase() ?? "";
2877
+ return forwardedHost.endsWith(".ts.net");
2878
+ }
2879
+ function ensureOriginAllowed(request) {
2880
+ const origin = request.headers.get("origin");
2881
+ if (!origin) return null;
2882
+ return resolveAllowedOrigin(origin);
2883
+ }
2884
+ function decodeBase64Url(value) {
2885
+ const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
2886
+ const padded = normalized + "=".repeat((4 - normalized.length % 4) % 4);
2887
+ return Uint8Array.from(Buffer.from(padded, "base64"));
2888
+ }
2889
+ function encodeUtf8(value) {
2890
+ return new TextEncoder().encode(value);
2891
+ }
2892
+ function canonicalizeP256Jwk(value) {
2893
+ const record = asRecord5(value);
2894
+ const kty = pickString(record?.kty);
2895
+ const crv = pickString(record?.crv);
2896
+ const x = pickString(record?.x);
2897
+ const y = pickString(record?.y);
2898
+ if (kty !== "EC" || crv !== "P-256" || !x || !y) {
2899
+ throw new Error("INVALID_PROOF_KEY");
2900
+ }
2901
+ return { kty: "EC", crv: "P-256", x, y };
2902
+ }
2903
+ function computeDeviceIdFromJwk(jwk) {
2904
+ const canonical = JSON.stringify(jwk);
2905
+ return crypto2.createHash("sha256").update(canonical).digest("hex");
2906
+ }
2907
+ function buildProofPayload(action, deviceId, nonce, signedAt, origin) {
2908
+ return `squad.${action}|${deviceId}|${nonce}|${signedAt}|${origin}`;
2909
+ }
2910
+ async function verifyBrowserProof(payload, origin, action, usedProofNonces) {
2911
+ const deviceId = pickString(payload.deviceId);
2912
+ const signature = pickString(payload.signature);
2913
+ const nonce = pickString(payload.nonce);
2914
+ const signedAtRaw = pickNumber(payload.signedAt);
2915
+ if (!deviceId || !signature || !nonce || signedAtRaw == null) {
2916
+ return {
2917
+ ok: false,
2918
+ code: "INVALID_PROOF",
2919
+ status: 400,
2920
+ message: "Missing browser proof fields"
2921
+ };
2922
+ }
2923
+ const signedAt = Math.trunc(signedAtRaw);
2924
+ if (Math.abs(Date.now() - signedAt) > PROOF_MAX_SKEW_MS) {
2925
+ return {
2926
+ ok: false,
2927
+ code: "INVALID_PROOF",
2928
+ status: 401,
2929
+ message: "Proof timestamp expired"
2930
+ };
2931
+ }
2932
+ const nonceKey = `${deviceId}:${nonce}`;
2933
+ const existingNonce = usedProofNonces.get(nonceKey);
2934
+ if (existingNonce && existingNonce > Date.now()) {
2935
+ return {
2936
+ ok: false,
2937
+ code: "INVALID_PROOF",
2938
+ status: 401,
2939
+ message: "Proof nonce replay detected"
2940
+ };
2941
+ }
2942
+ let jwk;
2943
+ try {
2944
+ jwk = canonicalizeP256Jwk(payload.publicKeyJwk);
2945
+ } catch {
2946
+ return {
2947
+ ok: false,
2948
+ code: "INVALID_PROOF",
2949
+ status: 400,
2950
+ message: "Invalid browser public key"
2951
+ };
2952
+ }
2953
+ const expectedDeviceId = computeDeviceIdFromJwk(jwk);
2954
+ if (expectedDeviceId !== deviceId) {
2955
+ return {
2956
+ ok: false,
2957
+ code: "INVALID_PROOF",
2958
+ status: 401,
2959
+ message: "Proof deviceId does not match browser key"
2960
+ };
2961
+ }
2962
+ const proofPayload = buildProofPayload(action, deviceId, nonce, signedAt, origin);
2963
+ let verified = false;
2964
+ try {
2965
+ const key = await crypto2.webcrypto.subtle.importKey(
2966
+ "jwk",
2967
+ jwk,
2968
+ { name: "ECDSA", namedCurve: "P-256" },
2969
+ false,
2970
+ ["verify"]
2971
+ );
2972
+ verified = await crypto2.webcrypto.subtle.verify(
2973
+ { name: "ECDSA", hash: "SHA-256" },
2974
+ key,
2975
+ decodeBase64Url(signature),
2976
+ encodeUtf8(proofPayload)
2977
+ );
2978
+ } catch {
2979
+ verified = false;
2980
+ }
2981
+ if (!verified) {
2982
+ return {
2983
+ ok: false,
2984
+ code: "INVALID_PROOF",
2985
+ status: 401,
2986
+ message: "Proof signature verification failed"
2987
+ };
2988
+ }
2989
+ usedProofNonces.set(nonceKey, Date.now() + NONCE_TTL_MS);
2990
+ return { ok: true, deviceId };
2991
+ }
2992
+ function normalizePairingStatus(value) {
2993
+ if (typeof value !== "string") return "unknown";
2994
+ const normalized = value.trim().toLowerCase();
2995
+ if (["pending", "requested", "waiting", "open"].includes(normalized)) return "pending";
2996
+ if (["approved", "paired", "accepted", "granted", "success"].includes(normalized)) return "approved";
2997
+ if (["rejected", "denied", "failed"].includes(normalized)) return "rejected";
2998
+ if (["expired", "timeout", "timed_out"].includes(normalized)) return "expired";
2999
+ return "unknown";
3000
+ }
3001
+ function extractRequestId(result) {
3002
+ const obj = asRecord5(result);
3003
+ if (!obj) return null;
3004
+ const nestedRequest = asRecord5(obj.request);
3005
+ return pickString(obj.requestId) ?? pickString(obj.id) ?? pickString(nestedRequest?.requestId) ?? pickString(nestedRequest?.id);
3006
+ }
3007
+ function extractExpiresAt(result) {
3008
+ const obj = asRecord5(result);
3009
+ const candidates = [
3010
+ obj?.expiresAt,
3011
+ obj?.expiresAtMs,
3012
+ asRecord5(obj?.request)?.expiresAt,
3013
+ asRecord5(obj?.request)?.expiresAtMs
3014
+ ];
3015
+ for (const candidate of candidates) {
3016
+ if (typeof candidate === "number" && Number.isFinite(candidate) && candidate > Date.now()) {
3017
+ return Math.trunc(candidate);
3018
+ }
3019
+ if (typeof candidate === "string") {
3020
+ const asNumber = Number(candidate);
3021
+ if (Number.isFinite(asNumber) && asNumber > Date.now()) {
3022
+ return Math.trunc(asNumber);
3023
+ }
3024
+ const parsedDate = Date.parse(candidate);
3025
+ if (Number.isFinite(parsedDate) && parsedDate > Date.now()) {
3026
+ return Math.trunc(parsedDate);
3027
+ }
3028
+ }
3029
+ }
3030
+ return Date.now() + DEFAULT_PAIRING_TTL_MS;
3031
+ }
3032
+ function extractPairingStatus(result) {
3033
+ const obj = asRecord5(result);
3034
+ if (!obj) return "unknown";
3035
+ const candidates = [
3036
+ obj.status,
3037
+ asRecord5(obj.request)?.status,
3038
+ asRecord5(obj.pairing)?.status
3039
+ ];
3040
+ for (const candidate of candidates) {
3041
+ const parsed = normalizePairingStatus(candidate);
3042
+ if (parsed !== "unknown") return parsed;
3043
+ }
3044
+ return "unknown";
3045
+ }
3046
+ function isRateLimited(bucket, key, limit, windowMs) {
3047
+ const now = Date.now();
3048
+ const existing = bucket.get(key);
3049
+ if (!existing || existing.resetAt <= now) {
3050
+ bucket.set(key, { count: 1, resetAt: now + windowMs });
3051
+ return false;
3052
+ }
3053
+ if (existing.count >= limit) {
3054
+ return true;
3055
+ }
3056
+ existing.count += 1;
3057
+ return false;
3058
+ }
3059
+ function normalizeTailnetHostname(value) {
3060
+ const trimmed = value.trim();
3061
+ if (!trimmed) return null;
3062
+ const withProtocol = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
3063
+ try {
3064
+ const parsed = new URL(withProtocol);
3065
+ const hostname = parsed.hostname.toLowerCase();
3066
+ if (!hostname.endsWith(".ts.net")) return null;
3067
+ return hostname.endsWith(".") ? hostname.slice(0, -1) : hostname;
3068
+ } catch {
3069
+ return null;
3070
+ }
3071
+ }
3072
+ function readRegistrationConfig() {
3073
+ const token = pickString(process.env.SQUAD_TAILNET_REGISTRATION_TOKEN) ?? pickString(process.env.OPENCLAW_SQUAD_TAILNET_REGISTRATION_TOKEN);
3074
+ const hostname = pickString(process.env.SQUAD_TAILNET_HOSTNAME) ?? pickString(process.env.TS_HOSTNAME) ?? pickString(process.env.TAILSCALE_HOSTNAME);
3075
+ const normalizedHostname = hostname ? normalizeTailnetHostname(hostname) : null;
3076
+ if (!token || !normalizedHostname) return null;
3077
+ const relayUrlRaw = pickString(process.env.OPENCLAW_SQUAD_RELAY_HTTP_URL) ?? pickString(process.env.SQUAD_RELAY_HTTP_URL) ?? pickString(process.env.OPENCLAW_SQUAD_RELAY_URL) ?? pickString(process.env.SQUAD_RELAY_URL) ?? "https://relay.squad.ceo";
3078
+ const relayApiBaseUrl = relayUrlRaw.replace(/^wss:/i, "https:").replace(/^ws:/i, "http:").replace(/\/+$/, "");
3079
+ const version = pickString(process.env.SQUAD_PLUGIN_VERSION) ?? pickString(process.env.npm_package_version) ?? "unknown";
3080
+ const displayName = pickString(process.env.SQUAD_TAILNET_DISPLAY_NAME) ?? "squad-openclaw";
3081
+ return {
3082
+ relayApiBaseUrl,
3083
+ registrationToken: token,
3084
+ hostname: normalizedHostname,
3085
+ displayName,
3086
+ version
3087
+ };
3088
+ }
3089
+ async function registerLocatorToRelay(config) {
3090
+ const response = await fetch(`${config.relayApiBaseUrl}/api/gateway/register-tailnet`, {
3091
+ method: "POST",
3092
+ headers: {
3093
+ "Content-Type": "application/json",
3094
+ Authorization: `Bearer ${config.registrationToken}`
3095
+ },
3096
+ body: JSON.stringify({
3097
+ hostname: config.hostname,
3098
+ displayName: config.displayName,
3099
+ version: config.version
3100
+ })
3101
+ });
3102
+ if (!response.ok) {
3103
+ const text = await response.text();
3104
+ throw new Error(`Locator registration failed (${response.status}): ${text}`);
3105
+ }
3106
+ }
3107
+ function registerTailnetInternalRoutes(api) {
3108
+ const pendingPairings = /* @__PURE__ */ new Map();
3109
+ const proofNonces = /* @__PURE__ */ new Map();
3110
+ const rateLimitBucket = /* @__PURE__ */ new Map();
3111
+ let preferredPairingRequestMethod = null;
3112
+ let preferredPairingStatusMethod = null;
3113
+ const cleanupCaches = () => {
3114
+ const now = Date.now();
3115
+ for (const [key, value] of pendingPairings) {
3116
+ if (value.expiresAt <= now) pendingPairings.delete(key);
3117
+ }
3118
+ for (const [key, expiresAt] of proofNonces) {
3119
+ if (expiresAt <= now) proofNonces.delete(key);
3120
+ }
3121
+ for (const [key, bucket] of rateLimitBucket) {
3122
+ if (bucket.resetAt <= now) rateLimitBucket.delete(key);
3123
+ }
3124
+ };
3125
+ const callGatewayMethod = async (method, params) => {
3126
+ return callGatewayAny({}, api, method, params);
3127
+ };
3128
+ const requestPairingFromGateway = async () => {
3129
+ const methods = preferredPairingRequestMethod ? [preferredPairingRequestMethod, ...PAIRING_REQUEST_METHODS.filter((m) => m !== preferredPairingRequestMethod)] : [...PAIRING_REQUEST_METHODS];
3130
+ let lastError = null;
3131
+ for (const method of methods) {
3132
+ try {
3133
+ const result = await callGatewayMethod(method, { displayName: "squad-browser" });
3134
+ const requestId = extractRequestId(result);
3135
+ if (!requestId) {
3136
+ throw new Error("Pairing request created but no requestId was returned");
3137
+ }
3138
+ preferredPairingRequestMethod = method;
3139
+ return {
3140
+ requestId,
3141
+ expiresAt: extractExpiresAt(result)
3142
+ };
3143
+ } catch (error) {
3144
+ lastError = error;
3145
+ const message = error instanceof Error ? error.message : String(error);
3146
+ if (isUnknownGatewayMethodError(message)) {
3147
+ continue;
3148
+ }
3149
+ }
3150
+ }
3151
+ const details = lastError instanceof Error ? lastError.message : String(lastError ?? "");
3152
+ throw new Error(
3153
+ details ? `PAIRING_REQUEST_UNAVAILABLE: ${details}` : "PAIRING_REQUEST_UNAVAILABLE: No pairing request method supported"
3154
+ );
3155
+ };
3156
+ const readPairingStatusFromGateway = async (requestId) => {
3157
+ const methods = preferredPairingStatusMethod ? [preferredPairingStatusMethod, ...PAIRING_STATUS_METHODS.filter((m) => m !== preferredPairingStatusMethod)] : [...PAIRING_STATUS_METHODS];
3158
+ for (const method of methods) {
3159
+ try {
3160
+ const result = await callGatewayMethod(method, { requestId });
3161
+ const status = extractPairingStatus(result);
3162
+ preferredPairingStatusMethod = method;
3163
+ return status;
3164
+ } catch (error) {
3165
+ const message = error instanceof Error ? error.message : String(error);
3166
+ if (isUnknownGatewayMethodError(message)) continue;
3167
+ }
3168
+ }
3169
+ return "unknown";
3170
+ };
3171
+ const handle = async (request) => {
3172
+ const url = new URL(request.url);
3173
+ const path14 = url.pathname;
3174
+ cleanupCaches();
3175
+ if (request.method === "OPTIONS" && path14.startsWith("/squad-internal/")) {
3176
+ const origin = ensureOriginAllowed(request);
3177
+ if (!origin) {
3178
+ return new Response(null, { status: 403 });
3179
+ }
3180
+ return withCors(
3181
+ request,
3182
+ new Response(null, {
3183
+ status: 204,
3184
+ headers: {
3185
+ "Access-Control-Allow-Origin": origin,
3186
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
3187
+ "Access-Control-Allow-Headers": "Content-Type",
3188
+ "Access-Control-Max-Age": "86400"
3189
+ }
3190
+ })
3191
+ );
3192
+ }
3193
+ if (request.method === "GET" && path14 === "/squad-internal/health") {
3194
+ if (!isTailnetContext(request)) {
3195
+ return jsonError(request, "TAILNET_REQUIRED", "Tailnet context required", 403);
3196
+ }
3197
+ return withCors(
3198
+ request,
3199
+ json({
3200
+ ok: true,
3201
+ mode: "tailnet-direct",
3202
+ pairing: {
3203
+ requestSupported: preferredPairingRequestMethod ?? PAIRING_REQUEST_METHODS[0],
3204
+ statusSupported: preferredPairingStatusMethod ?? "auto"
3205
+ }
3206
+ })
3207
+ );
3208
+ }
3209
+ if (request.method === "POST" && path14 === "/squad-internal/pairing/request") {
3210
+ const origin = ensureOriginAllowed(request);
3211
+ if (!origin) {
3212
+ return jsonError(request, "ORIGIN_NOT_ALLOWED", "Origin is not allowed", 403);
3213
+ }
3214
+ if (!isTailnetContext(request)) {
3215
+ return jsonError(request, "TAILNET_REQUIRED", "Tailnet context required", 403);
3216
+ }
3217
+ let body;
3218
+ try {
3219
+ body = await request.json();
3220
+ } catch {
3221
+ return jsonError(request, "INVALID_REQUEST", "Invalid JSON body", 400);
3222
+ }
3223
+ const proofCheck = await verifyBrowserProof(body, origin, "pairing.request", proofNonces);
3224
+ if (!proofCheck.ok) {
3225
+ return jsonError(request, proofCheck.code, proofCheck.message, proofCheck.status);
3226
+ }
3227
+ const forwardedFor = request.headers.get("x-forwarded-for") ?? "unknown";
3228
+ const ipKey = `ip:${forwardedFor}`;
3229
+ const deviceKey = `device:${proofCheck.deviceId}`;
3230
+ if (isRateLimited(rateLimitBucket, ipKey, 20, 6e4) || isRateLimited(rateLimitBucket, deviceKey, 8, 6e4)) {
3231
+ return jsonError(request, "RATE_LIMITED", "Too many pairing requests", 429);
3232
+ }
3233
+ try {
3234
+ const pairing = await requestPairingFromGateway();
3235
+ pendingPairings.set(pairing.requestId, {
3236
+ requestId: pairing.requestId,
3237
+ deviceId: proofCheck.deviceId,
3238
+ createdAt: Date.now(),
3239
+ expiresAt: pairing.expiresAt
3240
+ });
3241
+ return withCors(
3242
+ request,
3243
+ json({
3244
+ ok: true,
3245
+ requestId: pairing.requestId,
3246
+ approveCommand: `openclaw devices approve ${pairing.requestId}`,
3247
+ expiresAt: pairing.expiresAt
3248
+ })
3249
+ );
3250
+ } catch (error) {
3251
+ const message = error instanceof Error ? error.message : String(error);
3252
+ return jsonError(
3253
+ request,
3254
+ "PAIRING_REQUEST_UNAVAILABLE",
3255
+ message.replace(/^PAIRING_REQUEST_UNAVAILABLE:\s*/i, ""),
3256
+ 501,
3257
+ { nextStep: "openclaw devices list --json" }
3258
+ );
3259
+ }
3260
+ }
3261
+ if (request.method === "GET" && path14 === "/squad-internal/pairing/status") {
3262
+ const origin = ensureOriginAllowed(request);
3263
+ if (!origin) {
3264
+ return jsonError(request, "ORIGIN_NOT_ALLOWED", "Origin is not allowed", 403);
3265
+ }
3266
+ if (!isTailnetContext(request)) {
3267
+ return jsonError(request, "TAILNET_REQUIRED", "Tailnet context required", 403);
3268
+ }
3269
+ const requestId = pickString(url.searchParams.get("requestId"));
3270
+ const deviceId = pickString(url.searchParams.get("deviceId"));
3271
+ if (!requestId || !deviceId) {
3272
+ return jsonError(request, "INVALID_REQUEST", "requestId and deviceId are required", 400);
3273
+ }
3274
+ const record = pendingPairings.get(requestId);
3275
+ if (!record || record.deviceId !== deviceId) {
3276
+ return jsonError(request, "NOT_FOUND", "No pending pairing request for this device", 404);
3277
+ }
3278
+ if (record.expiresAt <= Date.now()) {
3279
+ pendingPairings.delete(requestId);
3280
+ return withCors(request, json({ ok: true, status: "expired" }));
3281
+ }
3282
+ const forwardedFor = request.headers.get("x-forwarded-for") ?? "unknown";
3283
+ if (isRateLimited(rateLimitBucket, `status:${forwardedFor}`, 90, 6e4)) {
3284
+ return jsonError(request, "RATE_LIMITED", "Too many status checks", 429);
3285
+ }
3286
+ const status = await readPairingStatusFromGateway(requestId);
3287
+ if (status === "approved" || status === "expired" || status === "rejected") {
3288
+ pendingPairings.delete(requestId);
3289
+ }
3290
+ return withCors(request, json({ ok: true, status }));
3291
+ }
3292
+ if (request.method === "POST" && path14 === "/squad-internal/locator/register") {
3293
+ const origin = ensureOriginAllowed(request);
3294
+ if (!origin) {
3295
+ return jsonError(request, "ORIGIN_NOT_ALLOWED", "Origin is not allowed", 403);
3296
+ }
3297
+ if (!isTailnetContext(request)) {
3298
+ return jsonError(request, "TAILNET_REQUIRED", "Tailnet context required", 403);
3299
+ }
3300
+ let body;
3301
+ try {
3302
+ body = await request.json();
3303
+ } catch {
3304
+ return jsonError(request, "INVALID_REQUEST", "Invalid JSON body", 400);
3305
+ }
3306
+ const proofCheck = await verifyBrowserProof(body, origin, "locator.register", proofNonces);
3307
+ if (!proofCheck.ok) {
3308
+ return jsonError(request, proofCheck.code, proofCheck.message, proofCheck.status);
3309
+ }
3310
+ const registrationToken = pickString(body.registrationToken);
3311
+ const hostname = normalizeTailnetHostname(pickString(body.hostname) ?? "");
3312
+ if (!registrationToken || !hostname) {
3313
+ return jsonError(
3314
+ request,
3315
+ "INVALID_REQUEST",
3316
+ "registrationToken and hostname (*.ts.net) are required",
3317
+ 400
3318
+ );
3319
+ }
3320
+ const relayApiBaseUrl = pickString(body.relayApiBaseUrl) ?? readRegistrationConfig()?.relayApiBaseUrl ?? "https://relay.squad.ceo";
3321
+ const displayName = pickString(body.displayName) ?? "squad-openclaw";
3322
+ const version = pickString(body.version) ?? pickString(process.env.SQUAD_PLUGIN_VERSION) ?? pickString(process.env.npm_package_version) ?? "unknown";
3323
+ const config = {
3324
+ relayApiBaseUrl: relayApiBaseUrl.replace(/\/+$/, ""),
3325
+ registrationToken,
3326
+ hostname,
3327
+ displayName,
3328
+ version
3329
+ };
3330
+ try {
3331
+ await registerLocatorToRelay(config);
3332
+ return withCors(
3333
+ request,
3334
+ json({
3335
+ ok: true,
3336
+ locator: {
3337
+ hostname,
3338
+ source: "plugin",
3339
+ lastSeenAt: Date.now(),
3340
+ displayName,
3341
+ version
3342
+ }
3343
+ })
3344
+ );
3345
+ } catch (error) {
3346
+ return jsonError(
3347
+ request,
3348
+ "LOCATOR_REGISTRATION_FAILED",
3349
+ error instanceof Error ? error.message : String(error),
3350
+ 502
3351
+ );
3352
+ }
3353
+ }
3354
+ return new Response("Not Found", { status: 404 });
3355
+ };
3356
+ if (typeof api.registerHttpHandler === "function") {
3357
+ api.registerHttpHandler(handle);
3358
+ } else if (typeof api.registerHttpRoute === "function") {
3359
+ api.registerHttpRoute("/squad-internal/health", handle);
3360
+ api.registerHttpRoute("/squad-internal/pairing/request", handle);
3361
+ api.registerHttpRoute("/squad-internal/pairing/status", handle);
3362
+ api.registerHttpRoute("/squad-internal/locator/register", handle);
3363
+ api.registerHttpRoute("/squad-internal/*", handle);
3364
+ } else if (typeof api.registerHttpMiddleware === "function") {
3365
+ api.registerHttpMiddleware(handle);
3366
+ } else {
3367
+ console.warn("[squad-openclaw] no supported HTTP registration API found for tailnet routes");
3368
+ }
3369
+ const registrationConfig = readRegistrationConfig();
3370
+ if (!registrationConfig) return;
3371
+ const runRegistration = async () => {
3372
+ try {
3373
+ await registerLocatorToRelay(registrationConfig);
3374
+ console.log("[squad-openclaw] tailnet locator registered");
3375
+ } catch (error) {
3376
+ console.warn(
3377
+ "[squad-openclaw] tailnet locator registration failed:",
3378
+ error instanceof Error ? error.message : String(error)
3379
+ );
3380
+ }
3381
+ };
3382
+ void runRegistration();
3383
+ const timer = setInterval(() => {
3384
+ void runRegistration();
3385
+ }, LOCATOR_REFRESH_INTERVAL_MS);
3386
+ const teardown = () => {
3387
+ clearInterval(timer);
3388
+ };
3389
+ if (typeof api.onShutdown === "function") {
3390
+ api.onShutdown(teardown);
3391
+ } else if (typeof api.on === "function") {
3392
+ const maybeOff = api.on("shutdown", teardown);
3393
+ if (typeof maybeOff === "function") {
3394
+ }
3395
+ }
3396
+ }
3397
+
3398
+ // src/index.ts
3399
+ function squadAppPlugin(api) {
3400
+ const sharedApi = registerSquadSharedApi(api);
3401
+ sharedApi.registerCoreGatewayMethods();
3402
+ registerTailnetInternalRoutes(api);
3403
+ void runStartupMigrations(api);
1078
3404
  }
1079
3405
  export {
1080
3406
  squadAppPlugin as default