@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.
- package/README.md +12 -1
- package/dist/binding-reaper.d.ts +46 -0
- package/dist/binding-reaper.js +73 -0
- package/dist/http-server.js +121 -0
- package/dist/index.js +310 -6
- package/dist/project-config.d.ts +46 -0
- package/dist/project-config.js +166 -0
- package/dist/tab-groups.d.ts +21 -1
- package/dist/tab-groups.js +184 -0
- package/dist/window-bindings.d.ts +48 -0
- package/dist/window-bindings.js +85 -0
- package/extension/background.js +38 -17
- package/extension/manifest.json +16 -1
- package/extension/perplexity-capability-manifest.json +1181 -0
- package/extension/perplexity-capability-manifest.schema.json +142 -0
- package/extension/session-logic.js +696 -25
- package/extension/session-manager.html +13 -1
- package/extension/sidepanel.css +21 -6
- package/extension/sidepanel.js +598 -68
- package/package.json +1 -1
- package/dist/discovery/capability-entry.d.ts +0 -215
- package/dist/discovery/capability-entry.js +0 -13
- package/dist/discovery/description-template.d.ts +0 -40
- package/dist/discovery/description-template.js +0 -61
- package/dist/discovery/golden-queries.fixture.d.ts +0 -22
- package/dist/discovery/golden-queries.fixture.js +0 -137
- package/dist/discovery/mcp-source.d.ts +0 -38
- package/dist/discovery/mcp-source.js +0 -70
- package/dist/discovery/metadata-completeness.d.ts +0 -48
- package/dist/discovery/metadata-completeness.js +0 -83
- package/dist/discovery/registry.d.ts +0 -35
- package/dist/discovery/registry.js +0 -35
- package/dist/discovery/safety.d.ts +0 -44
- package/dist/discovery/safety.js +0 -59
- package/dist/discovery/schema-validator.d.ts +0 -36
- package/dist/discovery/schema-validator.js +0 -257
- package/dist/discovery/source-error.d.ts +0 -47
- package/dist/discovery/source-error.js +0 -95
- package/dist/discovery/tool-meta.d.ts +0 -41
- package/dist/discovery/tool-meta.js +0 -229
- package/dist/discovery/virtual-tools.d.ts +0 -20
- package/dist/discovery/virtual-tools.js +0 -69
- package/dist/task-thread-aggregator.d.ts +0 -34
- package/dist/task-thread-aggregator.js +0 -480
- package/dist/task-thread-canonical.d.ts +0 -142
- package/dist/task-thread-canonical.js +0 -116
package/dist/tab-groups.js
CHANGED
|
@@ -243,6 +243,190 @@ export class TabGroupsClient {
|
|
|
243
243
|
})()
|
|
244
244
|
`);
|
|
245
245
|
}
|
|
246
|
+
/** List extension-owned descriptions, window labels, archives, and selected metadata. */
|
|
247
|
+
async listExtensionMetadata() {
|
|
248
|
+
return await this.evaluate(`
|
|
249
|
+
(async () => {
|
|
250
|
+
const stored = await chrome.storage.local.get([
|
|
251
|
+
"sessionEntityDescriptions",
|
|
252
|
+
"sidebarSettings",
|
|
253
|
+
"archivedGroups",
|
|
254
|
+
"recentlyClosedGroups",
|
|
255
|
+
"fsSelectedGroups",
|
|
256
|
+
"fsSelectedTabs"
|
|
257
|
+
]);
|
|
258
|
+
const normalizeArchiveWorkflowStatus = (status) => {
|
|
259
|
+
const raw = String(status || "").trim().toLowerCase();
|
|
260
|
+
if (raw === "done") return "done";
|
|
261
|
+
if (raw === "trashed") return "trashed";
|
|
262
|
+
return "pending";
|
|
263
|
+
};
|
|
264
|
+
const settings = stored.sidebarSettings || {};
|
|
265
|
+
const archives = Array.isArray(stored.archivedGroups) ? stored.archivedGroups : [];
|
|
266
|
+
return {
|
|
267
|
+
entityDescriptions: stored.sessionEntityDescriptions || {},
|
|
268
|
+
windowLabels: settings.windowLabels || {},
|
|
269
|
+
archives: archives.map((entry) => ({
|
|
270
|
+
taskThreadId: entry.taskThreadId || entry.id,
|
|
271
|
+
title: entry.title || entry.sessionName || "Untitled",
|
|
272
|
+
sessionName: entry.sessionName || null,
|
|
273
|
+
taskGoal: entry.taskGoal || null,
|
|
274
|
+
description: String(entry.description || entry.note || ""),
|
|
275
|
+
status: normalizeArchiveWorkflowStatus(entry.status),
|
|
276
|
+
rawStatus: entry.status || null,
|
|
277
|
+
tabCount: Array.isArray(entry.urls) ? entry.urls.length : 0,
|
|
278
|
+
urls: Array.isArray(entry.urls)
|
|
279
|
+
? entry.urls.map((tab) => ({
|
|
280
|
+
url: String(tab && tab.url ? tab.url : ""),
|
|
281
|
+
title: String(tab && tab.title ? tab.title : ""),
|
|
282
|
+
description: String(tab && tab.description ? tab.description : "")
|
|
283
|
+
}))
|
|
284
|
+
: [],
|
|
285
|
+
archivedAt: entry.archivedAt || null,
|
|
286
|
+
closedAt: entry.closedAt || null,
|
|
287
|
+
restoredAt: entry.restoredAt || null,
|
|
288
|
+
orchestratorUrl: entry.orchestratorUrl || null,
|
|
289
|
+
locked: !!entry.locked,
|
|
290
|
+
pinned: !!entry.pinned,
|
|
291
|
+
starred: !!entry.starred
|
|
292
|
+
})),
|
|
293
|
+
recentGroups: Array.isArray(stored.recentlyClosedGroups) ? stored.recentlyClosedGroups : [],
|
|
294
|
+
selectedItems: {
|
|
295
|
+
groups: Array.isArray(stored.fsSelectedGroups) ? stored.fsSelectedGroups : [],
|
|
296
|
+
tabs: Array.isArray(stored.fsSelectedTabs) ? stored.fsSelectedTabs : []
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
})()
|
|
300
|
+
`);
|
|
301
|
+
}
|
|
302
|
+
/** Update extension-owned metadata without touching native tab state. */
|
|
303
|
+
async updateExtensionMetadata(options) {
|
|
304
|
+
const payload = JSON.stringify(options || {});
|
|
305
|
+
return await this.evaluate(`
|
|
306
|
+
(async () => {
|
|
307
|
+
const input = ${payload};
|
|
308
|
+
const now = new Date().toISOString();
|
|
309
|
+
const entityType = String(input.entityType || "").toLowerCase();
|
|
310
|
+
const taskThreadId = String(input.taskThreadId || input.entityId || input.id || "");
|
|
311
|
+
const makeMetadataAudit = (target) => ({
|
|
312
|
+
intent: String(input.intent || input.action || "update_metadata"),
|
|
313
|
+
target,
|
|
314
|
+
activeWindow: {
|
|
315
|
+
windowId: String(input.activeWindowId || input.activeWindow?.windowId || "extension_service_worker")
|
|
316
|
+
},
|
|
317
|
+
policyTier: String(input.policyTier || "prime"),
|
|
318
|
+
status: "success",
|
|
319
|
+
createdAt: now
|
|
320
|
+
});
|
|
321
|
+
const coerceArchiveWorkflowStatusForWrite = (status) => {
|
|
322
|
+
const raw = String(status || "").trim().toLowerCase();
|
|
323
|
+
if (raw === "saved" || raw === "archived") return "pending";
|
|
324
|
+
if (raw === "pending" || raw === "done" || raw === "trashed") return raw;
|
|
325
|
+
throw new Error("Invalid status: " + status);
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
if (entityType === "window" || entityType === "window-label") {
|
|
329
|
+
const windowId = String(input.windowId || input.entityId || input.id || "");
|
|
330
|
+
if (!windowId) throw new Error("windowId is required for window metadata updates");
|
|
331
|
+
const stored = await chrome.storage.local.get("sidebarSettings");
|
|
332
|
+
const settings = stored.sidebarSettings || {};
|
|
333
|
+
settings.windowLabels = settings.windowLabels || {};
|
|
334
|
+
const existingWindowLabel = settings.windowLabels[windowId] || {};
|
|
335
|
+
const nextWindowLabel = { ...existingWindowLabel };
|
|
336
|
+
if (input.label !== undefined || input.title !== undefined) {
|
|
337
|
+
nextWindowLabel.label = String(input.label ?? input.title);
|
|
338
|
+
}
|
|
339
|
+
if (typeof nextWindowLabel.label !== "string") {
|
|
340
|
+
nextWindowLabel.label = String(nextWindowLabel.label ?? "");
|
|
341
|
+
}
|
|
342
|
+
settings.windowLabels[windowId] = {
|
|
343
|
+
...nextWindowLabel,
|
|
344
|
+
source: "mcp_metadata",
|
|
345
|
+
updatedAt: now
|
|
346
|
+
};
|
|
347
|
+
await chrome.storage.local.set({ sidebarSettings: settings });
|
|
348
|
+
return {
|
|
349
|
+
updated: true,
|
|
350
|
+
entityType: "window-label",
|
|
351
|
+
windowId,
|
|
352
|
+
audit: makeMetadataAudit({ entityType: "window-label", windowId })
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (entityType === "archive" || entityType === "task-thread") {
|
|
357
|
+
if (!taskThreadId) throw new Error("taskThreadId is required for archive metadata updates");
|
|
358
|
+
const stored = await chrome.storage.local.get("archivedGroups");
|
|
359
|
+
const archive = Array.isArray(stored.archivedGroups) ? stored.archivedGroups : [];
|
|
360
|
+
const entry = archive.find((item) => String(item.taskThreadId || item.id) === taskThreadId);
|
|
361
|
+
if (!entry) throw new Error("Archive entry not found: " + taskThreadId);
|
|
362
|
+
let changed = false;
|
|
363
|
+
if (input.title !== undefined) {
|
|
364
|
+
entry.title = String(input.title);
|
|
365
|
+
changed = true;
|
|
366
|
+
}
|
|
367
|
+
if (input.description !== undefined) {
|
|
368
|
+
entry.description = String(input.description);
|
|
369
|
+
changed = true;
|
|
370
|
+
}
|
|
371
|
+
if (input.status !== undefined) {
|
|
372
|
+
if (entry.locked) throw new Error("Cannot change status of a locked entry");
|
|
373
|
+
entry.status = coerceArchiveWorkflowStatusForWrite(input.status);
|
|
374
|
+
entry.statusUpdatedAt = now;
|
|
375
|
+
changed = true;
|
|
376
|
+
}
|
|
377
|
+
if (changed) entry.updatedAt = now;
|
|
378
|
+
await chrome.storage.local.set({ archivedGroups: archive });
|
|
379
|
+
return {
|
|
380
|
+
updated: true,
|
|
381
|
+
entityType: "archive",
|
|
382
|
+
taskThreadId,
|
|
383
|
+
audit: makeMetadataAudit({ entityType: "archive", taskThreadId })
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (entityType === "archive-tab") {
|
|
388
|
+
if (!taskThreadId) throw new Error("taskThreadId is required for archive tab metadata updates");
|
|
389
|
+
const tabIndex = Number(input.tabIndex);
|
|
390
|
+
if (!Number.isInteger(tabIndex) || tabIndex < 0) throw new Error("tabIndex is required");
|
|
391
|
+
const stored = await chrome.storage.local.get("archivedGroups");
|
|
392
|
+
const archive = Array.isArray(stored.archivedGroups) ? stored.archivedGroups : [];
|
|
393
|
+
const entry = archive.find((item) => String(item.taskThreadId || item.id) === taskThreadId);
|
|
394
|
+
if (!entry) throw new Error("Archive entry not found: " + taskThreadId);
|
|
395
|
+
entry.urls = Array.isArray(entry.urls) ? entry.urls : [];
|
|
396
|
+
if (!entry.urls[tabIndex]) throw new Error("Archive tab not found: " + tabIndex);
|
|
397
|
+
if (input.title !== undefined) entry.urls[tabIndex].title = String(input.title);
|
|
398
|
+
if (input.description !== undefined) entry.urls[tabIndex].description = String(input.description);
|
|
399
|
+
await chrome.storage.local.set({ archivedGroups: archive });
|
|
400
|
+
return {
|
|
401
|
+
updated: true,
|
|
402
|
+
entityType: "archive-tab",
|
|
403
|
+
taskThreadId,
|
|
404
|
+
tabIndex,
|
|
405
|
+
audit: makeMetadataAudit({ entityType: "archive-tab", taskThreadId, tabIndex })
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const entityId = String(input.entityId || input.id || "");
|
|
410
|
+
if (!entityType || !entityId) throw new Error("entityType and entityId are required");
|
|
411
|
+
const stored = await chrome.storage.local.get("sessionEntityDescriptions");
|
|
412
|
+
const descriptions = stored.sessionEntityDescriptions || {};
|
|
413
|
+
const key = entityType + ":" + entityId;
|
|
414
|
+
descriptions[key] = {
|
|
415
|
+
entityType,
|
|
416
|
+
entityId,
|
|
417
|
+
description: String(input.description ?? ""),
|
|
418
|
+
updatedAt: now
|
|
419
|
+
};
|
|
420
|
+
await chrome.storage.local.set({ sessionEntityDescriptions: descriptions });
|
|
421
|
+
return {
|
|
422
|
+
updated: true,
|
|
423
|
+
entityType,
|
|
424
|
+
entityId,
|
|
425
|
+
audit: makeMetadataAudit({ entityType, entityId })
|
|
426
|
+
};
|
|
427
|
+
})()
|
|
428
|
+
`);
|
|
429
|
+
}
|
|
246
430
|
/** Create a single tab and return its ID. Used by ETT restore_group. */
|
|
247
431
|
async createTab(url, active = false) {
|
|
248
432
|
return await this.evaluate(`
|
|
@@ -41,6 +41,13 @@ export interface CodexWindowBinding extends CodexSessionIdentity {
|
|
|
41
41
|
status: CodexBindingStatus;
|
|
42
42
|
createdAt: string;
|
|
43
43
|
updatedAt: string;
|
|
44
|
+
/**
|
|
45
|
+
* ISO 8601 timestamp first recorded when a reap cycle found this binding's
|
|
46
|
+
* window absent from the live CDP set. Cleared when the window reappears.
|
|
47
|
+
* Drives the TTL grace period in {@link CodexWindowBindingStore.reapExpiredBindings}.
|
|
48
|
+
* Optional/absent on pre-existing records (backward-compatible).
|
|
49
|
+
*/
|
|
50
|
+
missingSince?: string;
|
|
44
51
|
}
|
|
45
52
|
export interface RunBindingIndexEntry {
|
|
46
53
|
runId: string;
|
|
@@ -72,6 +79,35 @@ export interface BindingMutationOptions {
|
|
|
72
79
|
reason?: string;
|
|
73
80
|
audit?: boolean;
|
|
74
81
|
}
|
|
82
|
+
/** Options for {@link CodexWindowBindingStore.reapExpiredBindings}. */
|
|
83
|
+
export interface ReapOptions {
|
|
84
|
+
/** Distinct windowIds currently live in the browser (CDP). */
|
|
85
|
+
liveWindowIds: Iterable<number>;
|
|
86
|
+
/** Grace period in ms a binding may stay missing before it is reaped. */
|
|
87
|
+
ttlMs: number;
|
|
88
|
+
/** Injectable clock (ms epoch). Defaults to Date.now(). */
|
|
89
|
+
now?: number;
|
|
90
|
+
/**
|
|
91
|
+
* Archive callback invoked BEFORE a binding is deleted (e.g. recovery
|
|
92
|
+
* snapshot + ORPHAN_REAPED alert). If it throws, the binding is NOT deleted
|
|
93
|
+
* that cycle (fail-safe).
|
|
94
|
+
*/
|
|
95
|
+
archive?: (binding: CodexWindowBinding) => Promise<void>;
|
|
96
|
+
/**
|
|
97
|
+
* Predicate for profile-owned bindings the auto-reaper must skip.
|
|
98
|
+
* Default: profileOwner is set and not "agent".
|
|
99
|
+
*/
|
|
100
|
+
isProfileOwned?: (binding: CodexWindowBinding) => boolean;
|
|
101
|
+
}
|
|
102
|
+
/** Per-cycle counts returned by {@link CodexWindowBindingStore.reapExpiredBindings}. */
|
|
103
|
+
export interface ReapCycleResult {
|
|
104
|
+
evaluated: number;
|
|
105
|
+
newlyMissing: number;
|
|
106
|
+
retainedLive: number;
|
|
107
|
+
reaped: number;
|
|
108
|
+
skippedOwned: number;
|
|
109
|
+
reapedBindingIds: string[];
|
|
110
|
+
}
|
|
75
111
|
export declare class WindowBindingConflictError extends Error {
|
|
76
112
|
readonly conflicts: CodexWindowBinding[];
|
|
77
113
|
constructor(message: string, conflicts: CodexWindowBinding[]);
|
|
@@ -105,6 +141,18 @@ export declare class CodexWindowBindingStore {
|
|
|
105
141
|
transitionByRunId(runId: string, status: Exclude<CodexBindingStatus, "conflict">): Promise<CodexWindowBinding | null>;
|
|
106
142
|
classifyConflicts(): Promise<CodexWindowBinding[]>;
|
|
107
143
|
markStaleForMissingWindows(liveWindowIds: Iterable<number>): Promise<CodexWindowBinding[]>;
|
|
144
|
+
/**
|
|
145
|
+
* Reconcile every binding against the live window set and reap (delete) those
|
|
146
|
+
* whose window has been missing for at least {@link ReapOptions.ttlMs}.
|
|
147
|
+
*
|
|
148
|
+
* - Window live → clear `missingSince`, reactivate if the reaper marked it stale, retain.
|
|
149
|
+
* - Window missing, no `missingSince` → record `missingSince=now`, mark stale, retain (grace starts).
|
|
150
|
+
* - Window missing ≥ TTL, profile-owned → skip.
|
|
151
|
+
* - Window missing ≥ TTL, not owned → `archive()` then delete.
|
|
152
|
+
*
|
|
153
|
+
* Atomic + file-locked + idempotent. A no-op when nothing needs changing.
|
|
154
|
+
*/
|
|
155
|
+
reapExpiredBindings(opts: ReapOptions): Promise<ReapCycleResult>;
|
|
108
156
|
private assertNoActiveConflicts;
|
|
109
157
|
private findActiveConflicts;
|
|
110
158
|
}
|
package/dist/window-bindings.js
CHANGED
|
@@ -443,6 +443,91 @@ export class CodexWindowBindingStore {
|
|
|
443
443
|
return stale;
|
|
444
444
|
});
|
|
445
445
|
}
|
|
446
|
+
/**
|
|
447
|
+
* Reconcile every binding against the live window set and reap (delete) those
|
|
448
|
+
* whose window has been missing for at least {@link ReapOptions.ttlMs}.
|
|
449
|
+
*
|
|
450
|
+
* - Window live → clear `missingSince`, reactivate if the reaper marked it stale, retain.
|
|
451
|
+
* - Window missing, no `missingSince` → record `missingSince=now`, mark stale, retain (grace starts).
|
|
452
|
+
* - Window missing ≥ TTL, profile-owned → skip.
|
|
453
|
+
* - Window missing ≥ TTL, not owned → `archive()` then delete.
|
|
454
|
+
*
|
|
455
|
+
* Atomic + file-locked + idempotent. A no-op when nothing needs changing.
|
|
456
|
+
*/
|
|
457
|
+
async reapExpiredBindings(opts) {
|
|
458
|
+
const live = new Set(opts.liveWindowIds);
|
|
459
|
+
const now = opts.now ?? Date.now();
|
|
460
|
+
const ttlMs = opts.ttlMs;
|
|
461
|
+
const isOwned = opts.isProfileOwned ??
|
|
462
|
+
((b) => b.profileOwner != null && b.profileOwner !== "agent");
|
|
463
|
+
const result = {
|
|
464
|
+
evaluated: 0,
|
|
465
|
+
newlyMissing: 0,
|
|
466
|
+
retainedLive: 0,
|
|
467
|
+
reaped: 0,
|
|
468
|
+
skippedOwned: 0,
|
|
469
|
+
reapedBindingIds: [],
|
|
470
|
+
};
|
|
471
|
+
return withFileLock(this.lockDir, async () => {
|
|
472
|
+
const snapshot = await readSnapshot(this.file);
|
|
473
|
+
let mutated = false;
|
|
474
|
+
const reapCandidates = [];
|
|
475
|
+
for (const binding of Object.values(snapshot.bindings)) {
|
|
476
|
+
result.evaluated += 1;
|
|
477
|
+
if (live.has(binding.windowId)) {
|
|
478
|
+
if (binding.missingSince != null) {
|
|
479
|
+
delete binding.missingSince;
|
|
480
|
+
if (binding.status === "stale") {
|
|
481
|
+
binding.status = "active";
|
|
482
|
+
}
|
|
483
|
+
binding.updatedAt = nowIso();
|
|
484
|
+
mutated = true;
|
|
485
|
+
}
|
|
486
|
+
result.retainedLive += 1;
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
// Window is missing from the live set.
|
|
490
|
+
if (binding.missingSince == null) {
|
|
491
|
+
binding.missingSince = new Date(now).toISOString();
|
|
492
|
+
binding.status = "stale";
|
|
493
|
+
binding.updatedAt = nowIso();
|
|
494
|
+
mutated = true;
|
|
495
|
+
result.newlyMissing += 1;
|
|
496
|
+
continue; // retained this cycle; grace period starts
|
|
497
|
+
}
|
|
498
|
+
const missingForMs = now - Date.parse(binding.missingSince);
|
|
499
|
+
if (!Number.isFinite(missingForMs) || missingForMs < ttlMs) {
|
|
500
|
+
continue; // still within grace (or unparseable) → retain
|
|
501
|
+
}
|
|
502
|
+
if (isOwned(binding)) {
|
|
503
|
+
result.skippedOwned += 1;
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
reapCandidates.push(binding);
|
|
507
|
+
}
|
|
508
|
+
for (const binding of reapCandidates) {
|
|
509
|
+
if (opts.archive) {
|
|
510
|
+
try {
|
|
511
|
+
await opts.archive(binding);
|
|
512
|
+
}
|
|
513
|
+
catch {
|
|
514
|
+
// Archive failed — do NOT delete (fail-safe, recoverable next cycle).
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
delete snapshot.bindings[binding.bindingId];
|
|
519
|
+
result.reaped += 1;
|
|
520
|
+
result.reapedBindingIds.push(binding.bindingId);
|
|
521
|
+
mutated = true;
|
|
522
|
+
}
|
|
523
|
+
if (mutated) {
|
|
524
|
+
snapshot.updatedAt = nowIso();
|
|
525
|
+
snapshot.runBindingIndex = rebuildRunBindingIndex(snapshot.bindings);
|
|
526
|
+
await writeSnapshot(this.file, this.dir, snapshot);
|
|
527
|
+
}
|
|
528
|
+
return result;
|
|
529
|
+
});
|
|
530
|
+
}
|
|
446
531
|
assertNoActiveConflicts(snapshot) {
|
|
447
532
|
const conflicts = this.findActiveConflicts(snapshot);
|
|
448
533
|
if (conflicts.length > 0) {
|
package/extension/background.js
CHANGED
|
@@ -486,7 +486,7 @@ chrome.action.onClicked.addListener(async (tab) => {
|
|
|
486
486
|
})),
|
|
487
487
|
archivedAt: new Date().toISOString(),
|
|
488
488
|
restoredAt: null,
|
|
489
|
-
status: "
|
|
489
|
+
status: "pending",
|
|
490
490
|
// Spec 037: Orchestrator session data
|
|
491
491
|
orchestratorUrl: sessionData?.orchestratorUrl || null,
|
|
492
492
|
taskGoal: sessionData?.taskGoal || null,
|
|
@@ -594,6 +594,27 @@ function normalizeInstructionText(value) {
|
|
|
594
594
|
return value === undefined || value === null ? "" : String(value).trim();
|
|
595
595
|
}
|
|
596
596
|
|
|
597
|
+
function normalizeArchiveWorkflowStatus(status) {
|
|
598
|
+
if (globalThis.SessionLogic?.normalizeArchiveWorkflowStatus) {
|
|
599
|
+
return globalThis.SessionLogic.normalizeArchiveWorkflowStatus(status);
|
|
600
|
+
}
|
|
601
|
+
const raw = String(status || "")
|
|
602
|
+
.trim()
|
|
603
|
+
.toLowerCase();
|
|
604
|
+
if (raw === "done") return "done";
|
|
605
|
+
if (raw === "trashed") return "trashed";
|
|
606
|
+
return "pending";
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function coerceArchiveWorkflowStatusForWrite(status) {
|
|
610
|
+
const raw = String(status || "")
|
|
611
|
+
.trim()
|
|
612
|
+
.toLowerCase();
|
|
613
|
+
if (raw === "saved" || raw === "archived") return "pending";
|
|
614
|
+
if (raw === "pending" || raw === "done" || raw === "trashed") return raw;
|
|
615
|
+
throw new Error(`Invalid status: ${status}`);
|
|
616
|
+
}
|
|
617
|
+
|
|
597
618
|
function hasOwn(record, key) {
|
|
598
619
|
return !!record && Object.prototype.hasOwnProperty.call(record, key);
|
|
599
620
|
}
|
|
@@ -666,6 +687,7 @@ function normalizeArchiveEntry(entry) {
|
|
|
666
687
|
normalized.urls = Array.isArray(normalized.urls)
|
|
667
688
|
? normalized.urls.map((tab) => normalizeArchiveTab(tab, normalized.description))
|
|
668
689
|
: [];
|
|
690
|
+
normalized.status = normalizeArchiveWorkflowStatus(normalized.status);
|
|
669
691
|
return normalized;
|
|
670
692
|
}
|
|
671
693
|
|
|
@@ -743,7 +765,7 @@ async function buildArchiveEntryFromLiveGroup(groupId, title, snapshotOnly = fal
|
|
|
743
765
|
})),
|
|
744
766
|
archivedAt: new Date().toISOString(),
|
|
745
767
|
restoredAt: null,
|
|
746
|
-
status: "
|
|
768
|
+
status: "pending",
|
|
747
769
|
sourceWindowId: windowId,
|
|
748
770
|
sourceDisplayId,
|
|
749
771
|
starred: false,
|
|
@@ -912,7 +934,7 @@ registerHandler("restoreGroup", async ({ taskThreadId, target = "newWindow", sou
|
|
|
912
934
|
// Update archive entry status
|
|
913
935
|
archive[idx] = {
|
|
914
936
|
...entry,
|
|
915
|
-
status: "
|
|
937
|
+
status: "pending",
|
|
916
938
|
restoredAt: new Date().toISOString(),
|
|
917
939
|
};
|
|
918
940
|
await saveArchive(archive);
|
|
@@ -1056,15 +1078,14 @@ registerHandler("updateArchiveDescription", async ({ taskThreadId, description }
|
|
|
1056
1078
|
return { description: entry.description };
|
|
1057
1079
|
});
|
|
1058
1080
|
|
|
1059
|
-
// T052: Update status (
|
|
1081
|
+
// T052: Update workflow status (pending, done, trashed; legacy aliases saved/archived)
|
|
1060
1082
|
registerHandler("updateArchiveStatus", async ({ taskThreadId, status }) => {
|
|
1061
|
-
const
|
|
1062
|
-
if (!valid.includes(status)) throw new Error(`Invalid status: ${status}`);
|
|
1083
|
+
const nextStatus = coerceArchiveWorkflowStatusForWrite(status);
|
|
1063
1084
|
const archive = await loadArchive();
|
|
1064
1085
|
const entry = archive.find((e) => e.taskThreadId === taskThreadId);
|
|
1065
1086
|
if (!entry) throw new Error(`Not found: ${taskThreadId}`);
|
|
1066
1087
|
if (entry.locked) throw new Error("Cannot change status of a locked entry");
|
|
1067
|
-
entry.status =
|
|
1088
|
+
entry.status = nextStatus;
|
|
1068
1089
|
await saveArchive(archive);
|
|
1069
1090
|
return { status: entry.status };
|
|
1070
1091
|
});
|
|
@@ -1218,7 +1239,7 @@ registerHandler("saveAllTabs", async () => {
|
|
|
1218
1239
|
}),
|
|
1219
1240
|
archivedAt: new Date().toISOString(),
|
|
1220
1241
|
restoredAt: null,
|
|
1221
|
-
status: "
|
|
1242
|
+
status: "pending",
|
|
1222
1243
|
};
|
|
1223
1244
|
archive.unshift(entry);
|
|
1224
1245
|
archivedCount++;
|
|
@@ -1243,7 +1264,7 @@ registerHandler("saveAllTabs", async () => {
|
|
|
1243
1264
|
})),
|
|
1244
1265
|
archivedAt: new Date().toISOString(),
|
|
1245
1266
|
restoredAt: null,
|
|
1246
|
-
status: "
|
|
1267
|
+
status: "pending",
|
|
1247
1268
|
};
|
|
1248
1269
|
archive.unshift(entry);
|
|
1249
1270
|
archivedCount++;
|
|
@@ -1386,7 +1407,7 @@ registerHandler("importUrls", async ({ groups }) => {
|
|
|
1386
1407
|
})),
|
|
1387
1408
|
archivedAt: new Date().toISOString(),
|
|
1388
1409
|
restoredAt: null,
|
|
1389
|
-
status: "
|
|
1410
|
+
status: "pending",
|
|
1390
1411
|
};
|
|
1391
1412
|
archive.unshift(entry);
|
|
1392
1413
|
imported += entry.urls.length;
|
|
@@ -1767,7 +1788,7 @@ chrome.contextMenus.onClicked.addListener(async (info, tab) => {
|
|
|
1767
1788
|
urls: [{ url: tab.url || "", title: tab.title || "" }],
|
|
1768
1789
|
archivedAt: new Date().toISOString(),
|
|
1769
1790
|
restoredAt: null,
|
|
1770
|
-
status: "
|
|
1791
|
+
status: "pending",
|
|
1771
1792
|
};
|
|
1772
1793
|
// If tab is in a group, use the group name
|
|
1773
1794
|
if (tab.groupId && tab.groupId !== -1) {
|
|
@@ -1817,7 +1838,7 @@ chrome.contextMenus.onClicked.addListener(async (info, tab) => {
|
|
|
1817
1838
|
urls: tabs.map((t) => ({ url: t.url || "", title: t.title || "" })),
|
|
1818
1839
|
archivedAt: new Date().toISOString(),
|
|
1819
1840
|
restoredAt: null,
|
|
1820
|
-
status: "
|
|
1841
|
+
status: "pending",
|
|
1821
1842
|
};
|
|
1822
1843
|
archive.unshift(entry);
|
|
1823
1844
|
}
|
|
@@ -1856,7 +1877,7 @@ chrome.contextMenus.onClicked.addListener(async (info, tab) => {
|
|
|
1856
1877
|
urls: eligible.map((t) => ({ url: t.url || "", title: t.title || "" })),
|
|
1857
1878
|
archivedAt: new Date().toISOString(),
|
|
1858
1879
|
restoredAt: null,
|
|
1859
|
-
status: "
|
|
1880
|
+
status: "pending",
|
|
1860
1881
|
};
|
|
1861
1882
|
|
|
1862
1883
|
const archive = await loadArchive();
|
|
@@ -1883,7 +1904,7 @@ chrome.contextMenus.onClicked.addListener(async (info, tab) => {
|
|
|
1883
1904
|
urls: eligible.map((t) => ({ url: t.url || "", title: t.title || "" })),
|
|
1884
1905
|
archivedAt: new Date().toISOString(),
|
|
1885
1906
|
restoredAt: null,
|
|
1886
|
-
status: "
|
|
1907
|
+
status: "pending",
|
|
1887
1908
|
};
|
|
1888
1909
|
|
|
1889
1910
|
const archive = await loadArchive();
|
|
@@ -1921,7 +1942,7 @@ chrome.contextMenus.onClicked.addListener(async (info, tab) => {
|
|
|
1921
1942
|
urls: toArchive.map((t) => ({ url: t.url || "", title: t.title || "" })),
|
|
1922
1943
|
archivedAt: new Date().toISOString(),
|
|
1923
1944
|
restoredAt: null,
|
|
1924
|
-
status: "
|
|
1945
|
+
status: "pending",
|
|
1925
1946
|
};
|
|
1926
1947
|
|
|
1927
1948
|
const archive = await loadArchive();
|
|
@@ -1950,7 +1971,7 @@ chrome.contextMenus.onClicked.addListener(async (info, tab) => {
|
|
|
1950
1971
|
urls: toArchive.map((t) => ({ url: t.url || "", title: t.title || "" })),
|
|
1951
1972
|
archivedAt: new Date().toISOString(),
|
|
1952
1973
|
restoredAt: null,
|
|
1953
|
-
status: "
|
|
1974
|
+
status: "pending",
|
|
1954
1975
|
};
|
|
1955
1976
|
|
|
1956
1977
|
const archive = await loadArchive();
|
|
@@ -1979,7 +2000,7 @@ chrome.contextMenus.onClicked.addListener(async (info, tab) => {
|
|
|
1979
2000
|
urls: toArchive.map((t) => ({ url: t.url || "", title: t.title || "" })),
|
|
1980
2001
|
archivedAt: new Date().toISOString(),
|
|
1981
2002
|
restoredAt: null,
|
|
1982
|
-
status: "
|
|
2003
|
+
status: "pending",
|
|
1983
2004
|
};
|
|
1984
2005
|
|
|
1985
2006
|
const archive = await loadArchive();
|
package/extension/manifest.json
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
"permissions": [
|
|
7
7
|
"tabGroups",
|
|
8
8
|
"tabs",
|
|
9
|
+
"activeTab",
|
|
9
10
|
"alarms",
|
|
10
11
|
"sidePanel",
|
|
11
12
|
"sessions",
|
|
@@ -13,9 +14,14 @@
|
|
|
13
14
|
"contextMenus",
|
|
14
15
|
"system.display",
|
|
15
16
|
"scripting",
|
|
16
|
-
"bookmarks"
|
|
17
|
+
"bookmarks",
|
|
18
|
+
"debugger",
|
|
19
|
+
"webNavigation"
|
|
17
20
|
],
|
|
18
21
|
"host_permissions": [
|
|
22
|
+
"https://perplexity.ai/*",
|
|
23
|
+
"https://*.perplexity.ai/*",
|
|
24
|
+
"https://comet.perplexity.ai/*",
|
|
19
25
|
"http://localhost/*",
|
|
20
26
|
"http://127.0.0.1/*",
|
|
21
27
|
"http://localhost:11434/*",
|
|
@@ -27,6 +33,15 @@
|
|
|
27
33
|
"128": "icons/icon128.png"
|
|
28
34
|
},
|
|
29
35
|
"action": {},
|
|
36
|
+
"commands": {
|
|
37
|
+
"_execute_action": {
|
|
38
|
+
"suggested_key": {
|
|
39
|
+
"default": "Ctrl+Shift+Y",
|
|
40
|
+
"mac": "Command+Shift+Y"
|
|
41
|
+
},
|
|
42
|
+
"description": "Open Equabotz Browser Agents"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
30
45
|
"background": {
|
|
31
46
|
"service_worker": "background.js"
|
|
32
47
|
},
|