@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/.claude/skills/apex-cli/SKILL.md +3 -0
- package/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.mcp.claude.json +6 -2
- package/.mcp.codex.json +6 -2
- package/MARKETPLACE.md +1 -1
- package/README.md +91 -10
- package/dist/apex.js +21 -3
- package/dist/api-client.js +5 -0
- package/dist/commands.js +36 -0
- package/dist/config.js +4 -0
- package/dist/help.js +8 -1
- package/dist/mcp.js +101 -24
- package/dist/setup.js +177 -17
- package/dist/shell.js +27 -2
- package/dist/telemetry.js +755 -0
- package/dist/update.js +1 -1
- package/package.json +1 -1
- package/skills/apex-cli/SKILL.md +4 -1
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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", [
|
|
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
|
-
|
|
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" ? [
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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;
|