@commandgarden/cli 1.3.0 → 1.4.0

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/dist/main.js CHANGED
@@ -12201,12 +12201,12 @@ var {
12201
12201
  } = import_index.default;
12202
12202
 
12203
12203
  // src/main.ts
12204
- import { join as join4, dirname as dirname2, resolve } from "path";
12204
+ import { join as join4, dirname as dirname3, resolve } from "path";
12205
12205
  import { readFileSync as readFileSync8 } from "fs";
12206
12206
  import { homedir } from "os";
12207
12207
  import { fileURLToPath } from "url";
12208
12208
 
12209
- // ../shared/dist/capabilities.js
12209
+ // node_modules/@commandgarden/shared/dist/capabilities.js
12210
12210
  var CAPABILITIES = [
12211
12211
  "navigate",
12212
12212
  "cookie_read",
@@ -16268,7 +16268,7 @@ var coerce = {
16268
16268
  };
16269
16269
  var NEVER = INVALID;
16270
16270
 
16271
- // ../shared/dist/pipeline.js
16271
+ // node_modules/@commandgarden/shared/dist/pipeline.js
16272
16272
  var STEP_CAPABILITY_MAP = {
16273
16273
  navigate: "navigate",
16274
16274
  wait: "navigate",
@@ -16361,7 +16361,7 @@ var pipelineStepSchema = external_exports.discriminatedUnion("step", [
16361
16361
  jsEvaluateStepSchema
16362
16362
  ]);
16363
16363
 
16364
- // ../shared/dist/connector.js
16364
+ // node_modules/@commandgarden/shared/dist/connector.js
16365
16365
  var capabilityEnum = external_exports.enum(CAPABILITIES);
16366
16366
  var connectorArgSchema = external_exports.object({
16367
16367
  name: external_exports.string(),
@@ -16388,7 +16388,7 @@ var connectorSchema = external_exports.object({
16388
16388
  pipeline: external_exports.array(pipelineStepSchema).min(1)
16389
16389
  });
16390
16390
 
16391
- // ../shared/dist/loader.js
16391
+ // node_modules/@commandgarden/shared/dist/loader.js
16392
16392
  var import_yaml = __toESM(require_dist(), 1);
16393
16393
  function parseConnectorYaml(yamlContent) {
16394
16394
  let parsed;
@@ -16459,10 +16459,10 @@ function validateConnectorSemantics(connector) {
16459
16459
  return errors;
16460
16460
  }
16461
16461
 
16462
- // ../shared/dist/events.js
16462
+ // node_modules/@commandgarden/shared/dist/events.js
16463
16463
  import { randomUUID } from "crypto";
16464
16464
 
16465
- // ../shared/dist/daemon-client.js
16465
+ // node_modules/@commandgarden/shared/dist/daemon-client.js
16466
16466
  import { readFileSync } from "fs";
16467
16467
  function readToken(tokenPath) {
16468
16468
  try {
@@ -17405,9 +17405,40 @@ async function executeDown(cgHome) {
17405
17405
  return lines.join("\n");
17406
17406
  }
17407
17407
 
17408
+ // src/commands/extension-cmd.ts
17409
+ import { dirname as dirname2 } from "path";
17410
+ import { spawn as spawn3 } from "child_process";
17411
+ function executeExtensionSetup(baseDir) {
17412
+ const manifestPath = resolveScript(
17413
+ baseDir,
17414
+ "../../chrome/dist/manifest.json",
17415
+ "@commandgarden/chrome",
17416
+ "dist/manifest.json"
17417
+ );
17418
+ const extensionDir = dirname2(manifestPath);
17419
+ const [command, args] = process.platform === "darwin" ? ["open", ["chrome://extensions"]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", "chrome://extensions"]] : ["xdg-open", ["chrome://extensions"]];
17420
+ const child = spawn3(command, args, { stdio: "ignore", detached: true });
17421
+ child.on("error", () => {
17422
+ });
17423
+ child.unref();
17424
+ return [
17425
+ "",
17426
+ "Chrome extension path:",
17427
+ ` ${extensionDir}`,
17428
+ "",
17429
+ "To install:",
17430
+ ' 1. Enable "Developer mode" (top-right toggle)',
17431
+ ' 2. Click "Load unpacked"',
17432
+ " 3. Select the path above",
17433
+ "",
17434
+ "Opening chrome://extensions...",
17435
+ ""
17436
+ ].join("\n");
17437
+ }
17438
+
17408
17439
  // src/main.ts
17409
17440
  var __filename = fileURLToPath(import.meta.url);
17410
- var __dirname = dirname2(__filename);
17441
+ var __dirname = dirname3(__filename);
17411
17442
  var CLI_VERSION = (() => {
17412
17443
  try {
17413
17444
  const pkg = JSON.parse(readFileSync8(resolve(__dirname, "..", "package.json"), "utf-8"));
@@ -17511,6 +17542,10 @@ gui.command("stop").description("Stop the GUI").action(() => {
17511
17542
  gui.command("status").description("Check GUI status").action(() => {
17512
17543
  console.log(executeGuiStatus(CG_HOME));
17513
17544
  });
17545
+ var extension = program2.command("extension").description("Manage the Chrome extension");
17546
+ extension.command("setup").description("Show extension path and open Chrome for installation").action(() => {
17547
+ console.log(executeExtensionSetup(__dirname));
17548
+ });
17514
17549
  program2.command("up").description("Start daemon + GUI, open browser (use --no-open to skip)").option("--no-open", "Do not open browser").action(async (opts) => {
17515
17550
  console.log(await executeUp(BASE_URL, CG_HOME, getDaemonScript(), getAppScript(), CONFIG_PATH, {
17516
17551
  noOpen: opts.open === false
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ (() => {
3
+ // src/messages.ts
4
+ function createDomResponse(requestId, ok, data, error) {
5
+ return { requestId, ok, data, error };
6
+ }
7
+ function isDomRequest(value) {
8
+ if (value == null || typeof value !== "object") return false;
9
+ const obj = value;
10
+ return typeof obj.id === "string" && typeof obj.action === "string" && typeof obj.params === "object";
11
+ }
12
+
13
+ // src/content/dom-executor.ts
14
+ function waitForSelector(selector, timeout) {
15
+ return new Promise((resolve, reject) => {
16
+ if (document.querySelector(selector)) {
17
+ resolve();
18
+ return;
19
+ }
20
+ const interval = 100;
21
+ let elapsed = 0;
22
+ const timer = setInterval(() => {
23
+ if (document.querySelector(selector)) {
24
+ clearInterval(timer);
25
+ resolve();
26
+ return;
27
+ }
28
+ elapsed += interval;
29
+ if (elapsed >= timeout) {
30
+ clearInterval(timer);
31
+ reject(new Error(`Selector "${selector}" timed out after ${timeout}ms`));
32
+ }
33
+ }, interval);
34
+ });
35
+ }
36
+ function extractData(rowSelector, fields) {
37
+ const rows = document.querySelectorAll(rowSelector);
38
+ return Array.from(rows).map((row) => {
39
+ const record = {};
40
+ for (const [name, selector] of Object.entries(fields)) {
41
+ record[name] = row.querySelector(selector)?.textContent?.trim() ?? "";
42
+ }
43
+ return record;
44
+ });
45
+ }
46
+ async function clickElement(selector) {
47
+ const el = document.querySelector(selector);
48
+ if (!el) throw new Error(`Element "${selector}" not found`);
49
+ el.click();
50
+ }
51
+ async function typeIntoElement(selector, value) {
52
+ const el = document.querySelector(selector);
53
+ if (!el) throw new Error(`Element "${selector}" not found`);
54
+ el.focus();
55
+ el.value = value;
56
+ el.dispatchEvent(new Event("input", { bubbles: true }));
57
+ el.dispatchEvent(new Event("change", { bubbles: true }));
58
+ }
59
+ async function fetchFromPage(url, method = "GET", headers, body) {
60
+ const resp = await fetch(url, {
61
+ method,
62
+ headers,
63
+ body,
64
+ credentials: "include"
65
+ });
66
+ const contentType = resp.headers.get("content-type") ?? "";
67
+ if (contentType.includes("application/json")) return resp.json();
68
+ return resp.text();
69
+ }
70
+
71
+ // src/content/content-script.ts
72
+ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
73
+ if (!isDomRequest(message)) return false;
74
+ const req = message;
75
+ handleRequest(req).then(
76
+ (data) => sendResponse(createDomResponse(req.id, true, data)),
77
+ (err) => sendResponse(createDomResponse(req.id, false, void 0, err.message))
78
+ );
79
+ return true;
80
+ });
81
+ async function handleRequest(req) {
82
+ const p = req.params;
83
+ switch (req.action) {
84
+ case "wait":
85
+ await waitForSelector(p.selector, p.timeout ?? 1e4);
86
+ return void 0;
87
+ case "extract":
88
+ return extractData(p.selector, p.fields);
89
+ case "click":
90
+ await clickElement(p.selector);
91
+ return void 0;
92
+ case "type":
93
+ await typeIntoElement(p.selector, p.value);
94
+ return void 0;
95
+ case "fetch":
96
+ return fetchFromPage(
97
+ p.url,
98
+ p.method,
99
+ p.headers,
100
+ p.body
101
+ );
102
+ default:
103
+ throw new Error(`Unknown action: ${req.action}`);
104
+ }
105
+ }
106
+ })();
107
+ //# sourceMappingURL=content-script.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/messages.ts", "../src/content/dom-executor.ts", "../src/content/content-script.ts"],
4
+ "sourcesContent": ["// src/messages.ts\nconst randomUUID = () => crypto.randomUUID();\n\nexport type DomAction = 'wait' | 'extract' | 'click' | 'type' | 'fetch';\n\nexport interface DomRequest {\n id: string;\n action: DomAction;\n params: Record<string, unknown>;\n}\n\nexport interface DomResponse {\n requestId: string;\n ok: boolean;\n data?: unknown;\n error?: string;\n}\n\nexport function createDomRequest(action: DomAction, params: Record<string, unknown>): DomRequest {\n return { id: randomUUID(), action, params };\n}\n\nexport function createDomResponse(\n requestId: string, ok: boolean, data?: unknown, error?: string,\n): DomResponse {\n return { requestId, ok, data, error };\n}\n\nexport function isDomRequest(value: unknown): value is DomRequest {\n if (value == null || typeof value !== 'object') return false;\n const obj = value as Record<string, unknown>;\n return typeof obj.id === 'string' && typeof obj.action === 'string' && typeof obj.params === 'object';\n}\n\nexport function isDomResponse(value: unknown): value is DomResponse {\n if (value == null || typeof value !== 'object') return false;\n const obj = value as Record<string, unknown>;\n return typeof obj.requestId === 'string' && typeof obj.ok === 'boolean';\n}\n", "// src/content/dom-executor.ts\n\nexport function waitForSelector(selector: string, timeout: number): Promise<void> {\n return new Promise((resolve, reject) => {\n if (document.querySelector(selector)) { resolve(); return; }\n const interval = 100;\n let elapsed = 0;\n const timer = setInterval(() => {\n if (document.querySelector(selector)) { clearInterval(timer); resolve(); return; }\n elapsed += interval;\n if (elapsed >= timeout) {\n clearInterval(timer);\n reject(new Error(`Selector \"${selector}\" timed out after ${timeout}ms`));\n }\n }, interval);\n });\n}\n\nexport function extractData(\n rowSelector: string,\n fields: Record<string, string>,\n): Record<string, string>[] {\n const rows = document.querySelectorAll(rowSelector);\n return Array.from(rows).map(row => {\n const record: Record<string, string> = {};\n for (const [name, selector] of Object.entries(fields)) {\n record[name] = row.querySelector(selector)?.textContent?.trim() ?? '';\n }\n return record;\n });\n}\n\nexport async function clickElement(selector: string): Promise<void> {\n const el = document.querySelector(selector);\n if (!el) throw new Error(`Element \"${selector}\" not found`);\n (el as HTMLElement).click();\n}\n\nexport async function typeIntoElement(selector: string, value: string): Promise<void> {\n const el = document.querySelector(selector) as HTMLInputElement | null;\n if (!el) throw new Error(`Element \"${selector}\" not found`);\n el.focus();\n el.value = value;\n el.dispatchEvent(new Event('input', { bubbles: true }));\n el.dispatchEvent(new Event('change', { bubbles: true }));\n}\n\nexport async function fetchFromPage(\n url: string, method = 'GET', headers?: Record<string, string>, body?: string,\n): Promise<unknown> {\n const resp = await fetch(url, {\n method, headers, body, credentials: 'include',\n });\n const contentType = resp.headers.get('content-type') ?? '';\n if (contentType.includes('application/json')) return resp.json();\n return resp.text();\n}\n", "// src/content/content-script.ts\nimport { isDomRequest, createDomResponse, type DomRequest } from '../messages.js';\nimport { waitForSelector, extractData, clickElement, typeIntoElement, fetchFromPage } from './dom-executor.js';\n\nchrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {\n if (!isDomRequest(message)) return false;\n const req = message as DomRequest;\n handleRequest(req).then(\n (data) => sendResponse(createDomResponse(req.id, true, data)),\n (err) => sendResponse(createDomResponse(req.id, false, undefined, err.message)),\n );\n return true; // Keep channel open for async response\n});\n\nasync function handleRequest(req: DomRequest): Promise<unknown> {\n const p = req.params;\n switch (req.action) {\n case 'wait':\n await waitForSelector(p.selector as string, (p.timeout as number) ?? 10000);\n return undefined;\n case 'extract':\n return extractData(p.selector as string, p.fields as Record<string, string>);\n case 'click':\n await clickElement(p.selector as string);\n return undefined;\n case 'type':\n await typeIntoElement(p.selector as string, p.value as string);\n return undefined;\n case 'fetch':\n return fetchFromPage(\n p.url as string, p.method as string,\n p.headers as Record<string, string>, p.body as string,\n );\n default:\n throw new Error(`Unknown action: ${req.action}`);\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,44 @@
1
+ // fetch-interceptor.js — injected at document_start in MAIN world.
2
+ // Patches window.fetch BEFORE OWA's modules capture a reference to it.
3
+ // Minimal — only the fetch patch, nothing else.
4
+ (function () {
5
+ if (window.__rfb) return;
6
+ window.__rfb = [];
7
+ window.__rfb_urls = [];
8
+
9
+ // Intercept Service Worker messages — OWA sends getSchedule data
10
+ // from the SW to the page via postMessage, bypassing window.fetch.
11
+ if (navigator.serviceWorker) {
12
+ navigator.serviceWorker.addEventListener('message', function (e) {
13
+ try {
14
+ var str = typeof e.data === 'string' ? e.data : JSON.stringify(e.data);
15
+ if (str.indexOf('getSchedule') > -1 && str.indexOf('availabilityView') > -1) {
16
+ window.__rfb.push({ req: '', body: str });
17
+ }
18
+ } catch (_e) { /* ignore */ }
19
+ });
20
+ }
21
+
22
+ var origFetch = window.fetch;
23
+ window.fetch = function () {
24
+ var a = arguments;
25
+ var u = (a[0] && a[0].url) || a[0] || '';
26
+ var rb = (a[1] && a[1].body) || '';
27
+ var uStr = String(u);
28
+ if (window.__rfb_urls.length < 100) {
29
+ window.__rfb_urls.push(uStr.slice(0, 120));
30
+ }
31
+ return origFetch.apply(this, a).then(function (r) {
32
+ try {
33
+ if (uStr.indexOf('graphql') > -1) {
34
+ r.clone().text().then(function (t) {
35
+ if (t.indexOf('getSchedule') > -1 && t.indexOf('availabilityView') > -1) {
36
+ window.__rfb.push({ req: String(rb || ''), body: t });
37
+ }
38
+ }).catch(function () {});
39
+ }
40
+ } catch (_e) { /* ignore */ }
41
+ return r;
42
+ });
43
+ };
44
+ })();
@@ -0,0 +1,16 @@
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "commandGarden",
4
+ "version": "0.1.0",
5
+ "description": "Enterprise browser automation — secure, auditable CLI commands for websites",
6
+ "minimum_chrome_version": "116",
7
+ "permissions": ["alarms", "cookies", "debugger", "idle", "tabs", "scripting", "webRequest", "storage"],
8
+ "host_permissions": ["<all_urls>"],
9
+ "background": {
10
+ "service_worker": "service-worker.js",
11
+ "type": "module"
12
+ },
13
+ "action": {
14
+ "default_popup": "popup.html"
15
+ }
16
+ }
@@ -0,0 +1,197 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <style>
6
+ * { margin: 0; padding: 0; box-sizing: border-box; }
7
+ body {
8
+ width: 320px;
9
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
10
+ font-size: 13px;
11
+ background: #1e1e1e;
12
+ color: #ccc;
13
+ }
14
+ .header {
15
+ padding: 12px 16px;
16
+ font-family: 'SF Mono', SFMono-Regular, ui-monospace, Menlo, Consolas, monospace;
17
+ font-size: 13px;
18
+ font-weight: 600;
19
+ color: #e0e0e0;
20
+ border-bottom: 1px solid #333;
21
+ }
22
+ .status-row {
23
+ display: flex;
24
+ align-items: center;
25
+ gap: 8px;
26
+ padding: 10px 16px;
27
+ border-bottom: 1px solid #333;
28
+ }
29
+ .dot {
30
+ width: 8px;
31
+ height: 8px;
32
+ border-radius: 50%;
33
+ flex-shrink: 0;
34
+ }
35
+ .dot.connected { background: #4caf50; }
36
+ .dot.disconnected { background: #f44336; }
37
+ .dot.disabled { background: #666; }
38
+ .status-text { flex: 1; }
39
+ .toggle {
40
+ position: relative;
41
+ width: 36px;
42
+ height: 20px;
43
+ flex-shrink: 0;
44
+ cursor: pointer;
45
+ }
46
+ .toggle input {
47
+ opacity: 0;
48
+ width: 0;
49
+ height: 0;
50
+ }
51
+ .toggle-track {
52
+ position: absolute;
53
+ inset: 0;
54
+ background: #555;
55
+ border-radius: 10px;
56
+ transition: background 0.2s;
57
+ }
58
+ .toggle input:checked + .toggle-track {
59
+ background: #4caf50;
60
+ }
61
+ .toggle-knob {
62
+ position: absolute;
63
+ top: 2px;
64
+ left: 2px;
65
+ width: 16px;
66
+ height: 16px;
67
+ background: #fff;
68
+ border-radius: 50%;
69
+ transition: transform 0.2s;
70
+ pointer-events: none;
71
+ }
72
+ .toggle input:checked ~ .toggle-knob {
73
+ transform: translateX(16px);
74
+ }
75
+ .activity-list {
76
+ max-height: 240px;
77
+ overflow-y: auto;
78
+ }
79
+ .activity-item {
80
+ display: flex;
81
+ align-items: center;
82
+ gap: 8px;
83
+ padding: 8px 16px;
84
+ border-bottom: 1px solid #2a2a2a;
85
+ transition: background 0.1s;
86
+ }
87
+ .activity-item:hover {
88
+ background: #252525;
89
+ }
90
+ .activity-icon { font-size: 12px; flex-shrink: 0; }
91
+ .activity-icon.ok { color: #4caf50; }
92
+ .activity-icon.fail { color: #f44336; }
93
+ .activity-name { flex: 1; font-family: monospace; font-size: 12px; }
94
+ .activity-time { color: #888; font-size: 11px; flex-shrink: 0; }
95
+ .empty {
96
+ padding: 16px;
97
+ text-align: center;
98
+ color: #666;
99
+ font-style: italic;
100
+ }
101
+ /* Approval section */
102
+ .approval-section {
103
+ border-bottom: 1px solid #333;
104
+ background: #252525;
105
+ }
106
+ .approval-header {
107
+ padding: 10px 16px 4px;
108
+ font-size: 12px;
109
+ font-weight: 600;
110
+ color: #ccc;
111
+ }
112
+ .approval-count {
113
+ display: inline-block;
114
+ font-size: 10px;
115
+ min-width: 16px;
116
+ text-align: center;
117
+ padding: 0 5px;
118
+ line-height: 16px;
119
+ border-radius: 8px;
120
+ background: rgba(245, 158, 11, 0.18);
121
+ color: #F59E0B;
122
+ font-weight: 700;
123
+ margin-left: 6px;
124
+ vertical-align: 1px;
125
+ }
126
+ .approval-card {
127
+ padding: 10px 16px;
128
+ display: flex;
129
+ flex-direction: column;
130
+ gap: 6px;
131
+ }
132
+ .approval-card + .approval-card {
133
+ border-top: 1px solid #2a2a2a;
134
+ }
135
+ .approval-connector {
136
+ font-family: monospace;
137
+ font-size: 12px;
138
+ color: #e0e0e0;
139
+ }
140
+ .approval-detail {
141
+ font-size: 11px;
142
+ color: #999;
143
+ }
144
+ .approval-cap {
145
+ display: inline-block;
146
+ font-size: 10px;
147
+ padding: 1px 6px;
148
+ border-radius: 3px;
149
+ background: rgba(245, 158, 11, 0.15);
150
+ color: #F59E0B;
151
+ font-family: monospace;
152
+ }
153
+ .approval-actions {
154
+ display: flex;
155
+ gap: 8px;
156
+ margin-top: 2px;
157
+ }
158
+ .btn-approve, .btn-reject {
159
+ font-size: 11px;
160
+ padding: 4px 14px;
161
+ border-radius: 6px;
162
+ border: none;
163
+ cursor: pointer;
164
+ font-weight: 600;
165
+ transition: background 0.15s, color 0.15s;
166
+ }
167
+ .btn-approve {
168
+ background: #4caf50;
169
+ color: #fff;
170
+ }
171
+ .btn-approve:hover { background: #43a047; }
172
+ .btn-approve:active { background: #388e3c; }
173
+ .btn-reject {
174
+ background: transparent;
175
+ color: #999;
176
+ border: 1px solid #555;
177
+ }
178
+ .btn-reject:hover { background: #333; color: #ccc; }
179
+ .btn-reject:active { background: #2a2a2a; }
180
+ </style>
181
+ </head>
182
+ <body>
183
+ <div class="header">commandGarden</div>
184
+ <div class="status-row">
185
+ <div id="dot" class="dot disabled"></div>
186
+ <span id="status-text" class="status-text">Checking…</span>
187
+ <label class="toggle">
188
+ <input type="checkbox" id="toggle-input">
189
+ <div class="toggle-track"></div>
190
+ <div class="toggle-knob"></div>
191
+ </label>
192
+ </div>
193
+ <div id="approvals"></div>
194
+ <div id="activity" class="activity-list"></div>
195
+ <script src="popup.js"></script>
196
+ </body>
197
+ </html>
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ (() => {
3
+ // src/popup/time-ago.ts
4
+ function timeAgo(timestamp, now = Date.now()) {
5
+ const seconds = Math.floor((now - timestamp) / 1e3);
6
+ if (seconds < 60) return "just now";
7
+ const minutes = Math.floor(seconds / 60);
8
+ if (minutes < 60) return `${minutes}m ago`;
9
+ const hours = Math.floor(minutes / 60);
10
+ if (hours < 24) return `${hours}h ago`;
11
+ const days = Math.floor(hours / 24);
12
+ return `${days}d ago`;
13
+ }
14
+
15
+ // src/popup/popup.ts
16
+ var dot = document.getElementById("dot");
17
+ var statusText = document.getElementById("status-text");
18
+ var toggleInput = document.getElementById("toggle-input");
19
+ var activityEl = document.getElementById("activity");
20
+ var approvalsEl = document.getElementById("approvals");
21
+ var currentEnabled = true;
22
+ function escapeHtml(text) {
23
+ const el = document.createElement("span");
24
+ el.textContent = text;
25
+ return el.innerHTML;
26
+ }
27
+ function renderApprovals(approvals) {
28
+ if (approvals.length === 0) {
29
+ approvalsEl.innerHTML = "";
30
+ return;
31
+ }
32
+ const cards = approvals.map(
33
+ (a) => `<div class="approval-card" data-id="${escapeHtml(a.approvalId)}">
34
+ <div class="approval-connector">${escapeHtml(a.connectorKey)}</div>
35
+ <div class="approval-detail">Step ${a.stepIndex + 1} [${escapeHtml(a.stepType)}] <span class="approval-cap">${escapeHtml(a.capability)}</span></div>
36
+ <div class="approval-actions">
37
+ <button class="btn-approve" data-approval-id="${escapeHtml(a.approvalId)}" data-approved="true">Approve</button>
38
+ <button class="btn-reject" data-approval-id="${escapeHtml(a.approvalId)}" data-approved="false">Reject</button>
39
+ </div>
40
+ </div>`
41
+ ).join("");
42
+ approvalsEl.innerHTML = `<div class="approval-section">
43
+ <div class="approval-header">Pending approvals <span class="approval-count">${approvals.length}</span></div>
44
+ ${cards}
45
+ </div>`;
46
+ approvalsEl.querySelectorAll("[data-approval-id]").forEach((btn) => {
47
+ btn.addEventListener("click", () => {
48
+ const approvalId = btn.dataset.approvalId;
49
+ const approved = btn.dataset.approved === "true";
50
+ btn.disabled = true;
51
+ chrome.runtime.sendMessage(
52
+ { type: "approvalDecision", approvalId, approved },
53
+ () => {
54
+ fetchStatus();
55
+ }
56
+ );
57
+ });
58
+ });
59
+ }
60
+ function render(status) {
61
+ currentEnabled = status.enabled;
62
+ toggleInput.checked = status.enabled;
63
+ if (!status.enabled) {
64
+ dot.className = "dot disabled";
65
+ statusText.textContent = "Disabled";
66
+ } else if (status.connected) {
67
+ dot.className = "dot connected";
68
+ statusText.textContent = "Connected";
69
+ } else {
70
+ dot.className = "dot disconnected";
71
+ statusText.textContent = "Disconnected";
72
+ }
73
+ renderApprovals(status.pendingApprovals ?? []);
74
+ if (status.recentActivity.length === 0) {
75
+ activityEl.innerHTML = '<div class="empty">No recent activity</div>';
76
+ return;
77
+ }
78
+ const now = Date.now();
79
+ activityEl.innerHTML = status.recentActivity.map((entry) => {
80
+ const icon = entry.ok ? '<span class="activity-icon ok">\u2713</span>' : '<span class="activity-icon fail">\u2717</span>';
81
+ return `<div class="activity-item">${icon}<span class="activity-name">${escapeHtml(entry.connector)}</span><span class="activity-time">${timeAgo(entry.timestamp, now)}</span></div>`;
82
+ }).join("");
83
+ }
84
+ function fetchStatus() {
85
+ chrome.runtime.sendMessage({ type: "getStatus" }, (response) => {
86
+ if (chrome.runtime.lastError) {
87
+ statusText.textContent = "Error";
88
+ return;
89
+ }
90
+ render(response);
91
+ });
92
+ }
93
+ toggleInput.addEventListener("change", () => {
94
+ const newEnabled = toggleInput.checked;
95
+ toggleInput.disabled = true;
96
+ chrome.runtime.sendMessage({ type: "setEnabled", enabled: newEnabled }, (response) => {
97
+ toggleInput.disabled = false;
98
+ if (chrome.runtime.lastError) {
99
+ toggleInput.checked = currentEnabled;
100
+ return;
101
+ }
102
+ render(response);
103
+ });
104
+ });
105
+ chrome.runtime.onMessage.addListener((msg) => {
106
+ if (msg.type === "approvalUpdate") {
107
+ renderApprovals(msg.pendingApprovals ?? []);
108
+ }
109
+ });
110
+ fetchStatus();
111
+ })();
112
+ //# sourceMappingURL=popup.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/popup/time-ago.ts", "../src/popup/popup.ts"],
4
+ "sourcesContent": ["// src/popup/time-ago.ts\nexport function timeAgo(timestamp: number, now: number = Date.now()): string {\n const seconds = Math.floor((now - timestamp) / 1000);\n if (seconds < 60) return 'just now';\n const minutes = Math.floor(seconds / 60);\n if (minutes < 60) return `${minutes}m ago`;\n const hours = Math.floor(minutes / 60);\n if (hours < 24) return `${hours}h ago`;\n const days = Math.floor(hours / 24);\n return `${days}d ago`;\n}\n", "// src/popup/popup.ts\n/// <reference lib=\"dom\" />\nimport type { PopupStatusResponse, ApprovalInfo } from './popup-types.js';\nimport { timeAgo } from './time-ago.js';\n\nconst dot = document.getElementById('dot')!;\nconst statusText = document.getElementById('status-text')!;\nconst toggleInput = document.getElementById('toggle-input') as HTMLInputElement;\nconst activityEl = document.getElementById('activity')!;\nconst approvalsEl = document.getElementById('approvals')!;\n\nlet currentEnabled = true;\n\nfunction escapeHtml(text: string): string {\n const el = document.createElement('span');\n el.textContent = text;\n return el.innerHTML;\n}\n\nfunction renderApprovals(approvals: ApprovalInfo[]): void {\n if (approvals.length === 0) {\n approvalsEl.innerHTML = '';\n return;\n }\n\n const cards = approvals.map(a =>\n `<div class=\"approval-card\" data-id=\"${escapeHtml(a.approvalId)}\">\n <div class=\"approval-connector\">${escapeHtml(a.connectorKey)}</div>\n <div class=\"approval-detail\">Step ${a.stepIndex + 1} [${escapeHtml(a.stepType)}] <span class=\"approval-cap\">${escapeHtml(a.capability)}</span></div>\n <div class=\"approval-actions\">\n <button class=\"btn-approve\" data-approval-id=\"${escapeHtml(a.approvalId)}\" data-approved=\"true\">Approve</button>\n <button class=\"btn-reject\" data-approval-id=\"${escapeHtml(a.approvalId)}\" data-approved=\"false\">Reject</button>\n </div>\n </div>`\n ).join('');\n\n approvalsEl.innerHTML =\n `<div class=\"approval-section\">\n <div class=\"approval-header\">Pending approvals <span class=\"approval-count\">${approvals.length}</span></div>\n ${cards}\n </div>`;\n\n approvalsEl.querySelectorAll('[data-approval-id]').forEach(btn => {\n btn.addEventListener('click', () => {\n const approvalId = (btn as HTMLElement).dataset.approvalId!;\n const approved = (btn as HTMLElement).dataset.approved === 'true';\n (btn as HTMLButtonElement).disabled = true;\n chrome.runtime.sendMessage(\n { type: 'approvalDecision', approvalId, approved },\n () => { fetchStatus(); },\n );\n });\n });\n}\n\nfunction render(status: PopupStatusResponse): void {\n currentEnabled = status.enabled;\n toggleInput.checked = status.enabled;\n\n if (!status.enabled) {\n dot.className = 'dot disabled';\n statusText.textContent = 'Disabled';\n } else if (status.connected) {\n dot.className = 'dot connected';\n statusText.textContent = 'Connected';\n } else {\n dot.className = 'dot disconnected';\n statusText.textContent = 'Disconnected';\n }\n\n renderApprovals(status.pendingApprovals ?? []);\n\n if (status.recentActivity.length === 0) {\n activityEl.innerHTML = '<div class=\"empty\">No recent activity</div>';\n return;\n }\n\n const now = Date.now();\n activityEl.innerHTML = status.recentActivity\n .map(entry => {\n const icon = entry.ok\n ? '<span class=\"activity-icon ok\">\u2713</span>'\n : '<span class=\"activity-icon fail\">\u2717</span>';\n return `<div class=\"activity-item\">${icon}<span class=\"activity-name\">${escapeHtml(entry.connector)}</span><span class=\"activity-time\">${timeAgo(entry.timestamp, now)}</span></div>`;\n })\n .join('');\n}\n\nfunction fetchStatus(): void {\n chrome.runtime.sendMessage({ type: 'getStatus' }, (response: PopupStatusResponse) => {\n if (chrome.runtime.lastError) {\n statusText.textContent = 'Error';\n return;\n }\n render(response);\n });\n}\n\ntoggleInput.addEventListener('change', () => {\n const newEnabled = toggleInput.checked;\n toggleInput.disabled = true;\n chrome.runtime.sendMessage({ type: 'setEnabled', enabled: newEnabled }, (response: PopupStatusResponse) => {\n toggleInput.disabled = false;\n if (chrome.runtime.lastError) {\n toggleInput.checked = currentEnabled;\n return;\n }\n render(response);\n });\n});\n\n// Listen for push updates from the service worker (approval state changes)\nchrome.runtime.onMessage.addListener((msg) => {\n if (msg.type === 'approvalUpdate') {\n renderApprovals(msg.pendingApprovals ?? []);\n }\n});\n\nfetchStatus();\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }