@shawnowen/comet-mcp 2.3.1 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +86 -19
  2. package/dist/alert-dispatcher.d.ts +23 -0
  3. package/dist/alert-dispatcher.js +101 -0
  4. package/dist/bound-session.d.ts +23 -0
  5. package/dist/bound-session.js +119 -0
  6. package/dist/bridge-config.d.ts +6 -0
  7. package/dist/bridge-config.js +78 -0
  8. package/dist/cdp-client.d.ts +40 -4
  9. package/dist/cdp-client.js +502 -155
  10. package/dist/comet-ai.d.ts +15 -0
  11. package/dist/comet-ai.js +114 -38
  12. package/dist/delegate-binding.d.ts +19 -0
  13. package/dist/delegate-binding.js +73 -0
  14. package/dist/discovery/capability-entry.d.ts +215 -0
  15. package/dist/discovery/capability-entry.js +13 -0
  16. package/dist/discovery/description-template.d.ts +40 -0
  17. package/dist/discovery/description-template.js +61 -0
  18. package/dist/discovery/golden-queries.fixture.d.ts +22 -0
  19. package/dist/discovery/golden-queries.fixture.js +137 -0
  20. package/dist/discovery/mcp-source.d.ts +38 -0
  21. package/dist/discovery/mcp-source.js +70 -0
  22. package/dist/discovery/metadata-completeness.d.ts +48 -0
  23. package/dist/discovery/metadata-completeness.js +83 -0
  24. package/dist/discovery/registry.d.ts +35 -0
  25. package/dist/discovery/registry.js +35 -0
  26. package/dist/discovery/safety.d.ts +44 -0
  27. package/dist/discovery/safety.js +59 -0
  28. package/dist/discovery/schema-validator.d.ts +36 -0
  29. package/dist/discovery/schema-validator.js +257 -0
  30. package/dist/discovery/source-error.d.ts +47 -0
  31. package/dist/discovery/source-error.js +95 -0
  32. package/dist/discovery/tool-meta.d.ts +41 -0
  33. package/dist/discovery/tool-meta.js +229 -0
  34. package/dist/discovery/virtual-tools.d.ts +20 -0
  35. package/dist/discovery/virtual-tools.js +69 -0
  36. package/dist/http-server.js +2067 -47
  37. package/dist/index.js +3163 -710
  38. package/dist/observer.d.ts +47 -0
  39. package/dist/observer.js +516 -0
  40. package/dist/session-registry.d.ts +57 -0
  41. package/dist/session-registry.js +500 -0
  42. package/dist/sidecar-artifacts.d.ts +49 -0
  43. package/dist/sidecar-artifacts.js +146 -0
  44. package/dist/snapshot-capture.d.ts +3 -0
  45. package/dist/snapshot-capture.js +91 -0
  46. package/dist/tab-group-archive.js +3 -1
  47. package/dist/tab-groups.d.ts +7 -0
  48. package/dist/tab-groups.js +21 -3
  49. package/dist/task-thread-aggregator.d.ts +34 -0
  50. package/dist/task-thread-aggregator.js +480 -0
  51. package/dist/task-thread-canonical.d.ts +142 -0
  52. package/dist/task-thread-canonical.js +116 -0
  53. package/dist/types.d.ts +237 -0
  54. package/dist/window-bindings.d.ts +112 -0
  55. package/dist/window-bindings.js +476 -0
  56. package/extension/background.js +1556 -300
  57. package/extension/icons/icon.svg +9 -0
  58. package/extension/icons/icon128.png +0 -0
  59. package/extension/icons/icon16.png +0 -0
  60. package/extension/icons/icon48.png +0 -0
  61. package/extension/manifest.json +19 -4
  62. package/extension/session-logic.js +2383 -0
  63. package/extension/session-manager.html +299 -0
  64. package/extension/sidepanel.css +5323 -528
  65. package/extension/sidepanel.html +282 -2
  66. package/extension/sidepanel.js +10075 -951
  67. package/extension/window-policy.js +162 -0
  68. package/package.json +10 -7
  69. package/vendor/lifecycle-mcp-adapter.mjs +103 -0
  70. package/vendor/lifecycle-metadata.mjs +252 -0
  71. package/vendor/readiness-report.mjs +742 -0
  72. package/dist/cdp-client.d.ts.map +0 -1
  73. package/dist/cdp-client.js.map +0 -1
  74. package/dist/comet-ai.d.ts.map +0 -1
  75. package/dist/comet-ai.js.map +0 -1
  76. package/dist/http-server.d.ts.map +0 -1
  77. package/dist/http-server.js.map +0 -1
  78. package/dist/index.d.ts.map +0 -1
  79. package/dist/index.js.map +0 -1
  80. package/dist/tab-group-archive.d.ts.map +0 -1
  81. package/dist/tab-group-archive.js.map +0 -1
  82. package/dist/tab-groups.d.ts.map +0 -1
  83. package/dist/tab-groups.js.map +0 -1
  84. package/dist/types.d.ts.map +0 -1
  85. package/dist/types.js.map +0 -1
@@ -0,0 +1,2383 @@
1
+ // session-logic.js — Extracted pure logic functions (Spec 036)
2
+ // Dual-format: ESM exports for Node.js tests, globalThis for Chrome extension
3
+ // Wrapped in IIFE to avoid polluting global scope (individual function names
4
+ // must not leak, or background.js/sidepanel.js delegate consts will collide).
5
+
6
+ (function () {
7
+ const DEFAULT_DOMAIN_DISPLAY_MAP = {
8
+ "github.com": "GitHub",
9
+ "docs.google.com": "Google Docs",
10
+ "sheets.google.com": "Google Sheets",
11
+ "drive.google.com": "Google Drive",
12
+ "mail.google.com": "Gmail",
13
+ "app.asana.com": "Asana",
14
+ "linear.app": "Linear",
15
+ "slack.com": "Slack",
16
+ "notion.so": "Notion",
17
+ "figma.com": "Figma",
18
+ "app.shortwave.com": "Shortwave",
19
+ };
20
+
21
+ const EQUANAUT_PROVIDER_LABELS = {
22
+ gateway: "Equabot Gateway",
23
+ anthropic: "Anthropic",
24
+ openai: "OpenAI",
25
+ "openai-codex": "OpenAI Codex",
26
+ google: "Google Gemini",
27
+ xai: "xAI",
28
+ deepseek: "DeepSeek",
29
+ perplexity: "Perplexity",
30
+ ollama: "Local Ollama",
31
+ "ollama-cloud": "Ollama Cloud",
32
+ local: "Local Ollama",
33
+ };
34
+
35
+ const EQUANAUT_PROVIDER_ORDER = [
36
+ "gateway",
37
+ "anthropic",
38
+ "openai",
39
+ "openai-codex",
40
+ "google",
41
+ "xai",
42
+ "deepseek",
43
+ "perplexity",
44
+ "ollama-cloud",
45
+ "ollama",
46
+ "local",
47
+ ];
48
+
49
+ const EQUANAUT_PROVIDER_GLYPHS = {
50
+ gateway: "EQ",
51
+ anthropic: "AI",
52
+ openai: "OA",
53
+ "openai-codex": "CX",
54
+ google: "G",
55
+ xai: "xAI",
56
+ deepseek: "DS",
57
+ perplexity: "PX",
58
+ ollama: "OL",
59
+ "ollama-cloud": "OC",
60
+ local: "OL",
61
+ };
62
+
63
+ const EQUANAUT_MODEL_CAPABILITIES = {
64
+ fast: "Fast",
65
+ vision: "Vision",
66
+ reasoning: "Reasoning",
67
+ tools: "Tools",
68
+ image: "Image",
69
+ pdf: "PDF",
70
+ };
71
+
72
+ const EQUANAUT_MODEL_CAPABILITY_ORDER = ["fast", "vision", "reasoning", "tools", "image", "pdf"];
73
+
74
+ const EQUANAUT_COMMAND_CENTER_FALLBACK_MODELS = [
75
+ {
76
+ id: "gateway:default",
77
+ name: "Equabot Gateway (Default)",
78
+ provider: "gateway",
79
+ type: "cloud",
80
+ status: "unknown",
81
+ source: "command-center-fallback",
82
+ details: "Uses the Equa account gateway primary model",
83
+ isDefault: true,
84
+ },
85
+ {
86
+ id: "gateway:anthropic/claude-opus-4-5",
87
+ name: "Claude Opus 4.5",
88
+ provider: "anthropic",
89
+ type: "cloud",
90
+ status: "unknown",
91
+ source: "command-center-fallback",
92
+ contextLength: 200000,
93
+ },
94
+ {
95
+ id: "gateway:anthropic/claude-sonnet-4-20250514",
96
+ name: "Claude Sonnet 4",
97
+ provider: "anthropic",
98
+ type: "cloud",
99
+ status: "unknown",
100
+ source: "command-center-fallback",
101
+ contextLength: 200000,
102
+ },
103
+ {
104
+ id: "gateway:openai/gpt-5.2",
105
+ name: "GPT-5.2 (OpenAI)",
106
+ provider: "openai",
107
+ type: "cloud",
108
+ status: "unknown",
109
+ source: "command-center-fallback",
110
+ },
111
+ {
112
+ id: "gateway:google/gemini-3-pro-preview",
113
+ name: "Gemini 3.1 Pro (Google)",
114
+ provider: "google",
115
+ type: "cloud",
116
+ status: "unknown",
117
+ source: "command-center-fallback",
118
+ },
119
+ {
120
+ id: "gateway:google/gemini-2.5-pro",
121
+ name: "Gemini 2.5 Pro",
122
+ provider: "google",
123
+ type: "cloud",
124
+ status: "unknown",
125
+ source: "command-center-fallback",
126
+ },
127
+ {
128
+ id: "gateway:xai/grok-3",
129
+ name: "Grok 3",
130
+ provider: "xai",
131
+ type: "cloud",
132
+ status: "unknown",
133
+ source: "command-center-fallback",
134
+ },
135
+ {
136
+ id: "gateway:deepseek/deepseek-chat",
137
+ name: "DeepSeek Chat",
138
+ provider: "deepseek",
139
+ type: "cloud",
140
+ status: "unknown",
141
+ source: "command-center-fallback",
142
+ },
143
+ {
144
+ id: "deepseek-coder-v2:latest",
145
+ name: "deepseek-coder-v2:latest",
146
+ provider: "ollama",
147
+ type: "local",
148
+ status: "unknown",
149
+ source: "command-center-fallback",
150
+ details: "Command Center standalone fallback",
151
+ },
152
+ {
153
+ id: "qwen2.5:72b",
154
+ name: "qwen2.5:72b",
155
+ provider: "ollama",
156
+ type: "local",
157
+ status: "unknown",
158
+ source: "command-center-fallback",
159
+ details: "Command Center standalone fallback",
160
+ },
161
+ {
162
+ id: "llama3.3:70b",
163
+ name: "llama3.3:70b",
164
+ provider: "ollama",
165
+ type: "local",
166
+ status: "unknown",
167
+ source: "command-center-fallback",
168
+ details: "Command Center standalone fallback",
169
+ },
170
+ ];
171
+
172
+ function normalizeProviderKey(provider) {
173
+ return String(provider || "")
174
+ .trim()
175
+ .toLowerCase();
176
+ }
177
+
178
+ function providerLabel(provider) {
179
+ const key = normalizeProviderKey(provider);
180
+ return EQUANAUT_PROVIDER_LABELS[key] || (provider ? String(provider) : "Other");
181
+ }
182
+
183
+ function getEquanautProviderGlyph(provider) {
184
+ const key = normalizeProviderKey(provider);
185
+ if (EQUANAUT_PROVIDER_GLYPHS[key]) return EQUANAUT_PROVIDER_GLYPHS[key];
186
+ const label = providerLabel(key);
187
+ const letters = label.match(/[A-Za-z0-9]/g) || [];
188
+ return (letters[0] || "?").toUpperCase() + (letters[1] || "").toUpperCase();
189
+ }
190
+
191
+ function getEquanautProviderFilterKey(model) {
192
+ if (!model) return "gateway";
193
+ if (model.location === "local") return "ollama";
194
+ if (model.location === "cloud" && model.provider === "ollama") return "ollama-cloud";
195
+ return (
196
+ normalizeProviderKey(model.provider || inferEquanautProviderFromId(model.id)) || "gateway"
197
+ );
198
+ }
199
+
200
+ function getEquanautModelSearchText(model) {
201
+ const capabilities = Array.isArray(model?.capabilities)
202
+ ? model.capabilities
203
+ : getEquanautModelCapabilities(model);
204
+ return [
205
+ model?.id,
206
+ model?.sourceId,
207
+ model?.name,
208
+ model?.provider,
209
+ model?.providerLabel,
210
+ model?.location,
211
+ model?.route,
212
+ model?.meta,
213
+ model?.details,
214
+ model?.source,
215
+ model?.costTier || getEquanautModelCostTier(model),
216
+ ...capabilities,
217
+ ...capabilities.map((capability) => EQUANAUT_MODEL_CAPABILITIES[capability]),
218
+ ]
219
+ .filter(Boolean)
220
+ .join(" ")
221
+ .toLowerCase();
222
+ }
223
+
224
+ function getEquanautModelCostTier(model) {
225
+ const text = [
226
+ model?.id,
227
+ model?.name,
228
+ model?.provider,
229
+ model?.providerLabel,
230
+ model?.location,
231
+ model?.meta,
232
+ model?.details,
233
+ ]
234
+ .filter(Boolean)
235
+ .join(" ")
236
+ .toLowerCase();
237
+
238
+ if (model?.location === "local") return "$";
239
+ if (/(flash|fast|mini|nano|haiku|instant|cheap|low cost|gpt-oss)/i.test(text)) return "$";
240
+ if (/(opus|gpt-5|gpt5|grok|frontier|pro preview|3\.1 pro|4\.5)/i.test(text)) return "$$$";
241
+ if (/(sonnet|pro|default|gateway|claude|gemini|openai|anthropic)/i.test(text)) return "$$";
242
+ return "$$";
243
+ }
244
+
245
+ function getEquanautModelCapabilities(model) {
246
+ const text = [
247
+ model?.id,
248
+ model?.name,
249
+ model?.provider,
250
+ model?.providerLabel,
251
+ model?.location,
252
+ model?.route,
253
+ model?.meta,
254
+ model?.details,
255
+ model?.source,
256
+ ]
257
+ .filter(Boolean)
258
+ .join(" ")
259
+ .toLowerCase();
260
+ const caps = new Set();
261
+ const contextLength = Number(model?.contextLength) || 0;
262
+
263
+ if (/(flash|fast|mini|nano|haiku|instant|gpt-oss|qwen|llama|local|ollama)/.test(text)) {
264
+ caps.add("fast");
265
+ }
266
+ if (/(vision|visual|image|multimodal|gemini|gpt-4o|gpt-image|claude-3)/.test(text)) {
267
+ caps.add("vision");
268
+ }
269
+ if (
270
+ /(reason|thinking|opus|sonnet|pro|gpt-5|gpt5|gpt-oss|grok|deepseek|qwen|glm|claude)/.test(
271
+ text
272
+ ) ||
273
+ contextLength >= 64000
274
+ ) {
275
+ caps.add("reasoning");
276
+ }
277
+ if (/(tool|function|gateway|command center|equabot|comet|mcp)/.test(text)) {
278
+ caps.add("tools");
279
+ }
280
+ if (/(image|imagen|dall|nano banana|gemini)/.test(text)) {
281
+ caps.add("image");
282
+ }
283
+ if (
284
+ /(pdf|document|long context|claude|gemini|gpt-5|gpt5|gpt-4|opus|sonnet)/.test(text) ||
285
+ contextLength >= 128000
286
+ ) {
287
+ caps.add("pdf");
288
+ }
289
+
290
+ if (model?.route === "command-center") caps.add("tools");
291
+
292
+ return EQUANAUT_MODEL_CAPABILITY_ORDER.filter((capability) => caps.has(capability));
293
+ }
294
+
295
+ function getEquanautCapabilityLabel(capability) {
296
+ return EQUANAUT_MODEL_CAPABILITIES[capability] || String(capability || "");
297
+ }
298
+
299
+ function filterEquanautModels(models, opts) {
300
+ const options = opts || {};
301
+ const routingMode = options.routingMode || "auto";
302
+ const provider = normalizeProviderKey(options.provider || "all") || "all";
303
+ const capability = String(options.capability || "all")
304
+ .trim()
305
+ .toLowerCase();
306
+ const query = String(options.query || "")
307
+ .trim()
308
+ .toLowerCase();
309
+
310
+ return (models || []).filter((model) => {
311
+ if (!isEquanautModelAllowedForRouting(model, routingMode)) return false;
312
+ if (provider !== "all" && getEquanautProviderFilterKey(model) !== provider) return false;
313
+ if (
314
+ capability &&
315
+ capability !== "all" &&
316
+ !getEquanautModelCapabilities(model).includes(capability)
317
+ ) {
318
+ return false;
319
+ }
320
+ if (query && !getEquanautModelSearchText(model).includes(query)) return false;
321
+ return true;
322
+ });
323
+ }
324
+
325
+ function normalizeGatewayModelPath(provider, modelId) {
326
+ const id = String(modelId || "").trim();
327
+ if (!id) return "";
328
+ if (id === "default" || id.includes("/")) return id;
329
+ const key = normalizeProviderKey(provider);
330
+ return key ? `${key}/${id}` : id;
331
+ }
332
+
333
+ function formatEquanautContextLength(value) {
334
+ const n = Number(value);
335
+ if (!Number.isFinite(n) || n <= 0) return "";
336
+ if (n >= 1000000) return `${(n / 1000000).toFixed(n % 1000000 === 0 ? 0 : 1)}M ctx`;
337
+ if (n >= 1000) return `${Math.round(n / 1000)}k ctx`;
338
+ return `${n} ctx`;
339
+ }
340
+
341
+ function inferEquanautProviderFromId(id) {
342
+ const raw = String(id || "").replace(/^gateway:/, "");
343
+ if (raw.includes("/")) return raw.split("/")[0];
344
+ if (/^claude-|^sonnet-|^opus-/i.test(raw)) return "anthropic";
345
+ if (/^gpt-|^o[0-9]/i.test(raw)) return "openai";
346
+ if (/^gemini-/i.test(raw)) return "google";
347
+ if (/^grok-/i.test(raw)) return "xai";
348
+ if (/^deepseek/i.test(raw)) return raw.includes(":") ? "ollama" : "deepseek";
349
+ return raw.includes(":") ? "ollama" : "gateway";
350
+ }
351
+
352
+ function normalizeEquanautModelEntry(entry, opts) {
353
+ if (!entry || typeof entry !== "object") return null;
354
+ const options = opts || {};
355
+ const source = String(options.source || entry.source || "").trim() || "unknown";
356
+ const rawId = String(entry.id || entry.model || entry.name || "").trim();
357
+ if (!rawId || rawId === "ollama:offline") return null;
358
+
359
+ const rawProvider = normalizeProviderKey(entry.provider || entry.providerKey);
360
+ const rawType = String(entry.type || "")
361
+ .trim()
362
+ .toLowerCase();
363
+ const status =
364
+ String(entry.status || "")
365
+ .trim()
366
+ .toLowerCase() || "unknown";
367
+
368
+ let id = rawId;
369
+ let provider = rawProvider || inferEquanautProviderFromId(rawId);
370
+ let location = rawType === "local" ? "local" : "";
371
+ let route = "standalone";
372
+
373
+ if (id.startsWith("ollama:")) {
374
+ id = id.slice("ollama:".length);
375
+ provider = "ollama";
376
+ location = "local";
377
+ } else if (id.startsWith("gateway:")) {
378
+ provider = inferEquanautProviderFromId(id) || provider;
379
+ location = "gateway";
380
+ route = "command-center";
381
+ } else if (source === "ollama" && rawType === "cloud") {
382
+ provider = "ollama";
383
+ location = "cloud";
384
+ } else if (
385
+ source === "gateway-rpc" &&
386
+ provider &&
387
+ provider !== "ollama" &&
388
+ provider !== "local"
389
+ ) {
390
+ id = `gateway:${normalizeGatewayModelPath(provider, id)}`;
391
+ location = "gateway";
392
+ route = "command-center";
393
+ } else if (rawType === "cloud") {
394
+ location = "gateway";
395
+ route = "command-center";
396
+ } else if (!location) {
397
+ location = provider === "ollama" || id.includes(":") ? "local" : "gateway";
398
+ if (location === "gateway") route = "command-center";
399
+ }
400
+
401
+ if (location === "gateway") route = "command-center";
402
+ if (provider === "local") provider = "ollama";
403
+
404
+ const contextLength =
405
+ Number(entry.contextLength || entry.contextWindow || entry.contextTokens) || undefined;
406
+ const contextMeta = formatEquanautContextLength(contextLength);
407
+ const statusMeta = status && status !== "available" && status !== "unknown" ? status : "";
408
+ const detailMeta =
409
+ typeof entry.details === "string" && entry.details.trim() ? entry.details.trim() : "";
410
+ const sizeMeta =
411
+ typeof entry.sizeGb === "number" && entry.sizeGb > 0 ? `${entry.sizeGb}GB` : "";
412
+ const meta = [contextMeta, sizeMeta, detailMeta, statusMeta].filter(Boolean).join(" - ");
413
+
414
+ const normalized = {
415
+ id,
416
+ name: String(entry.name || id).trim() || id,
417
+ provider,
418
+ providerLabel: providerLabel(provider),
419
+ type: location === "local" ? "local" : "chat",
420
+ location,
421
+ route,
422
+ contextLength,
423
+ status,
424
+ enabled: entry.enabled !== false && status !== "offline",
425
+ isDefault: Boolean(entry.isDefault),
426
+ source,
427
+ sourceId: rawId,
428
+ meta,
429
+ };
430
+ normalized.providerKey = getEquanautProviderFilterKey(normalized);
431
+ normalized.providerGlyph = getEquanautProviderGlyph(normalized.providerKey);
432
+ normalized.costTier = getEquanautModelCostTier(normalized);
433
+ normalized.capabilities = getEquanautModelCapabilities(normalized);
434
+ return normalized;
435
+ }
436
+
437
+ function normalizeEquanautOllamaModel(entry) {
438
+ const id = String(entry?.name || entry?.model || "").trim();
439
+ if (!id) return null;
440
+ const isCloud = /:cloud$/.test(id) || (entry.size === 0 && !entry?.details?.family);
441
+ const family = entry?.details?.family || id.split(":")[0] || id;
442
+ const params = entry?.details?.parameter_size || "";
443
+ const sizeGb = entry.size ? (entry.size / 1024 ** 3).toFixed(1) + "GB" : "";
444
+ return normalizeEquanautModelEntry(
445
+ {
446
+ id,
447
+ name: id,
448
+ provider: isCloud ? "ollama" : family,
449
+ type: isCloud ? "cloud" : "local",
450
+ status: "available",
451
+ details: [params, sizeGb].filter(Boolean).join(" - ") || (isCloud ? "Ollama Cloud" : ""),
452
+ source: "ollama",
453
+ },
454
+ { source: "ollama" }
455
+ );
456
+ }
457
+
458
+ function modelsFromEquanautGatewayConfig(raw, opts) {
459
+ const config = raw && typeof raw === "object" ? raw : {};
460
+ const source = opts?.source || "gateway-config";
461
+ const models = [];
462
+ const primaryModel =
463
+ typeof config.primaryModel === "string" && config.primaryModel.trim()
464
+ ? config.primaryModel.trim()
465
+ : typeof config.primary === "string" && config.primary.trim()
466
+ ? config.primary.trim()
467
+ : "default";
468
+ models.push(
469
+ normalizeEquanautModelEntry(
470
+ {
471
+ id: primaryModel === "default" ? "gateway:default" : `gateway:${primaryModel}`,
472
+ name: primaryModel === "default" ? "Equabot Gateway (Default)" : primaryModel,
473
+ provider: inferEquanautProviderFromId(primaryModel),
474
+ type: "cloud",
475
+ status: "available",
476
+ source,
477
+ isDefault: true,
478
+ details: "Gateway primary model",
479
+ },
480
+ { source }
481
+ )
482
+ );
483
+
484
+ const providers = Array.isArray(config.providers)
485
+ ? config.providers
486
+ : config.providers && typeof config.providers === "object"
487
+ ? Object.entries(config.providers).map(([key, value]) =>
488
+ typeof value === "object" && value
489
+ ? { key, ...value }
490
+ : { key, models: Array.isArray(value) ? value : [] }
491
+ )
492
+ : [];
493
+ for (const provider of providers) {
494
+ const key = normalizeProviderKey(provider?.key || provider?.provider || provider?.id);
495
+ if (!key) continue;
496
+ const providerModels = Array.isArray(provider.models)
497
+ ? provider.models
498
+ : provider.models && typeof provider.models === "object"
499
+ ? Object.values(provider.models)
500
+ : [];
501
+ for (const model of providerModels) {
502
+ const modelId =
503
+ typeof model === "string"
504
+ ? model.trim()
505
+ : typeof model?.id === "string"
506
+ ? model.id.trim()
507
+ : "";
508
+ if (!modelId) continue;
509
+ const modelName =
510
+ typeof model === "object" && typeof model.name === "string" && model.name.trim()
511
+ ? model.name.trim()
512
+ : modelId;
513
+ models.push(
514
+ normalizeEquanautModelEntry(
515
+ {
516
+ id: `gateway:${normalizeGatewayModelPath(key, modelId)}`,
517
+ name: modelName,
518
+ provider: key,
519
+ type: "cloud",
520
+ status: "available",
521
+ source,
522
+ details: `Routes through Equabot Gateway via ${key}`,
523
+ },
524
+ { source }
525
+ )
526
+ );
527
+ }
528
+ }
529
+ return models.filter(Boolean);
530
+ }
531
+
532
+ function normalizeEquanautModelCatalog(raw, opts) {
533
+ if (!raw) return [];
534
+ const options = opts || {};
535
+ if (Array.isArray(raw)) {
536
+ return raw.map((entry) => normalizeEquanautModelEntry(entry, options)).filter(Boolean);
537
+ }
538
+ if (typeof raw !== "object") return [];
539
+ if (Array.isArray(raw.models) || Array.isArray(raw.items)) {
540
+ return (raw.models || raw.items)
541
+ .map((entry) => normalizeEquanautModelEntry(entry, options))
542
+ .filter(Boolean);
543
+ }
544
+ if (Array.isArray(raw.providers) || (raw.providers && typeof raw.providers === "object")) {
545
+ return modelsFromEquanautGatewayConfig(raw, options);
546
+ }
547
+ return [];
548
+ }
549
+
550
+ function sortEquanautModelCatalog(models) {
551
+ return [...(models || [])].sort((a, b) => {
552
+ const ai = EQUANAUT_PROVIDER_ORDER.indexOf(a.provider);
553
+ const bi = EQUANAUT_PROVIDER_ORDER.indexOf(b.provider);
554
+ const ar = ai === -1 ? 999 : ai;
555
+ const br = bi === -1 ? 999 : bi;
556
+ if (ar !== br) return ar - br;
557
+ const al = a.location === "local" ? 1 : 0;
558
+ const bl = b.location === "local" ? 1 : 0;
559
+ if (al !== bl) return al - bl;
560
+ return String(a.name || a.id).localeCompare(String(b.name || b.id));
561
+ });
562
+ }
563
+
564
+ function mergeEquanautModelCatalogs() {
565
+ const merged = new Map();
566
+ const lists = Array.from(arguments).flat().filter(Boolean);
567
+ for (const entry of lists) {
568
+ const normalized =
569
+ entry && entry.id && entry.location
570
+ ? entry
571
+ : normalizeEquanautModelEntry(entry, { source: "merge" });
572
+ if (!normalized) continue;
573
+ if (!merged.has(normalized.id)) {
574
+ merged.set(normalized.id, normalized);
575
+ continue;
576
+ }
577
+ const prior = merged.get(normalized.id);
578
+ merged.set(normalized.id, {
579
+ ...prior,
580
+ ...normalized,
581
+ meta: normalized.meta || prior.meta,
582
+ enabled: prior.enabled || normalized.enabled,
583
+ isDefault: prior.isDefault || normalized.isDefault,
584
+ });
585
+ }
586
+ return sortEquanautModelCatalog(Array.from(merged.values()));
587
+ }
588
+
589
+ function groupEquanautModelCatalog(models) {
590
+ const groups = [];
591
+ const byKey = new Map();
592
+ for (const model of sortEquanautModelCatalog(models || [])) {
593
+ const key =
594
+ model.location === "local"
595
+ ? "ollama"
596
+ : model.location === "cloud"
597
+ ? "ollama-cloud"
598
+ : model.provider || "gateway";
599
+ if (!byKey.has(key)) {
600
+ const group = { key, label: providerLabel(key), entries: [] };
601
+ byKey.set(key, group);
602
+ groups.push(group);
603
+ }
604
+ byKey.get(key).entries.push(model);
605
+ }
606
+ return groups;
607
+ }
608
+
609
+ function getEquanautCommandCenterFallbackModels() {
610
+ return EQUANAUT_COMMAND_CENTER_FALLBACK_MODELS.map((entry) =>
611
+ normalizeEquanautModelEntry(entry, { source: "command-center-fallback" })
612
+ ).filter(Boolean);
613
+ }
614
+
615
+ function getEquanautModelBadge(model) {
616
+ if (model?.location === "local") return { label: "Local", className: "local" };
617
+ if (model?.route === "command-center") return { label: "Gateway", className: "gateway" };
618
+ return { label: "Cloud", className: "cloud" };
619
+ }
620
+
621
+ function getEquanautModelDisplayName(modelOrId) {
622
+ const raw =
623
+ typeof modelOrId === "object" && modelOrId
624
+ ? modelOrId.name || modelOrId.id || modelOrId.sourceId
625
+ : modelOrId;
626
+ const value = String(raw || "").trim();
627
+ if (!value) return "Choose model";
628
+ if (value === "gateway:default" || /^equabot gateway \(default\)$/i.test(value)) {
629
+ return "Gateway Default";
630
+ }
631
+ const withoutGateway = value.replace(/^gateway:/, "");
632
+ const withoutProvider = withoutGateway.includes("/")
633
+ ? withoutGateway.split("/").slice(1).join("/")
634
+ : withoutGateway;
635
+ const compact = withoutProvider
636
+ .replace(/\s+\((openai|google|anthropic|xai|ollama|deepseek|perplexity)\)$/i, "")
637
+ .trim();
638
+ return compact || value;
639
+ }
640
+
641
+ function isEquanautModelConfigured(model) {
642
+ if (!model || typeof model !== "object") return false;
643
+ const status = String(model.status || "")
644
+ .trim()
645
+ .toLowerCase();
646
+ return model.enabled !== false && status === "available";
647
+ }
648
+
649
+ function getEquanautModelSettingsUrl(modelOrId, reason) {
650
+ const id =
651
+ typeof modelOrId === "object" && modelOrId
652
+ ? modelOrId.id || modelOrId.sourceId || modelOrId.name
653
+ : modelOrId;
654
+ const url = new URL("https://app.equa.cc/equabotz/settings");
655
+ url.searchParams.set("section", "models");
656
+ if (id) url.searchParams.set("model", String(id));
657
+ if (reason) url.searchParams.set("reason", String(reason));
658
+ url.searchParams.set("source", "browser-extension");
659
+ return url.toString();
660
+ }
661
+
662
+ function isEquanautCommandCenterModel(modelId) {
663
+ return String(modelId || "").startsWith("gateway:");
664
+ }
665
+
666
+ function resolveEquanautOllamaModelId(modelId) {
667
+ const id = String(modelId || "").trim();
668
+ if (!id || isEquanautCommandCenterModel(id)) return null;
669
+ return id.startsWith("ollama:") ? id.slice("ollama:".length) : id;
670
+ }
671
+
672
+ function normalizeEquanautRoutingMode(mode) {
673
+ const value = String(mode || "")
674
+ .trim()
675
+ .toLowerCase();
676
+ return value === "local" || value === "cloud-only" ? value : "auto";
677
+ }
678
+
679
+ function isEquanautModelAllowedForRouting(model, routingMode) {
680
+ const mode = normalizeEquanautRoutingMode(routingMode);
681
+ if (!model || model.enabled === false) return false;
682
+ if (mode === "local") return model.location === "local";
683
+ if (mode === "cloud-only") return model.route === "command-center";
684
+ return true;
685
+ }
686
+
687
+ function pickEquanautModelForRoutingMode(models, currentId, routingMode, fallbackId) {
688
+ const mode = normalizeEquanautRoutingMode(routingMode);
689
+ const list = Array.isArray(models) ? models : [];
690
+ const current = list.find((model) => model.id === currentId);
691
+ if (isEquanautModelAllowedForRouting(current, mode)) return current.id;
692
+
693
+ const preferredDefault = list.find(
694
+ (model) => model.isDefault && isEquanautModelAllowedForRouting(model, mode)
695
+ );
696
+ if (preferredDefault) return preferredDefault.id;
697
+
698
+ const available = list.find(
699
+ (model) => model.status === "available" && isEquanautModelAllowedForRouting(model, mode)
700
+ );
701
+ if (available) return available.id;
702
+
703
+ const anyAllowed = list.find((model) => isEquanautModelAllowedForRouting(model, mode));
704
+ if (anyAllowed) return anyAllowed.id;
705
+
706
+ const fallback = String(fallbackId || "").trim();
707
+ if (mode === "cloud-only")
708
+ return fallback.startsWith("gateway:") ? fallback : "gateway:default";
709
+ if (mode === "local") return fallback && !fallback.startsWith("gateway:") ? fallback : "";
710
+ return fallback || "gateway:default";
711
+ }
712
+
713
+ function timeAgo(ts, now) {
714
+ if (now === undefined) now = Date.now();
715
+ const seconds = Math.floor((now - ts) / 1000);
716
+ if (seconds < 60) return "just now";
717
+ const minutes = Math.floor(seconds / 60);
718
+ if (minutes < 60) return `${minutes} min ago`;
719
+ const hours = Math.floor(minutes / 60);
720
+ if (hours < 24) return `${hours}h ago`;
721
+ const days = Math.floor(hours / 24);
722
+ return `${days}d ago`;
723
+ }
724
+
725
+ function fsFuzzyMatch(query, text) {
726
+ const lower = text.toLowerCase();
727
+ const q = query.toLowerCase();
728
+ if (lower.includes(q)) return true;
729
+ let qi = 0;
730
+ for (let i = 0; i < lower.length && qi < q.length; i++) {
731
+ if (lower[i] === q[qi]) qi++;
732
+ }
733
+ return qi === q.length;
734
+ }
735
+
736
+ function fsFormatEventDescription(event) {
737
+ const title = event.title || event.groupTitle || "";
738
+ switch (event.type) {
739
+ case "group_created":
740
+ return "Created group" + (title ? ': "' + title + '"' : "");
741
+ case "group_archived":
742
+ return "Archived" + (title ? ': "' + title + '"' : "");
743
+ case "group_restored":
744
+ return "Restored" + (title ? ': "' + title + '"' : "");
745
+ case "group_deleted":
746
+ return "Deleted" + (title ? ': "' + title + '"' : "");
747
+ case "save_and_close":
748
+ return (
749
+ "Saved & closed " +
750
+ (event.tabCount || "?") +
751
+ " tabs in " +
752
+ (event.groupCount || "?") +
753
+ " groups"
754
+ );
755
+ case "lifecycle_start":
756
+ return "Agent started: " + (event.agentId || "unknown");
757
+ case "lifecycle_complete":
758
+ return "Agent completed: " + (event.agentId || "unknown");
759
+ case "lifecycle_abort":
760
+ return "Agent aborted: " + (event.agentId || "unknown");
761
+ case "tab_deleted":
762
+ return "Removed tab from " + (title || "group");
763
+ case "bulk_action":
764
+ return (event.action || "Bulk action") + " on " + (event.count || "?") + " items";
765
+ default:
766
+ return event.type || "Unknown action";
767
+ }
768
+ }
769
+
770
+ function fsApplyFilters(groups, query, filterMode, hideArchived) {
771
+ let filtered = groups;
772
+
773
+ if (filterMode === "named") {
774
+ filtered = filtered.filter(
775
+ (g) => g.title && g.title !== "Untitled" && g.title !== "Ungrouped Tabs"
776
+ );
777
+ } else if (filterMode === "folders") {
778
+ filtered = filtered.filter((g) => g.folderId);
779
+ }
780
+
781
+ if (hideArchived) {
782
+ filtered = filtered.filter((g) => g.status !== "archived");
783
+ }
784
+
785
+ if (query) {
786
+ filtered = filtered.filter((g) => {
787
+ const tabs = g.urls || g.tabs || [];
788
+ return (
789
+ (g.title || "").toLowerCase().includes(query) ||
790
+ tabs.some(
791
+ (t) =>
792
+ (t.title || "").toLowerCase().includes(query) ||
793
+ (t.url || "").toLowerCase().includes(query)
794
+ )
795
+ );
796
+ });
797
+ }
798
+
799
+ return filtered;
800
+ }
801
+
802
+ // Extended filter engine for the full-screen filter bar
803
+ function fsApplyAdvancedFilters(groups, opts) {
804
+ let filtered = groups;
805
+
806
+ // Sidebar filter mode (legacy compat)
807
+ if (opts.filterMode === "named") {
808
+ filtered = filtered.filter(
809
+ (g) => g.title && g.title !== "Untitled" && g.title !== "Ungrouped Tabs"
810
+ );
811
+ } else if (opts.filterMode === "folders") {
812
+ filtered = filtered.filter((g) => g.folderId);
813
+ }
814
+
815
+ // Status filter
816
+ if (opts.status && opts.status !== "all") {
817
+ filtered = filtered.filter((g) => (g.status || "archived") === opts.status);
818
+ }
819
+
820
+ // Color filter (array of active colors)
821
+ if (opts.colors && opts.colors.length > 0) {
822
+ filtered = filtered.filter((g) => opts.colors.includes(g.color || "grey"));
823
+ }
824
+
825
+ // Starred only
826
+ if (opts.starred) {
827
+ filtered = filtered.filter((g) => g.starred);
828
+ }
829
+
830
+ // Locked only
831
+ if (opts.locked) {
832
+ filtered = filtered.filter((g) =>
833
+ opts.lockedSet ? opts.lockedSet.has(g.taskThreadId || g.id) : g.locked
834
+ );
835
+ }
836
+
837
+ // Stale only (>7 days old)
838
+ if (opts.stale) {
839
+ const staleThreshold = 7 * 24 * 60 * 60 * 1000;
840
+ const now = Date.now();
841
+ filtered = filtered.filter((g) => {
842
+ const ts = g.archivedAt || g.closedAt;
843
+ return ts && now - new Date(ts).getTime() > staleThreshold;
844
+ });
845
+ }
846
+
847
+ // Tab count range
848
+ if (opts.minTabs > 0) {
849
+ filtered = filtered.filter((g) => (g.urls || g.tabs || []).length >= opts.minTabs);
850
+ }
851
+ if (opts.maxTabs > 0 && opts.maxTabs < Infinity) {
852
+ filtered = filtered.filter((g) => (g.urls || g.tabs || []).length <= opts.maxTabs);
853
+ }
854
+
855
+ // Text search (title + description + tab titles + tab URLs + tab descriptions)
856
+ if (opts.query) {
857
+ const q = opts.query.toLowerCase();
858
+ filtered = filtered.filter((g) => {
859
+ const tabs = g.urls || g.tabs || [];
860
+ return (
861
+ (g.title || "").toLowerCase().includes(q) ||
862
+ normalizeEntityDescription(g).toLowerCase().includes(q) ||
863
+ tabs.some(
864
+ (t) =>
865
+ (t.title || "").toLowerCase().includes(q) ||
866
+ (t.url || "").toLowerCase().includes(q) ||
867
+ normalizeEntityDescription(t).toLowerCase().includes(q)
868
+ )
869
+ );
870
+ });
871
+ }
872
+
873
+ return filtered;
874
+ }
875
+
876
+ // Sort groups by various criteria
877
+ function fsSortGroups(groups, sortBy) {
878
+ const sorted = [...groups];
879
+ switch (sortBy) {
880
+ case "newest":
881
+ sorted.sort((a, b) => {
882
+ const ta = new Date(a.archivedAt || a.closedAt || 0).getTime();
883
+ const tb = new Date(b.archivedAt || b.closedAt || 0).getTime();
884
+ return tb - ta;
885
+ });
886
+ break;
887
+ case "oldest":
888
+ sorted.sort((a, b) => {
889
+ const ta = new Date(a.archivedAt || a.closedAt || 0).getTime();
890
+ const tb = new Date(b.archivedAt || b.closedAt || 0).getTime();
891
+ return ta - tb;
892
+ });
893
+ break;
894
+ case "az":
895
+ sorted.sort((a, b) => (a.title || "Untitled").localeCompare(b.title || "Untitled"));
896
+ break;
897
+ case "za":
898
+ sorted.sort((a, b) => (b.title || "Untitled").localeCompare(a.title || "Untitled"));
899
+ break;
900
+ case "most-tabs":
901
+ sorted.sort((a, b) => (b.urls || b.tabs || []).length - (a.urls || a.tabs || []).length);
902
+ break;
903
+ case "fewest-tabs":
904
+ sorted.sort((a, b) => (a.urls || a.tabs || []).length - (b.urls || b.tabs || []).length);
905
+ break;
906
+ default:
907
+ break;
908
+ }
909
+ return sorted;
910
+ }
911
+
912
+ // Group groups into sections by a property
913
+ function fsGroupBy(groups, groupByKey) {
914
+ if (!groupByKey || groupByKey === "none") {
915
+ return [{ key: null, label: null, groups: groups }];
916
+ }
917
+
918
+ const sections = new Map();
919
+
920
+ for (const group of groups) {
921
+ let sectionKey, sectionLabel;
922
+
923
+ switch (groupByKey) {
924
+ case "color": {
925
+ sectionKey = group.color || "grey";
926
+ sectionLabel = sectionKey.charAt(0).toUpperCase() + sectionKey.slice(1);
927
+ break;
928
+ }
929
+ case "status": {
930
+ sectionKey = group.status || "archived";
931
+ const statusLabels = { pending: "Pending", done: "Done", archived: "Archived" };
932
+ sectionLabel = statusLabels[sectionKey] || sectionKey;
933
+ break;
934
+ }
935
+ case "date": {
936
+ const ts = new Date(group.archivedAt || group.closedAt || 0).getTime();
937
+ const now = Date.now();
938
+ const dayMs = 24 * 60 * 60 * 1000;
939
+ const age = now - ts;
940
+ if (age < dayMs) {
941
+ sectionKey = "today";
942
+ sectionLabel = "Today";
943
+ } else if (age < 7 * dayMs) {
944
+ sectionKey = "this-week";
945
+ sectionLabel = "This Week";
946
+ } else if (age < 30 * dayMs) {
947
+ sectionKey = "this-month";
948
+ sectionLabel = "This Month";
949
+ } else {
950
+ sectionKey = "older";
951
+ sectionLabel = "Older";
952
+ }
953
+ break;
954
+ }
955
+ case "size": {
956
+ const tabCount = (group.urls || group.tabs || []).length;
957
+ if (tabCount === 0) {
958
+ sectionKey = "empty";
959
+ sectionLabel = "Empty";
960
+ } else if (tabCount <= 3) {
961
+ sectionKey = "small";
962
+ sectionLabel = "Small (1-3 tabs)";
963
+ } else if (tabCount <= 10) {
964
+ sectionKey = "medium";
965
+ sectionLabel = "Medium (4-10 tabs)";
966
+ } else {
967
+ sectionKey = "large";
968
+ sectionLabel = "Large (10+ tabs)";
969
+ }
970
+ break;
971
+ }
972
+ default:
973
+ sectionKey = "other";
974
+ sectionLabel = "Other";
975
+ }
976
+
977
+ if (!sections.has(sectionKey)) {
978
+ sections.set(sectionKey, { key: sectionKey, label: sectionLabel, groups: [] });
979
+ }
980
+ sections.get(sectionKey).groups.push(group);
981
+ }
982
+
983
+ return Array.from(sections.values());
984
+ }
985
+
986
+ function fsResolveSelectedTabs(selectedSet, cachedGroups) {
987
+ const result = [];
988
+ for (const key of selectedSet) {
989
+ const lastDash = key.lastIndexOf("-");
990
+ if (lastDash === -1) continue;
991
+ const groupId = key.slice(0, lastDash);
992
+ const idx = parseInt(key.slice(lastDash + 1));
993
+ const group = cachedGroups.find((g) => (g.taskThreadId || g.id) === groupId);
994
+ if (!group) continue;
995
+ const tabs = group.urls || group.tabs || [];
996
+ if (idx >= 0 && idx < tabs.length) {
997
+ result.push({ groupId, tabIdx: idx, tab: tabs[idx] });
998
+ }
999
+ }
1000
+ return result;
1001
+ }
1002
+
1003
+ function getDomainDisplayName(url, domainMap) {
1004
+ if (domainMap === undefined) domainMap = DEFAULT_DOMAIN_DISPLAY_MAP;
1005
+ try {
1006
+ const hostname = new URL(url).hostname.replace(/^www\./, "");
1007
+ return domainMap[hostname] || hostname;
1008
+ } catch {
1009
+ return "Other";
1010
+ }
1011
+ }
1012
+
1013
+ const DEFAULT_SAFETY_REMINDER_HOST_ALLOWLIST = Object.freeze([
1014
+ "localhost",
1015
+ "127.0.0.1",
1016
+ "*.equa.test",
1017
+ "*.up.railway.app",
1018
+ "staging.equa.cc",
1019
+ "*.staging.equa.cc",
1020
+ "*.preview.equa.cc",
1021
+ "*.dev.equa.cc",
1022
+ ]);
1023
+
1024
+ const EQUANAUT_COMMAND_LIBRARY_GROUPS = Object.freeze([
1025
+ {
1026
+ plugin: "Comet",
1027
+ pluginId: "comet",
1028
+ source: "skill",
1029
+ category: "browser_control",
1030
+ rootPath: "/Users/shawnowen/.agents/skills",
1031
+ order: 10,
1032
+ requiredTools: ["comet_connect"],
1033
+ keywords: ["browser", "comet", "agent", "sidecar"],
1034
+ template:
1035
+ "Use this Comet browser skill only after comet_connect establishes the owned tab group and top-display window proof.",
1036
+ commands: [
1037
+ {
1038
+ id: "comet-agent",
1039
+ title: "Comet agent",
1040
+ description:
1041
+ "Load the comet-agent browser orchestration skill and route browser work through Comet-Bridge.",
1042
+ requiredTools: ["comet_connect", "comet_ask", "comet_screenshot", "comet_read_page"],
1043
+ aliases: ["browser-agent", "comet-browser-agent"],
1044
+ keywords: ["browser", "orchestrator", "agent", "sidecar", "skills"],
1045
+ template:
1046
+ "Use the comet-agent skill. Start with comet_connect, verify top-display windowed full-display placement, then use the owned tab group only.",
1047
+ },
1048
+ "comet-assistant",
1049
+ "comet-auto",
1050
+ "comet-browse",
1051
+ { id: "comet-connect", title: "comet_connect", aliases: ["connect", "browser-connect"] },
1052
+ "comet-delegate",
1053
+ {
1054
+ id: "comet-ask",
1055
+ skillName: "comet-agent",
1056
+ title: "comet_ask",
1057
+ requiredTools: ["comet_connect", "comet_ask"],
1058
+ aliases: ["ask", "browser-ask"],
1059
+ },
1060
+ "comet-interact",
1061
+ "comet-keepalive",
1062
+ "comet-labs",
1063
+ "comet-monitor",
1064
+ "comet-network",
1065
+ "comet-pdf",
1066
+ "comet-read",
1067
+ {
1068
+ id: "comet-read-page",
1069
+ skillName: "comet-read",
1070
+ title: "comet_read_page",
1071
+ requiredTools: ["comet_connect", "comet_read_page"],
1072
+ aliases: ["read-page", "browser-read"],
1073
+ },
1074
+ "comet-research",
1075
+ "comet-scrape",
1076
+ {
1077
+ id: "comet-screenshot",
1078
+ title: "comet_screenshot",
1079
+ requiredTools: ["comet_connect", "comet_screenshot"],
1080
+ aliases: ["screenshot", "browser-screenshot"],
1081
+ },
1082
+ "comet-search",
1083
+ "comet-session",
1084
+ "comet-shortcuts",
1085
+ "comet-skill-inventory",
1086
+ "comet-workspace",
1087
+ ],
1088
+ },
1089
+ {
1090
+ plugin: "Spequa",
1091
+ pluginId: "spequa",
1092
+ source: "plugin",
1093
+ category: "spequa",
1094
+ rootPath: "/Users/shawnowen/.codex/plugins/cache/equa-local/spequa/0.9.2/skills",
1095
+ prefix: "spequa:",
1096
+ order: 200,
1097
+ keywords: ["spequa", "sdd", "spec", "pipeline"],
1098
+ template:
1099
+ "Route to the Spequa command skill and follow its source prompt under /Users/shawnowen/dev/equa/tools/spequa/.codex/prompts/.",
1100
+ commands: [
1101
+ { id: "spequa", fullId: "spequa", aliases: ["spequa:spequa"] },
1102
+ "0-pipeline",
1103
+ "1-constitution",
1104
+ "2-problems",
1105
+ "3-spequafy",
1106
+ "3.0-diagnose",
1107
+ "3.1-analyze-domain",
1108
+ "4-clarify",
1109
+ "4.1-expert-decision",
1110
+ "5-plan",
1111
+ "5.1-research",
1112
+ "6-tasks",
1113
+ "6.1-taskstoissues",
1114
+ "7-checklist",
1115
+ "8-analyze",
1116
+ "9-test",
1117
+ "10-implement",
1118
+ "11-docs",
1119
+ "12-score-docs",
1120
+ "13-code-review",
1121
+ "14-create-pull-request",
1122
+ "14.1-pr-checks",
1123
+ "15-comet-review",
1124
+ "15.1-comet-pr-review",
1125
+ "16-close",
1126
+ ],
1127
+ },
1128
+ {
1129
+ plugin: "Equa Web Test Skills",
1130
+ pluginId: "equa-web-test-skills",
1131
+ source: "skill",
1132
+ category: "qa",
1133
+ rootPath: "/Users/shawnowen/.agents/skills",
1134
+ order: 500,
1135
+ keywords: ["equa", "qa", "test", "smoke", "browser"],
1136
+ template:
1137
+ "Route to the requested Equa QA skill and preserve the repo-specific browser validation contract.",
1138
+ commands: [
1139
+ "auth-flow-qa",
1140
+ "dataroom-document-checklist",
1141
+ "e2e-auth",
1142
+ "e2e-captable",
1143
+ "e2e-full",
1144
+ "e2e-org",
1145
+ "e2e-smoke",
1146
+ "equa-orchestrator-qa",
1147
+ "equa-start-dev",
1148
+ "equa-web-test-skills",
1149
+ "mattermost-messaging-qa",
1150
+ "pr-review-browser-qa",
1151
+ ],
1152
+ },
1153
+ {
1154
+ plugin: "Knowledge Base",
1155
+ pluginId: "knowledge-base",
1156
+ source: "skill",
1157
+ category: "knowledge",
1158
+ rootPath: "/Users/shawnowen/.agents/skills",
1159
+ order: 650,
1160
+ keywords: ["kb", "knowledge", "entity", "search"],
1161
+ template:
1162
+ "Route to the Knowledge Base skill and use the user's arguments as the lookup query.",
1163
+ commands: [
1164
+ "kb-ask",
1165
+ "kb-entity",
1166
+ "kb-index",
1167
+ "kb-search",
1168
+ "task-threads",
1169
+ "task-blocker-analysis",
1170
+ ],
1171
+ },
1172
+ {
1173
+ plugin: "Local Agents",
1174
+ pluginId: "local-agents",
1175
+ source: "skill",
1176
+ category: "local_agent",
1177
+ rootPath: "/Users/shawnowen/.agents/skills",
1178
+ order: 760,
1179
+ keywords: ["local", "agent", "handoff", "llm"],
1180
+ template:
1181
+ "Route to the requested local agent skill and keep the invocation as structured sidecar context.",
1182
+ commands: [
1183
+ "cursor-agent-handoff",
1184
+ "email-thread-research",
1185
+ "gitguardian-remediate",
1186
+ "linear-tasks",
1187
+ "local-analysis",
1188
+ "local-code",
1189
+ "local-creative",
1190
+ "local-llm",
1191
+ "mattermost-infra-deploy",
1192
+ "source-command-handoff-cursor-agent",
1193
+ "switch-model",
1194
+ ],
1195
+ },
1196
+ {
1197
+ plugin: "QuickBooks Ops",
1198
+ pluginId: "qbo",
1199
+ source: "skill",
1200
+ category: "finance",
1201
+ rootPath: "/Users/shawnowen/.agents/skills",
1202
+ order: 850,
1203
+ keywords: ["qbo", "quickbooks", "accounting", "finance"],
1204
+ template: "Route to the requested QBO skill and preserve accounting workflow context.",
1205
+ commands: [
1206
+ "qbo-accounting",
1207
+ "qbo-ar-review",
1208
+ "qbo-bank-fetch",
1209
+ "qbo-bill-email",
1210
+ "qbo-bill-open",
1211
+ "qbo-journal-entry",
1212
+ "qbo-month-close",
1213
+ "qbo-pay-bill",
1214
+ "qbo-reconcile",
1215
+ "qbo-report",
1216
+ "qbo-tax-prep",
1217
+ "qbo-vendor-reconcile",
1218
+ ],
1219
+ },
1220
+ {
1221
+ plugin: "Prescription Refill SOP",
1222
+ pluginId: "prescription-refill",
1223
+ source: "skill",
1224
+ category: "personal_ops",
1225
+ rootPath: "/Users/shawnowen/.agents/skills/prescription-refill-skills",
1226
+ order: 970,
1227
+ keywords: ["prescription", "refill", "mychart", "costco"],
1228
+ template:
1229
+ "Route to the requested prescription refill SOP skill and preserve health workflow context.",
1230
+ commands: [
1231
+ "costco-pharmacy-phone-call",
1232
+ "log-refill-event-to-workbook",
1233
+ "monitor-shortwave-refill-emails",
1234
+ "prescription-refill-sop-orchestrator",
1235
+ "submit-mychart-refill-request",
1236
+ "update-refill-tracker-doc",
1237
+ "verify-mychart-refill-approval",
1238
+ ],
1239
+ },
1240
+ {
1241
+ plugin: "SVF Operations",
1242
+ pluginId: "svf",
1243
+ source: "skill",
1244
+ category: "property_ops",
1245
+ rootPath: "/Users/shawnowen/.codex/skills",
1246
+ order: 1080,
1247
+ keywords: ["svf", "property", "documents", "sales"],
1248
+ template: "Route to the requested Sugarloaf Valley Farms skill.",
1249
+ commands: [
1250
+ "svf-chain-of-title",
1251
+ "svf-document-intake",
1252
+ "svf-north-lots-sales-ops",
1253
+ "svf-property-manager",
1254
+ "svf-task-thread-orchestrator",
1255
+ ],
1256
+ },
1257
+ {
1258
+ plugin: "Codex Local Skills",
1259
+ pluginId: "codex-local",
1260
+ source: "skill",
1261
+ category: "codex",
1262
+ rootPath: "/Users/shawnowen/.codex/skills",
1263
+ order: 1160,
1264
+ keywords: ["codex", "local", "screen", "migration"],
1265
+ template: "Route to the requested local Codex skill.",
1266
+ commands: ["chronicle", "migrate-to-codex"],
1267
+ },
1268
+ {
1269
+ plugin: "Browser Use",
1270
+ pluginId: "browser-use",
1271
+ source: "plugin",
1272
+ category: "browser",
1273
+ rootPath:
1274
+ "/Users/shawnowen/.codex/plugins/cache/openai-bundled/browser-use/0.1.0-alpha2/skills",
1275
+ prefix: "browser-use:",
1276
+ order: 1240,
1277
+ keywords: ["browser", "in-app", "openai"],
1278
+ template:
1279
+ "Route to the Browser Use plugin skill when the in-app browser is explicitly requested.",
1280
+ commands: ["browser"],
1281
+ },
1282
+ {
1283
+ plugin: "Computer Use",
1284
+ pluginId: "computer-use",
1285
+ source: "plugin",
1286
+ category: "desktop",
1287
+ rootPath: "/Users/shawnowen/.codex/plugins/cache/openai-bundled/computer-use/1.0.780/skills",
1288
+ prefix: "computer-use:",
1289
+ order: 1260,
1290
+ keywords: ["computer", "desktop", "macos"],
1291
+ template: "Route to the Computer Use plugin skill for desktop-level evidence or UI control.",
1292
+ commands: ["computer-use"],
1293
+ },
1294
+ {
1295
+ plugin: "Figma",
1296
+ pluginId: "figma",
1297
+ source: "plugin",
1298
+ category: "design",
1299
+ rootPath: "/Users/shawnowen/.codex/plugins/cache/openai-curated/figma/63976030/skills",
1300
+ prefix: "figma:",
1301
+ order: 1300,
1302
+ keywords: ["figma", "design", "figjam"],
1303
+ template: "Route to the requested Figma skill and follow its prerequisite instructions.",
1304
+ commands: [
1305
+ { id: "figma-code-connect", skillName: "figma-code-connect-components" },
1306
+ "figma-create-design-system-rules",
1307
+ "figma-create-new-file",
1308
+ "figma-generate-design",
1309
+ "figma-generate-library",
1310
+ "figma-implement-design",
1311
+ "figma-use",
1312
+ ],
1313
+ },
1314
+ {
1315
+ plugin: "GitHub",
1316
+ pluginId: "github",
1317
+ source: "plugin",
1318
+ category: "devops",
1319
+ rootPath: "/Users/shawnowen/.codex/plugins/cache/openai-curated/github/63976030/skills",
1320
+ prefix: "github:",
1321
+ order: 1410,
1322
+ keywords: ["github", "pr", "issue", "ci"],
1323
+ template: "Route to the requested GitHub skill and preserve repository context.",
1324
+ commands: ["github", "gh-address-comments", "gh-fix-ci", "yeet"],
1325
+ },
1326
+ {
1327
+ plugin: "Gmail",
1328
+ pluginId: "gmail",
1329
+ source: "plugin",
1330
+ category: "workspace",
1331
+ rootPath: "/Users/shawnowen/.codex/plugins/cache/openai-curated/gmail/63976030/skills",
1332
+ prefix: "gmail:",
1333
+ order: 1480,
1334
+ keywords: ["gmail", "email", "inbox"],
1335
+ template: "Route to the requested Gmail skill with the user's mailbox context.",
1336
+ commands: ["gmail", "gmail-inbox-triage"],
1337
+ },
1338
+ {
1339
+ plugin: "Google Calendar",
1340
+ pluginId: "google-calendar",
1341
+ source: "plugin",
1342
+ category: "workspace",
1343
+ rootPath:
1344
+ "/Users/shawnowen/.codex/plugins/cache/openai-curated/google-calendar/63976030/skills",
1345
+ prefix: "google-calendar:",
1346
+ order: 1520,
1347
+ keywords: ["google", "calendar", "schedule"],
1348
+ template: "Route to the requested Google Calendar skill.",
1349
+ commands: [
1350
+ "google-calendar",
1351
+ "google-calendar-daily-brief",
1352
+ "google-calendar-free-up-time",
1353
+ "google-calendar-group-scheduler",
1354
+ "google-calendar-meeting-prep",
1355
+ ],
1356
+ },
1357
+ {
1358
+ plugin: "Google Drive",
1359
+ pluginId: "google-drive",
1360
+ source: "plugin",
1361
+ category: "workspace",
1362
+ rootPath: "/Users/shawnowen/.codex/plugins/cache/openai-curated/google-drive/63976030/skills",
1363
+ prefix: "google-drive:",
1364
+ order: 1580,
1365
+ keywords: ["google", "drive", "docs", "sheets", "slides"],
1366
+ template: "Route to the requested Google Drive skill.",
1367
+ commands: [
1368
+ "google-drive",
1369
+ "google-docs",
1370
+ "google-drive-comments",
1371
+ "google-sheets",
1372
+ "google-slides",
1373
+ ],
1374
+ },
1375
+ {
1376
+ plugin: "Linear",
1377
+ pluginId: "linear",
1378
+ source: "plugin",
1379
+ category: "planning",
1380
+ rootPath: "/Users/shawnowen/.codex/plugins/cache/openai-curated/linear/63976030/skills",
1381
+ prefix: "linear:",
1382
+ order: 1640,
1383
+ keywords: ["linear", "issues", "projects"],
1384
+ template: "Route to the Linear skill with issue or project context.",
1385
+ commands: ["linear"],
1386
+ },
1387
+ {
1388
+ plugin: "OpenAI Developers",
1389
+ pluginId: "openai-developers",
1390
+ source: "plugin",
1391
+ category: "developer",
1392
+ rootPath:
1393
+ "/Users/shawnowen/.codex/plugins/cache/openai-curated/openai-developers/63976030/skills",
1394
+ prefix: "openai-developers:",
1395
+ order: 1690,
1396
+ keywords: ["openai", "api", "agents", "chatgpt"],
1397
+ template: "Route to the requested OpenAI Developers skill.",
1398
+ commands: [
1399
+ "agents-sdk",
1400
+ "build-chatgpt-app",
1401
+ "chatgpt-app-submission",
1402
+ "openai-api-troubleshooting",
1403
+ "openai-platform-api-key",
1404
+ ],
1405
+ },
1406
+ {
1407
+ plugin: "Documents",
1408
+ pluginId: "documents",
1409
+ source: "plugin",
1410
+ category: "document",
1411
+ rootPath:
1412
+ "/Users/shawnowen/.codex/plugins/cache/openai-primary-runtime/documents/26.506.11943/skills",
1413
+ prefix: "documents:",
1414
+ order: 1760,
1415
+ keywords: ["documents", "docx"],
1416
+ template: "Route to the Documents runtime skill.",
1417
+ commands: ["documents"],
1418
+ },
1419
+ {
1420
+ plugin: "Presentations",
1421
+ pluginId: "presentations",
1422
+ source: "plugin",
1423
+ category: "document",
1424
+ rootPath:
1425
+ "/Users/shawnowen/.codex/plugins/cache/openai-primary-runtime/presentations/26.506.11943/skills",
1426
+ prefix: "presentations:",
1427
+ order: 1780,
1428
+ keywords: ["presentations", "slides", "pptx"],
1429
+ template: "Route to the Presentations runtime skill.",
1430
+ commands: ["presentations"],
1431
+ },
1432
+ {
1433
+ plugin: "Spreadsheets",
1434
+ pluginId: "spreadsheets",
1435
+ source: "plugin",
1436
+ category: "document",
1437
+ rootPath:
1438
+ "/Users/shawnowen/.codex/plugins/cache/openai-primary-runtime/spreadsheets/26.506.11943/skills",
1439
+ prefix: "spreadsheets:",
1440
+ order: 1800,
1441
+ keywords: ["spreadsheets", "sheets", "xlsx"],
1442
+ template: "Route to the Spreadsheets runtime skill.",
1443
+ commands: ["spreadsheets"],
1444
+ },
1445
+ {
1446
+ plugin: "Superpowers",
1447
+ pluginId: "superpowers",
1448
+ source: "plugin",
1449
+ category: "workflow",
1450
+ rootPath:
1451
+ "/Users/shawnowen/.codex/plugins/cache/claude-plugins-official/superpowers/5.1.0/skills",
1452
+ prefix: "superpowers:",
1453
+ order: 1850,
1454
+ keywords: ["superpowers", "workflow", "planning", "tdd"],
1455
+ template: "Route to the requested Superpowers workflow skill.",
1456
+ commands: [
1457
+ "brainstorming",
1458
+ "dispatching-parallel-agents",
1459
+ "executing-plans",
1460
+ "finishing-a-development-branch",
1461
+ "receiving-code-review",
1462
+ "requesting-code-review",
1463
+ "subagent-driven-development",
1464
+ "systematic-debugging",
1465
+ "test-driven-development",
1466
+ "using-git-worktrees",
1467
+ "using-superpowers",
1468
+ "verification-before-completion",
1469
+ "writing-plans",
1470
+ "writing-skills",
1471
+ ],
1472
+ },
1473
+ {
1474
+ plugin: "Security",
1475
+ pluginId: "codex-security",
1476
+ source: "plugin",
1477
+ category: "security",
1478
+ rootPath:
1479
+ "/Users/shawnowen/.codex/plugins/cache/openai-curated/codex-security/63976030/skills",
1480
+ prefix: "codex-security:",
1481
+ order: 2020,
1482
+ keywords: ["security", "threat", "scan"],
1483
+ template: "Route to the requested Codex Security skill.",
1484
+ commands: [
1485
+ "attack-path-analysis",
1486
+ "finding-discovery",
1487
+ "fix-finding",
1488
+ "security-scan",
1489
+ "threat-model",
1490
+ "validation",
1491
+ ],
1492
+ },
1493
+ {
1494
+ plugin: "CodeRabbit",
1495
+ pluginId: "coderabbit",
1496
+ source: "plugin",
1497
+ category: "code_review",
1498
+ rootPath: "/Users/shawnowen/.codex/plugins/cache/openai-curated/coderabbit/63976030/skills",
1499
+ prefix: "coderabbit:",
1500
+ order: 2120,
1501
+ keywords: ["coderabbit", "review"],
1502
+ template: "Route to the CodeRabbit review skill.",
1503
+ commands: ["coderabbit-review"],
1504
+ },
1505
+ {
1506
+ plugin: "Frontend Design",
1507
+ pluginId: "frontend-design",
1508
+ source: "plugin",
1509
+ category: "design",
1510
+ rootPath:
1511
+ "/Users/shawnowen/.codex/plugins/cache/claude-plugins-official/frontend-design/local/skills",
1512
+ prefix: "frontend-design:",
1513
+ order: 2180,
1514
+ keywords: ["frontend", "design", "ui"],
1515
+ template: "Route to the Frontend Design skill.",
1516
+ commands: ["frontend-design"],
1517
+ },
1518
+ {
1519
+ plugin: "Hookify",
1520
+ pluginId: "hookify",
1521
+ source: "plugin",
1522
+ category: "writing",
1523
+ rootPath:
1524
+ "/Users/shawnowen/.codex/plugins/cache/claude-plugins-official/hookify/local/skills",
1525
+ prefix: "hookify:",
1526
+ order: 2220,
1527
+ keywords: ["hookify", "writing", "rules"],
1528
+ template: "Route to the Hookify writing-rules skill.",
1529
+ commands: [{ id: "writing-hookify-rules", skillName: "writing-rules" }],
1530
+ },
1531
+ {
1532
+ plugin: "Skill Creator",
1533
+ pluginId: "skill-creator",
1534
+ source: "plugin",
1535
+ category: "developer",
1536
+ rootPath:
1537
+ "/Users/shawnowen/.codex/plugins/cache/claude-plugins-official/skill-creator/local/skills",
1538
+ prefix: "skill-creator:",
1539
+ order: 2260,
1540
+ keywords: ["skill", "creator"],
1541
+ template: "Route to the Skill Creator skill.",
1542
+ commands: ["skill-creator"],
1543
+ },
1544
+ ]);
1545
+
1546
+ const EQUANAUT_PLUGIN_LIBRARY_GROUPS = Object.freeze([
1547
+ ["comet", "Comet Browser Skills", "local", "browser_control"],
1548
+ ["spequa", "Spequa", "plugin", "spequa"],
1549
+ ["equa-web-test-skills", "Equa Web Test Skills", "local", "qa"],
1550
+ ["knowledge-base", "Knowledge Base", "local", "knowledge"],
1551
+ ["local-agents", "Local Agents", "local", "local_agent"],
1552
+ ["qbo", "QuickBooks Ops", "local", "finance"],
1553
+ ["prescription-refill", "Prescription Refill SOP", "local", "personal_ops"],
1554
+ ["svf", "SVF Operations", "local", "property_ops"],
1555
+ ["codex-local", "Codex Local Skills", "local", "codex"],
1556
+ ["browser-use", "Browser Use", "plugin", "browser"],
1557
+ ["computer-use", "Computer Use", "plugin", "desktop"],
1558
+ ["figma", "Figma", "plugin", "design"],
1559
+ ["github", "GitHub", "plugin", "devops"],
1560
+ ["gmail", "Gmail", "plugin", "workspace"],
1561
+ ["google-calendar", "Google Calendar", "plugin", "workspace"],
1562
+ ["google-drive", "Google Drive", "plugin", "workspace"],
1563
+ ["linear", "Linear", "plugin", "planning"],
1564
+ ["openai-developers", "OpenAI Developers", "plugin", "developer"],
1565
+ ["documents", "Documents", "plugin", "document"],
1566
+ ["presentations", "Presentations", "plugin", "document"],
1567
+ ["spreadsheets", "Spreadsheets", "plugin", "document"],
1568
+ ["superpowers", "Superpowers", "plugin", "workflow"],
1569
+ ["codex-security", "Codex Security", "plugin", "security"],
1570
+ ["coderabbit", "CodeRabbit", "plugin", "code_review"],
1571
+ ["frontend-design", "Frontend Design", "plugin", "design"],
1572
+ ["hookify", "Hookify", "plugin", "writing"],
1573
+ ["skill-creator", "Skill Creator", "plugin", "developer"],
1574
+ ]);
1575
+
1576
+ function humanizeSlashCommandTitle(value) {
1577
+ const tail = String(value || "")
1578
+ .split(":")
1579
+ .pop()
1580
+ .replace(/^spequa$/, "spequa")
1581
+ .replace(/[._-]+/g, " ")
1582
+ .trim();
1583
+ return tail ? tail.replace(/\b\w/g, (letter) => letter.toUpperCase()) : "Command";
1584
+ }
1585
+
1586
+ function slashSkillPath(rootPath, skillName) {
1587
+ if (!rootPath || !skillName) return null;
1588
+ return `${rootPath}/${skillName}/SKILL.md`;
1589
+ }
1590
+
1591
+ function expandCommandLibraryGroup(group, groupIndex) {
1592
+ const commands = Array.isArray(group.commands) ? group.commands : [];
1593
+ const prefix = group.prefix || "";
1594
+ return commands.map((entry, index) => {
1595
+ const item = typeof entry === "string" ? { id: entry } : entry || {};
1596
+ const skillName = item.skillName || item.skill || item.id;
1597
+ const id = item.fullId || `${prefix}${item.id}`;
1598
+ const requiredTools = normalizeSlashCommandArray(item.requiredTools || group.requiredTools);
1599
+ const keywords = [
1600
+ ...normalizeSlashCommandArray(group.keywords),
1601
+ ...normalizeSlashCommandArray(item.keywords),
1602
+ ];
1603
+ return {
1604
+ id,
1605
+ order: Number(group.order || groupIndex * 1000) + index,
1606
+ title: item.title || humanizeSlashCommandTitle(id),
1607
+ description:
1608
+ item.description ||
1609
+ `Load ${id} from the ${group.plugin} ${group.source === "plugin" ? "plugin" : "skill library"}.`,
1610
+ source: item.source || group.source || "skill",
1611
+ plugin: group.plugin,
1612
+ pluginId: group.pluginId,
1613
+ category: item.category || group.category || "general",
1614
+ skillName,
1615
+ skillPath:
1616
+ item.skillPath || slashSkillPath(group.rootPath, item.skillPathSegment || skillName),
1617
+ requiredTools,
1618
+ aliases: normalizeSlashCommandArray(item.aliases),
1619
+ keywords,
1620
+ template:
1621
+ item.template ||
1622
+ group.template ||
1623
+ `Route to ${id} and include the user's arguments as structured command context.`,
1624
+ };
1625
+ });
1626
+ }
1627
+
1628
+ function buildDefaultEquanautSlashCommands() {
1629
+ return EQUANAUT_COMMAND_LIBRARY_GROUPS.flatMap(expandCommandLibraryGroup);
1630
+ }
1631
+
1632
+ function pluginCommandCount(pluginId) {
1633
+ return buildDefaultEquanautSlashCommands().filter((command) => command.pluginId === pluginId)
1634
+ .length;
1635
+ }
1636
+
1637
+ const DEFAULT_EQUANAUT_SLASH_COMMANDS = Object.freeze(buildDefaultEquanautSlashCommands());
1638
+ const DEFAULT_EQUANAUT_PLUGIN_LIBRARY = Object.freeze(
1639
+ EQUANAUT_PLUGIN_LIBRARY_GROUPS.map(([id, name, source, category], index) => ({
1640
+ id,
1641
+ name,
1642
+ title: name,
1643
+ source,
1644
+ sourceFamily: "plugin",
1645
+ category,
1646
+ status: "installed",
1647
+ installed: true,
1648
+ commandCount: pluginCommandCount(id),
1649
+ readOnly: true,
1650
+ allowedOperations: ["inspect", "search", "invoke_slash_command"],
1651
+ order: (index + 1) * 10,
1652
+ }))
1653
+ );
1654
+
1655
+ function normalizeHostname(url) {
1656
+ try {
1657
+ return new URL(url).hostname.replace(/^www\./, "").toLowerCase();
1658
+ } catch {
1659
+ return "";
1660
+ }
1661
+ }
1662
+
1663
+ function hostMatchesPattern(hostname, pattern) {
1664
+ const normalizedPattern = String(pattern || "")
1665
+ .trim()
1666
+ .replace(/^www\./, "")
1667
+ .toLowerCase();
1668
+ if (!hostname || !normalizedPattern) return false;
1669
+ if (normalizedPattern.startsWith("*.")) {
1670
+ const suffix = normalizedPattern.slice(2);
1671
+ return hostname === suffix || hostname.endsWith(`.${suffix}`);
1672
+ }
1673
+ return hostname === normalizedPattern;
1674
+ }
1675
+
1676
+ function isSafetyReminderAllowlisted(url, allowlist) {
1677
+ const hostname = normalizeHostname(url);
1678
+ const patterns = Array.isArray(allowlist) ? allowlist : DEFAULT_SAFETY_REMINDER_HOST_ALLOWLIST;
1679
+ return patterns.some((pattern) => hostMatchesPattern(hostname, pattern));
1680
+ }
1681
+
1682
+ function shouldInjectSafetyReminder(url, allowlist) {
1683
+ return !isSafetyReminderAllowlisted(url, allowlist);
1684
+ }
1685
+
1686
+ function normalizeSlashCommandId(value) {
1687
+ return String(value || "")
1688
+ .trim()
1689
+ .replace(/^\/+/, "")
1690
+ .toLowerCase();
1691
+ }
1692
+
1693
+ function normalizeSlashCommandArray(value) {
1694
+ return Array.isArray(value)
1695
+ ? value.map((item) => String(item || "").trim()).filter(Boolean)
1696
+ : [];
1697
+ }
1698
+
1699
+ function normalizeEquanautSlashCommand(record, index) {
1700
+ const input = record || {};
1701
+ const id = normalizeSlashCommandId(
1702
+ input.id || input.name || input.title || `command-${index + 1}`
1703
+ );
1704
+ const title = String(input.title || input.name || `/${id}`).replace(/^\/+/, "");
1705
+ const requiredTools = normalizeSlashCommandArray(input.requiredTools || input.tools);
1706
+ const aliases = normalizeSlashCommandArray(input.aliases);
1707
+ const keywords = normalizeSlashCommandArray(input.keywords);
1708
+ return {
1709
+ id,
1710
+ name: `/${id}`,
1711
+ order: Number.isFinite(Number(input.order)) ? Number(input.order) : index + 1,
1712
+ title,
1713
+ description: String(input.description || "").trim(),
1714
+ sourceFamily: "slash_command",
1715
+ source: input.source || "skill",
1716
+ plugin: input.plugin || "Comet",
1717
+ category: input.category || "general",
1718
+ skillName: input.skillName || input.skill || null,
1719
+ skillPath: input.skillPath || null,
1720
+ requiredTools,
1721
+ aliases,
1722
+ keywords,
1723
+ template: String(input.template || "").trim(),
1724
+ status: input.installed === false ? "unavailable" : "installed",
1725
+ installed: input.installed !== false,
1726
+ readOnly: true,
1727
+ allowedOperations: ["inspect", "search", "invoke_slash_command"],
1728
+ };
1729
+ }
1730
+
1731
+ function normalizeEquanautPlugin(record, index) {
1732
+ const input = record || {};
1733
+ const id = normalizeSlashCommandId(
1734
+ input.id || input.name || input.title || `plugin-${index + 1}`
1735
+ );
1736
+ return {
1737
+ id,
1738
+ name: String(input.name || input.title || id).trim() || id,
1739
+ title: String(input.title || input.name || id).trim() || id,
1740
+ sourceFamily: "plugin",
1741
+ source: input.source || "plugin",
1742
+ category: input.category || "general",
1743
+ status: input.installed === false ? "unavailable" : input.status || "installed",
1744
+ installed: input.installed !== false,
1745
+ commandCount: Number.isFinite(Number(input.commandCount)) ? Number(input.commandCount) : 0,
1746
+ version: input.version || null,
1747
+ rootPath: input.rootPath || null,
1748
+ readOnly: true,
1749
+ allowedOperations: ["inspect", "search", "invoke_slash_command"],
1750
+ order: Number.isFinite(Number(input.order)) ? Number(input.order) : index + 1,
1751
+ };
1752
+ }
1753
+
1754
+ function buildEquanautSlashCommandCatalog(input) {
1755
+ const records = Array.isArray(input)
1756
+ ? input
1757
+ : Array.isArray(input?.commands)
1758
+ ? input.commands
1759
+ : DEFAULT_EQUANAUT_SLASH_COMMANDS;
1760
+ return records
1761
+ .map((record, index) => normalizeEquanautSlashCommand(record, index))
1762
+ .filter((record) => record.id)
1763
+ .sort((a, b) => a.order - b.order || a.id.localeCompare(b.id));
1764
+ }
1765
+
1766
+ function buildEquanautPluginCatalog(input) {
1767
+ const records = Array.isArray(input)
1768
+ ? input
1769
+ : Array.isArray(input?.plugins)
1770
+ ? input.plugins
1771
+ : DEFAULT_EQUANAUT_PLUGIN_LIBRARY;
1772
+ return records
1773
+ .map((record, index) => normalizeEquanautPlugin(record, index))
1774
+ .filter((record) => record.id)
1775
+ .sort((a, b) => a.order - b.order || a.id.localeCompare(b.id));
1776
+ }
1777
+
1778
+ function filterEquanautPlugins(catalog, query) {
1779
+ const plugins = buildEquanautPluginCatalog(catalog);
1780
+ const normalizedQuery = normalizeSlashCommandId(query);
1781
+ if (!normalizedQuery) return plugins;
1782
+ return plugins.filter((plugin) => {
1783
+ const haystack = [
1784
+ plugin.id,
1785
+ plugin.name,
1786
+ plugin.title,
1787
+ plugin.source,
1788
+ plugin.category,
1789
+ plugin.status,
1790
+ plugin.rootPath,
1791
+ ]
1792
+ .filter(Boolean)
1793
+ .join(" ")
1794
+ .toLowerCase();
1795
+ return haystack.includes(normalizedQuery);
1796
+ });
1797
+ }
1798
+
1799
+ function filterEquanautSlashCommands(catalog, query) {
1800
+ const commands = buildEquanautSlashCommandCatalog(catalog);
1801
+ const normalizedQuery = normalizeSlashCommandId(query);
1802
+ if (!normalizedQuery) return commands;
1803
+ return commands.filter((command) => {
1804
+ const haystack = [
1805
+ command.id,
1806
+ command.name,
1807
+ command.title,
1808
+ command.description,
1809
+ command.source,
1810
+ command.plugin,
1811
+ command.category,
1812
+ command.skillName,
1813
+ command.skillPath,
1814
+ ...command.requiredTools,
1815
+ ...command.aliases,
1816
+ ...command.keywords,
1817
+ ]
1818
+ .filter(Boolean)
1819
+ .join(" ")
1820
+ .toLowerCase();
1821
+ return haystack.includes(normalizedQuery);
1822
+ });
1823
+ }
1824
+
1825
+ function parseEquanautSlashCommandInvocation(text) {
1826
+ const raw = String(text || "").trim();
1827
+ const match = raw.match(/^\/([A-Za-z0-9][A-Za-z0-9:_-]*)(?:\s+([\s\S]*))?$/);
1828
+ if (!match) return null;
1829
+ const commandId = normalizeSlashCommandId(match[1]);
1830
+ return {
1831
+ raw,
1832
+ commandId,
1833
+ commandText: `/${commandId}`,
1834
+ args: String(match[2] || "").trim(),
1835
+ hasArgs: Boolean(match[2] && String(match[2]).trim()),
1836
+ };
1837
+ }
1838
+
1839
+ function resolveEquanautSlashCommand(invocation, catalog) {
1840
+ const commandId = normalizeSlashCommandId(
1841
+ typeof invocation === "string" ? invocation : invocation?.commandId
1842
+ );
1843
+ if (!commandId) return null;
1844
+ const commands = buildEquanautSlashCommandCatalog(catalog);
1845
+ return (
1846
+ commands.find((command) => {
1847
+ if (command.id === commandId) return true;
1848
+ if (normalizeSlashCommandId(command.title) === commandId) return true;
1849
+ return command.aliases.some((alias) => normalizeSlashCommandId(alias) === commandId);
1850
+ }) || null
1851
+ );
1852
+ }
1853
+
1854
+ function normalizeBindingWindowSnapshot(snapshot) {
1855
+ const windows = Array.isArray(snapshot?.windows) ? snapshot.windows : [];
1856
+ const map = new Map();
1857
+ for (const windowInfo of windows) {
1858
+ if (windowInfo?.windowId === undefined || windowInfo?.windowId === null) continue;
1859
+ map.set(String(windowInfo.windowId), {
1860
+ windowId: Number(windowInfo.windowId),
1861
+ binding: windowInfo.binding || null,
1862
+ bindingStatus: windowInfo.bindingStatus || windowInfo.binding?.status || "unbound",
1863
+ });
1864
+ }
1865
+ return map;
1866
+ }
1867
+
1868
+ function formatBindingStatusLabel(windowInfo) {
1869
+ const status = String(windowInfo?.bindingStatus || windowInfo?.binding?.status || "unbound");
1870
+ const labels = {
1871
+ active: "Bound",
1872
+ completed: "Complete",
1873
+ stale: "Stale",
1874
+ reaped: "Reaped",
1875
+ conflict: "Conflict",
1876
+ unbound: "Unbound",
1877
+ };
1878
+ return labels[status] || status;
1879
+ }
1880
+
1881
+ function getBindingStatusClass(windowInfo) {
1882
+ const status = String(windowInfo?.bindingStatus || windowInfo?.binding?.status || "unbound")
1883
+ .toLowerCase()
1884
+ .replace(/[^a-z0-9_-]/g, "");
1885
+ return `binding-status-${status || "unbound"}`;
1886
+ }
1887
+
1888
+ function escapeHtml(str) {
1889
+ return String(str)
1890
+ .replace(/&/g, "&amp;")
1891
+ .replace(/</g, "&lt;")
1892
+ .replace(/>/g, "&gt;")
1893
+ .replace(/"/g, "&quot;")
1894
+ .replace(/'/g, "&#039;");
1895
+ }
1896
+
1897
+ function groupAndSortForDeletion(selectedTabs) {
1898
+ const byGroup = new Map();
1899
+ for (const s of selectedTabs) {
1900
+ if (!byGroup.has(s.groupId)) byGroup.set(s.groupId, []);
1901
+ byGroup.get(s.groupId).push(s.tabIdx);
1902
+ }
1903
+ for (const [, indices] of byGroup) {
1904
+ indices.sort((a, b) => b - a);
1905
+ }
1906
+ return byGroup;
1907
+ }
1908
+
1909
+ const SUPPORTED_ROUTER_INTENTS = Object.freeze([
1910
+ "inspect",
1911
+ "search",
1912
+ "open",
1913
+ "restore",
1914
+ "archive",
1915
+ "rename",
1916
+ "mark_pending",
1917
+ "mark_done",
1918
+ "dispatch_to_fleet",
1919
+ "get_fleet_status",
1920
+ "open_orchestrator_url",
1921
+ "invoke_slash_command",
1922
+ ]);
1923
+
1924
+ const READ_ONLY_ROUTER_INTENTS = new Set(["inspect", "search", "get_fleet_status"]);
1925
+
1926
+ function isCapabilityUsable(capability) {
1927
+ return !!(
1928
+ capability &&
1929
+ capability.reachable === true &&
1930
+ capability.fresh !== false &&
1931
+ capability.connected !== false &&
1932
+ capability.usable !== false
1933
+ );
1934
+ }
1935
+
1936
+ function selectEquanautRouterRoute(capabilities) {
1937
+ const caps = capabilities || {};
1938
+ if (isCapabilityUsable(caps.equaGateway)) {
1939
+ return { route: "gateway", degraded: false };
1940
+ }
1941
+ if (isCapabilityUsable(caps.equaApi)) {
1942
+ return {
1943
+ route: "equa_api_enriched",
1944
+ degraded: true,
1945
+ degradedReason: "gateway unavailable; using Equa API context enrichment",
1946
+ };
1947
+ }
1948
+ if (isCapabilityUsable(caps.cometBridgeAsk)) {
1949
+ return {
1950
+ route: "comet_bridge",
1951
+ degraded: true,
1952
+ degradedReason: "gateway and Equa API unavailable; using Comet bridge",
1953
+ };
1954
+ }
1955
+ if (isCapabilityUsable(caps.localModel)) {
1956
+ return {
1957
+ route: "local_model",
1958
+ degraded: true,
1959
+ degradedReason: "gateway, Equa API, and Comet bridge unavailable; using local model",
1960
+ };
1961
+ }
1962
+ return {
1963
+ route: "unavailable",
1964
+ degraded: true,
1965
+ degradedReason: "no router backend is reachable",
1966
+ };
1967
+ }
1968
+
1969
+ function stringOrNull(value) {
1970
+ if (value === undefined || value === null || value === "") return null;
1971
+ return String(value);
1972
+ }
1973
+
1974
+ function hasOwn(record, key) {
1975
+ return !!record && Object.prototype.hasOwnProperty.call(record, key);
1976
+ }
1977
+
1978
+ function normalizeEntityDescription(record) {
1979
+ if (!record || typeof record !== "object") return "";
1980
+ if (hasOwn(record, "description")) return String(record.description ?? "");
1981
+ if (hasOwn(record, "note")) return String(record.note ?? "");
1982
+ if (hasOwn(record, "notes")) {
1983
+ const notes = record.notes;
1984
+ if (Array.isArray(notes)) return notes.map((note) => String(note ?? "")).join("\n");
1985
+ return String(notes ?? "");
1986
+ }
1987
+ return "";
1988
+ }
1989
+
1990
+ function normalizeCatalogTabs(record) {
1991
+ const tabs = Array.isArray(record?.tabs)
1992
+ ? record.tabs
1993
+ : Array.isArray(record?.urls)
1994
+ ? record.urls
1995
+ : [];
1996
+ return tabs.map((tab, index) => {
1997
+ const description = normalizeEntityDescription(tab);
1998
+ return {
1999
+ id: stringOrNull(
2000
+ tab.id || tab.tabId || `${record.taskThreadId || record.id || "tab"}-${index + 1}`
2001
+ ),
2002
+ title: String(tab.title || tab.url || "Untitled"),
2003
+ url: stringOrNull(tab.url),
2004
+ active: !!tab.active,
2005
+ description,
2006
+ inheritedDescription: description
2007
+ ? ""
2008
+ : String(tab.inheritedDescription || record?.description || ""),
2009
+ };
2010
+ });
2011
+ }
2012
+
2013
+ function catalogTitle(record) {
2014
+ return (
2015
+ record.title ||
2016
+ record.sessionName ||
2017
+ record.taskGoal ||
2018
+ record.displayName ||
2019
+ record.name ||
2020
+ record.label ||
2021
+ record.url ||
2022
+ "Untitled"
2023
+ );
2024
+ }
2025
+
2026
+ function catalogStatus(record, fallback) {
2027
+ return record.status || record.taskStatus || record.state || fallback || "unknown";
2028
+ }
2029
+
2030
+ function normalizeCatalogRecord(record, sourceFamily, index) {
2031
+ const source = sourceFamily || "unknown";
2032
+ const nativeId =
2033
+ record.taskThreadId ||
2034
+ record.sessionKey ||
2035
+ record.id ||
2036
+ record.sessionId ||
2037
+ record.tabGroupId ||
2038
+ record.groupId ||
2039
+ record.windowId ||
2040
+ `${source}-${index + 1}`;
2041
+ return {
2042
+ id: String(nativeId),
2043
+ sourceFamily: source,
2044
+ title: String(catalogTitle(record)).trim() || "Untitled",
2045
+ description: normalizeEntityDescription(record),
2046
+ agentInstructions: String(
2047
+ record.agentInstructions || record.equanautInstructions || ""
2048
+ ).trim(),
2049
+ status: catalogStatus(record, source === "archived" ? "archived" : null),
2050
+ windowId: stringOrNull(record.windowId),
2051
+ taskThreadId: stringOrNull(record.taskThreadId || record.threadId),
2052
+ agentId: stringOrNull(record.agentId),
2053
+ orchestratorUrl: stringOrNull(record.orchestratorUrl || record.perplexityUrl),
2054
+ tabs: normalizeCatalogTabs(record),
2055
+ lastActivityAt:
2056
+ record.lastActivityAt ||
2057
+ record.lastActivity ||
2058
+ record.updatedAt ||
2059
+ record.archivedAt ||
2060
+ record.closedAt ||
2061
+ null,
2062
+ allowedOperations: ["inspect", "search"],
2063
+ readOnly: true,
2064
+ };
2065
+ }
2066
+
2067
+ function normalizeFleetRecord(record, index) {
2068
+ const base = normalizeCatalogRecord(record, "fleet", index);
2069
+ return {
2070
+ ...base,
2071
+ status: catalogStatus(record, "unknown"),
2072
+ agentType: record.agentType || record.type || null,
2073
+ allowedOperations: ["inspect", "search", "get_fleet_status", "dispatch_to_fleet"],
2074
+ readOnly: true,
2075
+ };
2076
+ }
2077
+
2078
+ function buildGlobalContextCatalog(input) {
2079
+ const data = input || {};
2080
+ const now = data.now || new Date().toISOString();
2081
+ const liveSessions = (data.liveSessions || []).map((r, i) =>
2082
+ normalizeCatalogRecord(r, "live", i)
2083
+ );
2084
+ const archivedSessions = (data.archivedSessions || []).map((r, i) =>
2085
+ normalizeCatalogRecord(r, "archived", i)
2086
+ );
2087
+ const recentlyClosed = (data.recentlyClosed || []).map((r, i) =>
2088
+ normalizeCatalogRecord(r, "recent", i)
2089
+ );
2090
+ const unifiedThreads = (data.unifiedThreads || []).map((r, i) =>
2091
+ normalizeCatalogRecord(r, "unified", i)
2092
+ );
2093
+ const fleetMembers = (data.fleetMembers || []).map((r, i) => normalizeFleetRecord(r, i));
2094
+ const gatewayTools = (data.gatewayTools || []).map((r, i) =>
2095
+ normalizeCatalogRecord(r, "gateway", i)
2096
+ );
2097
+ const slashCommands = Array.isArray(data.slashCommands)
2098
+ ? buildEquanautSlashCommandCatalog(data.slashCommands)
2099
+ : buildEquanautSlashCommandCatalog();
2100
+ const plugins = Array.isArray(data.plugins)
2101
+ ? buildEquanautPluginCatalog(data.plugins)
2102
+ : buildEquanautPluginCatalog();
2103
+
2104
+ return {
2105
+ liveSessions,
2106
+ archivedSessions,
2107
+ recentlyClosed,
2108
+ unifiedThreads,
2109
+ fleetMembers,
2110
+ gatewayTools,
2111
+ slashCommands,
2112
+ plugins,
2113
+ freshness: {
2114
+ liveSessionsAt: data.liveSessionsAt || now,
2115
+ catalogAt: data.catalogAt || now,
2116
+ staleSources: Array.isArray(data.staleSources) ? data.staleSources : [],
2117
+ },
2118
+ };
2119
+ }
2120
+
2121
+ function countGlobalContextCatalog(catalog) {
2122
+ const c = catalog || {};
2123
+ return {
2124
+ live: (c.liveSessions || []).length,
2125
+ archived: (c.archivedSessions || []).length,
2126
+ recent: (c.recentlyClosed || []).length,
2127
+ unified: (c.unifiedThreads || []).length,
2128
+ fleet: (c.fleetMembers || []).length,
2129
+ gatewayTools: (c.gatewayTools || []).length,
2130
+ slashCommands: (c.slashCommands || []).length,
2131
+ plugins: (c.plugins || []).length,
2132
+ };
2133
+ }
2134
+
2135
+ function buildAgentContextEnvelope(input) {
2136
+ const data = input || {};
2137
+ const submittedAt = data.submittedAt || new Date().toISOString();
2138
+ const globalCatalog = data.globalCatalog || buildGlobalContextCatalog({ now: submittedAt });
2139
+ const capabilities = data.capabilities || {};
2140
+ const activeWindowScope = data.activeWindowScope || {
2141
+ windowId: "unknown",
2142
+ groups: [],
2143
+ ungroupedTabs: [],
2144
+ mutationPolicy: {},
2145
+ };
2146
+ const catalogCounts = countGlobalContextCatalog(globalCatalog);
2147
+ const scopedGroupCount = Array.isArray(activeWindowScope.groups)
2148
+ ? activeWindowScope.groups.length
2149
+ : 0;
2150
+ const scopedTabCount =
2151
+ (activeWindowScope.groups || []).reduce((sum, group) => {
2152
+ return sum + (Array.isArray(group.tabs) ? group.tabs.length : group.tabCount || 0);
2153
+ }, 0) +
2154
+ (Array.isArray(activeWindowScope.ungroupedTabs) ? activeWindowScope.ungroupedTabs.length : 0);
2155
+
2156
+ return {
2157
+ schemaVersion: data.schemaVersion || "1.0",
2158
+ submittedAt,
2159
+ userText: String(data.userText || ""),
2160
+ activeWindowScope,
2161
+ globalCatalog,
2162
+ capabilities,
2163
+ allowedIntents: data.allowedIntents || Array.from(SUPPORTED_ROUTER_INTENTS),
2164
+ slashCommand: data.slashCommand || null,
2165
+ attachments: data.attachments || [],
2166
+ telemetry: {
2167
+ ...(data.telemetry || {}),
2168
+ activeWindowId: activeWindowScope.windowId || "unknown",
2169
+ scopedGroupCount,
2170
+ scopedTabCount,
2171
+ catalogCounts,
2172
+ slashCommandId: data.slashCommand?.commandId || data.slashCommand?.command?.id || null,
2173
+ },
2174
+ };
2175
+ }
2176
+
2177
+ function denyRouterIntent(intent, code, message, activeWindowScope) {
2178
+ return {
2179
+ intent,
2180
+ scope: "denied",
2181
+ mutation: !READ_ONLY_ROUTER_INTENTS.has(intent),
2182
+ requiresConfirmation: false,
2183
+ preview: message,
2184
+ denial: { code, message },
2185
+ audit: {
2186
+ requestedBy: "sidepanel",
2187
+ activeWindowId: activeWindowScope?.windowId || "unknown",
2188
+ createdAt: new Date().toISOString(),
2189
+ },
2190
+ allowed: false,
2191
+ };
2192
+ }
2193
+
2194
+ function validateRouterIntent(input) {
2195
+ const data = input || {};
2196
+ const intent = String(data.intent || "");
2197
+ const target = data.target || {};
2198
+ const activeWindowScope = data.activeWindowScope || {};
2199
+ const activeWindowId = activeWindowScope.windowId ? String(activeWindowScope.windowId) : "";
2200
+
2201
+ if (!SUPPORTED_ROUTER_INTENTS.includes(intent)) {
2202
+ return denyRouterIntent(
2203
+ intent || "unknown",
2204
+ "UNSUPPORTED_INTENT",
2205
+ `Unsupported router intent: ${intent || "unknown"}`,
2206
+ activeWindowScope
2207
+ );
2208
+ }
2209
+
2210
+ const mutation = !READ_ONLY_ROUTER_INTENTS.has(intent);
2211
+ if (!mutation) {
2212
+ return {
2213
+ intent,
2214
+ target,
2215
+ scope: "global_read_only",
2216
+ mutation: false,
2217
+ requiresConfirmation: false,
2218
+ preview: `Read-only ${intent} for ${target.title || target.id || "router context"}`,
2219
+ audit: {
2220
+ requestedBy: "sidepanel",
2221
+ activeWindowId: activeWindowId || "unknown",
2222
+ createdAt: new Date().toISOString(),
2223
+ },
2224
+ allowed: true,
2225
+ };
2226
+ }
2227
+
2228
+ if (activeWindowScope.profileOwner === "human" || target.profileOwner === "human") {
2229
+ return denyRouterIntent(
2230
+ intent,
2231
+ "HUMAN_PROFILE_DENIED",
2232
+ "Normal agents cannot mutate human-owned browser profile or window state.",
2233
+ activeWindowScope
2234
+ );
2235
+ }
2236
+
2237
+ if (target.displayRole === "primary_user" || target.displayRole === "bottom_user") {
2238
+ return denyRouterIntent(
2239
+ intent,
2240
+ "BOTTOM_DISPLAY_MUTATION_DENIED",
2241
+ "Normal agents cannot mutate bottom-display user browser windows.",
2242
+ activeWindowScope
2243
+ );
2244
+ }
2245
+
2246
+ const targetWindowId = target.windowId ? String(target.windowId) : "";
2247
+ const allowOtherWindow =
2248
+ activeWindowScope.mutationPolicy && activeWindowScope.mutationPolicy.allowOtherWindowMutation;
2249
+ if (
2250
+ targetWindowId &&
2251
+ activeWindowId &&
2252
+ targetWindowId !== activeWindowId &&
2253
+ !allowOtherWindow
2254
+ ) {
2255
+ return denyRouterIntent(
2256
+ intent,
2257
+ "OTHER_WINDOW_MUTATION_DENIED",
2258
+ "Normal sidepanel requests may mutate only the active sidepanel window. Open or rebind the target first.",
2259
+ activeWindowScope
2260
+ );
2261
+ }
2262
+
2263
+ if (
2264
+ ["open", "restore", "open_orchestrator_url"].includes(intent) &&
2265
+ activeWindowScope.displayRole &&
2266
+ activeWindowScope.displayRole !== "top_agents"
2267
+ ) {
2268
+ return denyRouterIntent(
2269
+ intent,
2270
+ "TOP_DISPLAY_REQUIRED",
2271
+ "Open and restore actions must use the top-display agent workspace.",
2272
+ activeWindowScope
2273
+ );
2274
+ }
2275
+
2276
+ if (intent === "dispatch_to_fleet") {
2277
+ const status = String(target.status || "").toLowerCase();
2278
+ if (!["active", "running", "idle", "online"].includes(status)) {
2279
+ return denyRouterIntent(
2280
+ intent,
2281
+ "FLEET_TARGET_UNAVAILABLE",
2282
+ "Dispatch requires a live or reachable fleet target.",
2283
+ activeWindowScope
2284
+ );
2285
+ }
2286
+ }
2287
+
2288
+ return {
2289
+ intent,
2290
+ target,
2291
+ scope: intent === "dispatch_to_fleet" ? "fleet" : "active_window",
2292
+ mutation: true,
2293
+ requiresConfirmation: !!data.requiresConfirmation,
2294
+ preview: data.preview || `${intent} ${target.title || target.id || "target"}`,
2295
+ audit: {
2296
+ requestedBy: "sidepanel",
2297
+ activeWindowId: activeWindowId || "unknown",
2298
+ reason: data.reason || "user_request",
2299
+ createdAt: new Date().toISOString(),
2300
+ },
2301
+ allowed: true,
2302
+ };
2303
+ }
2304
+
2305
+ const SessionLogic = {
2306
+ timeAgo,
2307
+ fsFuzzyMatch,
2308
+ fsFormatEventDescription,
2309
+ fsApplyFilters,
2310
+ fsApplyAdvancedFilters,
2311
+ fsSortGroups,
2312
+ fsGroupBy,
2313
+ fsResolveSelectedTabs,
2314
+ getDomainDisplayName,
2315
+ DEFAULT_SAFETY_REMINDER_HOST_ALLOWLIST,
2316
+ DEFAULT_EQUANAUT_SLASH_COMMANDS,
2317
+ DEFAULT_EQUANAUT_PLUGIN_LIBRARY,
2318
+ isSafetyReminderAllowlisted,
2319
+ shouldInjectSafetyReminder,
2320
+ normalizeEquanautModelEntry,
2321
+ normalizeEquanautModelCatalog,
2322
+ normalizeEquanautOllamaModel,
2323
+ modelsFromEquanautGatewayConfig,
2324
+ mergeEquanautModelCatalogs,
2325
+ groupEquanautModelCatalog,
2326
+ getEquanautCommandCenterFallbackModels,
2327
+ getEquanautModelBadge,
2328
+ getEquanautProviderGlyph,
2329
+ getEquanautProviderFilterKey,
2330
+ getEquanautModelCostTier,
2331
+ getEquanautModelCapabilities,
2332
+ getEquanautCapabilityLabel,
2333
+ filterEquanautModels,
2334
+ getEquanautModelDisplayName,
2335
+ isEquanautModelConfigured,
2336
+ getEquanautModelSettingsUrl,
2337
+ EQUANAUT_MODEL_CAPABILITIES,
2338
+ EQUANAUT_MODEL_CAPABILITY_ORDER,
2339
+ isEquanautCommandCenterModel,
2340
+ resolveEquanautOllamaModelId,
2341
+ normalizeEquanautRoutingMode,
2342
+ isEquanautModelAllowedForRouting,
2343
+ pickEquanautModelForRoutingMode,
2344
+ normalizeSlashCommandId,
2345
+ normalizeEquanautSlashCommand,
2346
+ normalizeEquanautPlugin,
2347
+ buildEquanautSlashCommandCatalog,
2348
+ buildEquanautPluginCatalog,
2349
+ filterEquanautSlashCommands,
2350
+ filterEquanautPlugins,
2351
+ parseEquanautSlashCommandInvocation,
2352
+ resolveEquanautSlashCommand,
2353
+ normalizeBindingWindowSnapshot,
2354
+ formatBindingStatusLabel,
2355
+ getBindingStatusClass,
2356
+ escapeHtml,
2357
+ groupAndSortForDeletion,
2358
+ SUPPORTED_ROUTER_INTENTS,
2359
+ selectEquanautRouterRoute,
2360
+ normalizeEntityDescription,
2361
+ normalizeCatalogRecord,
2362
+ buildGlobalContextCatalog,
2363
+ countGlobalContextCatalog,
2364
+ buildAgentContextEnvelope,
2365
+ validateRouterIntent,
2366
+ DEFAULT_DOMAIN_DISPLAY_MAP,
2367
+ };
2368
+
2369
+ // Browser: register on globalThis
2370
+ if (typeof globalThis !== "undefined") {
2371
+ globalThis.SessionLogic = SessionLogic;
2372
+ // sidepanel.js calls timeAgo() unqualified at 9 sites — keep that contract
2373
+ // intact even though the IIFE otherwise hides individual names. Without
2374
+ // this, fsRenderGroups throws "timeAgo is not defined" on the first card
2375
+ // and the middle column of session-manager.html renders nothing.
2376
+ globalThis.timeAgo = timeAgo;
2377
+ }
2378
+
2379
+ // Node.js CJS: export for test require()
2380
+ if (typeof module !== "undefined" && module.exports) {
2381
+ module.exports = SessionLogic;
2382
+ }
2383
+ })();