@commandgarden/cli 0.1.0 → 0.2.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.
Files changed (2) hide show
  1. package/dist/main.js +221 -37
  2. package/package.json +3 -3
package/dist/main.js CHANGED
@@ -7,7 +7,7 @@ var __export = (target, all) => {
7
7
 
8
8
  // src/main.ts
9
9
  import { Command } from "commander";
10
- import { join as join2, dirname as dirname2 } from "path";
10
+ import { join as join2, dirname } from "path";
11
11
  import { homedir } from "os";
12
12
  import { fileURLToPath } from "url";
13
13
 
@@ -58,6 +58,54 @@ var DaemonClient = class {
58
58
  }
59
59
  return data;
60
60
  }
61
+ async connectSSE(path, onEvent, signal) {
62
+ const resp = await this.rawFetch(path, {
63
+ method: "GET",
64
+ headers: {
65
+ "Authorization": `Bearer ${this.token}`,
66
+ "X-CommandGarden": "1",
67
+ "Accept": "text/event-stream"
68
+ },
69
+ signal
70
+ });
71
+ if (!resp.ok || !resp.body) {
72
+ throw new Error(`SSE connection failed: HTTP ${resp.status}`);
73
+ }
74
+ const reader = resp.body.getReader();
75
+ const decoder = new TextDecoder();
76
+ let buffer = "";
77
+ try {
78
+ while (true) {
79
+ const { done, value } = await reader.read();
80
+ if (done) break;
81
+ buffer += decoder.decode(value, { stream: true });
82
+ const lines = buffer.split("\n");
83
+ buffer = lines.pop() ?? "";
84
+ let currentEvent = "message";
85
+ let currentData = "";
86
+ for (const line of lines) {
87
+ if (line.startsWith("event: ")) {
88
+ currentEvent = line.slice(7).trim();
89
+ } else if (line.startsWith("data: ")) {
90
+ currentData = line.slice(6);
91
+ } else if (line === "") {
92
+ if (currentData) {
93
+ try {
94
+ onEvent(currentEvent, JSON.parse(currentData));
95
+ } catch {
96
+ onEvent(currentEvent, currentData);
97
+ }
98
+ currentData = "";
99
+ currentEvent = "message";
100
+ }
101
+ }
102
+ }
103
+ }
104
+ } catch (err) {
105
+ if (signal?.aborted) return;
106
+ throw err;
107
+ }
108
+ }
61
109
  rawFetch(path, init) {
62
110
  return fetch(`${this.baseUrl}${path}`, init);
63
111
  }
@@ -79,6 +127,9 @@ function parseDuration(input) {
79
127
  return new Date(Date.now() - value * UNITS[unit]);
80
128
  }
81
129
 
130
+ // src/commands/run.ts
131
+ import { createInterface } from "readline";
132
+
82
133
  // src/formatters.ts
83
134
  import Table from "cli-table3";
84
135
  function formatTable(data, columns) {
@@ -138,6 +189,30 @@ function parseConnectorArgs(rawArgs) {
138
189
  }
139
190
  return result;
140
191
  }
192
+ function promptApproval(request) {
193
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
194
+ let cancelled = false;
195
+ const cancel = () => {
196
+ cancelled = true;
197
+ rl.close();
198
+ };
199
+ const prompt = `
200
+ \u26A0 Step ${request.stepIndex + 1} [${request.stepType}] in ${request.connectorKey} requires approval.
201
+ Capability: ${request.capability}
202
+ ${request.description}
203
+ Approve? (y/n): `;
204
+ const promise = new Promise((resolve) => {
205
+ rl.question(prompt, (answer) => {
206
+ rl.close();
207
+ if (cancelled) return;
208
+ resolve(answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes");
209
+ });
210
+ rl.on("close", () => {
211
+ if (cancelled) resolve(true);
212
+ });
213
+ });
214
+ return { promise, cancel };
215
+ }
141
216
  async function executeRun(client, connector, args, outputFormat) {
142
217
  try {
143
218
  const resp = await client.post("/api/run", {
@@ -145,10 +220,57 @@ async function executeRun(client, connector, args, outputFormat) {
145
220
  args,
146
221
  format: outputFormat
147
222
  });
148
- if (!resp.ok) {
149
- return `Error: ${resp.error ?? "Unknown error from extension"} (${resp.durationMs}ms)`;
223
+ if (!resp.requiresApproval) {
224
+ if (!resp.ok) {
225
+ return `Error: ${resp.error ?? "Unknown error from extension"} (${resp.durationMs}ms)`;
226
+ }
227
+ return format(resp.data, resp.columns, connector, outputFormat);
150
228
  }
151
- return format(resp.data, resp.columns, connector, outputFormat);
229
+ const requestId = resp.requestId;
230
+ process.stderr.write(`Pipeline requires approval. Waiting for steps...
231
+ `);
232
+ return new Promise((resolve) => {
233
+ const abortController = new AbortController();
234
+ let activePrompt = null;
235
+ client.connectSSE(
236
+ `/api/run/events/${requestId}`,
237
+ (event, data) => {
238
+ if (event === "approval") {
239
+ const approvalReq = data;
240
+ activePrompt = promptApproval(approvalReq);
241
+ activePrompt.promise.then(async (approved) => {
242
+ activePrompt = null;
243
+ try {
244
+ await client.post("/api/approval", {
245
+ approvalId: approvalReq.approvalId,
246
+ approved
247
+ });
248
+ } catch {
249
+ }
250
+ if (!approved) {
251
+ abortController.abort();
252
+ resolve(`Aborted: Step ${approvalReq.stepIndex + 1} [${approvalReq.stepType}] was rejected.`);
253
+ }
254
+ });
255
+ } else if (event === "result") {
256
+ if (activePrompt) {
257
+ activePrompt.cancel();
258
+ activePrompt = null;
259
+ process.stderr.write("\n (approved via extension)\n");
260
+ }
261
+ abortController.abort();
262
+ const result = data;
263
+ if (!result.ok) {
264
+ resolve(`Error: ${result.error ?? "Unknown error"} (${result.durationMs}ms)`);
265
+ } else {
266
+ resolve(format(result.data, result.columns ?? [], connector, outputFormat));
267
+ }
268
+ }
269
+ },
270
+ abortController.signal
271
+ ).catch(() => {
272
+ });
273
+ });
152
274
  } catch (err) {
153
275
  return `Error: ${err instanceof Error ? err.message : String(err)}`;
154
276
  }
@@ -4603,6 +4725,7 @@ function buildQueryString(filter) {
4603
4725
  const params = new URLSearchParams();
4604
4726
  if (filter.since) params.set("since", filter.since);
4605
4727
  if (filter.connector) params.set("connector", filter.connector);
4728
+ if (filter.type) params.set("type", filter.type);
4606
4729
  if (filter.limit) params.set("limit", String(filter.limit));
4607
4730
  const qs = params.toString();
4608
4731
  return qs ? `?${qs}` : "";
@@ -4614,7 +4737,7 @@ async function executeAuditList(client, filter) {
4614
4737
  );
4615
4738
  if (resp.events.length === 0) return "No audit events found.";
4616
4739
  const table = new Table3({
4617
- head: ["Timestamp", "Type", "Connector", "User", "Duration", "Rows", "Error"],
4740
+ head: ["Timestamp", "Type", "Connector", "User", "Duration", "Rows", "Source", "Error"],
4618
4741
  style: { head: ["cyan"] }
4619
4742
  });
4620
4743
  for (const e of resp.events) {
@@ -4625,6 +4748,7 @@ async function executeAuditList(client, filter) {
4625
4748
  e.user,
4626
4749
  `${e.durationMs}ms`,
4627
4750
  e.rowCount?.toString() ?? "-",
4751
+ e.source ?? "-",
4628
4752
  e.error ?? e.denialReason ?? ""
4629
4753
  ]);
4630
4754
  }
@@ -4633,7 +4757,24 @@ async function executeAuditList(client, filter) {
4633
4757
  return `Error: ${err instanceof Error ? err.message : String(err)}`;
4634
4758
  }
4635
4759
  }
4636
- var AUDIT_COLUMNS = ["id", "timestamp", "type", "user", "connector", "durationMs", "rowCount", "error", "denialReason"];
4760
+ var AUDIT_COLUMNS = [
4761
+ "id",
4762
+ "timestamp",
4763
+ "type",
4764
+ "user",
4765
+ "connector",
4766
+ "durationMs",
4767
+ "rowCount",
4768
+ "error",
4769
+ "denialReason",
4770
+ "correlationId",
4771
+ "connectorHash",
4772
+ "source",
4773
+ "args",
4774
+ "domains",
4775
+ "capabilities",
4776
+ "steps"
4777
+ ];
4637
4778
  async function executeAuditExport(client, filter, format2) {
4638
4779
  try {
4639
4780
  const resp = await client.get(
@@ -4646,8 +4787,15 @@ async function executeAuditExport(client, filter, format2) {
4646
4787
  const rows = resp.events.map(
4647
4788
  (e) => AUDIT_COLUMNS.map((col) => {
4648
4789
  const val = e[col];
4649
- const str = String(val ?? "");
4650
- return str.includes(",") || str.includes('"') ? `"${str.replace(/"/g, '""')}"` : str;
4790
+ let str;
4791
+ if (val === null || val === void 0) {
4792
+ str = "";
4793
+ } else if (typeof val === "object") {
4794
+ str = JSON.stringify(val);
4795
+ } else {
4796
+ str = String(val);
4797
+ }
4798
+ return str.includes(",") || str.includes('"') || str.includes("\n") ? `"${str.replace(/"/g, '""')}"` : str;
4651
4799
  }).join(",")
4652
4800
  );
4653
4801
  return [header, ...rows].join("\n") + "\n";
@@ -4655,12 +4803,54 @@ async function executeAuditExport(client, filter, format2) {
4655
4803
  return `Error: ${err instanceof Error ? err.message : String(err)}`;
4656
4804
  }
4657
4805
  }
4806
+ async function executeAuditShow(client, id) {
4807
+ try {
4808
+ const resp = await client.get(`/api/audit/${id}`);
4809
+ const e = resp.event;
4810
+ const lines = [
4811
+ `Event: ${e.id}`,
4812
+ `Type: ${e.type}`,
4813
+ `Connector: ${e.connector}${e.connectorHash ? ` (hash: ${e.connectorHash})` : ""}`,
4814
+ `Correlation: ${e.correlationId ?? "-"}`,
4815
+ `User: ${e.user}`,
4816
+ `Timestamp: ${e.timestamp.replace("T", " ").slice(0, 19)}`,
4817
+ `Duration: ${e.durationMs}ms`
4818
+ ];
4819
+ if (e.rowCount !== void 0) lines.push(`Rows: ${e.rowCount}`);
4820
+ if (e.error) lines.push(`Error: ${e.error}`);
4821
+ if (e.denialReason) lines.push(`Denial: ${e.denialReason}`);
4822
+ if (e.source) lines.push(`Source: ${e.source}`);
4823
+ if (e.previousValue !== void 0) lines.push(`Previous: ${e.previousValue}`);
4824
+ if (e.newValue !== void 0) lines.push(`New: ${e.newValue}`);
4825
+ if (Object.keys(e.args).length > 0) {
4826
+ lines.push(`Args: ${JSON.stringify(e.args)}`);
4827
+ }
4828
+ if (e.steps && e.steps.length > 0) {
4829
+ lines.push("");
4830
+ lines.push("Pipeline Steps:");
4831
+ const stepTable = new Table3({
4832
+ head: ["#", "Step", "Capability", "Duration", "Error"],
4833
+ style: { head: ["cyan"] }
4834
+ });
4835
+ for (const s of e.steps) {
4836
+ stepTable.push([
4837
+ s.index + 1,
4838
+ s.step,
4839
+ s.capability ?? "-",
4840
+ `${s.durationMs}ms`,
4841
+ s.error ?? ""
4842
+ ]);
4843
+ }
4844
+ lines.push(stepTable.toString());
4845
+ }
4846
+ return lines.join("\n");
4847
+ } catch (err) {
4848
+ return `Error: ${err instanceof Error ? err.message : String(err)}`;
4849
+ }
4850
+ }
4658
4851
 
4659
4852
  // src/commands/config-cmd.ts
4660
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
4661
- import { dirname } from "path";
4662
- import { parse as parseYaml2, stringify as stringifyYaml } from "yaml";
4663
- var VALID_SECTIONS = ["daemon", "security", "connectors", "audit", "output"];
4853
+ import { readFileSync as readFileSync4, existsSync as existsSync2 } from "fs";
4664
4854
  function executeConfigShow(configPath) {
4665
4855
  if (!existsSync2(configPath)) {
4666
4856
  return [
@@ -4671,31 +4861,18 @@ function executeConfigShow(configPath) {
4671
4861
  }
4672
4862
  return readFileSync4(configPath, "utf-8");
4673
4863
  }
4674
- function executeConfigSet(configPath, key, value) {
4675
- const parts = key.split(".");
4676
- if (parts.length !== 2 || !VALID_SECTIONS.includes(parts[0])) {
4677
- return `Unknown config section: "${parts[0]}". Valid sections: ${VALID_SECTIONS.join(", ")}`;
4678
- }
4679
- let config2 = {};
4680
- if (existsSync2(configPath)) {
4681
- const raw = readFileSync4(configPath, "utf-8");
4682
- config2 = parseYaml2(raw) ?? {};
4683
- }
4684
- const [section, prop] = parts;
4685
- if (!config2[section]) config2[section] = {};
4686
- let parsed = value;
4687
- if (value === "true") parsed = true;
4688
- else if (value === "false") parsed = false;
4689
- else if (!isNaN(Number(value)) && value !== "") parsed = Number(value);
4690
- config2[section][prop] = parsed;
4691
- mkdirSync2(dirname(configPath), { recursive: true });
4692
- writeFileSync2(configPath, stringifyYaml(config2), "utf-8");
4693
- return `Set ${key} = ${value}`;
4864
+ async function executeConfigSet(client, key, value) {
4865
+ try {
4866
+ await client.post("/api/config", { key, value });
4867
+ return `Set ${key} = ${value}`;
4868
+ } catch (err) {
4869
+ return `Error: ${err instanceof Error ? err.message : String(err)}`;
4870
+ }
4694
4871
  }
4695
4872
 
4696
4873
  // src/main.ts
4697
4874
  var __filename = fileURLToPath(import.meta.url);
4698
- var __dirname = dirname2(__filename);
4875
+ var __dirname = dirname(__filename);
4699
4876
  var DAEMON_SCRIPT = join2(__dirname, "..", "..", "daemon", "dist", "main.js");
4700
4877
  var CG_HOME = join2(homedir(), ".commandgarden");
4701
4878
  var TOKEN_PATH = join2(CG_HOME, "session-token");
@@ -4739,26 +4916,33 @@ daemon.command("stop").description("Stop the daemon").action(async () => {
4739
4916
  console.log(await executeDaemonStop(CG_HOME));
4740
4917
  });
4741
4918
  var audit = program.command("audit").description("View audit event log");
4742
- audit.command("list").description("List recent audit events").option("--since <duration>", "time window, e.g. 7d, 2w, 12h").option("--connector <pattern>", "filter by connector pattern, e.g. test/*").option("--limit <n>", "max events to return", "100").action(async (opts) => {
4919
+ audit.command("list").description("List recent audit events").option("--since <duration>", "time window, e.g. 7d, 2w, 12h").option("--connector <pattern>", "filter by connector pattern, e.g. test/*").option("--type <pattern>", "filter by event type, e.g. auth.failed, command.*").option("--limit <n>", "max events to return", "100").action(async (opts) => {
4743
4920
  const client = createClient();
4744
4921
  const filter = {};
4745
4922
  if (opts.since) filter.since = parseDuration(opts.since).toISOString();
4746
4923
  if (opts.connector) filter.connector = opts.connector;
4924
+ if (opts.type) filter.type = opts.type;
4747
4925
  filter.limit = parseInt(opts.limit, 10);
4748
4926
  console.log(await executeAuditList(client, filter));
4749
4927
  });
4750
- audit.command("export").description("Export audit events").option("--format <format>", "export format: json, csv", "json").option("--since <duration>", "time window, e.g. 7d, 30d").option("--connector <pattern>", "filter by connector pattern").action(async (opts) => {
4928
+ audit.command("export").description("Export audit events").option("--format <format>", "export format: json, csv", "json").option("--since <duration>", "time window, e.g. 7d, 30d").option("--connector <pattern>", "filter by connector pattern").option("--type <pattern>", "filter by event type").action(async (opts) => {
4751
4929
  const client = createClient();
4752
4930
  const filter = {};
4753
4931
  if (opts.since) filter.since = parseDuration(opts.since).toISOString();
4754
4932
  if (opts.connector) filter.connector = opts.connector;
4933
+ if (opts.type) filter.type = opts.type;
4755
4934
  console.log(await executeAuditExport(client, filter, opts.format));
4756
4935
  });
4936
+ audit.command("show <id>").description("Show full details of an audit event").action(async (id) => {
4937
+ const client = createClient();
4938
+ console.log(await executeAuditShow(client, id));
4939
+ });
4757
4940
  var config = program.command("config").description("Manage daemon configuration");
4758
4941
  config.command("show").description("Show current configuration").action(() => {
4759
4942
  console.log(executeConfigShow(CONFIG_PATH));
4760
4943
  });
4761
- config.command("set <key> <value>").description("Set a configuration value (e.g. daemon.port 9999)").action((key, value) => {
4762
- console.log(executeConfigSet(CONFIG_PATH, key, value));
4944
+ config.command("set <key> <value>").description("Set a configuration value (e.g. daemon.port 9999)").action(async (key, value) => {
4945
+ const client = createClient();
4946
+ console.log(await executeConfigSet(client, key, value));
4763
4947
  });
4764
4948
  program.parse();
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@commandgarden/cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Enterprise browser automation CLI",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "main": "./dist/main.js",
8
8
  "bin": {
9
- "commandgarden": "./dist/main.js",
10
- "cg": "./dist/main.js"
9
+ "commandgarden": "dist/main.js",
10
+ "cg": "dist/main.js"
11
11
  },
12
12
  "files": [
13
13
  "dist"