@cantinasecurity/apex-cli 0.1.9 → 0.1.11

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/mcp.js CHANGED
@@ -6,6 +6,7 @@ import { getMe, logout, startDeviceLogin, waitForDeviceLoginApproval } from "./a
6
6
  import { ApexApiClient, formatApiError } from "./api-client.js";
7
7
  import { commandCancelScan, commandConnect, commandCredits, commandDoctor, commandExportFindings, commandFindingComment, commandFindingFeedback, commandFindingFixReview, commandFindings, commandScan, commandScans, commandStatus, commandWorkspace, commandWorkspaceUse, commandWorkspaces, } from "./commands.js";
8
8
  import { CLI_HELP_TEXT } from "./help.js";
9
+ import { createMcpServerTelemetryInvocation, createMcpTelemetryInvocation, emitInvocationCompleted, emitInvocationStarted, emitMcpServerStarted, withTelemetryContext, } from "./telemetry.js";
9
10
  import { APEX_CLI_VERSION } from "./version.js";
10
11
  const MCP_WORKFLOW_GUIDE = `Use Apex through these tools instead of shelling out to the CLI.
11
12
 
@@ -139,14 +140,20 @@ function errorResult(toolName, error) {
139
140
  isError: true,
140
141
  };
141
142
  }
142
- async function runTool(toolName, action, summary) {
143
- try {
144
- const value = await action();
145
- return successResult(summary(value), value);
146
- }
147
- catch (error) {
148
- return errorResult(toolName, error);
149
- }
143
+ async function runTool(toolName, action, summary, input = {}) {
144
+ const invocation = createMcpTelemetryInvocation(toolName, input);
145
+ emitInvocationStarted(invocation);
146
+ return withTelemetryContext(invocation, async () => {
147
+ try {
148
+ const value = await action();
149
+ emitInvocationCompleted(invocation);
150
+ return successResult(summary(value), value);
151
+ }
152
+ catch (error) {
153
+ emitInvocationCompleted(invocation, error);
154
+ return errorResult(toolName, error);
155
+ }
156
+ });
150
157
  }
151
158
  async function requireAuthenticated(client) {
152
159
  const me = await getMe(client);
@@ -235,7 +242,7 @@ function registerTools(server) {
235
242
  return "Device login expired before approval.";
236
243
  }
237
244
  return "Device login is still pending.";
238
- }));
245
+ }, { deviceCode, intervalSeconds, expiresAt, timeoutSeconds }));
239
246
  server.registerTool("apex-logout", {
240
247
  title: "Log Out Of Apex",
241
248
  description: "Clear the locally stored Apex session.",
@@ -268,7 +275,13 @@ function registerTools(server) {
268
275
  cwd: targetCwd,
269
276
  ...payload,
270
277
  };
271
- }, (value) => `Apex doctor completed for ${String(value.cwd)}.`));
278
+ }, (value) => `Apex doctor completed for ${String(value.cwd)}.`, {
279
+ cwd,
280
+ company,
281
+ workspaceName,
282
+ repoPaths,
283
+ sourceMode,
284
+ }));
272
285
  server.registerTool("apex-credits", {
273
286
  title: "Get Apex Credits",
274
287
  description: "Show scan credits plus audit and fix review scan entitlements for the active or selected company.",
@@ -287,7 +300,10 @@ function registerTools(server) {
287
300
  cwd: targetCwd,
288
301
  ...payload,
289
302
  };
290
- }, (value) => `Fetched Apex credits for ${String(value.cwd)}.`));
303
+ }, (value) => `Fetched Apex credits for ${String(value.cwd)}.`, {
304
+ cwd,
305
+ company,
306
+ }));
291
307
  server.registerTool("apex-workspace", {
292
308
  title: "Get Current Apex Workspace Binding",
293
309
  description: "Show the current Apex workspace bound to a directory, if any.",
@@ -301,7 +317,9 @@ function registerTools(server) {
301
317
  cwd: targetCwd,
302
318
  ...payload,
303
319
  };
304
- }, (value) => `Loaded workspace binding for ${String(value.cwd)}.`));
320
+ }, (value) => `Loaded workspace binding for ${String(value.cwd)}.`, {
321
+ cwd,
322
+ }));
305
323
  server.registerTool("apex-workspaces", {
306
324
  title: "List Apex Workspaces",
307
325
  description: "List workspaces available to the active or selected company.",
@@ -320,7 +338,10 @@ function registerTools(server) {
320
338
  cwd: targetCwd,
321
339
  ...payload,
322
340
  };
323
- }, (value) => `Listed Apex workspaces for ${String(value.cwd)}.`));
341
+ }, (value) => `Listed Apex workspaces for ${String(value.cwd)}.`, {
342
+ cwd,
343
+ company,
344
+ }));
324
345
  server.registerTool("apex-workspace-use", {
325
346
  title: "Bind Directory To Apex Workspace",
326
347
  description: "Bind a directory to an existing Apex workspace by id, prefix, or name.",
@@ -340,7 +361,11 @@ function registerTools(server) {
340
361
  cwd: targetCwd,
341
362
  ...payload,
342
363
  };
343
- }, (value) => `Bound ${String(value.cwd)} to an Apex workspace.`));
364
+ }, (value) => `Bound ${String(value.cwd)} to an Apex workspace.`, {
365
+ cwd,
366
+ company,
367
+ workspaceRef,
368
+ }));
344
369
  server.registerTool("apex-scan", {
345
370
  title: "Start Apex Scan",
346
371
  description: "Start a new Apex scan for the provided cwd by default. Pass repoPaths only to scan explicit alternate local roots. Use mode audit for audit scans and mode pr for GitHub pull request scans.",
@@ -378,7 +403,16 @@ function registerTools(server) {
378
403
  cwd: targetCwd,
379
404
  ...payload,
380
405
  };
381
- }, (value) => `Started an Apex scan for ${String(value.cwd)}.`));
406
+ }, (value) => `Started an Apex scan for ${String(value.cwd)}.`, {
407
+ cwd,
408
+ company,
409
+ workspaceName,
410
+ repoPaths,
411
+ mode,
412
+ pullRequests,
413
+ sourceMode,
414
+ force,
415
+ }));
382
416
  server.registerTool("apex-status", {
383
417
  title: "Get Apex Scan Status",
384
418
  description: "Show progress for the most recent or selected Apex scan in a directory.",
@@ -395,7 +429,10 @@ function registerTools(server) {
395
429
  cwd: targetCwd,
396
430
  ...payload,
397
431
  };
398
- }, (value) => `Fetched scan status for ${String(value.cwd)}.`));
432
+ }, (value) => `Fetched scan status for ${String(value.cwd)}.`, {
433
+ cwd,
434
+ scanId,
435
+ }));
399
436
  server.registerTool("apex-scans", {
400
437
  title: "List Apex Scans",
401
438
  description: "List scans for the Apex workspace bound to a directory.",
@@ -411,7 +448,9 @@ function registerTools(server) {
411
448
  cwd: targetCwd,
412
449
  ...payload,
413
450
  };
414
- }, (value) => `Listed scans for ${String(value.cwd)}.`));
451
+ }, (value) => `Listed scans for ${String(value.cwd)}.`, {
452
+ cwd,
453
+ }));
415
454
  server.registerTool("apex-cancel-scan", {
416
455
  title: "Cancel Apex Scan",
417
456
  description: "Cancel a running Apex scan in the bound workspace.",
@@ -428,7 +467,10 @@ function registerTools(server) {
428
467
  cwd: targetCwd,
429
468
  ...payload,
430
469
  };
431
- }, (value) => `Cancelled the selected Apex scan for ${String(value.cwd)}.`));
470
+ }, (value) => `Cancelled the selected Apex scan for ${String(value.cwd)}.`, {
471
+ cwd,
472
+ scanId,
473
+ }));
432
474
  server.registerTool("apex-findings", {
433
475
  title: "List Apex Findings",
434
476
  description: "List findings for the latest or selected Apex scan in a directory.",
@@ -449,7 +491,11 @@ function registerTools(server) {
449
491
  cwd: targetCwd,
450
492
  ...payload,
451
493
  };
452
- }, (value) => `Fetched Apex findings for ${String(value.cwd)}.`));
494
+ }, (value) => `Fetched Apex findings for ${String(value.cwd)}.`, {
495
+ cwd,
496
+ scanId,
497
+ limit,
498
+ }));
453
499
  server.registerTool("apex-finding-comment", {
454
500
  title: "Add Apex Finding Comment",
455
501
  description: "Add a comment or note to an Apex finding using the current Apex login.",
@@ -470,7 +516,13 @@ function registerTools(server) {
470
516
  cwd: targetCwd,
471
517
  ...payload,
472
518
  };
473
- }, (value) => `Added a finding comment for ${String(value.findingRef)}.`));
519
+ }, (value) => `Added a finding comment for ${String(value.findingRef)}.`, {
520
+ cwd,
521
+ findingRef,
522
+ content,
523
+ parentCommentId,
524
+ scanId,
525
+ }));
474
526
  server.registerTool("apex-finding-feedback", {
475
527
  title: "Leave Apex Finding Feedback",
476
528
  description: "Leave valid or invalid feedback on an Apex finding using the current Apex login. To attach a fix PR, send status valid with label fixed and fixPrUrls.",
@@ -508,7 +560,17 @@ function registerTools(server) {
508
560
  ...payload,
509
561
  };
510
562
  }, (value) => `Submitted ${String((value.feedback?.feedbackType ??
511
- "finding"))} feedback for ${String(value.findingRef)}.`));
563
+ "finding"))} feedback for ${String(value.findingRef)}.`, {
564
+ cwd,
565
+ findingRef,
566
+ status,
567
+ comment,
568
+ labels,
569
+ fixPrUrls,
570
+ suggestedSeverity,
571
+ dismissalReason,
572
+ scanId,
573
+ }));
512
574
  server.registerTool("apex-finding-fix-review", {
513
575
  title: "Start Apex Finding Fix Review Scan",
514
576
  description: "Start a fix review scan for a finding after fixed feedback with one or more Fix PR URLs has been saved.",
@@ -527,7 +589,11 @@ function registerTools(server) {
527
589
  cwd: targetCwd,
528
590
  ...payload,
529
591
  };
530
- }, (value) => `Started fix review scan for ${String(value.findingRef)}.`));
592
+ }, (value) => `Started fix review scan for ${String(value.findingRef)}.`, {
593
+ cwd,
594
+ findingRef,
595
+ scanId,
596
+ }));
531
597
  server.registerTool("apex-export-findings", {
532
598
  title: "Export Apex Findings",
533
599
  description: "Export findings for the latest or selected Apex scan to a file on disk.",
@@ -556,7 +622,12 @@ function registerTools(server) {
556
622
  scan: payload.scan,
557
623
  outputPath: payload.outputPath,
558
624
  };
559
- }, (value) => `Exported Apex findings for ${String(value.cwd)}.`));
625
+ }, (value) => `Exported Apex findings for ${String(value.cwd)}.`, {
626
+ cwd,
627
+ scanId,
628
+ format,
629
+ output,
630
+ }));
560
631
  server.registerTool("apex-connect-provider", {
561
632
  title: "Get Apex Provider Connection URL",
562
633
  description: "Return the browser URL a user needs to connect GitHub or GitLab access for Apex. This tool never opens a browser.",
@@ -576,16 +647,22 @@ function registerTools(server) {
576
647
  cwd: targetCwd,
577
648
  ...payload,
578
649
  };
579
- }, (value) => `Fetched the ${String(value.provider)} connection URL.`));
650
+ }, (value) => `Fetched the ${String(value.provider)} connection URL.`, {
651
+ cwd,
652
+ company,
653
+ provider,
654
+ }));
580
655
  }
581
656
  export async function runMcpServer() {
582
657
  const server = new McpServer({
583
658
  name: "apex-cli",
584
659
  version: APEX_CLI_VERSION,
585
660
  });
661
+ const invocation = createMcpServerTelemetryInvocation();
586
662
  registerResources(server);
587
663
  registerTools(server);
588
664
  const transport = new StdioServerTransport();
665
+ emitMcpServerStarted(invocation);
589
666
  await server.connect(transport);
590
667
  }
591
668
  export const testing = {
package/dist/setup.js CHANGED
@@ -9,7 +9,10 @@ import { logLine, printJson } from "./output.js";
9
9
  const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
10
10
  const CODEX_SKILL_NAME = "apex-cli";
11
11
  const MCP_SERVER_NAME = "apex";
12
+ const MCP_CLIENT_ENV_NAME = "APEX_MCP_CLIENT";
13
+ const MCP_CLIENT_INTEGRATION_ENV_NAME = "APEX_CLIENT_INTEGRATION";
12
14
  const execFile = promisify(execFileCallback);
15
+ const SETUP_CLIENTS = ["codex", "claude", "copilot"];
13
16
  function quoteShellArg(value) {
14
17
  return /[^A-Za-z0-9_./:-]/.test(value)
15
18
  ? `'${value.replace(/'/g, `'\\''`)}'`
@@ -73,24 +76,82 @@ function getCodexHome() {
73
76
  }
74
77
  return path.join(os.homedir(), ".codex");
75
78
  }
79
+ function getCopilotHome() {
80
+ const configured = process.env.COPILOT_HOME?.trim();
81
+ if (configured) {
82
+ return configured;
83
+ }
84
+ return path.join(os.homedir(), ".copilot");
85
+ }
76
86
  function normalizeArgs(value) {
77
87
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
78
88
  }
89
+ function normalizeTools(value) {
90
+ if (Array.isArray(value)) {
91
+ return value.filter((item) => typeof item === "string");
92
+ }
93
+ if (typeof value === "string") {
94
+ return value
95
+ .split(",")
96
+ .map((item) => item.trim())
97
+ .filter(Boolean);
98
+ }
99
+ return [];
100
+ }
101
+ function mcpClientEnv(client) {
102
+ return {
103
+ [MCP_CLIENT_ENV_NAME]: client,
104
+ [MCP_CLIENT_INTEGRATION_ENV_NAME]: client,
105
+ };
106
+ }
107
+ function normalizeEnvKeys(value) {
108
+ if (Array.isArray(value)) {
109
+ return value.filter((item) => typeof item === "string");
110
+ }
111
+ if (value && typeof value === "object") {
112
+ return Object.keys(value);
113
+ }
114
+ return [];
115
+ }
116
+ function envMatches(value, expected) {
117
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
118
+ return false;
119
+ }
120
+ const env = value;
121
+ return Object.entries(expected).every(([key, expectedValue]) => env[key] === expectedValue);
122
+ }
123
+ function envKeyListMatches(value, expected) {
124
+ const keys = normalizeEnvKeys(value);
125
+ return Object.keys(expected).every((key) => keys.includes(key));
126
+ }
79
127
  function codexConfigMatches(existing, launch) {
80
128
  if (!existing?.transport) {
81
129
  return false;
82
130
  }
131
+ const expectedEnv = mcpClientEnv("codex");
83
132
  return (existing.transport.type === "stdio" &&
84
133
  existing.transport.command === launch.command &&
85
- JSON.stringify(normalizeArgs(existing.transport.args)) === JSON.stringify(launch.args));
134
+ JSON.stringify(normalizeArgs(existing.transport.args)) === JSON.stringify(launch.args) &&
135
+ (envMatches(existing.transport.env, expectedEnv) ||
136
+ envKeyListMatches(existing.transport.env_vars, expectedEnv)));
86
137
  }
87
138
  function claudeConfigMatches(existing, launch) {
139
+ const expectedEnv = mcpClientEnv("claude");
140
+ return (existing?.type === "stdio" &&
141
+ existing.command === launch.command &&
142
+ JSON.stringify(normalizeArgs(existing.args)) === JSON.stringify(launch.args) &&
143
+ envMatches(existing.env, expectedEnv));
144
+ }
145
+ function copilotConfigMatches(existing, launch) {
88
146
  const env = existing?.env;
89
147
  const normalizedEnv = env && typeof env === "object" && !Array.isArray(env) ? Object.keys(env).length : 0;
90
- return (existing?.type === "stdio" &&
148
+ const normalizedTools = normalizeTools(existing?.tools);
149
+ return ((existing?.type === "stdio" || existing?.type === "local") &&
91
150
  existing.command === launch.command &&
92
151
  JSON.stringify(normalizeArgs(existing.args)) === JSON.stringify(launch.args) &&
93
- normalizedEnv === 0);
152
+ normalizedEnv === 0 &&
153
+ normalizedTools.length === 1 &&
154
+ normalizedTools[0] === "*");
94
155
  }
95
156
  async function writeManagedFile(filePath, content) {
96
157
  const current = await readTextFile(filePath);
@@ -127,14 +188,40 @@ async function readClaudeUserMcpConfig() {
127
188
  return null;
128
189
  }
129
190
  }
191
+ async function readCopilotUserMcpConfig() {
192
+ const configPath = path.join(getCopilotHome(), "mcp-config.json");
193
+ const raw = await readTextFile(configPath);
194
+ if (!raw) {
195
+ return null;
196
+ }
197
+ try {
198
+ const parsed = JSON.parse(raw);
199
+ return parsed.mcpServers?.[MCP_SERVER_NAME] ?? null;
200
+ }
201
+ catch {
202
+ return null;
203
+ }
204
+ }
130
205
  async function configureCodex(launch) {
131
206
  const existing = await readCodexMcpConfig();
207
+ const env = mcpClientEnv("codex");
132
208
  const mcpStatus = existing === null
133
209
  ? "installed"
134
210
  : codexConfigMatches(existing, launch)
135
211
  ? "unchanged"
136
212
  : "updated";
137
- await execText("codex", ["mcp", "add", MCP_SERVER_NAME, "--", launch.command, ...launch.args]);
213
+ await execText("codex", [
214
+ "mcp",
215
+ "add",
216
+ MCP_SERVER_NAME,
217
+ "--env",
218
+ `${MCP_CLIENT_ENV_NAME}=${env[MCP_CLIENT_ENV_NAME]}`,
219
+ "--env",
220
+ `${MCP_CLIENT_INTEGRATION_ENV_NAME}=${env[MCP_CLIENT_INTEGRATION_ENV_NAME]}`,
221
+ "--",
222
+ launch.command,
223
+ ...launch.args,
224
+ ]);
138
225
  const skillSource = path.join(PACKAGE_ROOT, "skills", CODEX_SKILL_NAME, "SKILL.md");
139
226
  const skillTarget = path.join(getCodexHome(), "skills", CODEX_SKILL_NAME, "SKILL.md");
140
227
  const skillContent = await readFile(skillSource, "utf8");
@@ -158,6 +245,7 @@ async function configureCodex(launch) {
158
245
  }
159
246
  async function configureClaude(cwd, launch) {
160
247
  const existing = await readClaudeUserMcpConfig();
248
+ const env = mcpClientEnv("claude");
161
249
  const mcpStatus = existing === null
162
250
  ? "installed"
163
251
  : claudeConfigMatches(existing, launch)
@@ -171,6 +259,10 @@ async function configureClaude(cwd, launch) {
171
259
  "add",
172
260
  "--scope",
173
261
  "user",
262
+ "-e",
263
+ `${MCP_CLIENT_ENV_NAME}=${env[MCP_CLIENT_ENV_NAME]}`,
264
+ "-e",
265
+ `${MCP_CLIENT_INTEGRATION_ENV_NAME}=${env[MCP_CLIENT_INTEGRATION_ENV_NAME]}`,
174
266
  MCP_SERVER_NAME,
175
267
  "--",
176
268
  launch.command,
@@ -197,6 +289,49 @@ async function configureClaude(cwd, launch) {
197
289
  },
198
290
  ];
199
291
  }
292
+ async function configureCopilot(launch) {
293
+ const existing = await readCopilotUserMcpConfig();
294
+ const mcpStatus = existing === null
295
+ ? "installed"
296
+ : copilotConfigMatches(existing, launch)
297
+ ? "unchanged"
298
+ : "updated";
299
+ if (existing) {
300
+ await execText("copilot", ["mcp", "remove", MCP_SERVER_NAME]);
301
+ }
302
+ await execText("copilot", [
303
+ "mcp",
304
+ "add",
305
+ MCP_SERVER_NAME,
306
+ "--type",
307
+ "stdio",
308
+ "--tools",
309
+ "*",
310
+ "--",
311
+ launch.command,
312
+ ...launch.args,
313
+ ]);
314
+ const skillSource = path.join(PACKAGE_ROOT, "skills", CODEX_SKILL_NAME, "SKILL.md");
315
+ const skillTarget = path.join(getCopilotHome(), "skills", CODEX_SKILL_NAME, "SKILL.md");
316
+ const skillContent = await readFile(skillSource, "utf8");
317
+ const skillStatus = await writeManagedFile(skillTarget, skillContent);
318
+ return [
319
+ {
320
+ client: "copilot",
321
+ kind: "mcp",
322
+ status: mcpStatus,
323
+ path: path.join(getCopilotHome(), "mcp-config.json"),
324
+ detail: `Registered ${MCP_SERVER_NAME} -> ${launch.label} in GitHub Copilot CLI user config`,
325
+ },
326
+ {
327
+ client: "copilot",
328
+ kind: "skill",
329
+ status: skillStatus,
330
+ path: skillTarget,
331
+ detail: "Installed the Apex GitHub Copilot CLI skill",
332
+ },
333
+ ];
334
+ }
200
335
  function summarizeStep(step) {
201
336
  const prefix = `${step.client} ${step.kind}`;
202
337
  if (step.path) {
@@ -204,29 +339,51 @@ function summarizeStep(step) {
204
339
  }
205
340
  return `${prefix}: ${step.status}`;
206
341
  }
342
+ function isSetupClient(value) {
343
+ return SETUP_CLIENTS.includes(value);
344
+ }
345
+ function displayClientName(client) {
346
+ switch (client) {
347
+ case "codex":
348
+ return "Codex";
349
+ case "claude":
350
+ return "Claude Code";
351
+ case "copilot":
352
+ return "GitHub Copilot CLI";
353
+ }
354
+ }
355
+ function isMissingClientError(error) {
356
+ return error instanceof Error && /spawn (codex|claude|copilot) ENOENT/i.test(error.message);
357
+ }
358
+ async function configureClient(client, cwd, launch) {
359
+ switch (client) {
360
+ case "codex":
361
+ return configureCodex(launch);
362
+ case "claude":
363
+ return configureClaude(cwd, launch);
364
+ case "copilot":
365
+ return configureCopilot(launch);
366
+ }
367
+ }
207
368
  export async function commandSetup(cwd, flags, requestedTarget, packageRoot = PACKAGE_ROOT) {
208
- if (requestedTarget !== null &&
209
- requestedTarget !== "all" &&
210
- requestedTarget !== "codex" &&
211
- requestedTarget !== "claude") {
212
- throw new Error("Usage: apex setup [all|codex|claude]");
369
+ if (requestedTarget !== null && requestedTarget !== "all" && !isSetupClient(requestedTarget)) {
370
+ throw new Error("Usage: apex setup [all|codex|claude|copilot]");
213
371
  }
214
372
  const target = requestedTarget ?? "all";
215
373
  const launch = await resolveMcpLaunchSpec(packageRoot);
216
374
  const steps = [];
217
- const requestedClients = target === "all" ? ["codex", "claude"] : [target];
375
+ const requestedClients = target === "all" ? [...SETUP_CLIENTS] : [target];
218
376
  let configuredClients = 0;
219
377
  for (const client of requestedClients) {
220
378
  try {
221
- const clientSteps = client === "codex" ? await configureCodex(launch) : await configureClaude(cwd, launch);
379
+ const clientSteps = await configureClient(client, cwd, launch);
222
380
  steps.push(...clientSteps);
223
381
  configuredClients += 1;
224
382
  }
225
383
  catch (error) {
226
- const missingClient = error instanceof Error &&
227
- /spawn (codex|claude) ENOENT/i.test(error.message);
384
+ const missingClient = isMissingClientError(error);
228
385
  if (missingClient && target !== "all") {
229
- throw new Error(`${client === "codex" ? "Codex" : "Claude Code"} is not installed on this machine. Install it first, then re-run \`apex setup ${client}\`.`);
386
+ throw new Error(`${displayClientName(client)} is not installed on this machine. Install it first, then re-run \`apex setup ${client}\`.`);
230
387
  }
231
388
  if (!missingClient) {
232
389
  throw error;
@@ -236,19 +393,19 @@ export async function commandSetup(cwd, flags, requestedTarget, packageRoot = PA
236
393
  kind: "mcp",
237
394
  status: "skipped",
238
395
  path: null,
239
- detail: `${client} is not installed on this machine`,
396
+ detail: `${displayClientName(client)} is not installed on this machine`,
240
397
  });
241
398
  steps.push({
242
399
  client,
243
400
  kind: "skill",
244
401
  status: "skipped",
245
402
  path: null,
246
- detail: `${client} is not installed on this machine`,
403
+ detail: `${displayClientName(client)} is not installed on this machine`,
247
404
  });
248
405
  }
249
406
  }
250
407
  if (configuredClients === 0) {
251
- throw new Error("Neither Codex nor Claude Code is installed. Install one of them first, then re-run `apex setup`.");
408
+ throw new Error("Neither Codex, Claude Code, nor GitHub Copilot CLI is installed. Install one of them first, then re-run `apex setup`.");
252
409
  }
253
410
  const payload = {
254
411
  target,
@@ -271,5 +428,8 @@ export async function commandSetup(cwd, flags, requestedTarget, packageRoot = PA
271
428
  if (requestedClients.includes("claude")) {
272
429
  logLine("Re-run `apex setup claude` in each repository where you want the Claude project skill.", flags);
273
430
  }
431
+ if (requestedClients.includes("copilot")) {
432
+ logLine("Restart GitHub Copilot CLI or run `/skills reload` to pick up a newly installed or updated skill.", flags);
433
+ }
274
434
  return payload;
275
435
  }
package/dist/shell.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { SHELL_HELP_TEXT } from "./help.js";
2
- import { commandCancelScan, commandConnect, commandCredits, commandDoctor, commandExportFindings, commandFindingComment, commandFindingFeedback, commandFindingFixReview, commandFindings, commandLogout, commandScan, commandScans, commandStatus, commandUpdate, commandWorkspace, commandWorkspaceUse, commandWorkspaces, initializeInteractiveSession, openCurrentApexView, } from "./commands.js";
2
+ import { commandCancelScan, commandConnect, commandCredits, commandDoctor, commandExportFindings, commandFindingComment, commandFindingFeedback, commandFindingFixReview, commandFindings, commandLogout, commandScan, commandScans, commandStatus, commandTelemetry, commandUpdate, commandWorkspace, commandWorkspaceUse, commandWorkspaces, initializeInteractiveSession, openCurrentApexView, } from "./commands.js";
3
3
  import { withFlag } from "./args.js";
4
4
  import { printCompanyDetails, printInteractiveSessionSummary, printSourceList, printWorkspaceDetails, } from "./output.js";
5
5
  import { readLine } from "./prompt.js";
6
6
  import { formatApiError } from "./api-client.js";
7
+ import { createShellTelemetryInvocation, emitInvocationCompleted, emitInvocationStarted, withTelemetryContext, } from "./telemetry.js";
7
8
  const SHELL_COMPLETIONS = [
8
9
  { command: "credits" },
9
10
  { command: "scan", args: ["standard", "audit", "ultra", "pr"] },
@@ -15,6 +16,7 @@ const SHELL_COMPLETIONS = [
15
16
  { command: "cancel" },
16
17
  { command: "status" },
17
18
  { command: "doctor" },
19
+ { command: "telemetry", args: ["status", "enable", "disable"] },
18
20
  { command: "update" },
19
21
  { command: "logout" },
20
22
  { command: "repos" },
@@ -151,7 +153,7 @@ export async function runInteractiveShell(client, cwd, initialFlags) {
151
153
  }
152
154
  let shouldContinue;
153
155
  try {
154
- shouldContinue = await runShellCommand(client, cwd, parsed, shellFlags, session);
156
+ shouldContinue = await runShellCommandWithTelemetry(client, cwd, parsed, shellFlags, session);
155
157
  }
156
158
  catch (error) {
157
159
  process.stderr.write(`${formatApiError(error)}\n`);
@@ -168,6 +170,21 @@ export async function runInteractiveShell(client, cwd, initialFlags) {
168
170
  }
169
171
  }
170
172
  }
173
+ async function runShellCommandWithTelemetry(client, cwd, parsed, shellFlags, session) {
174
+ const invocation = createShellTelemetryInvocation(parsed, shellFlags);
175
+ emitInvocationStarted(invocation);
176
+ return withTelemetryContext(invocation, async () => {
177
+ try {
178
+ const result = await runShellCommand(client, cwd, parsed, shellFlags, session);
179
+ emitInvocationCompleted(invocation);
180
+ return result;
181
+ }
182
+ catch (error) {
183
+ emitInvocationCompleted(invocation, error);
184
+ throw error;
185
+ }
186
+ });
187
+ }
171
188
  async function runShellCommand(client, cwd, parsed, shellFlags, session) {
172
189
  switch (parsed.command) {
173
190
  case "help":
@@ -300,6 +317,14 @@ async function runShellCommand(client, cwd, parsed, shellFlags, session) {
300
317
  case "doctor":
301
318
  await commandDoctor(client, cwd, shellFlags);
302
319
  return {};
320
+ case "telemetry": {
321
+ if (parsed.args.length > 1) {
322
+ process.stderr.write("Usage: /telemetry [status|enable|disable]\n");
323
+ return {};
324
+ }
325
+ await commandTelemetry(shellFlags, parsed.args[0] ?? null);
326
+ return {};
327
+ }
303
328
  case "update":
304
329
  await commandUpdate(shellFlags);
305
330
  return false;