@shawnowen/comet-mcp 2.3.0 → 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
@@ -1,4 +1,7 @@
1
+ import { CometCDPClient } from "./cdp-client.js";
1
2
  export declare class CometAI {
3
+ private cdp;
4
+ constructor(cdpClient?: CometCDPClient);
2
5
  /**
3
6
  * Find the first matching element from a list of selectors
4
7
  */
@@ -26,6 +29,18 @@ export declare class CometAI {
26
29
  hasStopButton: boolean;
27
30
  agentBrowsingUrl: string;
28
31
  }>;
32
+ /**
33
+ * Detect voice/listening mode — Perplexity entered microphone mode instead of processing text.
34
+ */
35
+ detectVoiceMode(): Promise<boolean>;
36
+ /**
37
+ * Detect IDLE without navigation — task returned IDLE but never left the Perplexity home page.
38
+ */
39
+ detectIdleNoNavigation(initialUrl?: string): Promise<boolean>;
40
+ /**
41
+ * Detect context bleed — response content URL doesn't match the requested URL.
42
+ */
43
+ detectContextBleed(requestedUrl?: string): Promise<boolean>;
29
44
  /**
30
45
  * Stop the current agent task
31
46
  */
package/dist/comet-ai.js CHANGED
@@ -1,21 +1,25 @@
1
1
  // Comet AI interaction module
2
2
  // Handles sending prompts to Comet's AI assistant and reading responses
3
- import { cometClient } from "./cdp-client.js";
3
+ import { cometClient as globalCometClient } from "./cdp-client.js";
4
4
  // Input selectors - contenteditable div is primary for Perplexity
5
5
  const INPUT_SELECTORS = [
6
6
  '[contenteditable="true"]',
7
7
  'textarea[placeholder*="Ask"]',
8
8
  'textarea[placeholder*="Search"]',
9
- 'textarea',
9
+ "textarea",
10
10
  'input[type="text"]',
11
11
  ];
12
12
  export class CometAI {
13
+ cdp;
14
+ constructor(cdpClient) {
15
+ this.cdp = cdpClient || globalCometClient;
16
+ }
13
17
  /**
14
18
  * Find the first matching element from a list of selectors
15
19
  */
16
20
  async findInputElement() {
17
21
  for (const selector of INPUT_SELECTORS) {
18
- const result = await cometClient.evaluate(`
22
+ const result = await this.cdp.evaluate(`
19
23
  document.querySelector(${JSON.stringify(selector)}) !== null
20
24
  `);
21
25
  if (result.result.value === true) {
@@ -33,7 +37,7 @@ export class CometAI {
33
37
  throw new Error("Could not find input element. Navigate to Perplexity first.");
34
38
  }
35
39
  // Use execCommand for contenteditable elements (works with React/Vue)
36
- const result = await cometClient.evaluate(`
40
+ const result = await this.cdp.evaluate(`
37
41
  (() => {
38
42
  const el = document.querySelector('[contenteditable="true"]');
39
43
  if (el) {
@@ -59,16 +63,16 @@ export class CometAI {
59
63
  }
60
64
  // Submit the prompt
61
65
  await this.submitPrompt();
62
- return `Prompt sent: "${prompt.substring(0, 50)}${prompt.length > 50 ? '...' : ''}"`;
66
+ return `Prompt sent: "${prompt.substring(0, 50)}${prompt.length > 50 ? "..." : ""}"`;
63
67
  }
64
68
  /**
65
69
  * Submit the current prompt
66
70
  */
67
71
  async submitPrompt() {
68
72
  // Wait for React to process the typed content
69
- await new Promise(resolve => setTimeout(resolve, 500));
73
+ await new Promise((resolve) => setTimeout(resolve, 500));
70
74
  // Verify text was typed before attempting submit
71
- const hasContent = await cometClient.evaluate(`
75
+ const hasContent = await this.cdp.evaluate(`
72
76
  (() => {
73
77
  const el = document.querySelector('[contenteditable="true"]');
74
78
  if (el && el.innerText.trim().length > 0) return true;
@@ -81,17 +85,17 @@ export class CometAI {
81
85
  throw new Error("Prompt text not found in input - typing may have failed");
82
86
  }
83
87
  // Strategy 1: Use Enter key (most reliable for Perplexity)
84
- await cometClient.evaluate(`
88
+ await this.cdp.evaluate(`
85
89
  (() => {
86
90
  const el = document.querySelector('[contenteditable="true"]') ||
87
91
  document.querySelector('textarea');
88
92
  if (el) el.focus();
89
93
  })()
90
94
  `);
91
- await cometClient.pressKey("Enter");
92
- await new Promise(resolve => setTimeout(resolve, 500));
95
+ await this.cdp.pressKey("Enter");
96
+ await new Promise((resolve) => setTimeout(resolve, 500));
93
97
  // Check if submission worked
94
- const submitted = await cometClient.evaluate(`
98
+ const submitted = await this.cdp.evaluate(`
95
99
  (() => {
96
100
  const el = document.querySelector('[contenteditable="true"]');
97
101
  if (el && el.innerText.trim().length < 5) return true;
@@ -102,7 +106,7 @@ export class CometAI {
102
106
  if (submitted.result.value)
103
107
  return;
104
108
  // Strategy 2: Click submit button
105
- await cometClient.evaluate(`
109
+ await this.cdp.evaluate(`
106
110
  (() => {
107
111
  const selectors = [
108
112
  'button[aria-label*="Submit"]',
@@ -156,8 +160,8 @@ export class CometAI {
156
160
  })()
157
161
  `);
158
162
  // Final check and retry with Enter if still not submitted
159
- await new Promise(resolve => setTimeout(resolve, 500));
160
- const finalCheck = await cometClient.evaluate(`
163
+ await new Promise((resolve) => setTimeout(resolve, 500));
164
+ const finalCheck = await this.cdp.evaluate(`
161
165
  (() => {
162
166
  const el = document.querySelector('[contenteditable="true"]');
163
167
  if (el && el.innerText.trim().length < 5) return true;
@@ -168,7 +172,7 @@ export class CometAI {
168
172
  `);
169
173
  if (!finalCheck.result.value) {
170
174
  // Last resort: try Enter one more time
171
- await cometClient.pressKey("Enter");
175
+ await this.cdp.pressKey("Enter");
172
176
  }
173
177
  }
174
178
  /**
@@ -180,7 +184,7 @@ export class CometAI {
180
184
  throw new Error("Could not find input element. Navigate to Perplexity first.");
181
185
  }
182
186
  // Focus and clear the input
183
- await cometClient.evaluate(`
187
+ await this.cdp.evaluate(`
184
188
  (() => {
185
189
  const el = document.querySelector('[contenteditable="true"]');
186
190
  if (el) {
@@ -199,7 +203,7 @@ export class CometAI {
199
203
  `);
200
204
  // Type the shortcut command
201
205
  const shortcutCmd = `/${shortcut}`;
202
- await cometClient.evaluate(`
206
+ await this.cdp.evaluate(`
203
207
  (() => {
204
208
  const el = document.querySelector('[contenteditable="true"]');
205
209
  if (el) {
@@ -218,13 +222,13 @@ export class CometAI {
218
222
  })()
219
223
  `);
220
224
  // Wait for shortcut dropdown/autocomplete to appear
221
- await new Promise(resolve => setTimeout(resolve, 800));
225
+ await new Promise((resolve) => setTimeout(resolve, 800));
222
226
  // Press Enter to select the shortcut
223
- await cometClient.pressKey("Enter");
227
+ await this.cdp.pressKey("Enter");
224
228
  // If context is provided, type it after a brief pause
225
229
  if (context) {
226
- await new Promise(resolve => setTimeout(resolve, 500));
227
- await cometClient.evaluate(`
230
+ await new Promise((resolve) => setTimeout(resolve, 500));
231
+ await this.cdp.evaluate(`
228
232
  (() => {
229
233
  const el = document.querySelector('[contenteditable="true"]');
230
234
  if (el) {
@@ -250,26 +254,25 @@ export class CometAI {
250
254
  `);
251
255
  }
252
256
  // Submit
253
- await new Promise(resolve => setTimeout(resolve, 300));
254
- await cometClient.pressKey("Enter");
255
- return `Shortcut /${shortcut} sent${context ? ` with context: "${context.substring(0, 50)}${context.length > 50 ? '...' : ''}"` : ''}`;
257
+ await new Promise((resolve) => setTimeout(resolve, 300));
258
+ await this.cdp.pressKey("Enter");
259
+ return `Shortcut /${shortcut} sent${context ? ` with context: "${context.substring(0, 50)}${context.length > 50 ? "..." : ""}"` : ""}`;
256
260
  }
257
261
  /**
258
262
  * Get current agent status and progress (for polling)
259
263
  */
260
264
  async getAgentStatus() {
261
- // Get browsing URL from agent's tab
262
- let agentBrowsingUrl = '';
265
+ // Report the bound tab URL only. Global target scans can leak another
266
+ // agent's authenticated workflow into this session's status.
267
+ let agentBrowsingUrl = "";
263
268
  try {
264
- const tabs = await cometClient.listTabsCategorized();
265
- if (tabs.agentBrowsing) {
266
- agentBrowsingUrl = tabs.agentBrowsing.url;
267
- }
269
+ const urlResult = await this.cdp.safeEvaluate("window.location.href");
270
+ agentBrowsingUrl = urlResult.result.value || "";
268
271
  }
269
272
  catch {
270
273
  // Continue without URL
271
274
  }
272
- const result = await cometClient.safeEvaluate(`
275
+ const result = await this.cdp.safeEvaluate(`
273
276
  (() => {
274
277
  const body = document.body.innerText;
275
278
 
@@ -285,7 +288,9 @@ export class CometAI {
285
288
  }
286
289
  }
287
290
 
288
- const hasLoadingSpinner = document.querySelector('[class*="animate-spin"], [class*="animate-pulse"]') !== null;
291
+ // Scope spinner detection to response content area only (not page-wide UI)
292
+ const responseArea = document.querySelector('main') || document.body;
293
+ const hasLoadingSpinner = responseArea.querySelector('[class*="animate-spin"], [class*="animate-pulse"]') !== null;
289
294
  const hasStepsCompleted = /\\d+ steps? completed/i.test(body);
290
295
  const hasFinishedMarker = body.includes('Finished') && !hasActiveStopButton;
291
296
  const hasReviewedSources = /Reviewed \\d+ sources?/i.test(body);
@@ -300,18 +305,18 @@ export class CometAI {
300
305
  ];
301
306
  const hasWorkingText = workingPatterns.some(p => body.includes(p));
302
307
 
303
- // Determine status
308
+ // Determine status — prioritize positive completion signals
304
309
  let status = 'idle';
305
- if (hasActiveStopButton || hasLoadingSpinner) {
306
- status = 'working';
310
+ if (hasAskFollowUp && hasProseContent && !hasActiveStopButton) {
311
+ status = 'completed';
307
312
  } else if (hasStepsCompleted || hasFinishedMarker) {
308
313
  status = 'completed';
309
314
  } else if (hasReviewedSources && !hasWorkingText) {
310
315
  status = 'completed';
316
+ } else if (hasActiveStopButton || hasLoadingSpinner) {
317
+ status = 'working';
311
318
  } else if (hasWorkingText) {
312
319
  status = 'working';
313
- } else if (hasAskFollowUp && hasProseContent && !hasActiveStopButton) {
314
- status = 'completed';
315
320
  }
316
321
 
317
322
  // Extract steps
@@ -368,11 +373,82 @@ export class CometAI {
368
373
  agentBrowsingUrl,
369
374
  };
370
375
  }
376
+ // ---- Failure Detection Heuristics (Spec 016, FR-002, T014-T016) ----
377
+ /**
378
+ * Detect voice/listening mode — Perplexity entered microphone mode instead of processing text.
379
+ */
380
+ async detectVoiceMode() {
381
+ try {
382
+ const result = await this.cdp.safeEvaluate(`
383
+ (() => {
384
+ const body = document.body.innerText;
385
+ // Check for "Listening..." text which appears in voice mode
386
+ if (body.includes('Listening...') || body.includes('Listening…')) return true;
387
+ // Check for active microphone UI elements
388
+ const micButtons = document.querySelectorAll('button[aria-label*="microphone" i], button[aria-label*="voice" i], button[aria-label*="listen" i]');
389
+ for (const btn of micButtons) {
390
+ if (btn.classList.contains('active') || btn.getAttribute('aria-pressed') === 'true') return true;
391
+ }
392
+ return false;
393
+ })()
394
+ `);
395
+ return result.result.value === true;
396
+ }
397
+ catch (err) {
398
+ console.warn(`[comet-bridge] Voice mode detection failed: ${err instanceof Error ? err.message : err}`);
399
+ return false;
400
+ }
401
+ }
402
+ /**
403
+ * Detect IDLE without navigation — task returned IDLE but never left the Perplexity home page.
404
+ */
405
+ async detectIdleNoNavigation(initialUrl) {
406
+ try {
407
+ const result = await this.cdp.safeEvaluate(`window.location.href`);
408
+ const currentUrl = result.result.value;
409
+ // If still on Perplexity home/search and status is idle, no navigation occurred
410
+ const isPerplexityHome = currentUrl === "https://www.perplexity.ai/" ||
411
+ currentUrl === "https://www.perplexity.ai" ||
412
+ currentUrl?.endsWith("/home");
413
+ if (isPerplexityHome)
414
+ return true;
415
+ // If we have an initial URL and it hasn't changed, no navigation occurred
416
+ if (initialUrl && currentUrl === initialUrl)
417
+ return true;
418
+ return false;
419
+ }
420
+ catch (err) {
421
+ console.warn(`[comet-bridge] Idle/no-navigation detection failed: ${err instanceof Error ? err.message : err}`);
422
+ return false;
423
+ }
424
+ }
425
+ /**
426
+ * Detect context bleed — response content URL doesn't match the requested URL.
427
+ */
428
+ async detectContextBleed(requestedUrl) {
429
+ if (!requestedUrl)
430
+ return false;
431
+ try {
432
+ const status = await this.getAgentStatus();
433
+ // If the response mentions browsing a different URL than requested, it's context bleed
434
+ if (status.agentBrowsingUrl && requestedUrl) {
435
+ const requested = new URL(requestedUrl).hostname;
436
+ const actual = new URL(status.agentBrowsingUrl).hostname;
437
+ if (requested !== actual)
438
+ return true;
439
+ }
440
+ return false;
441
+ }
442
+ catch (err) {
443
+ console.warn(`[comet-bridge] Context bleed detection failed: ${err instanceof Error ? err.message : err}`);
444
+ return false;
445
+ }
446
+ }
371
447
  /**
372
448
  * Stop the current agent task
373
449
  */
374
450
  async stopAgent() {
375
- const result = await cometClient.evaluate(`
451
+ const result = await this.cdp.evaluate(`
376
452
  (() => {
377
453
  // Try aria-label buttons first
378
454
  for (const btn of document.querySelectorAll('button[aria-label*="Stop"], button[aria-label*="Cancel"]')) {
@@ -0,0 +1,19 @@
1
+ import { type CodexIdentityInput, type CodexWindowBinding, type CodexWindowBindingStore } from "./window-bindings.js";
2
+ export interface DelegateBindingInput extends CodexIdentityInput {
3
+ taskId: string;
4
+ threadId: string;
5
+ agentId: string;
6
+ currentBinding?: CodexWindowBinding | null;
7
+ bindingId?: string;
8
+ windowId?: number;
9
+ tabGroupId?: number | null;
10
+ targetId?: string | null;
11
+ }
12
+ export interface DelegateBindingDispatch {
13
+ bindingId: string | null;
14
+ windowId: number | null;
15
+ tabGroupId: number | null;
16
+ dispatchStatus: string;
17
+ }
18
+ export declare function createOrReuseDelegateBinding(input: DelegateBindingInput, store?: CodexWindowBindingStore): Promise<DelegateBindingDispatch>;
19
+ //# sourceMappingURL=delegate-binding.d.ts.map
@@ -0,0 +1,73 @@
1
+ import { deriveCodexSessionIdentity, windowBindingStore, } from "./window-bindings.js";
2
+ function numericValue(value) {
3
+ if (typeof value === "number" && Number.isFinite(value))
4
+ return value;
5
+ if (typeof value === "string" && value.trim().length > 0) {
6
+ const parsed = Number(value);
7
+ if (Number.isFinite(parsed))
8
+ return parsed;
9
+ }
10
+ return undefined;
11
+ }
12
+ export async function createOrReuseDelegateBinding(input, store = windowBindingStore) {
13
+ const currentBinding = input.currentBinding ?? null;
14
+ if (input.bindingId) {
15
+ const binding = await store.addRunId(input.bindingId, input.taskId);
16
+ return {
17
+ bindingId: binding.bindingId,
18
+ windowId: binding.windowId,
19
+ tabGroupId: binding.tabGroupId,
20
+ dispatchStatus: "binding-reused",
21
+ };
22
+ }
23
+ if (currentBinding &&
24
+ input.windowId === undefined &&
25
+ input.tabGroupId === undefined &&
26
+ input.targetId === undefined) {
27
+ const persisted = await store.get(currentBinding.bindingId);
28
+ if (persisted) {
29
+ const binding = await store.addRunId(currentBinding.bindingId, input.taskId);
30
+ return {
31
+ bindingId: binding.bindingId,
32
+ windowId: binding.windowId,
33
+ tabGroupId: binding.tabGroupId,
34
+ dispatchStatus: "binding-reused",
35
+ };
36
+ }
37
+ }
38
+ const windowId = numericValue(input.windowId) ?? currentBinding?.windowId;
39
+ if (windowId === undefined) {
40
+ return {
41
+ bindingId: null,
42
+ windowId: null,
43
+ tabGroupId: null,
44
+ dispatchStatus: "binding-unavailable: windowId required",
45
+ };
46
+ }
47
+ const identity = deriveCodexSessionIdentity({
48
+ ...input,
49
+ strict: input.strict === true,
50
+ fallbackAgentId: input.agentId,
51
+ fallbackTaskThreadId: input.threadId,
52
+ });
53
+ const bindingResult = await store.createOrReuse({
54
+ ...identity,
55
+ windowId,
56
+ tabGroupId: numericValue(input.tabGroupId) ??
57
+ currentBinding?.tabGroupId ??
58
+ (typeof input.tabGroupId === "number" ? input.tabGroupId : null),
59
+ targetId: input.targetId ?? currentBinding?.targetId ?? null,
60
+ runIds: [input.taskId],
61
+ profileId: currentBinding?.profileId ?? "agent",
62
+ profileAlias: currentBinding?.profileAlias ?? "oe",
63
+ profileOwner: currentBinding?.profileOwner ?? "agent",
64
+ sidecarContextKey: currentBinding?.sidecarContextKey,
65
+ });
66
+ return {
67
+ bindingId: bindingResult.binding.bindingId,
68
+ windowId: bindingResult.binding.windowId,
69
+ tabGroupId: bindingResult.binding.tabGroupId,
70
+ dispatchStatus: `binding-${bindingResult.action}`,
71
+ };
72
+ }
73
+ //# sourceMappingURL=delegate-binding.js.map
@@ -0,0 +1,215 @@
1
+ /**
2
+ * capability-entry.ts
3
+ * Core type definitions for the Comet Discovery Layer (Spec 042).
4
+ * All types are derived from data-model.md and capability-entry.schema.json.
5
+ * No runtime logic — pure type declarations + tagged unions.
6
+ */
7
+ /** Which canonical source owns a capability entry. */
8
+ export type SourceLayer = "mcp" | "plugin" | "extension";
9
+ /** Rank order for dedup: plugin (3) > extension (2) > mcp (1). */
10
+ export declare const SOURCE_LAYER_RANK: Record<SourceLayer, number>;
11
+ /**
12
+ * Safety classification — reused verbatim from docs/TOOL-SAFETY-REFERENCE.md.
13
+ * No default, no UNKNOWN class. Every capability must have exactly one.
14
+ *
15
+ * SAFE — Read-only, zero side effects, no session required.
16
+ * SESSION — Requires an active Comet CDP session; scoped to the calling agent's tab group.
17
+ * CAUTION — Cross-session side effects possible; requires explicit multi-agent awareness.
18
+ */
19
+ export type SafetyClass = "SAFE" | "SESSION" | "CAUTION";
20
+ export type Precondition = {
21
+ kind: "cdp_session";
22
+ note: string;
23
+ } | {
24
+ kind: "profile";
25
+ value: "oe" | "moon";
26
+ } | {
27
+ kind: "task_group";
28
+ note: string;
29
+ } | {
30
+ kind: "task_thread";
31
+ note: string;
32
+ } | {
33
+ kind: "free";
34
+ note: string;
35
+ };
36
+ export type Invocation = {
37
+ kind: "native";
38
+ /** Name of the existing MCP tool — pass-through, no behavior change (FR-009). */
39
+ toolName: string;
40
+ } | {
41
+ kind: "guide";
42
+ /** Claude Code Skill name (e.g. "comet-browse"). */
43
+ skill: string;
44
+ /** Args to pass to Skill(). */
45
+ normalizedArgs: Record<string, unknown>;
46
+ /** Human hint: "Invoke via Skill('comet-browse') with these args." */
47
+ nextStepHint: string;
48
+ } | {
49
+ kind: "cdp";
50
+ /** Chrome DevTools Protocol method string. */
51
+ method: string;
52
+ params?: Record<string, unknown>;
53
+ } | {
54
+ kind: "filesystem";
55
+ path: string;
56
+ mode: "read" | "signal";
57
+ } | {
58
+ kind: "cli";
59
+ command: string;
60
+ args: string[];
61
+ };
62
+ /**
63
+ * One CapabilityEntry per source-side definition.
64
+ * A plugin command that wraps a native MCP tool produces TWO entries sharing a canonicalId.
65
+ */
66
+ export interface CapabilityEntry {
67
+ /**
68
+ * Globally unique tool-facing identifier.
69
+ * Native: existing tool name (e.g. "comet_screenshot").
70
+ * Plugin: "plugin_<slug>".
71
+ * Extension: "ext_<slug>".
72
+ * Regex: /^[a-z][a-z0-9_]{2,63}$/
73
+ */
74
+ name: string;
75
+ /** Which canonical source owns this entry. */
76
+ sourceLayer: SourceLayer;
77
+ /**
78
+ * Repo-relative path + optional #fragment pointing to the canonical definition.
79
+ * e.g. "comet-mcp/src/index.ts#tool_comet_screenshot"
80
+ */
81
+ sourcePath: string;
82
+ /**
83
+ * Shared identity key for dedup grouping across layers.
84
+ * Entries sharing a canonicalId form a CapabilityGroup.
85
+ * Regex: /^[a-z][a-z0-9_-]{2,63}$/
86
+ */
87
+ canonicalId: string;
88
+ /**
89
+ * Human-readable verb-phrase description.
90
+ * Template: "<verb> <object> [<modifier>]. Source: <layer>. Safety: <class>."
91
+ * Min 24 chars, max 400 chars.
92
+ */
93
+ description: string;
94
+ /**
95
+ * At least 3 lowercase keywords capturing common search intents.
96
+ * Drives natural-language ToolSearch ranking (FR-006, SC-002).
97
+ */
98
+ intentKeywords: string[];
99
+ /**
100
+ * Safety class — must be set; no default.
101
+ * Reused verbatim from docs/TOOL-SAFETY-REFERENCE.md.
102
+ */
103
+ safety: SafetyClass;
104
+ /**
105
+ * Prerequisites the agent must satisfy before calling this tool.
106
+ * May be empty array, never missing.
107
+ */
108
+ preconditions: Precondition[];
109
+ /**
110
+ * JSON Schema draft 2020-12 fragment for the tool's arguments.
111
+ * Empty schema `{ type: "object", properties: {} }` is valid.
112
+ */
113
+ argsSchema: Record<string, unknown>;
114
+ /**
115
+ * True iff this capability has no agent-reachable programmatic entry point
116
+ * and is deliberately excluded from virtual-tool registration.
117
+ * MUST be accompanied by a matching HumanOnlyException.
118
+ */
119
+ humanOnly: boolean;
120
+ /** Determines what the virtual MCP tool body returns or dispatches. */
121
+ invocation: Invocation;
122
+ /**
123
+ * The `name` of the paired entry in this entry's CapabilityGroup, or null for singletons.
124
+ */
125
+ crossRef: string | null;
126
+ /**
127
+ * True for the highest-layer entry in a group (plugin > extension > mcp).
128
+ * Singletons are always recommended: true.
129
+ */
130
+ recommended: boolean;
131
+ /** For non-recommended entries: "lower-level". For recommended: null. */
132
+ alternativeLabel: "lower-level" | null;
133
+ }
134
+ /**
135
+ * Transient collection of CapabilityEntry instances sharing a canonicalId.
136
+ * Emits exactly one "recommended" virtual tool and up to one "alternative (lower-level)".
137
+ */
138
+ export interface CapabilityGroup {
139
+ canonicalId: string;
140
+ /** Highest-layer member (plugin > extension > mcp). */
141
+ recommendedEntry: CapabilityEntry;
142
+ /** Lowest-layer member when distinct from recommendedEntry. null for singletons. */
143
+ alternativeEntry: CapabilityEntry | null;
144
+ /**
145
+ * Intermediate members NOT registered as virtual tools.
146
+ * Still listed for traceability under the recommended entry's alternates.
147
+ */
148
+ suppressedEntries: CapabilityEntry[];
149
+ }
150
+ export interface HumanOnlyException {
151
+ /** Must equal the canonicalId of a capability with humanOnly: true. */
152
+ name: string;
153
+ /** Why no programmatic entry point exists. Min 24 chars. */
154
+ reason: string;
155
+ /** GitHub handle (@username) or team (@org/team). */
156
+ owner: string;
157
+ /** Plan to make this capability agent-accessible, or null if genuinely blocked. */
158
+ enhancementPath: string | null;
159
+ /** ISO 8601 date string. Staleness warning fires if older than 90 days. */
160
+ lastReviewed: string;
161
+ }
162
+ export interface SourceError {
163
+ /** Which source layer failed. */
164
+ source: SourceLayer;
165
+ /** Human-readable reason. */
166
+ reason: string;
167
+ /** Original error, if any. */
168
+ originalError?: unknown;
169
+ }
170
+ export type DriftStatus = "pass" | "warn" | "fail";
171
+ export interface DriftReport {
172
+ generatedAt: string;
173
+ emittedBy: "ci" | "session-start";
174
+ available: SourceLayer[];
175
+ unavailable: Array<{
176
+ source: SourceLayer;
177
+ reason: string;
178
+ }>;
179
+ capabilityCount: number;
180
+ virtualToolCount: number;
181
+ /** canonicalIds in canonical source but missing from registry → hard failure. */
182
+ missingFromRegistry: string[];
183
+ /** Virtual tool names in registry with no canonical source → hard failure. */
184
+ extrasInRegistry: string[];
185
+ /** Entry names missing safety field → hard failure. */
186
+ safetyGaps: string[];
187
+ /** humanOnly: true capabilities lacking a matching exception → hard failure. */
188
+ humanOnlyDangling: string[];
189
+ /** Exception names that don't resolve to any humanOnly: true capability → hard failure. */
190
+ exceptionsDangling: string[];
191
+ /** Exceptions with lastReviewed older than review window → warning only. */
192
+ exceptionsStale: string[];
193
+ status: DriftStatus;
194
+ }
195
+ export interface GoldenQueryResult {
196
+ query: string;
197
+ expectedCanonicalId: string;
198
+ top5Names: string[];
199
+ hit: boolean;
200
+ }
201
+ export interface CoverageReport {
202
+ sources: Record<SourceLayer, number>;
203
+ totalCapabilities: number;
204
+ coverageRatio: number;
205
+ goldenQueries: GoldenQueryResult[];
206
+ top5HitRate: number;
207
+ metadataCompleteness: number;
208
+ }
209
+ /** Minimal MCP tool definition shape accepted by the comet-mcp Server. */
210
+ export interface McpToolDefinition {
211
+ name: string;
212
+ description: string;
213
+ inputSchema: Record<string, unknown>;
214
+ }
215
+ //# sourceMappingURL=capability-entry.d.ts.map
@@ -0,0 +1,13 @@
1
+ /**
2
+ * capability-entry.ts
3
+ * Core type definitions for the Comet Discovery Layer (Spec 042).
4
+ * All types are derived from data-model.md and capability-entry.schema.json.
5
+ * No runtime logic — pure type declarations + tagged unions.
6
+ */
7
+ /** Rank order for dedup: plugin (3) > extension (2) > mcp (1). */
8
+ export const SOURCE_LAYER_RANK = {
9
+ plugin: 3,
10
+ extension: 2,
11
+ mcp: 1,
12
+ };
13
+ //# sourceMappingURL=capability-entry.js.map