@shawnowen/comet-mcp 2.4.1 → 2.4.2

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 (46) hide show
  1. package/README.md +12 -1
  2. package/dist/binding-reaper.d.ts +46 -0
  3. package/dist/binding-reaper.js +73 -0
  4. package/dist/http-server.js +121 -0
  5. package/dist/index.js +310 -6
  6. package/dist/project-config.d.ts +46 -0
  7. package/dist/project-config.js +166 -0
  8. package/dist/tab-groups.d.ts +21 -1
  9. package/dist/tab-groups.js +184 -0
  10. package/dist/window-bindings.d.ts +48 -0
  11. package/dist/window-bindings.js +85 -0
  12. package/extension/background.js +38 -17
  13. package/extension/manifest.json +16 -1
  14. package/extension/perplexity-capability-manifest.json +1181 -0
  15. package/extension/perplexity-capability-manifest.schema.json +142 -0
  16. package/extension/session-logic.js +696 -25
  17. package/extension/session-manager.html +13 -1
  18. package/extension/sidepanel.css +21 -6
  19. package/extension/sidepanel.js +598 -68
  20. package/package.json +1 -1
  21. package/dist/discovery/capability-entry.d.ts +0 -215
  22. package/dist/discovery/capability-entry.js +0 -13
  23. package/dist/discovery/description-template.d.ts +0 -40
  24. package/dist/discovery/description-template.js +0 -61
  25. package/dist/discovery/golden-queries.fixture.d.ts +0 -22
  26. package/dist/discovery/golden-queries.fixture.js +0 -137
  27. package/dist/discovery/mcp-source.d.ts +0 -38
  28. package/dist/discovery/mcp-source.js +0 -70
  29. package/dist/discovery/metadata-completeness.d.ts +0 -48
  30. package/dist/discovery/metadata-completeness.js +0 -83
  31. package/dist/discovery/registry.d.ts +0 -35
  32. package/dist/discovery/registry.js +0 -35
  33. package/dist/discovery/safety.d.ts +0 -44
  34. package/dist/discovery/safety.js +0 -59
  35. package/dist/discovery/schema-validator.d.ts +0 -36
  36. package/dist/discovery/schema-validator.js +0 -257
  37. package/dist/discovery/source-error.d.ts +0 -47
  38. package/dist/discovery/source-error.js +0 -95
  39. package/dist/discovery/tool-meta.d.ts +0 -41
  40. package/dist/discovery/tool-meta.js +0 -229
  41. package/dist/discovery/virtual-tools.d.ts +0 -20
  42. package/dist/discovery/virtual-tools.js +0 -69
  43. package/dist/task-thread-aggregator.d.ts +0 -34
  44. package/dist/task-thread-aggregator.js +0 -480
  45. package/dist/task-thread-canonical.d.ts +0 -142
  46. package/dist/task-thread-canonical.js +0 -116
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // Comet Browser MCP Server
3
3
  // Claude Code ↔ Perplexity Comet bidirectional interaction
4
- // 25 tools: 9 browsing + 2 direct interaction + 1 tab groups + 4 lifecycle + 2 orchestration + 5 parity + 2 safe observe (observe + peek)
4
+ // 26 tools: 9 browsing + 2 direct interaction + 1 tab groups + 4 lifecycle + 2 orchestration + 6 parity + 2 safe observe (observe + peek)
5
5
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
6
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
7
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
@@ -68,6 +68,7 @@ const BROWSING_TOOLS = new Set([
68
68
  "comet_pdf",
69
69
  "comet_scrape",
70
70
  "comet_network",
71
+ "comet_monitor",
71
72
  "comet_automate",
72
73
  "comet_domain",
73
74
  ]);
@@ -87,6 +88,7 @@ const BOUND_ROUTING_TOOLS = new Set([
87
88
  "comet_pdf",
88
89
  "comet_scrape",
89
90
  "comet_network",
91
+ "comet_monitor",
90
92
  "comet_automate",
91
93
  "comet_domain",
92
94
  ]);
@@ -202,6 +204,8 @@ import { emitLifecycleEvent, createMCPLifecycleEnvelope } from "../vendor/lifecy
202
204
  import { appendFileSync, readFileSync, writeFileSync, mkdirSync } from "fs";
203
205
  import { homedir } from "os";
204
206
  import { join, dirname } from "path";
207
+ import { createHash } from "crypto";
208
+ import { execFileSync } from "child_process";
205
209
  // Duplicate process detection (Spec 016, FR-010, T005)
206
210
  const PID_FILE_PATH = join(homedir(), ".claude", "comet-browser", "comet-mcp.pid");
207
211
  function checkDuplicateProcess() {
@@ -438,7 +442,8 @@ const TOOLS = [
438
442
  "Actions: list (all groups), list_tabs (all tabs with group info), create (new group from tab IDs), " +
439
443
  "update (rename/recolor/collapse), move (reorder), ungroup (remove tabs from group), delete (ungroup all tabs in a group), " +
440
444
  "save_group (persist tab URLs to archive), restore_group (reopen tabs from archive), archive_group (save + close tabs), " +
441
- "list_archived (show all archived tab groups).",
445
+ "list_archived (show all archived tab groups), list_metadata (show extension metadata), " +
446
+ "update_metadata (update extension-owned descriptions, labels, archived records, and tab rows).",
442
447
  inputSchema: {
443
448
  type: "object",
444
449
  properties: {
@@ -456,6 +461,8 @@ const TOOLS = [
456
461
  "restore_group",
457
462
  "archive_group",
458
463
  "list_archived",
464
+ "list_metadata",
465
+ "update_metadata",
459
466
  ],
460
467
  description: "The tab group operation to perform",
461
468
  },
@@ -470,7 +477,7 @@ const TOOLS = [
470
477
  },
471
478
  title: {
472
479
  type: "string",
473
- description: "Group title (for create, update)",
480
+ description: "Group or metadata title (for create, update, archive update_metadata)",
474
481
  },
475
482
  color: {
476
483
  type: "string",
@@ -487,12 +494,52 @@ const TOOLS = [
487
494
  },
488
495
  taskThreadId: {
489
496
  type: "string",
490
- description: "Task thread ID (for save_group, restore_group, archive_group)",
497
+ description: "Task thread ID (for save_group, restore_group, archive_group, archive and archive-tab update_metadata)",
491
498
  },
492
499
  closeTabs: {
493
500
  type: "boolean",
494
501
  description: "Close tabs after saving (for save_group; archive_group always closes)",
495
502
  },
503
+ entityType: {
504
+ type: "string",
505
+ description: "Extension metadata entity type (for update_metadata)",
506
+ },
507
+ entityId: {
508
+ type: "string",
509
+ description: "Extension metadata entity ID (for update_metadata)",
510
+ },
511
+ sourceFamily: {
512
+ type: "string",
513
+ description: "Extension metadata source family (for update_metadata)",
514
+ },
515
+ windowId: {
516
+ type: "string",
517
+ description: "Window ID for window-label metadata updates",
518
+ },
519
+ activeWindowId: {
520
+ type: "string",
521
+ description: "Active window ID to include in update_metadata audit output",
522
+ },
523
+ policyTier: {
524
+ type: "string",
525
+ description: "Policy tier to include in update_metadata audit output",
526
+ },
527
+ description: {
528
+ type: "string",
529
+ description: "Canonical description to store (for update_metadata)",
530
+ },
531
+ label: {
532
+ type: "string",
533
+ description: "Window label to store (for update_metadata)",
534
+ },
535
+ status: {
536
+ type: "string",
537
+ description: "Archived task-thread status to store (for update_metadata)",
538
+ },
539
+ tabIndex: {
540
+ type: "number",
541
+ description: "Archived tab row index (for archive-tab update_metadata)",
542
+ },
496
543
  },
497
544
  required: ["action"],
498
545
  },
@@ -953,6 +1000,53 @@ const TOOLS = [
953
1000
  required: ["action"],
954
1001
  },
955
1002
  },
1003
+ {
1004
+ name: "comet_monitor",
1005
+ description: "Monitor the current page or a URL for content changes from the caller-owned Comet binding. " +
1006
+ "Stores a SHA-256 baseline by monitor ID, compares later checks, reports line-level diffs, " +
1007
+ "and can capture a screenshot or send a macOS notification on change. Bounded by count/interval.",
1008
+ inputSchema: {
1009
+ type: "object",
1010
+ properties: {
1011
+ url: {
1012
+ type: "string",
1013
+ description: "URL to navigate to before monitoring. If omitted, monitors the current page.",
1014
+ },
1015
+ selector: {
1016
+ type: "string",
1017
+ description: "Optional CSS selector to monitor instead of the full body text.",
1018
+ },
1019
+ id: {
1020
+ type: "string",
1021
+ description: "Stable monitor ID. Defaults to a hash of URL/current page and selector.",
1022
+ },
1023
+ once: {
1024
+ type: "boolean",
1025
+ description: "Check once and compare against the stored baseline. Defaults to true unless count is provided.",
1026
+ },
1027
+ interval: {
1028
+ type: "number",
1029
+ description: "Seconds between checks in continuous mode. Default: 60, max: 300.",
1030
+ },
1031
+ count: {
1032
+ type: "number",
1033
+ description: "Number of checks in continuous mode. Default: 10, max: 20.",
1034
+ },
1035
+ screenshot: {
1036
+ type: "boolean",
1037
+ description: "Capture a PNG screenshot when a change is detected.",
1038
+ },
1039
+ notify: {
1040
+ type: "boolean",
1041
+ description: "Send a macOS notification when a change is detected.",
1042
+ },
1043
+ maxLength: {
1044
+ type: "number",
1045
+ description: "Maximum captured content characters. Default: 20000.",
1046
+ },
1047
+ },
1048
+ },
1049
+ },
956
1050
  {
957
1051
  name: "comet_automate",
958
1052
  description: "Execute a multi-step browser workflow. Each step is a tool+args object. Supports: " +
@@ -1076,11 +1170,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1076
1170
  isError: true,
1077
1171
  };
1078
1172
  }
1173
+ // Spec 087 / FR-006, FR-007, FR-009 — load per-project config and
1174
+ // apply tab_group_prefix if set. Existing groups untouched (clarify.md C-2).
1175
+ const { readProjectConfig } = await import("./project-config.js");
1176
+ const { config: projectConfig, warnings: projectConfigWarnings } = readProjectConfig();
1177
+ let taskThreadIdIn = args?.taskThreadId || undefined;
1178
+ if (projectConfig.tab_group_prefix && taskThreadIdIn) {
1179
+ if (!taskThreadIdIn.startsWith(projectConfig.tab_group_prefix)) {
1180
+ taskThreadIdIn = projectConfig.tab_group_prefix + taskThreadIdIn;
1181
+ }
1182
+ }
1079
1183
  // Spec 034: Use SessionRegistry for isolated, safe connections.
1080
1184
  // NEVER closes existing tabs. NEVER kills the browser.
1081
1185
  const session = await sessionRegistry.register({
1082
1186
  agentId: args?.agentId || undefined,
1083
- taskThreadId: args?.taskThreadId || undefined,
1187
+ taskThreadId: taskThreadIdIn,
1084
1188
  url: args?.url || undefined,
1085
1189
  tabGroupColor: args?.tabGroupColor || undefined,
1086
1190
  port: 9222,
@@ -2349,12 +2453,47 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2349
2453
  ],
2350
2454
  };
2351
2455
  }
2456
+ case "list_metadata": {
2457
+ const metadata = await tabGroupsClient.listExtensionMetadata();
2458
+ return {
2459
+ content: [
2460
+ {
2461
+ type: "text",
2462
+ text: `Extension metadata:\n${JSON.stringify(metadata, null, 2)}`,
2463
+ },
2464
+ ],
2465
+ };
2466
+ }
2467
+ case "update_metadata": {
2468
+ const result = await tabGroupsClient.updateExtensionMetadata({
2469
+ entityType: args?.entityType,
2470
+ entityId: args?.entityId,
2471
+ windowId: args?.windowId,
2472
+ activeWindowId: args?.activeWindowId,
2473
+ taskThreadId: args?.taskThreadId,
2474
+ tabIndex: args?.tabIndex,
2475
+ sourceFamily: args?.sourceFamily,
2476
+ policyTier: args?.policyTier,
2477
+ description: args?.description,
2478
+ title: args?.title,
2479
+ label: args?.label,
2480
+ status: args?.status,
2481
+ });
2482
+ return {
2483
+ content: [
2484
+ {
2485
+ type: "text",
2486
+ text: `Updated extension metadata:\n${JSON.stringify(result, null, 2)}`,
2487
+ },
2488
+ ],
2489
+ };
2490
+ }
2352
2491
  default:
2353
2492
  return {
2354
2493
  content: [
2355
2494
  {
2356
2495
  type: "text",
2357
- text: `Unknown action: ${action}. Use: list, list_tabs, create, update, move, ungroup, delete, save_group, restore_group, archive_group, list_archived`,
2496
+ text: `Unknown action: ${action}. Use: list, list_tabs, create, update, move, ungroup, delete, save_group, restore_group, archive_group, list_archived, list_metadata, update_metadata`,
2358
2497
  },
2359
2498
  ],
2360
2499
  isError: true,
@@ -3259,6 +3398,171 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3259
3398
  ],
3260
3399
  };
3261
3400
  }
3401
+ case "comet_monitor": {
3402
+ const monitorUrl = args?.url;
3403
+ const monitorSelector = args?.selector;
3404
+ const monitorIdArg = args?.id;
3405
+ const monitorOnce = args?.once !== false && args?.count === undefined;
3406
+ const monitorIntervalSec = Math.max(1, Math.min(args?.interval || 60, 300));
3407
+ const monitorCount = monitorOnce
3408
+ ? 1
3409
+ : Math.max(1, Math.min(args?.count || 10, 20));
3410
+ const monitorScreenshot = args?.screenshot || false;
3411
+ const monitorNotify = args?.notify || false;
3412
+ const monitorMaxLength = Math.max(1000, Math.min(args?.maxLength || 20000, 100000));
3413
+ if (monitorUrl) {
3414
+ await cometClient.navigate(monitorUrl, true, true);
3415
+ }
3416
+ const outputRoot = join(homedir(), ".Codex", "comet-browser", "output");
3417
+ const scrapedDir = join(outputRoot, "scraped");
3418
+ const screenshotDir = join(outputRoot, "screenshots");
3419
+ mkdirSync(scrapedDir, { recursive: true });
3420
+ mkdirSync(screenshotDir, { recursive: true });
3421
+ async function captureMonitorState() {
3422
+ const result = await cometClient.evaluate(`
3423
+ (() => {
3424
+ const selector = ${monitorSelector ? safeSelector(monitorSelector) : "null"};
3425
+ const node = selector ? document.querySelector(selector) : document.body;
3426
+ const content = node ? ((node.innerText || node.textContent || '').trim()) : '';
3427
+ return {
3428
+ title: document.title,
3429
+ url: window.location.href,
3430
+ selector,
3431
+ content: content.slice(0, ${monitorMaxLength})
3432
+ };
3433
+ })()
3434
+ `);
3435
+ const value = (result.result.value || {});
3436
+ const content = value.content || "";
3437
+ return {
3438
+ title: value.title || "",
3439
+ url: value.url || monitorUrl || "",
3440
+ selector: value.selector || monitorSelector || null,
3441
+ content,
3442
+ hash: createHash("sha256").update(content).digest("hex"),
3443
+ timestamp: new Date().toISOString(),
3444
+ };
3445
+ }
3446
+ const firstState = await captureMonitorState();
3447
+ const monitorId = monitorIdArg ||
3448
+ createHash("sha256")
3449
+ .update(`${firstState.url}:${firstState.selector || ""}`)
3450
+ .digest("hex")
3451
+ .slice(0, 12);
3452
+ const stateFile = join(scrapedDir, `monitor-${monitorId}-state.json`);
3453
+ const previousState = readJsonSafe(stateFile);
3454
+ function diffLines(previousContent, currentContent) {
3455
+ const previousLines = previousContent.split("\n").filter(Boolean);
3456
+ const currentLines = currentContent.split("\n").filter(Boolean);
3457
+ return {
3458
+ added: currentLines.filter((line) => !previousLines.includes(line)).slice(0, 20),
3459
+ removed: previousLines.filter((line) => !currentLines.includes(line)).slice(0, 20),
3460
+ };
3461
+ }
3462
+ async function saveScreenshotOnChange(changeIndex) {
3463
+ if (!monitorScreenshot)
3464
+ return null;
3465
+ const shot = await cometClient.screenshot("png");
3466
+ const shotPath = join(screenshotDir, `monitor-${monitorId}-change-${changeIndex}.png`);
3467
+ writeFileSync(shotPath, Buffer.from(shot.data, "base64"));
3468
+ return shotPath;
3469
+ }
3470
+ function notifyOnChange(message) {
3471
+ if (!monitorNotify)
3472
+ return null;
3473
+ try {
3474
+ execFileSync("osascript", [
3475
+ "-e",
3476
+ `display notification ${JSON.stringify(message)} with title "Comet Monitor"`,
3477
+ ]);
3478
+ return "sent";
3479
+ }
3480
+ catch (err) {
3481
+ return `failed: ${err instanceof Error ? err.message : String(err)}`;
3482
+ }
3483
+ }
3484
+ if (monitorOnce) {
3485
+ const changed = Boolean(previousState && previousState.hash !== firstState.hash);
3486
+ const diff = changed && previousState?.content
3487
+ ? diffLines(previousState.content, firstState.content)
3488
+ : { added: [], removed: [] };
3489
+ const screenshotPath = changed ? await saveScreenshotOnChange(1) : null;
3490
+ const notification = changed ? notifyOnChange(`Page changed: ${firstState.url}`) : null;
3491
+ writeFileSync(stateFile, JSON.stringify(firstState, null, 2));
3492
+ return {
3493
+ content: [
3494
+ {
3495
+ type: "text",
3496
+ text: JSON.stringify({
3497
+ monitorId,
3498
+ mode: "once",
3499
+ status: previousState ? (changed ? "changed" : "unchanged") : "baseline",
3500
+ url: firstState.url,
3501
+ selector: firstState.selector,
3502
+ timestamp: firstState.timestamp,
3503
+ hash: firstState.hash,
3504
+ previousHash: previousState?.hash || null,
3505
+ contentLength: firstState.content.length,
3506
+ stateFile,
3507
+ screenshotPath,
3508
+ notification,
3509
+ diff,
3510
+ }, null, 2),
3511
+ },
3512
+ ],
3513
+ };
3514
+ }
3515
+ const changes = [];
3516
+ let previous = previousState || firstState;
3517
+ if (!previousState) {
3518
+ writeFileSync(stateFile, JSON.stringify(firstState, null, 2));
3519
+ }
3520
+ for (let index = 0; index < monitorCount; index++) {
3521
+ const current = index === 0 ? firstState : await captureMonitorState();
3522
+ const changed = previous.hash !== current.hash;
3523
+ if (changed) {
3524
+ const changeIndex = changes.length + 1;
3525
+ const screenshotPath = await saveScreenshotOnChange(changeIndex);
3526
+ const notification = notifyOnChange(`Page changed: ${current.url}`);
3527
+ changes.push({
3528
+ check: index + 1,
3529
+ timestamp: current.timestamp,
3530
+ hash: current.hash,
3531
+ previousHash: previous.hash,
3532
+ screenshotPath,
3533
+ notification,
3534
+ diff: diffLines(previous.content, current.content),
3535
+ });
3536
+ writeFileSync(stateFile, JSON.stringify(current, null, 2));
3537
+ }
3538
+ previous = current;
3539
+ if (index < monitorCount - 1) {
3540
+ await new Promise((resolve) => setTimeout(resolve, monitorIntervalSec * 1000));
3541
+ }
3542
+ }
3543
+ const logFile = join(scrapedDir, `monitor-${monitorId}-log-${Date.now()}.json`);
3544
+ const summary = {
3545
+ monitorId,
3546
+ mode: "continuous",
3547
+ url: firstState.url,
3548
+ selector: firstState.selector,
3549
+ checks: monitorCount,
3550
+ intervalSeconds: monitorIntervalSec,
3551
+ changesDetected: changes.length,
3552
+ stateFile,
3553
+ logFile,
3554
+ changes,
3555
+ };
3556
+ writeFileSync(logFile, JSON.stringify(summary, null, 2));
3557
+ return {
3558
+ content: [
3559
+ {
3560
+ type: "text",
3561
+ text: JSON.stringify(summary, null, 2),
3562
+ },
3563
+ ],
3564
+ };
3565
+ }
3262
3566
  case "comet_automate": {
3263
3567
  const autoSteps = args?.steps;
3264
3568
  const autoVerbose = args?.verbose || false;
@@ -0,0 +1,46 @@
1
+ export interface ProjectConfig {
2
+ /** Owning organization (HH, SVF, BR, Personal, Equa, ...). Used by skills for confirmation prompts. */
3
+ organization?: string;
4
+ /** Comet profile name. Defaults to "oe" per constitution Article I. */
5
+ default_profile?: string;
6
+ /** Whitelist of entity-specific skill names to surface in this project. */
7
+ entity_skills?: string[];
8
+ /** Enabled domain playbooks for comet_domain. */
9
+ domain_playbooks?: ("qbo" | "mercury" | "salt" | "github" | "google")[];
10
+ /** Prepended to new tab-group titles created by comet_connect. Existing groups untouched. */
11
+ tab_group_prefix?: string;
12
+ /** Drive root for this project's outputs. */
13
+ google_drive_root?: string;
14
+ /** Pin for @shawnowen/comet-mcp version. Empty / absent → latest. */
15
+ mcp_pin?: string;
16
+ }
17
+ /**
18
+ * Walk up from `cwd` to either a directory containing `.comet/config.json`,
19
+ * the user's $HOME (boundary), or filesystem root. Returns the absolute path
20
+ * to the config file, or null.
21
+ */
22
+ export declare function discoverProjectConfigPath(cwd?: string): string | null;
23
+ /** Clear the discovery cache. Test-only / on cwd change. */
24
+ export declare function clearProjectConfigCache(): void;
25
+ /**
26
+ * Validate a parsed config object. Returns the cleaned config + a list of
27
+ * warning strings for any keys that were ignored. Unknown keys are warned
28
+ * about but do not cause failure.
29
+ */
30
+ export declare function validateProjectConfig(raw: unknown): {
31
+ config: ProjectConfig;
32
+ warnings: string[];
33
+ };
34
+ /**
35
+ * Read and validate the project config. Returns an empty config + warnings if
36
+ * the file is absent, unreadable, or invalid JSON. NEVER throws.
37
+ *
38
+ * Per Spec 087 U-1 / clarify.md C-1 (revised): re-reads on every call.
39
+ * Discovery (walking up from cwd) IS cached by cwd; file contents are NOT.
40
+ */
41
+ export declare function readProjectConfig(cwd?: string): {
42
+ config: ProjectConfig;
43
+ path: string | null;
44
+ warnings: string[];
45
+ };
46
+ //# sourceMappingURL=project-config.d.ts.map
@@ -0,0 +1,166 @@
1
+ // Spec 087 / T040 / FR-006, FR-007, FR-008, FR-009
2
+ //
3
+ // Per-project configuration loader. Reads <project_root>/.comet/config.json
4
+ // when present, validates against a small schema, and re-reads on each access
5
+ // (mirrors bridge-config.ts pattern — no per-session caching of file contents).
6
+ //
7
+ // Discovery: walks upward from cwd to repo root or $HOME, looking for .comet/config.json.
8
+ // Caches only the discovered PATH for the session, not the file contents.
9
+ //
10
+ // All fields optional. Backwards-compatible: absent config = pre-Spec 087 behavior.
11
+ import { readFileSync, existsSync, statSync } from "fs";
12
+ import { homedir } from "os";
13
+ import { join, resolve, sep } from "path";
14
+ const VALID_PLAYBOOKS = new Set(["qbo", "mercury", "salt", "github", "google"]);
15
+ let discoveryCache = null;
16
+ /**
17
+ * Walk up from `cwd` to either a directory containing `.comet/config.json`,
18
+ * the user's $HOME (boundary), or filesystem root. Returns the absolute path
19
+ * to the config file, or null.
20
+ */
21
+ export function discoverProjectConfigPath(cwd = process.cwd()) {
22
+ // Cache discovery by cwd — file contents are NOT cached.
23
+ if (discoveryCache && discoveryCache.cwd === cwd) {
24
+ return discoveryCache.path;
25
+ }
26
+ const home = homedir();
27
+ let dir = resolve(cwd);
28
+ let result = null;
29
+ while (true) {
30
+ const candidate = join(dir, ".comet", "config.json");
31
+ if (existsSync(candidate)) {
32
+ try {
33
+ if (statSync(candidate).isFile()) {
34
+ result = candidate;
35
+ break;
36
+ }
37
+ }
38
+ catch {
39
+ // unreadable: keep walking
40
+ }
41
+ }
42
+ if (dir === home || dir === sep || dir === resolve(dir, ".."))
43
+ break;
44
+ dir = resolve(dir, "..");
45
+ }
46
+ discoveryCache = { cwd, path: result };
47
+ return result;
48
+ }
49
+ /** Clear the discovery cache. Test-only / on cwd change. */
50
+ export function clearProjectConfigCache() {
51
+ discoveryCache = null;
52
+ }
53
+ /**
54
+ * Validate a parsed config object. Returns the cleaned config + a list of
55
+ * warning strings for any keys that were ignored. Unknown keys are warned
56
+ * about but do not cause failure.
57
+ */
58
+ export function validateProjectConfig(raw) {
59
+ const warnings = [];
60
+ const config = {};
61
+ if (raw == null || typeof raw !== "object") {
62
+ warnings.push("project-config: root must be an object — falling back to defaults");
63
+ return { config, warnings };
64
+ }
65
+ const obj = raw;
66
+ const known = new Set([
67
+ "organization",
68
+ "default_profile",
69
+ "entity_skills",
70
+ "domain_playbooks",
71
+ "tab_group_prefix",
72
+ "google_drive_root",
73
+ "mcp_pin",
74
+ ]);
75
+ for (const key of Object.keys(obj)) {
76
+ if (!known.has(key)) {
77
+ warnings.push(`project-config: unknown key '${key}' ignored`);
78
+ }
79
+ }
80
+ if (typeof obj.organization === "string" && obj.organization.length <= 32) {
81
+ config.organization = obj.organization;
82
+ }
83
+ else if (obj.organization !== undefined) {
84
+ warnings.push("project-config: 'organization' must be a string ≤ 32 chars — ignored");
85
+ }
86
+ if (typeof obj.default_profile === "string" && /^[a-z0-9_-]{1,16}$/.test(obj.default_profile)) {
87
+ config.default_profile = obj.default_profile;
88
+ }
89
+ else if (obj.default_profile !== undefined) {
90
+ warnings.push("project-config: 'default_profile' must match /^[a-z0-9_-]{1,16}$/ — falling back to 'oe'");
91
+ }
92
+ if (Array.isArray(obj.entity_skills) && obj.entity_skills.every((x) => typeof x === "string")) {
93
+ config.entity_skills = obj.entity_skills;
94
+ }
95
+ else if (obj.entity_skills !== undefined) {
96
+ warnings.push("project-config: 'entity_skills' must be string[] — ignored");
97
+ }
98
+ if (Array.isArray(obj.domain_playbooks)) {
99
+ const valid = [];
100
+ for (const item of obj.domain_playbooks) {
101
+ if (typeof item === "string" && VALID_PLAYBOOKS.has(item)) {
102
+ valid.push(item);
103
+ }
104
+ else {
105
+ warnings.push(`project-config: unknown domain_playbook '${item}' ignored`);
106
+ }
107
+ }
108
+ if (valid.length > 0)
109
+ config.domain_playbooks = valid;
110
+ }
111
+ else if (obj.domain_playbooks !== undefined) {
112
+ warnings.push("project-config: 'domain_playbooks' must be string[] — ignored");
113
+ }
114
+ if (typeof obj.tab_group_prefix === "string" &&
115
+ obj.tab_group_prefix.length <= 16 &&
116
+ !/[,:]/.test(obj.tab_group_prefix)) {
117
+ config.tab_group_prefix = obj.tab_group_prefix;
118
+ }
119
+ else if (obj.tab_group_prefix !== undefined) {
120
+ warnings.push("project-config: 'tab_group_prefix' must be ≤ 16 chars with no comma or colon — ignored");
121
+ }
122
+ if (typeof obj.google_drive_root === "string" &&
123
+ (obj.google_drive_root.startsWith("drive://") ||
124
+ obj.google_drive_root.startsWith("https://drive.google.com/"))) {
125
+ config.google_drive_root = obj.google_drive_root;
126
+ }
127
+ else if (obj.google_drive_root !== undefined) {
128
+ warnings.push("project-config: 'google_drive_root' must start with drive:// or https://drive.google.com/ — ignored");
129
+ }
130
+ if (typeof obj.mcp_pin === "string" && /^\d+\.\d+\.\d+(-[A-Za-z0-9.-]+)?$/.test(obj.mcp_pin)) {
131
+ config.mcp_pin = obj.mcp_pin;
132
+ }
133
+ else if (obj.mcp_pin !== undefined) {
134
+ warnings.push("project-config: 'mcp_pin' must be a semver string — ignored");
135
+ }
136
+ return { config, warnings };
137
+ }
138
+ /**
139
+ * Read and validate the project config. Returns an empty config + warnings if
140
+ * the file is absent, unreadable, or invalid JSON. NEVER throws.
141
+ *
142
+ * Per Spec 087 U-1 / clarify.md C-1 (revised): re-reads on every call.
143
+ * Discovery (walking up from cwd) IS cached by cwd; file contents are NOT.
144
+ */
145
+ export function readProjectConfig(cwd = process.cwd()) {
146
+ const path = discoverProjectConfigPath(cwd);
147
+ if (!path) {
148
+ return { config: {}, path: null, warnings: [] };
149
+ }
150
+ let raw;
151
+ try {
152
+ raw = JSON.parse(readFileSync(path, "utf8"));
153
+ }
154
+ catch (err) {
155
+ return {
156
+ config: {},
157
+ path,
158
+ warnings: [
159
+ `project-config: failed to parse ${path} — ${err.message} — falling back to defaults`,
160
+ ],
161
+ };
162
+ }
163
+ const { config, warnings } = validateProjectConfig(raw);
164
+ return { config, path, warnings };
165
+ }
166
+ //# sourceMappingURL=project-config.js.map
@@ -31,6 +31,22 @@ export interface UpdateGroupOptions {
31
31
  color?: TabGroupColor;
32
32
  collapsed?: boolean;
33
33
  }
34
+ export interface ExtensionMetadataUpdateOptions {
35
+ entityType?: string;
36
+ entityId?: string | number;
37
+ windowId?: string | number;
38
+ activeWindowId?: string | number;
39
+ id?: string | number;
40
+ taskThreadId?: string;
41
+ tabIndex?: number;
42
+ sourceFamily?: string;
43
+ intent?: string;
44
+ policyTier?: string;
45
+ description?: string;
46
+ title?: string;
47
+ label?: string;
48
+ status?: string;
49
+ }
34
50
  export interface TabGroupArchiveEntry {
35
51
  taskThreadId: string;
36
52
  title: string;
@@ -42,7 +58,7 @@ export interface TabGroupArchiveEntry {
42
58
  }[];
43
59
  archivedAt: string;
44
60
  restoredAt?: string;
45
- status: "saved" | "archived";
61
+ status: "pending" | "done" | "trashed" | "saved" | "archived";
46
62
  }
47
63
  export declare class TabGroupsClient {
48
64
  private client;
@@ -83,6 +99,10 @@ export declare class TabGroupsClient {
83
99
  ungroupTabs(tabIds: number[]): Promise<void>;
84
100
  /** List all tabs with their groupId (−1 = ungrouped). */
85
101
  listTabs(): Promise<TabInfo[]>;
102
+ /** List extension-owned descriptions, window labels, archives, and selected metadata. */
103
+ listExtensionMetadata(): Promise<Record<string, unknown>>;
104
+ /** Update extension-owned metadata without touching native tab state. */
105
+ updateExtensionMetadata(options: ExtensionMetadataUpdateOptions): Promise<Record<string, unknown>>;
86
106
  /** Create a single tab and return its ID. Used by ETT restore_group. */
87
107
  createTab(url: string, active?: boolean): Promise<number>;
88
108
  /** Close multiple tabs by ID. Used by ETT archive_group. */