@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.
- package/README.md +86 -19
- package/dist/alert-dispatcher.d.ts +23 -0
- package/dist/alert-dispatcher.js +101 -0
- package/dist/bound-session.d.ts +23 -0
- package/dist/bound-session.js +119 -0
- package/dist/bridge-config.d.ts +6 -0
- package/dist/bridge-config.js +78 -0
- package/dist/cdp-client.d.ts +40 -4
- package/dist/cdp-client.js +502 -155
- package/dist/comet-ai.d.ts +15 -0
- package/dist/comet-ai.js +114 -38
- package/dist/delegate-binding.d.ts +19 -0
- package/dist/delegate-binding.js +73 -0
- package/dist/discovery/capability-entry.d.ts +215 -0
- package/dist/discovery/capability-entry.js +13 -0
- package/dist/discovery/description-template.d.ts +40 -0
- package/dist/discovery/description-template.js +61 -0
- package/dist/discovery/golden-queries.fixture.d.ts +22 -0
- package/dist/discovery/golden-queries.fixture.js +137 -0
- package/dist/discovery/mcp-source.d.ts +38 -0
- package/dist/discovery/mcp-source.js +70 -0
- package/dist/discovery/metadata-completeness.d.ts +48 -0
- package/dist/discovery/metadata-completeness.js +83 -0
- package/dist/discovery/registry.d.ts +35 -0
- package/dist/discovery/registry.js +35 -0
- package/dist/discovery/safety.d.ts +44 -0
- package/dist/discovery/safety.js +59 -0
- package/dist/discovery/schema-validator.d.ts +36 -0
- package/dist/discovery/schema-validator.js +257 -0
- package/dist/discovery/source-error.d.ts +47 -0
- package/dist/discovery/source-error.js +95 -0
- package/dist/discovery/tool-meta.d.ts +41 -0
- package/dist/discovery/tool-meta.js +229 -0
- package/dist/discovery/virtual-tools.d.ts +20 -0
- package/dist/discovery/virtual-tools.js +69 -0
- package/dist/http-server.js +2067 -47
- package/dist/index.js +3163 -710
- package/dist/observer.d.ts +47 -0
- package/dist/observer.js +516 -0
- package/dist/session-registry.d.ts +57 -0
- package/dist/session-registry.js +500 -0
- package/dist/sidecar-artifacts.d.ts +49 -0
- package/dist/sidecar-artifacts.js +146 -0
- package/dist/snapshot-capture.d.ts +3 -0
- package/dist/snapshot-capture.js +91 -0
- package/dist/tab-group-archive.js +3 -1
- package/dist/tab-groups.d.ts +7 -0
- package/dist/tab-groups.js +21 -3
- package/dist/task-thread-aggregator.d.ts +34 -0
- package/dist/task-thread-aggregator.js +480 -0
- package/dist/task-thread-canonical.d.ts +142 -0
- package/dist/task-thread-canonical.js +116 -0
- package/dist/types.d.ts +237 -0
- package/dist/window-bindings.d.ts +112 -0
- package/dist/window-bindings.js +476 -0
- package/extension/background.js +1556 -300
- package/extension/icons/icon.svg +9 -0
- package/extension/icons/icon128.png +0 -0
- package/extension/icons/icon16.png +0 -0
- package/extension/icons/icon48.png +0 -0
- package/extension/manifest.json +19 -4
- package/extension/session-logic.js +2383 -0
- package/extension/session-manager.html +299 -0
- package/extension/sidepanel.css +5323 -528
- package/extension/sidepanel.html +282 -2
- package/extension/sidepanel.js +10075 -951
- package/extension/window-policy.js +162 -0
- package/package.json +10 -7
- package/vendor/lifecycle-mcp-adapter.mjs +103 -0
- package/vendor/lifecycle-metadata.mjs +252 -0
- package/vendor/readiness-report.mjs +742 -0
- package/dist/cdp-client.d.ts.map +0 -1
- package/dist/cdp-client.js.map +0 -1
- package/dist/comet-ai.d.ts.map +0 -1
- package/dist/comet-ai.js.map +0 -1
- package/dist/http-server.d.ts.map +0 -1
- package/dist/http-server.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/tab-group-archive.d.ts.map +0 -1
- package/dist/tab-group-archive.js.map +0 -1
- package/dist/tab-groups.d.ts.map +0 -1
- package/dist/tab-groups.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// Shared extension window policy for MV3 service worker and sidepanel contexts.
|
|
2
|
+
(function installCometWindowPolicy(globalScope) {
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
const TOP_DISPLAY_FULLSCREEN_WINDOW = Object.freeze({
|
|
6
|
+
left: 0,
|
|
7
|
+
top: -1440,
|
|
8
|
+
width: 2560,
|
|
9
|
+
height: 1440,
|
|
10
|
+
state: "normal",
|
|
11
|
+
focused: false,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const LOCKED_FULLSCREEN_DISPLAY_POLICY = Object.freeze({
|
|
15
|
+
mode: "locked",
|
|
16
|
+
topDisplayBounds: Object.freeze({
|
|
17
|
+
left: TOP_DISPLAY_FULLSCREEN_WINDOW.left,
|
|
18
|
+
top: TOP_DISPLAY_FULLSCREEN_WINDOW.top,
|
|
19
|
+
width: TOP_DISPLAY_FULLSCREEN_WINDOW.width,
|
|
20
|
+
height: TOP_DISPLAY_FULLSCREEN_WINDOW.height,
|
|
21
|
+
}),
|
|
22
|
+
windowState: TOP_DISPLAY_FULLSCREEN_WINDOW.state,
|
|
23
|
+
nativeFullscreen: false,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const LOCKED_GROUPS_PER_WINDOW_POLICY = Object.freeze({
|
|
27
|
+
value: 1,
|
|
28
|
+
mode: "hard",
|
|
29
|
+
locked: true,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const managedWindowIds = new Set();
|
|
33
|
+
|
|
34
|
+
function registerManagedWindow(windowId) {
|
|
35
|
+
if (windowId) managedWindowIds.add(windowId);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isManagedWindow(windowId) {
|
|
39
|
+
return Boolean(windowId && managedWindowIds.has(windowId));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function createTopDisplayFullscreenWindow(createData = {}) {
|
|
43
|
+
const win = await chrome.windows.create({
|
|
44
|
+
...createData,
|
|
45
|
+
left: TOP_DISPLAY_FULLSCREEN_WINDOW.left,
|
|
46
|
+
top: TOP_DISPLAY_FULLSCREEN_WINDOW.top,
|
|
47
|
+
width: TOP_DISPLAY_FULLSCREEN_WINDOW.width,
|
|
48
|
+
height: TOP_DISPLAY_FULLSCREEN_WINDOW.height,
|
|
49
|
+
focused: TOP_DISPLAY_FULLSCREEN_WINDOW.focused,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (win?.id) {
|
|
53
|
+
registerManagedWindow(win.id);
|
|
54
|
+
await chrome.windows.update(win.id, { state: "normal" }).catch(() => {});
|
|
55
|
+
await chrome.windows
|
|
56
|
+
.update(win.id, {
|
|
57
|
+
left: TOP_DISPLAY_FULLSCREEN_WINDOW.left,
|
|
58
|
+
top: TOP_DISPLAY_FULLSCREEN_WINDOW.top,
|
|
59
|
+
width: TOP_DISPLAY_FULLSCREEN_WINDOW.width,
|
|
60
|
+
height: TOP_DISPLAY_FULLSCREEN_WINDOW.height,
|
|
61
|
+
focused: TOP_DISPLAY_FULLSCREEN_WINDOW.focused,
|
|
62
|
+
})
|
|
63
|
+
.catch(() => {});
|
|
64
|
+
await chrome.windows.update(win.id, { state: "normal" }).catch(() => {});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return win;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function enforceTopDisplayFullscreenWindow(windowId) {
|
|
71
|
+
if (!windowId) return;
|
|
72
|
+
await chrome.windows.update(windowId, { state: "normal" }).catch(() => {});
|
|
73
|
+
await chrome.windows
|
|
74
|
+
.update(windowId, {
|
|
75
|
+
left: TOP_DISPLAY_FULLSCREEN_WINDOW.left,
|
|
76
|
+
top: TOP_DISPLAY_FULLSCREEN_WINDOW.top,
|
|
77
|
+
width: TOP_DISPLAY_FULLSCREEN_WINDOW.width,
|
|
78
|
+
height: TOP_DISPLAY_FULLSCREEN_WINDOW.height,
|
|
79
|
+
focused: TOP_DISPLAY_FULLSCREEN_WINDOW.focused,
|
|
80
|
+
})
|
|
81
|
+
.catch(() => {});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function moveGroupToDedicatedFullscreenWindow(group) {
|
|
85
|
+
const groupTabs = await chrome.tabs.query({ groupId: group.id });
|
|
86
|
+
if (!groupTabs.length) {
|
|
87
|
+
return { moved: false, groupId: group.id, group };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const win = await createTopDisplayFullscreenWindow({ tabId: groupTabs[0].id });
|
|
91
|
+
if (groupTabs.length > 1) {
|
|
92
|
+
await chrome.tabs.move(
|
|
93
|
+
groupTabs.slice(1).map((tab) => tab.id),
|
|
94
|
+
{ windowId: win.id, index: -1 }
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const regroupedId = await chrome.tabs.group({ tabIds: groupTabs.map((tab) => tab.id) });
|
|
99
|
+
await chrome.tabGroups.update(regroupedId, {
|
|
100
|
+
title: group.title || "",
|
|
101
|
+
color: group.color || "grey",
|
|
102
|
+
collapsed: group.collapsed || false,
|
|
103
|
+
});
|
|
104
|
+
const regrouped = await chrome.tabGroups.get(regroupedId);
|
|
105
|
+
await enforceTopDisplayFullscreenWindow(regrouped.windowId);
|
|
106
|
+
return { moved: true, groupId: regrouped.id, group: regrouped };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function ensureOneGroupPerWindow(changedGroupId = null, options = {}) {
|
|
110
|
+
const groups = await chrome.tabGroups.query({});
|
|
111
|
+
if (changedGroupId && options.markChangedWindowManaged) {
|
|
112
|
+
const changedGroup = groups.find((group) => group.id === changedGroupId);
|
|
113
|
+
if (changedGroup?.windowId) registerManagedWindow(changedGroup.windowId);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const groupsByWindow = new Map();
|
|
117
|
+
for (const group of groups) {
|
|
118
|
+
if (!isManagedWindow(group.windowId)) continue;
|
|
119
|
+
const existing = groupsByWindow.get(group.windowId) || [];
|
|
120
|
+
existing.push(group);
|
|
121
|
+
groupsByWindow.set(group.windowId, existing);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let tracked = null;
|
|
125
|
+
for (const [windowId, windowGroups] of groupsByWindow.entries()) {
|
|
126
|
+
if (windowGroups.length <= LOCKED_GROUPS_PER_WINDOW_POLICY.value) {
|
|
127
|
+
await enforceTopDisplayFullscreenWindow(windowId);
|
|
128
|
+
if (changedGroupId && windowGroups[0]?.id === changedGroupId) {
|
|
129
|
+
tracked = { moved: false, groupId: windowGroups[0].id, group: windowGroups[0] };
|
|
130
|
+
}
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const keepGroup =
|
|
135
|
+
windowGroups.find((group) => group.id !== changedGroupId) || windowGroups[0];
|
|
136
|
+
for (const group of windowGroups) {
|
|
137
|
+
if (group.id === keepGroup.id) continue;
|
|
138
|
+
const result = await moveGroupToDedicatedFullscreenWindow(group);
|
|
139
|
+
if (group.id === changedGroupId) tracked = result;
|
|
140
|
+
}
|
|
141
|
+
await enforceTopDisplayFullscreenWindow(keepGroup.windowId);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (changedGroupId && !tracked) {
|
|
145
|
+
const group = await chrome.tabGroups.get(changedGroupId).catch(() => null);
|
|
146
|
+
if (group) tracked = { moved: false, groupId: group.id, group };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return tracked;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
globalThis.CometWindowPolicy = Object.freeze({
|
|
153
|
+
TOP_DISPLAY_FULLSCREEN_WINDOW,
|
|
154
|
+
LOCKED_FULLSCREEN_DISPLAY_POLICY,
|
|
155
|
+
LOCKED_GROUPS_PER_WINDOW_POLICY,
|
|
156
|
+
createTopDisplayFullscreenWindow,
|
|
157
|
+
enforceTopDisplayFullscreenWindow,
|
|
158
|
+
ensureOneGroupPerWindow,
|
|
159
|
+
isManagedWindow,
|
|
160
|
+
registerManagedWindow,
|
|
161
|
+
});
|
|
162
|
+
})(globalThis);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shawnowen/comet-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.1",
|
|
4
4
|
"description": "MCP Server that gives Claude Code superpowers with Perplexity Comet browser - agentic web browsing, deep research, and real-time monitoring",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -8,12 +8,15 @@
|
|
|
8
8
|
"comet-mcp": "dist/index.js"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
|
-
"dist",
|
|
12
|
-
"
|
|
11
|
+
"dist/**",
|
|
12
|
+
"vendor/**",
|
|
13
|
+
"extension/**",
|
|
13
14
|
"README.md",
|
|
14
|
-
"LICENSE"
|
|
15
|
+
"LICENSE",
|
|
16
|
+
"!**/*.map"
|
|
15
17
|
],
|
|
16
18
|
"scripts": {
|
|
19
|
+
"prebuild": "node -e \"const fs=require('fs'),p=require('path');const src=p.resolve('../scripts');const dst=p.resolve('vendor');fs.mkdirSync(dst,{recursive:true});for(const f of ['lifecycle-mcp-adapter.mjs','lifecycle-metadata.mjs','readiness-report.mjs'])fs.copyFileSync(p.join(src,f),p.join(dst,f));console.log('vendored lifecycle scripts -> vendor/');\"",
|
|
17
20
|
"build": "tsc",
|
|
18
21
|
"test": "vitest run",
|
|
19
22
|
"prepublishOnly": "npm run build",
|
|
@@ -37,11 +40,11 @@
|
|
|
37
40
|
"license": "MIT",
|
|
38
41
|
"repository": {
|
|
39
42
|
"type": "git",
|
|
40
|
-
"url": "git+https://github.com/
|
|
43
|
+
"url": "git+https://github.com/EQUAStart/equa-comet-browser-control.git"
|
|
41
44
|
},
|
|
42
|
-
"homepage": "https://github.com/
|
|
45
|
+
"homepage": "https://github.com/EQUAStart/equa-comet-browser-control#readme",
|
|
43
46
|
"bugs": {
|
|
44
|
-
"url": "https://github.com/
|
|
47
|
+
"url": "https://github.com/EQUAStart/equa-comet-browser-control/issues"
|
|
45
48
|
},
|
|
46
49
|
"engines": {
|
|
47
50
|
"node": ">=18.0.0"
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* MCP lifecycle adapter (Spec 070, S070-T015).
|
|
4
|
+
*
|
|
5
|
+
* Ensures MCP tool calls emit the same lifecycle metadata as the CLI path
|
|
6
|
+
* (cursor-comet.mjs), satisfying AC-3, AC-5, AC-6 parity requirements.
|
|
7
|
+
*
|
|
8
|
+
* MCP servers import this adapter to wrap tool executions with lifecycle
|
|
9
|
+
* events that produce identical fields to the CLI path. Both paths use
|
|
10
|
+
* the shared lifecycle-metadata.mjs module.
|
|
11
|
+
*
|
|
12
|
+
* Usage from comet-mcp or any MCP handler:
|
|
13
|
+
* import { wrapMCPToolCall, createMCPLifecycleEnvelope } from '../scripts/lifecycle-mcp-adapter.mjs';
|
|
14
|
+
*
|
|
15
|
+
* // Option A: wrap an entire tool call
|
|
16
|
+
* const result = await wrapMCPToolCall({ taskThreadId, agentId, toolName }, async (envelope) => {
|
|
17
|
+
* // ... tool execution ...
|
|
18
|
+
* return toolResult;
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* // Option B: manual envelope management
|
|
22
|
+
* const envelope = createMCPLifecycleEnvelope({ taskThreadId });
|
|
23
|
+
* emitLifecycleEvent('start', envelope);
|
|
24
|
+
* // ... tool execution ...
|
|
25
|
+
* transitionEnvelope(envelope, 'completed');
|
|
26
|
+
* emitLifecycleEvent('complete', envelope);
|
|
27
|
+
*
|
|
28
|
+
* @module lifecycle-mcp-adapter
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import {
|
|
32
|
+
createLifecycleEnvelope,
|
|
33
|
+
transitionEnvelope,
|
|
34
|
+
emitLifecycleEvent,
|
|
35
|
+
lifecycleEnvVars,
|
|
36
|
+
validateEnvelope,
|
|
37
|
+
} from "./lifecycle-metadata.mjs";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Create a lifecycle envelope pre-configured for the MCP route.
|
|
41
|
+
* Equivalent to what cursor-comet.mjs creates for CLI, but with route='mcp'.
|
|
42
|
+
*
|
|
43
|
+
* @param {object} opts
|
|
44
|
+
* @param {string} [opts.taskThreadId]
|
|
45
|
+
* @param {string} [opts.agentId]
|
|
46
|
+
* @param {string} [opts.runId]
|
|
47
|
+
* @param {string} [opts.toolName] - MCP tool name (stored as workflowId for traceability)
|
|
48
|
+
* @param {boolean} [opts.fallbackUsed] - True if this is a fallback from failed CLI/HTTP path
|
|
49
|
+
* @returns {object} Lifecycle envelope with route='mcp'
|
|
50
|
+
*/
|
|
51
|
+
export function createMCPLifecycleEnvelope(opts = {}) {
|
|
52
|
+
return createLifecycleEnvelope({
|
|
53
|
+
...opts,
|
|
54
|
+
route: "mcp",
|
|
55
|
+
workflowId: opts.toolName || opts.workflowId,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Wrap an MCP tool call with lifecycle events.
|
|
61
|
+
* Creates an envelope, emits start, runs the handler, and emits complete/fail.
|
|
62
|
+
*
|
|
63
|
+
* The handler receives the envelope and may update it (e.g. set auditSessionId).
|
|
64
|
+
*
|
|
65
|
+
* @param {object} opts - Lifecycle identity options
|
|
66
|
+
* @param {string} [opts.taskThreadId]
|
|
67
|
+
* @param {string} [opts.agentId]
|
|
68
|
+
* @param {string} [opts.runId]
|
|
69
|
+
* @param {string} [opts.toolName]
|
|
70
|
+
* @param {boolean} [opts.fallbackUsed]
|
|
71
|
+
* @param {(envelope: object) => Promise<any>} handler - Tool execution function
|
|
72
|
+
* @returns {Promise<{ result: any, envelope: object }>}
|
|
73
|
+
*/
|
|
74
|
+
export async function wrapMCPToolCall(opts, handler) {
|
|
75
|
+
const envelope = createMCPLifecycleEnvelope(opts);
|
|
76
|
+
emitLifecycleEvent("start", envelope, { persist: true });
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const result = await handler(envelope);
|
|
80
|
+
transitionEnvelope(envelope, "completed");
|
|
81
|
+
emitLifecycleEvent("complete", envelope, { persist: true });
|
|
82
|
+
return { result, envelope };
|
|
83
|
+
} catch (err) {
|
|
84
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
85
|
+
transitionEnvelope(envelope, "failed", { failureReason: reason });
|
|
86
|
+
emitLifecycleEvent("fail", envelope, { persist: true });
|
|
87
|
+
throw err;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Re-export shared primitives for convenience
|
|
92
|
+
export {
|
|
93
|
+
createLifecycleEnvelope,
|
|
94
|
+
transitionEnvelope,
|
|
95
|
+
emitLifecycleEvent,
|
|
96
|
+
lifecycleEnvVars,
|
|
97
|
+
validateEnvelope,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export default {
|
|
101
|
+
createMCPLifecycleEnvelope,
|
|
102
|
+
wrapMCPToolCall,
|
|
103
|
+
};
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Shared lifecycle metadata emitter (Spec 070, S070-T015).
|
|
4
|
+
*
|
|
5
|
+
* Both the CLI path (cursor-comet.mjs) and MCP path (lifecycle-mcp-adapter)
|
|
6
|
+
* use this module to create and emit lifecycle envelopes, ensuring both
|
|
7
|
+
* routes produce equivalent fields per integration-contract.md §1.
|
|
8
|
+
*
|
|
9
|
+
* @module lifecycle-metadata
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { randomUUID } from "crypto";
|
|
13
|
+
import { writeFileSync, mkdirSync, existsSync } from "fs";
|
|
14
|
+
import { join } from "path";
|
|
15
|
+
import { recordTelemetryEvent } from "./readiness-report.mjs";
|
|
16
|
+
|
|
17
|
+
/** Canonical lifecycle fields per integration-contract.md §1.1 / §1.2 */
|
|
18
|
+
const REQUIRED_FIELDS = ["taskThreadId", "runId", "status", "startedAt"];
|
|
19
|
+
const OPTIONAL_FIELDS = [
|
|
20
|
+
"agentId",
|
|
21
|
+
"tabGroupId",
|
|
22
|
+
"auditSessionId",
|
|
23
|
+
"workflowId",
|
|
24
|
+
"completedAt",
|
|
25
|
+
"failureReason",
|
|
26
|
+
"route",
|
|
27
|
+
"fallbackUsed",
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Create a lifecycle envelope with the canonical fields.
|
|
32
|
+
* Missing required fields are auto-filled with sensible defaults.
|
|
33
|
+
*
|
|
34
|
+
* @param {object} opts
|
|
35
|
+
* @param {string} opts.taskThreadId - Task-thread identifier.
|
|
36
|
+
* @param {string} [opts.agentId] - Agent identity.
|
|
37
|
+
* @param {string} [opts.runId] - Run id (auto-generated if omitted).
|
|
38
|
+
* @param {'pending'|'running'|'completed'|'aborted'|'failed'} [opts.status] - Initial status.
|
|
39
|
+
* @param {'mcp'|'cli'|'http'} [opts.route] - Execution channel.
|
|
40
|
+
* @param {boolean} [opts.fallbackUsed] - Whether a fallback path was used.
|
|
41
|
+
* @param {string} [opts.tabGroupId]
|
|
42
|
+
* @param {string} [opts.auditSessionId]
|
|
43
|
+
* @param {string} [opts.workflowId]
|
|
44
|
+
* @param {boolean} [opts.deferred] - If true, start as 'pending' instead of 'running'.
|
|
45
|
+
* @returns {object} Lifecycle envelope
|
|
46
|
+
*/
|
|
47
|
+
export function createLifecycleEnvelope(opts = {}) {
|
|
48
|
+
const now = new Date().toISOString();
|
|
49
|
+
return {
|
|
50
|
+
taskThreadId: opts.taskThreadId || process.env.COMET_TASK_GROUP || "unknown",
|
|
51
|
+
agentId: opts.agentId || process.env.COMET_AGENT_ID || undefined,
|
|
52
|
+
runId: opts.runId || randomUUID(),
|
|
53
|
+
status: opts.deferred ? "pending" : opts.status || "running",
|
|
54
|
+
startedAt: opts.startedAt || now,
|
|
55
|
+
route: opts.route || "cli",
|
|
56
|
+
fallbackUsed: opts.fallbackUsed ?? false,
|
|
57
|
+
tabGroupId: opts.tabGroupId || undefined,
|
|
58
|
+
auditSessionId: opts.auditSessionId || undefined,
|
|
59
|
+
workflowId: opts.workflowId || undefined,
|
|
60
|
+
completedAt: undefined,
|
|
61
|
+
failureReason: undefined,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Transition a lifecycle envelope to a new status, enforcing the
|
|
67
|
+
* transition table from integration-contract.md §2.3.
|
|
68
|
+
*
|
|
69
|
+
* @param {object} envelope - Existing lifecycle envelope.
|
|
70
|
+
* @param {'running'|'completed'|'aborted'|'failed'} newStatus
|
|
71
|
+
* @param {{ failureReason?: string }} [meta]
|
|
72
|
+
* @returns {object} Updated envelope (mutated in place + returned)
|
|
73
|
+
*/
|
|
74
|
+
export function transitionEnvelope(envelope, newStatus, meta = {}) {
|
|
75
|
+
const ALLOWED = {
|
|
76
|
+
pending: new Set(["running", "aborted", "failed"]),
|
|
77
|
+
running: new Set(["completed", "aborted", "failed"]),
|
|
78
|
+
completed: new Set(),
|
|
79
|
+
aborted: new Set(),
|
|
80
|
+
failed: new Set(),
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const allowed = ALLOWED[envelope.status];
|
|
84
|
+
if (!allowed || !allowed.has(newStatus)) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`Invalid lifecycle transition for run ${envelope.runId}: ${envelope.status} → ${newStatus}`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
envelope.status = newStatus;
|
|
91
|
+
if (newStatus === "completed" || newStatus === "aborted" || newStatus === "failed") {
|
|
92
|
+
envelope.completedAt = new Date().toISOString();
|
|
93
|
+
}
|
|
94
|
+
if (meta.failureReason) {
|
|
95
|
+
envelope.failureReason = meta.failureReason;
|
|
96
|
+
}
|
|
97
|
+
return envelope;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Emit a lifecycle event to stderr (human-readable) and optionally persist
|
|
102
|
+
* a JSON artifact under the output audit directory.
|
|
103
|
+
*
|
|
104
|
+
* @param {'start'|'update'|'complete'|'abort'|'fail'} eventType
|
|
105
|
+
* @param {object} envelope - The lifecycle envelope.
|
|
106
|
+
* @param {{ silent?: boolean, outputDir?: string, persist?: boolean }} [opts]
|
|
107
|
+
*/
|
|
108
|
+
export function emitLifecycleEvent(eventType, envelope, opts = {}) {
|
|
109
|
+
const event = {
|
|
110
|
+
event: eventType,
|
|
111
|
+
timestamp: new Date().toISOString(),
|
|
112
|
+
...envelope,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
if (!opts.silent) {
|
|
116
|
+
const routeTag = envelope.route ? `[${envelope.route}]` : "[unknown]";
|
|
117
|
+
const statusTag = envelope.status || "?";
|
|
118
|
+
console.error(
|
|
119
|
+
`[comet-lifecycle] ${routeTag} ${eventType} run=${envelope.runId} task=${envelope.taskThreadId} status=${statusTag}`
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (opts.persist !== false) {
|
|
124
|
+
const outDir = opts.outputDir || getDefaultAuditDir();
|
|
125
|
+
persistEvent(outDir, event);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// S070-T019: feed operational telemetry pipeline
|
|
129
|
+
emitTelemetryForLifecycle(eventType, envelope);
|
|
130
|
+
|
|
131
|
+
return event;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Validate that an envelope has all required fields.
|
|
136
|
+
* Returns { valid: true } or { valid: false, missing: string[] }.
|
|
137
|
+
*/
|
|
138
|
+
export function validateEnvelope(envelope) {
|
|
139
|
+
const missing = REQUIRED_FIELDS.filter((f) => !envelope[f]);
|
|
140
|
+
return missing.length === 0 ? { valid: true, missing: [] } : { valid: false, missing };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Build environment variables map to propagate lifecycle context to child scripts.
|
|
145
|
+
* Child scripts can read these to participate in the same lifecycle scope.
|
|
146
|
+
*/
|
|
147
|
+
export function lifecycleEnvVars(envelope) {
|
|
148
|
+
const vars = {
|
|
149
|
+
COMET_RUN_ID: envelope.runId,
|
|
150
|
+
COMET_RUN_ROUTE: envelope.route || "cli",
|
|
151
|
+
COMET_RUN_STATUS: envelope.status,
|
|
152
|
+
};
|
|
153
|
+
if (envelope.taskThreadId && envelope.taskThreadId !== "unknown") {
|
|
154
|
+
vars.COMET_TASK_GROUP = envelope.taskThreadId;
|
|
155
|
+
}
|
|
156
|
+
if (envelope.agentId) {
|
|
157
|
+
vars.COMET_AGENT_ID = envelope.agentId;
|
|
158
|
+
}
|
|
159
|
+
if (envelope.auditSessionId) {
|
|
160
|
+
vars.COMET_AUDIT_SESSION_ID = envelope.auditSessionId;
|
|
161
|
+
}
|
|
162
|
+
return vars;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Collect lifecycle context from environment variables.
|
|
167
|
+
* Inverse of lifecycleEnvVars; used by child scripts to discover parent lifecycle.
|
|
168
|
+
*/
|
|
169
|
+
export function readLifecycleFromEnv() {
|
|
170
|
+
return {
|
|
171
|
+
runId: process.env.COMET_RUN_ID || null,
|
|
172
|
+
route: process.env.COMET_RUN_ROUTE || null,
|
|
173
|
+
status: process.env.COMET_RUN_STATUS || null,
|
|
174
|
+
taskThreadId: process.env.COMET_TASK_GROUP || null,
|
|
175
|
+
agentId: process.env.COMET_AGENT_ID || null,
|
|
176
|
+
auditSessionId: process.env.COMET_AUDIT_SESSION_ID || null,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ── Internal helpers ───────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Map lifecycle events to operational telemetry (S070-T019).
|
|
184
|
+
* Captures route usage on start, fallback triggers, and completion outcomes.
|
|
185
|
+
*/
|
|
186
|
+
function emitTelemetryForLifecycle(eventType, envelope) {
|
|
187
|
+
try {
|
|
188
|
+
if (eventType === "start") {
|
|
189
|
+
recordTelemetryEvent({
|
|
190
|
+
type: "route",
|
|
191
|
+
route: envelope.route || "cli",
|
|
192
|
+
channel: `comet-${envelope.route || "cli"}`,
|
|
193
|
+
agentId: envelope.agentId,
|
|
194
|
+
taskThreadId: envelope.taskThreadId,
|
|
195
|
+
runId: envelope.runId,
|
|
196
|
+
});
|
|
197
|
+
if (envelope.fallbackUsed) {
|
|
198
|
+
recordTelemetryEvent({
|
|
199
|
+
type: "fallback",
|
|
200
|
+
fallbackUsed: true,
|
|
201
|
+
fallbackReason: `primary-${envelope.route === "cli" ? "mcp" : envelope.route}-unavailable`,
|
|
202
|
+
agentId: envelope.agentId,
|
|
203
|
+
taskThreadId: envelope.taskThreadId,
|
|
204
|
+
runId: envelope.runId,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (eventType === "complete" || eventType === "abort" || eventType === "fail") {
|
|
209
|
+
const outcomeMap = { complete: "completed", abort: "aborted", fail: "failed" };
|
|
210
|
+
const durationMs =
|
|
211
|
+
envelope.startedAt && envelope.completedAt
|
|
212
|
+
? new Date(envelope.completedAt).getTime() - new Date(envelope.startedAt).getTime()
|
|
213
|
+
: undefined;
|
|
214
|
+
recordTelemetryEvent({
|
|
215
|
+
type: "completion",
|
|
216
|
+
outcome: outcomeMap[eventType] || envelope.status,
|
|
217
|
+
runId: envelope.runId,
|
|
218
|
+
taskThreadId: envelope.taskThreadId,
|
|
219
|
+
agentId: envelope.agentId,
|
|
220
|
+
durationMs: durationMs > 0 ? durationMs : undefined,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
} catch {
|
|
224
|
+
// telemetry is best-effort
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function getDefaultAuditDir() {
|
|
229
|
+
const base =
|
|
230
|
+
process.env.COMET_OUTPUT_BASE ||
|
|
231
|
+
join(process.env.HOME || "/tmp", ".claude/comet-browser/output");
|
|
232
|
+
return join(base, "audit");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function persistEvent(dir, event) {
|
|
236
|
+
try {
|
|
237
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
238
|
+
const filename = `lifecycle-${event.event}-${event.runId}-${Date.now()}.json`;
|
|
239
|
+
writeFileSync(join(dir, filename), JSON.stringify(event, null, 2));
|
|
240
|
+
} catch (err) {
|
|
241
|
+
console.error(`[comet-lifecycle] Failed to persist event: ${err.message}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export default {
|
|
246
|
+
createLifecycleEnvelope,
|
|
247
|
+
transitionEnvelope,
|
|
248
|
+
emitLifecycleEvent,
|
|
249
|
+
validateEnvelope,
|
|
250
|
+
lifecycleEnvVars,
|
|
251
|
+
readLifecycleFromEnv,
|
|
252
|
+
};
|