@controllerai/cli 0.2.14 → 0.2.16

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 (62) hide show
  1. package/README.md +13 -4
  2. package/dist/assistantHosts.js +234 -0
  3. package/dist/assistantHosts.js.map +1 -0
  4. package/dist/commands/agent.js +2 -0
  5. package/dist/commands/agent.js.map +1 -1
  6. package/dist/commands/agentSetup.js +627 -0
  7. package/dist/commands/agentSetup.js.map +1 -0
  8. package/dist/commands/connect.js +29 -26
  9. package/dist/commands/connect.js.map +1 -1
  10. package/dist/commands/connection.js +98 -13
  11. package/dist/commands/connection.js.map +1 -1
  12. package/dist/commands/data.js +26 -15
  13. package/dist/commands/data.js.map +1 -1
  14. package/dist/commands/docs.js +95 -0
  15. package/dist/commands/docs.js.map +1 -0
  16. package/dist/commands/doctor.js +357 -13
  17. package/dist/commands/doctor.js.map +1 -1
  18. package/dist/commands/exec.js +284 -133
  19. package/dist/commands/exec.js.map +1 -1
  20. package/dist/commands/integration.js +170 -49
  21. package/dist/commands/integration.js.map +1 -1
  22. package/dist/commands/node.js +11 -287
  23. package/dist/commands/node.js.map +1 -1
  24. package/dist/commands/organize.js +182 -219
  25. package/dist/commands/organize.js.map +1 -1
  26. package/dist/commands/run.js +1 -1
  27. package/dist/commands/run.js.map +1 -1
  28. package/dist/commands/skills.js +2 -3
  29. package/dist/commands/skills.js.map +1 -1
  30. package/dist/commands/trigger.js +14 -8
  31. package/dist/commands/trigger.js.map +1 -1
  32. package/dist/commands/upgrade.js +69 -0
  33. package/dist/commands/upgrade.js.map +1 -0
  34. package/dist/commands/workflow.js +203 -61
  35. package/dist/commands/workflow.js.map +1 -1
  36. package/dist/index.js +9 -1
  37. package/dist/index.js.map +1 -1
  38. package/dist/nodeProps.js +1 -1
  39. package/dist/nodeProps.js.map +1 -1
  40. package/package.json +2 -2
  41. package/skills/controllerai-agents/SKILL.md +26 -18
  42. package/skills/controllerai-agents/references/instructions-and-versioning.md +2 -0
  43. package/skills/controllerai-ai-nodes/SKILL.md +9 -5
  44. package/skills/controllerai-ai-nodes/references/selection-and-testing.md +2 -0
  45. package/skills/controllerai-data/SKILL.md +17 -7
  46. package/skills/controllerai-data/references/data-rails.md +4 -2
  47. package/skills/controllerai-expressions/SKILL.md +17 -5
  48. package/skills/controllerai-expressions/references/dsl-and-recipes.md +5 -1
  49. package/skills/controllerai-flow-control/SKILL.md +11 -4
  50. package/skills/controllerai-flow-control/references/patterns.md +4 -0
  51. package/skills/controllerai-integration-actions/SKILL.md +20 -12
  52. package/skills/controllerai-node-config/SKILL.md +30 -9
  53. package/skills/controllerai-node-config/references/prop-and-readiness.md +9 -3
  54. package/skills/controllerai-runs/SKILL.md +20 -10
  55. package/skills/controllerai-runs/references/debugging.md +11 -7
  56. package/skills/controllerai-start/SKILL.md +36 -8
  57. package/skills/controllerai-start/references/mcp-tool-map.md +170 -0
  58. package/skills/controllerai-triggers/SKILL.md +13 -7
  59. package/skills/controllerai-triggers/references/lifecycle.md +5 -3
  60. package/skills/controllerai-workflows/SKILL.md +16 -10
  61. package/skills/controllerai-workflows/references/command-map.md +21 -6
  62. package/skills/manifest.json +79 -73
@@ -0,0 +1,627 @@
1
+ import { createInterface } from 'node:readline/promises';
2
+ import { readFileSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { stdin, stderr } from 'node:process';
6
+ import { ASSISTANT_DOCS, CLAUDE_PLUGIN_ID, CLAUDE_PLUGIN_MARKETPLACE_SOURCE, CLAUDE_REPOSITORY_PLUGIN_ID, CODEX_PLUGIN_ID, CODEX_PLUGIN_MARKETPLACE_SOURCE, CONTROLLERAI_MCP_URL, commandAlreadySatisfied, commandDisplay, detectAssistants, parsePluginInstallation, runExternalCommand, } from '../assistantHosts.js';
7
+ import { userError } from '../errors.js';
8
+ import { emit, printKeyValues, printTable, warn } from '../output.js';
9
+ import { authenticationStatus, ensureAuthentication } from './connect.js';
10
+ import { PUBLIC_SKILLS, installSkills, installedSkillStatus, } from './skills.js';
11
+ const manualSetup = () => [
12
+ { client: 'claude', url: ASSISTANT_DOCS.claude },
13
+ { client: 'cowork', url: ASSISTANT_DOCS.cowork },
14
+ { client: 'chatgpt', url: ASSISTANT_DOCS.chatgpt },
15
+ ];
16
+ export const CURSOR_MCP_CONFIG = {
17
+ mcpServers: {
18
+ controllerai: { type: 'http', url: CONTROLLERAI_MCP_URL },
19
+ },
20
+ };
21
+ const selected = (client, detection) => client === 'all' || client === detection.client;
22
+ const targetFor = (client, scope) => scope === 'project' ? 'project' : client === 'claude-code' ? 'claude' : client;
23
+ const summarizeSkills = (target, rows) => {
24
+ const present = rows.filter(row => row.status !== 'missing');
25
+ const statuses = rows.reduce((counts, row) => {
26
+ counts[row.status] = (counts[row.status] ?? 0) + 1;
27
+ return counts;
28
+ }, {});
29
+ return {
30
+ target,
31
+ installed: present.length,
32
+ total: PUBLIC_SKILLS.length,
33
+ current: rows.length === PUBLIC_SKILLS.length && rows.every(row => row.status === 'current'),
34
+ versions: [
35
+ ...new Set(present
36
+ .map(row => row.skillSetVersion)
37
+ .filter((version) => typeof version === 'string')),
38
+ ],
39
+ statuses,
40
+ };
41
+ };
42
+ const urlsIn = (value) => (value.match(/https?:\/\/[^\s"'`<>]+/g) ?? []).map(url => url.replace(/[),.;\]}]+$/g, ''));
43
+ const mcpInstalled = (result) => result.exitCode === 0 &&
44
+ urlsIn(`${result.stdout}\n${result.stderr}`).includes(CONTROLLERAI_MCP_URL);
45
+ const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
46
+ const codexMcpServer = (result) => {
47
+ if (result.exitCode !== 0)
48
+ return undefined;
49
+ let parsed;
50
+ try {
51
+ parsed = JSON.parse(result.stdout);
52
+ }
53
+ catch {
54
+ return undefined;
55
+ }
56
+ if (!Array.isArray(parsed))
57
+ return undefined;
58
+ const server = parsed.find(row => isRecord(row) && row.name === 'controllerai');
59
+ return isRecord(server) ? server : undefined;
60
+ };
61
+ const codexMcpInstalled = (result) => {
62
+ const server = codexMcpServer(result);
63
+ if (!server || !isRecord(server.transport))
64
+ return false;
65
+ return (server.transport.type === 'streamable_http' && server.transport.url === CONTROLLERAI_MCP_URL);
66
+ };
67
+ export const parseCodexMcpAuthentication = (result) => {
68
+ const server = codexMcpServer(result);
69
+ if (!server || typeof server.auth_status !== 'string')
70
+ return 'unknown';
71
+ if (['oauth', 'bearer_token'].includes(server.auth_status))
72
+ return 'authenticated';
73
+ if (server.auth_status === 'not_logged_in')
74
+ return 'unauthenticated';
75
+ return 'unknown';
76
+ };
77
+ const cursorConfigHasControllerAi = (path) => {
78
+ try {
79
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
80
+ if (!isRecord(parsed) || !isRecord(parsed.mcpServers))
81
+ return false;
82
+ const server = parsed.mcpServers.controllerai;
83
+ return isRecord(server) && server.type === 'http' && server.url === CONTROLLERAI_MCP_URL;
84
+ }
85
+ catch {
86
+ return false;
87
+ }
88
+ };
89
+ export const cursorMcpConfigured = (scope, options = {}) => {
90
+ const root = scope === 'project' ? (options.projectDir ?? process.cwd()) : (options.homeDir ?? homedir());
91
+ return cursorConfigHasControllerAi(join(root, '.cursor', 'mcp.json'));
92
+ };
93
+ const probePlugin = async (client, runner, codexMarketplaceSource) => {
94
+ const command = client === 'claude-code' ? 'claude' : 'codex';
95
+ const ids = client === 'claude-code' ? [CLAUDE_PLUGIN_ID, CLAUDE_REPOSITORY_PLUGIN_ID] : [CODEX_PLUGIN_ID];
96
+ const listing = await runner(command, ['plugin', 'list', '--json'], {
97
+ echoCommand: true,
98
+ echoOutput: false,
99
+ });
100
+ const installation = listing.exitCode === 0
101
+ ? parsePluginInstallation(listing.stdout, ids)
102
+ : { installed: false };
103
+ if (installation.installed)
104
+ return { ...installation, supported: true };
105
+ const help = await runner(command, ['plugin', '--help'], {
106
+ echoCommand: true,
107
+ echoOutput: false,
108
+ });
109
+ return {
110
+ ...installation,
111
+ supported: help.exitCode === 0 && (client === 'claude-code' || Boolean(codexMarketplaceSource)),
112
+ };
113
+ };
114
+ export const inventoryAgentSetup = async (options, dependencies = {}) => {
115
+ const requestedClient = options.client ?? 'all';
116
+ const scope = options.skillsScope ?? 'global';
117
+ const detected = typeof dependencies.detections === 'function'
118
+ ? dependencies.detections()
119
+ : (dependencies.detections ?? detectAssistants());
120
+ const runner = dependencies.runCommand ?? runExternalCommand;
121
+ const skillStatus = dependencies.skillStatus ?? (target => installedSkillStatus({ target }));
122
+ const codexMarketplaceSource = dependencies.codexMarketplaceSource ?? CODEX_PLUGIN_MARKETPLACE_SOURCE;
123
+ const states = [];
124
+ for (const detection of detected.filter(row => selected(requestedClient, row))) {
125
+ const target = targetFor(detection.client, scope);
126
+ const skills = summarizeSkills(target, skillStatus(target));
127
+ if (!detection.detected) {
128
+ states.push({
129
+ ...detection,
130
+ plugin: { installed: false, supported: false },
131
+ mcp: { installed: false },
132
+ skills,
133
+ current: false,
134
+ note: 'assistant is not installed',
135
+ });
136
+ continue;
137
+ }
138
+ if (detection.client === 'cursor') {
139
+ const mcp = (dependencies.cursorMcpConfigured ?? cursorMcpConfigured)(scope);
140
+ states.push({
141
+ ...detection,
142
+ plugin: { installed: false, supported: false },
143
+ mcp: { installed: mcp },
144
+ skills,
145
+ current: mcp && skills.current,
146
+ ...(!mcp || !skills.current
147
+ ? {
148
+ note: mcp
149
+ ? 'skills need installation'
150
+ : skills.current
151
+ ? 'paste the printed MCP configuration'
152
+ : 'skills and MCP config needed',
153
+ }
154
+ : {}),
155
+ });
156
+ continue;
157
+ }
158
+ const plugin = await probePlugin(detection.client, runner, codexMarketplaceSource);
159
+ const command = detection.client === 'claude-code' ? 'claude' : 'codex';
160
+ const mcpArgs = detection.client === 'claude-code'
161
+ ? ['mcp', 'get', 'controllerai']
162
+ : ['mcp', 'list', '--json'];
163
+ const mcpResult = await runner(command, mcpArgs, {
164
+ echoCommand: true,
165
+ echoOutput: false,
166
+ });
167
+ const mcp = detection.client === 'codex' ? codexMcpInstalled(mcpResult) : mcpInstalled(mcpResult);
168
+ const mcpAuthentication = detection.client === 'codex' ? parseCodexMcpAuthentication(mcpResult) : undefined;
169
+ const current = (plugin.installed || (mcp && skills.current)) &&
170
+ (detection.client !== 'codex' || mcpAuthentication === 'authenticated');
171
+ states.push({
172
+ ...detection,
173
+ plugin,
174
+ mcp: {
175
+ installed: mcp,
176
+ ...(mcpAuthentication ? { authentication: mcpAuthentication } : {}),
177
+ },
178
+ skills,
179
+ current,
180
+ });
181
+ }
182
+ return states;
183
+ };
184
+ const action = (client, kind, argv, condition, reason) => ({
185
+ id: `${client}:${kind}`,
186
+ client,
187
+ kind,
188
+ ...(argv ? { command: commandDisplay(argv[0], argv.slice(1)), argv } : {}),
189
+ condition,
190
+ reason,
191
+ });
192
+ export const buildAgentSetupPlan = (input) => {
193
+ const plannedActions = [];
194
+ const detectedClients = input.assistants.filter(row => row.detected).map(row => row.client);
195
+ if (input.authentication.status === 'missing') {
196
+ plannedActions.push(action('account', 'connect', ['cai', 'connect'], 'required', 'authorize this machine'));
197
+ }
198
+ const addSkillAction = (client, condition, reason) => {
199
+ const target = targetFor(client, input.skillsScope);
200
+ const argv = ['cai', 'skills', 'install', '--target', target];
201
+ const command = commandDisplay(argv[0], argv.slice(1));
202
+ const existing = plannedActions.find(row => row.kind === 'skills-install' && row.command === command);
203
+ if (existing) {
204
+ if (condition === 'required' && existing.condition !== 'required') {
205
+ existing.condition = 'required';
206
+ existing.reason = reason;
207
+ }
208
+ return;
209
+ }
210
+ plannedActions.push(action(input.skillsScope === 'project' ? 'shared' : client, 'skills-install', argv, condition, reason));
211
+ };
212
+ for (const assistant of input.assistants) {
213
+ if (!assistant.detected)
214
+ continue;
215
+ if (assistant.client === 'cursor') {
216
+ if (assistant.current)
217
+ continue;
218
+ if (!assistant.skills.current) {
219
+ addSkillAction('cursor', 'required', 'install Controller AI skills for Cursor');
220
+ }
221
+ if (!assistant.mcp.installed) {
222
+ plannedActions.push(action('cursor', 'print-mcp-config', undefined, 'required', 'Cursor MCP configuration must be pasted manually'));
223
+ }
224
+ continue;
225
+ }
226
+ const pluginPreferred = !assistant.plugin.installed &&
227
+ assistant.plugin.supported &&
228
+ (assistant.client === 'claude-code' || Boolean(input.codexMarketplaceSource));
229
+ const codexLoginRequired = assistant.client === 'codex' && assistant.mcp.authentication !== 'authenticated';
230
+ if (assistant.current && !pluginPreferred && !codexLoginRequired)
231
+ continue;
232
+ if (pluginPreferred) {
233
+ if (assistant.client === 'claude-code') {
234
+ plannedActions.push(action(assistant.client, 'plugin-marketplace-add', ['claude', 'plugin', 'marketplace', 'add', CLAUDE_PLUGIN_MARKETPLACE_SOURCE], 'required', 'add the Controller AI Claude marketplace'), action(assistant.client, 'plugin-install', ['claude', 'plugin', 'install', CLAUDE_REPOSITORY_PLUGIN_ID], 'required', 'install the Controller AI plugin'));
235
+ }
236
+ else if (input.codexMarketplaceSource) {
237
+ plannedActions.push(action(assistant.client, 'plugin-marketplace-add', ['codex', 'plugin', 'marketplace', 'add', input.codexMarketplaceSource], 'required', 'add the Controller AI Codex marketplace'), action(assistant.client, 'plugin-install', ['codex', 'plugin', 'add', CODEX_PLUGIN_ID], 'required', 'install the Controller AI plugin'));
238
+ }
239
+ }
240
+ const fallbackCondition = assistant.plugin.supported
241
+ ? 'if-plugin-fails'
242
+ : 'required';
243
+ if (!assistant.mcp.installed) {
244
+ const argv = assistant.client === 'claude-code'
245
+ ? ['claude', 'mcp', 'add', '--transport', 'http', 'controllerai', CONTROLLERAI_MCP_URL]
246
+ : ['codex', 'mcp', 'add', 'controllerai', '--url', CONTROLLERAI_MCP_URL];
247
+ plannedActions.push(action(assistant.client, 'mcp-add', argv, fallbackCondition, 'plugin unavailable; configure Controller AI MCP directly'));
248
+ }
249
+ if (!assistant.skills.current) {
250
+ addSkillAction(assistant.client, fallbackCondition, 'plugin unavailable; install the bundled Controller AI skills');
251
+ }
252
+ if (codexLoginRequired) {
253
+ const authenticationKnown = assistant.mcp.authentication === 'unauthenticated';
254
+ plannedActions.push(action('codex', 'mcp-login', ['codex', 'mcp', 'login', 'controllerai'], 'required', authenticationKnown
255
+ ? 'Controller AI MCP is not authenticated in Codex'
256
+ : 'Codex could not determine Controller AI MCP authentication; run login to verify it'));
257
+ }
258
+ }
259
+ const nextSteps = [];
260
+ if (plannedActions.some(row => row.client === 'claude-code' && ['plugin-install', 'mcp-add'].includes(row.kind))) {
261
+ nextSteps.push({
262
+ client: 'claude-code',
263
+ text: 'In Claude Code, open /mcp, select controllerai, then choose Authenticate.',
264
+ });
265
+ }
266
+ if (plannedActions.some(row => row.client === 'cursor' && row.kind === 'print-mcp-config')) {
267
+ nextSteps.push({
268
+ client: 'cursor',
269
+ text: "Paste the printed MCP configuration into Cursor, then sign in from Cursor's MCP settings.",
270
+ });
271
+ }
272
+ return {
273
+ statusOnly: input.statusOnly,
274
+ requestedClient: input.requestedClient,
275
+ skillsScope: input.skillsScope,
276
+ authentication: input.authentication,
277
+ detectedClients,
278
+ assistants: input.assistants,
279
+ plannedActions,
280
+ nextSteps,
281
+ manualSetup: manualSetup(),
282
+ cursorMcpConfig: CURSOR_MCP_CONFIG,
283
+ };
284
+ };
285
+ const defaultConfirm = async (question) => {
286
+ if (!stdin.isTTY)
287
+ return false;
288
+ const prompt = createInterface({ input: stdin, output: stderr });
289
+ try {
290
+ const answer = (await prompt.question(`${question} [y/N] `)).trim().toLowerCase();
291
+ return answer === 'y' || answer === 'yes';
292
+ }
293
+ finally {
294
+ prompt.close();
295
+ }
296
+ };
297
+ const failedCommandMessage = (result) => [result.error, result.stderr.trim(), result.stdout.trim()].filter(Boolean).join('; ') ||
298
+ `exit ${result.exitCode}`;
299
+ const commandResult = (planned, result) => {
300
+ // Only marketplace registration has a safe idempotent "already added"
301
+ // outcome. A similarly worded MCP/plugin error can mean an unhealthy plugin
302
+ // or a same-name MCP entry pointing at the wrong URL, which must fall back or
303
+ // fail visibly instead of being reported as repaired.
304
+ const succeeded = result.exitCode === 0;
305
+ const alreadySatisfied = result.exitCode !== 0 &&
306
+ planned.kind === 'plugin-marketplace-add' &&
307
+ commandAlreadySatisfied(result);
308
+ return {
309
+ ...planned,
310
+ status: succeeded ? 'succeeded' : alreadySatisfied ? 'skipped' : 'failed',
311
+ exitCode: result.exitCode,
312
+ ...(alreadySatisfied
313
+ ? { detail: 'marketplace was already configured' }
314
+ : !succeeded
315
+ ? { detail: failedCommandMessage(result) }
316
+ : {}),
317
+ };
318
+ };
319
+ const findAction = (plan, client, kind) => plan.plannedActions.find(row => row.kind === kind && (row.client === client || row.client === 'shared'));
320
+ const printApprovalPlan = (plan) => {
321
+ process.stderr.write('Setup plan:\n');
322
+ for (const planned of plan.plannedActions) {
323
+ const description = planned.command ?? 'print the Cursor MCP configuration';
324
+ process.stderr.write(` [${planned.condition}] ${description}\n`);
325
+ }
326
+ };
327
+ export const runAgentSetup = async (options, dependencies = {}) => {
328
+ const requestedClient = options.client ?? 'all';
329
+ const skillsScope = options.skillsScope ?? 'global';
330
+ const codexMarketplaceSource = dependencies.codexMarketplaceSource ?? CODEX_PLUGIN_MARKETPLACE_SOURCE;
331
+ const authentication = dependencies.authentication
332
+ ? await dependencies.authentication()
333
+ : { status: 'missing' };
334
+ const assistants = await inventoryAgentSetup(options, dependencies);
335
+ const plan = buildAgentSetupPlan({
336
+ statusOnly: options.status === true,
337
+ requestedClient,
338
+ skillsScope,
339
+ authentication,
340
+ assistants,
341
+ ...(codexMarketplaceSource ? { codexMarketplaceSource } : {}),
342
+ });
343
+ if (options.status)
344
+ return { ...plan, results: [], changed: false };
345
+ if (authentication.status === 'unavailable') {
346
+ throw userError(`Could not verify Controller AI authentication: ${authentication.detail ?? 'unavailable'}`, 'AUTH_STATUS_UNAVAILABLE');
347
+ }
348
+ if (plan.plannedActions.length > 0 && !options.yes) {
349
+ printApprovalPlan(plan);
350
+ const confirmed = await (dependencies.confirm ?? defaultConfirm)(`Approve ${plan.plannedActions.length} planned setup action(s)?`);
351
+ if (!confirmed) {
352
+ throw userError('Setup was not changed. Rerun with --yes to approve the printed plan.', 'SETUP_CONFIRMATION_REQUIRED');
353
+ }
354
+ }
355
+ const results = [];
356
+ const runner = dependencies.runCommand ?? runExternalCommand;
357
+ const skillInstall = dependencies.skillInstall ??
358
+ ((target) => installSkills({
359
+ target,
360
+ ...(target === 'project' ? { projectDir: process.cwd() } : {}),
361
+ }));
362
+ const installedSkillCommands = new Set();
363
+ const installedSkillResults = new Map();
364
+ let connected = false;
365
+ const connect = findAction(plan, 'account', 'connect');
366
+ if (connect) {
367
+ process.stderr.write(`$ ${connect.command}\n`);
368
+ if (!dependencies.ensureAuthenticated) {
369
+ throw userError('Setup authentication handler is unavailable.', 'SETUP_AUTH_UNAVAILABLE');
370
+ }
371
+ await dependencies.ensureAuthenticated();
372
+ connected = true;
373
+ results.push({ ...connect, status: 'succeeded' });
374
+ }
375
+ for (const assistant of assistants) {
376
+ if (!assistant.detected)
377
+ continue;
378
+ if (assistant.client === 'cursor') {
379
+ if (assistant.current)
380
+ continue;
381
+ const skill = findAction(plan, 'cursor', 'skills-install');
382
+ if (skill?.command && !installedSkillCommands.has(skill.command)) {
383
+ process.stderr.write(`$ ${skill.command}\n`);
384
+ const skillResult = await skillInstall(assistant.skills.target);
385
+ for (const message of [...skillResult.migrationWarnings, ...skillResult.cleanupWarnings]) {
386
+ warn(message);
387
+ }
388
+ installedSkillCommands.add(skill.command);
389
+ installedSkillResults.set(assistant.skills.target, skillResult);
390
+ results.push({ ...skill, status: 'succeeded' });
391
+ }
392
+ const config = findAction(plan, 'cursor', 'print-mcp-config');
393
+ if (config)
394
+ results.push({ ...config, status: 'instructions' });
395
+ continue;
396
+ }
397
+ const pluginPreferred = !assistant.plugin.installed &&
398
+ assistant.plugin.supported &&
399
+ (assistant.client === 'claude-code' || Boolean(codexMarketplaceSource));
400
+ if (assistant.current && !pluginPreferred)
401
+ continue;
402
+ let pluginReady = assistant.plugin.installed;
403
+ const marketplace = findAction(plan, assistant.client, 'plugin-marketplace-add');
404
+ const plugin = findAction(plan, assistant.client, 'plugin-install');
405
+ if (!pluginReady && marketplace && plugin && marketplace.argv && plugin.argv) {
406
+ const marketplaceResult = await runner(marketplace.argv[0], marketplace.argv.slice(1), {
407
+ echoCommand: true,
408
+ echoOutput: true,
409
+ });
410
+ const recordedMarketplace = commandResult(marketplace, marketplaceResult);
411
+ results.push(recordedMarketplace);
412
+ if (recordedMarketplace.status !== 'failed') {
413
+ const pluginResult = await runner(plugin.argv[0], plugin.argv.slice(1), {
414
+ echoCommand: true,
415
+ echoOutput: true,
416
+ });
417
+ const recordedPlugin = commandResult(plugin, pluginResult);
418
+ results.push(recordedPlugin);
419
+ pluginReady = recordedPlugin.status === 'succeeded';
420
+ }
421
+ }
422
+ if (!pluginReady) {
423
+ const mcp = findAction(plan, assistant.client, 'mcp-add');
424
+ if (mcp?.argv) {
425
+ const result = await runner(mcp.argv[0], mcp.argv.slice(1), {
426
+ echoCommand: true,
427
+ echoOutput: true,
428
+ });
429
+ const recorded = commandResult(mcp, result);
430
+ results.push(recorded);
431
+ if (recorded.status === 'failed') {
432
+ throw userError(`${assistant.client} MCP setup failed: ${recorded.detail ?? 'unknown error'}`, 'ASSISTANT_MCP_SETUP_FAILED');
433
+ }
434
+ }
435
+ const skill = findAction(plan, assistant.client, 'skills-install');
436
+ if (skill?.command && !installedSkillCommands.has(skill.command)) {
437
+ process.stderr.write(`$ ${skill.command}\n`);
438
+ const skillResult = await skillInstall(assistant.skills.target);
439
+ for (const message of [...skillResult.migrationWarnings, ...skillResult.cleanupWarnings]) {
440
+ warn(message);
441
+ }
442
+ installedSkillCommands.add(skill.command);
443
+ installedSkillResults.set(assistant.skills.target, skillResult);
444
+ results.push({ ...skill, status: 'succeeded' });
445
+ }
446
+ }
447
+ const login = findAction(plan, assistant.client, 'mcp-login');
448
+ if (login?.argv) {
449
+ const loginResult = await runner(login.argv[0], login.argv.slice(1), {
450
+ echoCommand: true,
451
+ inheritStdio: true,
452
+ });
453
+ results.push(commandResult(login, loginResult));
454
+ }
455
+ }
456
+ const completedActionIds = new Set(results.map(row => row.id));
457
+ for (const planned of plan.plannedActions) {
458
+ if (completedActionIds.has(planned.id))
459
+ continue;
460
+ results.push({
461
+ ...planned,
462
+ status: 'skipped',
463
+ detail: planned.condition === 'if-plugin-fails'
464
+ ? 'plugin setup succeeded; fallback was not needed'
465
+ : 'an earlier prerequisite did not complete',
466
+ });
467
+ }
468
+ const updatedAssistants = assistants.map(assistant => {
469
+ const clientResults = results.filter(row => row.client === assistant.client || row.client === 'shared');
470
+ const pluginInstalled = clientResults.some(row => row.kind === 'plugin-install' && row.status === 'succeeded');
471
+ const mcpConfigured = clientResults.some(row => row.kind === 'mcp-add' && row.status === 'succeeded');
472
+ const mcpAuthenticated = clientResults.some(row => row.kind === 'mcp-login' && row.status === 'succeeded');
473
+ const skillResult = installedSkillResults.get(assistant.skills.target);
474
+ const plugin = pluginInstalled
475
+ ? {
476
+ installed: true,
477
+ supported: true,
478
+ id: assistant.client === 'claude-code' ? CLAUDE_REPOSITORY_PLUGIN_ID : CODEX_PLUGIN_ID,
479
+ }
480
+ : assistant.plugin;
481
+ const mcp = {
482
+ installed: assistant.mcp.installed || mcpConfigured,
483
+ ...(assistant.client === 'codex'
484
+ ? {
485
+ authentication: mcpAuthenticated
486
+ ? 'authenticated'
487
+ : (assistant.mcp.authentication ?? 'unknown'),
488
+ }
489
+ : {}),
490
+ };
491
+ const installedVersions = skillResult?.availableSkillSetVersions?.length
492
+ ? skillResult.availableSkillSetVersions
493
+ : skillResult?.updateSkillSetVersion
494
+ ? [skillResult.updateSkillSetVersion]
495
+ : assistant.skills.versions;
496
+ const skills = skillResult
497
+ ? {
498
+ ...assistant.skills,
499
+ installed: assistant.skills.total,
500
+ current: true,
501
+ versions: installedVersions,
502
+ statuses: { current: assistant.skills.total },
503
+ }
504
+ : assistant.skills;
505
+ const current = (plugin.installed || (mcp.installed && skills.current)) &&
506
+ (assistant.client !== 'codex' || mcp.authentication === 'authenticated');
507
+ return {
508
+ ...assistant,
509
+ plugin,
510
+ mcp,
511
+ skills,
512
+ current,
513
+ ...(current ? { note: undefined } : {}),
514
+ };
515
+ });
516
+ return {
517
+ ...plan,
518
+ authentication: connected ? { status: 'authenticated' } : plan.authentication,
519
+ assistants: updatedAssistants,
520
+ results,
521
+ changed: results.some(row => [
522
+ 'connect',
523
+ 'plugin-marketplace-add',
524
+ 'plugin-install',
525
+ 'mcp-add',
526
+ 'mcp-login',
527
+ 'skills-install',
528
+ ].includes(row.kind) && row.status === 'succeeded'),
529
+ };
530
+ };
531
+ const humanSetup = (result) => {
532
+ printKeyValues([
533
+ ['mode', result.statusOnly ? 'status (no changes)' : 'setup'],
534
+ ['requested client', result.requestedClient],
535
+ ['skills scope', result.skillsScope],
536
+ ['authentication', result.authentication.status],
537
+ ['changed', result.changed ? 'yes' : 'no'],
538
+ ]);
539
+ process.stdout.write('\nAssistants\n');
540
+ printTable(['client', 'detected', 'plugin', 'MCP', 'skills', 'current'], result.assistants.map(row => [
541
+ row.client,
542
+ row.detected ? 'yes' : 'no',
543
+ row.plugin.installed
544
+ ? `${row.plugin.id ?? 'installed'}${row.plugin.version ? ` ${row.plugin.version}` : ''}`
545
+ : row.plugin.supported
546
+ ? 'available'
547
+ : 'unavailable',
548
+ row.mcp.installed ? 'configured' : 'not configured',
549
+ `${row.skills.installed}/${row.skills.total} (${row.skills.versions.join(', ') || '-'})`,
550
+ row.current ? 'yes' : (row.note ?? 'no'),
551
+ ]));
552
+ process.stdout.write('\nPlanned actions\n');
553
+ printTable(['condition', 'client', 'action', 'command'], result.plannedActions.map(row => [
554
+ row.condition,
555
+ row.client,
556
+ row.kind,
557
+ row.command ?? 'print the MCP configuration below',
558
+ ]));
559
+ if (result.results.length > 0) {
560
+ process.stdout.write('\nResults\n');
561
+ printTable(['client', 'action', 'status', 'detail'], result.results.map(row => [row.client, row.kind, row.status, row.detail]));
562
+ }
563
+ if (result.assistants.some(row => row.client === 'cursor' && row.detected)) {
564
+ process.stdout.write(`\nCursor MCP config\n${JSON.stringify(result.cursorMcpConfig, null, 2)}\n`);
565
+ }
566
+ process.stdout.write('\nManual setup\n');
567
+ printTable(['client', 'docs'], result.manualSetup.map(row => [row.client, row.url]));
568
+ if (!result.statusOnly && result.nextSteps.length > 0) {
569
+ process.stdout.write('\nNext\n');
570
+ for (const step of result.nextSteps) {
571
+ process.stdout.write(`- ${step.client}: ${step.text}\n`);
572
+ }
573
+ }
574
+ };
575
+ const assertClient = (value) => {
576
+ if (!['claude-code', 'codex', 'cursor', 'all'].includes(value)) {
577
+ throw userError('--client must be claude-code, codex, cursor, or all.', 'BAD_SETUP_CLIENT');
578
+ }
579
+ return value;
580
+ };
581
+ const assertSkillsScope = (value) => {
582
+ if (!['global', 'project'].includes(value)) {
583
+ throw userError('--skills-scope must be global or project.', 'BAD_SKILLS_SCOPE');
584
+ }
585
+ return value;
586
+ };
587
+ export const probeSetupAuthentication = async (statusOnly, probe) => {
588
+ try {
589
+ return { status: (await probe()) ? 'authenticated' : 'missing' };
590
+ }
591
+ catch (error) {
592
+ if (!statusOnly)
593
+ throw error;
594
+ return {
595
+ status: 'unavailable',
596
+ detail: error instanceof Error ? error.message : String(error),
597
+ };
598
+ }
599
+ };
600
+ export const registerAgentSetup = (agent) => {
601
+ agent
602
+ .command('setup')
603
+ .description('Set up Controller AI end to end for installed coding assistants')
604
+ .option('--client <client>', 'claude-code, codex, cursor, or all', 'all')
605
+ .option('--status', 'show installed state and the action plan without changing anything', false)
606
+ .option('--yes', 'approve setup actions without prompting', false)
607
+ .option('--skills-scope <scope>', 'global or project', 'global')
608
+ .action(async (opts, command) => {
609
+ const statusProbe = () => probeSetupAuthentication(opts.status, async () => {
610
+ const result = await authenticationStatus(command);
611
+ return result.authenticated;
612
+ });
613
+ const result = await runAgentSetup({
614
+ client: assertClient(opts.client),
615
+ status: opts.status,
616
+ yes: opts.yes,
617
+ skillsScope: assertSkillsScope(opts.skillsScope),
618
+ }, {
619
+ authentication: statusProbe,
620
+ ensureAuthenticated: async () => {
621
+ await ensureAuthentication(command);
622
+ },
623
+ });
624
+ emit(result, () => humanSetup(result));
625
+ });
626
+ };
627
+ //# sourceMappingURL=agentSetup.js.map