@intutic/cli 2.7.0 → 2.8.1

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 (60) hide show
  1. package/README.md +18 -7
  2. package/dist/cli.js +188 -47
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/apiCommand.d.ts +9 -0
  5. package/dist/commands/apiCommand.d.ts.map +1 -1
  6. package/dist/commands/apiCommand.js +21 -0
  7. package/dist/commands/apiCommand.js.map +1 -1
  8. package/dist/commands/budget.d.ts.map +1 -1
  9. package/dist/commands/budget.js +6 -7
  10. package/dist/commands/budget.js.map +1 -1
  11. package/dist/commands/connect.d.ts.map +1 -1
  12. package/dist/commands/connect.js +21 -62
  13. package/dist/commands/connect.js.map +1 -1
  14. package/dist/commands/doctor.d.ts +4 -0
  15. package/dist/commands/doctor.d.ts.map +1 -1
  16. package/dist/commands/doctor.js +30 -14
  17. package/dist/commands/doctor.js.map +1 -1
  18. package/dist/commands/exec.js +1 -1
  19. package/dist/commands/exec.js.map +1 -1
  20. package/dist/commands/guardrails.d.ts.map +1 -1
  21. package/dist/commands/guardrails.js +2 -18
  22. package/dist/commands/guardrails.js.map +1 -1
  23. package/dist/commands/init.d.ts.map +1 -1
  24. package/dist/commands/init.js +5 -2
  25. package/dist/commands/init.js.map +1 -1
  26. package/dist/commands/keys.d.ts +40 -0
  27. package/dist/commands/keys.d.ts.map +1 -0
  28. package/dist/commands/keys.js +105 -0
  29. package/dist/commands/keys.js.map +1 -0
  30. package/dist/commands/optimizer.d.ts +26 -0
  31. package/dist/commands/optimizer.d.ts.map +1 -0
  32. package/dist/commands/optimizer.js +54 -0
  33. package/dist/commands/optimizer.js.map +1 -0
  34. package/dist/commands/setup.d.ts.map +1 -1
  35. package/dist/commands/setup.js +7 -2
  36. package/dist/commands/setup.js.map +1 -1
  37. package/dist/commands/skill.d.ts.map +1 -1
  38. package/dist/commands/skill.js +8 -9
  39. package/dist/commands/skill.js.map +1 -1
  40. package/dist/commands/sops.d.ts.map +1 -1
  41. package/dist/commands/sops.js +106 -27
  42. package/dist/commands/sops.js.map +1 -1
  43. package/dist/commands/status.js +2 -2
  44. package/dist/commands/status.js.map +1 -1
  45. package/dist/commands/tasks.d.ts +36 -0
  46. package/dist/commands/tasks.d.ts.map +1 -0
  47. package/dist/commands/tasks.js +75 -0
  48. package/dist/commands/tasks.js.map +1 -0
  49. package/dist/config/paths.d.ts +17 -1
  50. package/dist/config/paths.d.ts.map +1 -1
  51. package/dist/config/paths.js +25 -4
  52. package/dist/config/paths.js.map +1 -1
  53. package/dist/lib/onboarding.d.ts.map +1 -1
  54. package/dist/lib/onboarding.js +4 -2
  55. package/dist/lib/onboarding.js.map +1 -1
  56. package/dist/lib/sopFrontMatter.d.ts +26 -0
  57. package/dist/lib/sopFrontMatter.d.ts.map +1 -1
  58. package/dist/lib/sopFrontMatter.js +23 -0
  59. package/dist/lib/sopFrontMatter.js.map +1 -1
  60. package/package.json +5 -5
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @intutic/cli
2
2
 
3
- > AI governance control plane for developer workspaces.
3
+ > AI governance for developer workspaces.
4
4
 
5
5
  Intutic intercepts agent-to-LLM traffic, enforces SOPs, detects anomalies, and tracks costs — all without changing how your team codes.
6
6
 
@@ -21,6 +21,7 @@ intutic start # Run the proxy standalone — no account needed
21
21
  intutic traces list # See governance verdicts
22
22
  ```
23
23
 
24
+ <!-- UNRELEASED:cloud
24
25
  **With a control plane:**
25
26
 
26
27
  ```bash
@@ -29,6 +30,7 @@ intutic init # Detect harnesses, record them for sync
29
30
  intutic connect # Start sync daemon — writes harness rules and gates
30
31
  intutic traces list # See governance verdicts
31
32
  ```
33
+ -->
32
34
 
33
35
  ## Commands
34
36
 
@@ -36,13 +38,17 @@ intutic traces list # See governance verdicts
36
38
  |---------|-------------|
37
39
  | `intutic init` | Initialize workspace — detect harnesses, record them in `~/.intutic/config.json` |
38
40
  | `intutic start` | Start the proxy standalone — no account or control plane needed (`--port`, `--upstream-url`) |
41
+ | `intutic status` | Show workspace status — auth, harnesses, sync state |
42
+ | `intutic disconnect` | Undo `intutic connect` — restore the harness configs it changed, remove its services, log out (`--harness`, `--dry-run`, `--keep-login`) |
43
+ | `intutic doctor` | Diagnose workspace health — proxy, auth, daemon, configs, logs |
44
+ | `intutic traces list` | List execution traces (`--limit`, `--since`, `--action` with a control plane, `--verdict` without one, `--model`, `--json`) |
45
+ | `intutic traces inspect <id>` | Show full detail of a single trace |
46
+ <!-- UNRELEASED:cloud
39
47
  | `intutic login` | Authenticate with control plane (`--api-key`, `--control-plane-url`, `--dev`) |
40
48
  | `intutic logout` | Clear stored credentials |
41
- | `intutic status` | Show workspace status — auth, harnesses, sync state |
42
49
  | `intutic whoami` | Show current authenticated identity |
43
50
  | `intutic connect` | Start sync daemon — bidirectional config sync (`--interval`) |
44
- | `intutic traces list` | List execution traces (`--limit`, `--since`, `--action`, `--verdict`, `--model`, `--json`) |
45
- | `intutic traces inspect <id>` | Show full detail of a single trace |
51
+ -->
46
52
 
47
53
  ## Supported Harnesses
48
54
 
@@ -58,7 +64,7 @@ Intutic auto-detects and configures AI coding harnesses, among them:
58
64
  | **n8n** | Running n8n instance (`N8N_URL`, default `http://localhost:5678`) | Workflow JSON file + external hook gate `~/.intutic/hooks/n8n-governance-hook.js` |
59
65
  | **OpenClaw** | `~/.openclaw/` or `.openclaw/openclaw.json` | `AGENTS.md` section in its agent workspace + `~/.intutic/hooks/openclaw/intutic-governance.cjs` (plugin gate, listed in `~/.openclaw/openclaw.json`) |
60
66
  | **Hermes** | `~/.hermes/` or `.hermes/config.yaml` | `AGENTS.md` section + `hooks.pre_tool_call` gate in `~/.hermes/config.yaml` |
61
- | **OpenCode** | `.opencode/`, `opencode.json`, `~/.config/opencode/` or `opencode` on PATH | `AGENTS.md` + `.opencode/plugins/intutic-governance.js` (plugin gate) |
67
+ | **OpenCode** | `.opencode/`, `opencode.json`, `~/.config/opencode/` or `opencode` on PATH | `AGENTS.md` + `.opencode/plugins/intutic-governance.js` (plugin gate; OpenCode 2.x: `.opencode/plugins/intutic-governance/index.js`) |
62
68
 
63
69
  ## Trace Inspection
64
70
 
@@ -68,13 +74,16 @@ View governance verdicts directly from the terminal:
68
74
  # List recent traces (last 24h)
69
75
  intutic traces list
70
76
 
71
- # Filter by enforcement action
77
+ # Filter by enforcement action (connected to a control plane)
72
78
  intutic traces list --action KILL
73
79
 
80
+ # Filter by the proxy's verdict (standalone: `intutic start`, no login)
81
+ intutic traces list --verdict killed
82
+
74
83
  # Filter by model and time window
75
84
  intutic traces list --model claude-4 --since 7d
76
85
 
77
- # JSON output for scripting
86
+ # JSON output for scripting (connected; standalone rows carry `.verdict`)
78
87
  intutic traces list --json | jq '.traces[] | select(.enforcementAction == "HIJACK")'
79
88
 
80
89
  # Inspect a specific trace
@@ -93,6 +102,7 @@ Credentials are stored at `~/.intutic/credentials.json` (mode `0600`).
93
102
  | `INTUTIC_DEV` | `1` targets the local control plane (`http://localhost:3001`), like `--dev` |
94
103
  | `INTUTIC_PROXY_URL` | Local proxy URL; its port is the proxy port (default `4000`) |
95
104
 
105
+ <!-- UNRELEASED:cloud
96
106
  For non-interactive auth, use `intutic login --api-key vk_...`.
97
107
 
98
108
  ### Development Mode
@@ -104,6 +114,7 @@ intutic login --dev
104
114
  intutic connect --dev
105
115
  intutic traces list --dev
106
116
  ```
117
+ -->
107
118
 
108
119
  ## Documentation
109
120
 
package/dist/cli.js CHANGED
@@ -9,8 +9,9 @@
9
9
  *
10
10
  * @module
11
11
  */
12
- import { Command } from 'commander';
12
+ import { Command, Option } from 'commander';
13
13
  import { createRequire } from 'node:module';
14
+ import { CLOUD_RELEASED, CONTROL_PLANE_PRODUCT_RELEASED, HOSTED_CONTROL_PLANE_URL, SELF_HOST_RELEASED, } from '@intutic/shared-types';
14
15
  import { DEFAULT_SANDBOX_IMAGE } from './lib/sandbox/image.js';
15
16
  // Read the version from package.json rather than repeating it here. The literal
16
17
  // that used to live below said 1.6.0 for three releases running, so
@@ -20,13 +21,37 @@ import { DEFAULT_SANDBOX_IMAGE } from './lib/sandbox/image.js';
20
21
  // but JSON imports still need an assertion on older runtimes.
21
22
  const { version } = createRequire(import.meta.url)('../package.json');
22
23
  const program = new Command();
24
+ // Release flags (packages/shared-types/src/release.ts). A command that only
25
+ // works against a control plane, or only in the self-hosted distribution, is
26
+ // left out of help until that product is released. Hidden is all it is: the
27
+ // command still parses and runs, so staging and existing users are unaffected.
28
+ const needsControlPlane = { hidden: !CONTROL_PLANE_PRODUCT_RELEASED };
29
+ const needsSelfHost = { hidden: !SELF_HOST_RELEASED };
30
+ /** An option of a visible command that only does anything against a control plane. */
31
+ function controlPlaneOption(flags, description) {
32
+ const option = new Option(flags, description);
33
+ return CONTROL_PLANE_PRODUCT_RELEASED ? option : option.hideHelp();
34
+ }
35
+ // A command that needs a control plane, run with none configured, throws
36
+ // NoControlPlaneError (config/paths.ts) from an async action. Say that plainly
37
+ // rather than as an unhandled rejection with a stack; anything else still
38
+ // crashes as before.
39
+ process.on('unhandledRejection', (err) => {
40
+ if (err instanceof Error && err.name === 'NoControlPlaneError') {
41
+ console.error(err.message);
42
+ process.exit(1);
43
+ }
44
+ throw err;
45
+ });
23
46
  // Positional options: `--version` and `--help` belong to the program only when
24
47
  // they come before a subcommand. Without this, commander matches program
25
48
  // options anywhere on the line, so a subcommand option named `--version` would
26
49
  // print the CLI version and exit before its command ever saw it.
27
50
  program
28
51
  .name('intutic')
29
- .description('Intutic CLI — AI governance control plane for developer workspaces')
52
+ .description(CONTROL_PLANE_PRODUCT_RELEASED
53
+ ? 'Intutic CLI — AI governance control plane for developer workspaces'
54
+ : 'Intutic CLI — open-core AI governance for developer workspaces: a local proxy, gates and policy for coding agents')
30
55
  .version(version)
31
56
  .enablePositionalOptions();
32
57
  program
@@ -49,7 +74,7 @@ program
49
74
  await runSetup(opts);
50
75
  });
51
76
  const judgeCmd = program
52
- .command('judge')
77
+ .command('judge', needsSelfHost)
53
78
  .description('Configure the LLM-as-judge path');
54
79
  judgeCmd
55
80
  .command('configure')
@@ -60,7 +85,7 @@ judgeCmd
60
85
  await runJudgeConfigure(opts);
61
86
  });
62
87
  program
63
- .command('login')
88
+ .command('login', needsControlPlane)
64
89
  .description('Authenticate with the Intutic control plane')
65
90
  .option('--api-key <key>', 'Authenticate with an API key (vk_*)')
66
91
  .option('--control-plane-url <url>', 'Control plane to log in to (e.g. a self-hosted one); saved for every later command')
@@ -80,7 +105,7 @@ program
80
105
  await runDisconnect({ harness: opts.harness, dryRun: opts.dryRun, keepLogin: opts.keepLogin });
81
106
  });
82
107
  program
83
- .command('logout')
108
+ .command('logout', needsControlPlane)
84
109
  .description('Clear stored credentials')
85
110
  .action(async () => {
86
111
  const { runLogout } = await import('./commands/logout.js');
@@ -111,7 +136,9 @@ program
111
136
  });
112
137
  const budgetCmd = program
113
138
  .command('budget')
114
- .description('Check remaining daily/monthly budget and list active loops; set workspace, key and member budgets')
139
+ .description(CONTROL_PLANE_PRODUCT_RELEASED
140
+ ? 'Check remaining daily/monthly budget and list active loops; set workspace, key and member budgets'
141
+ : 'Check the local spending cap and what this machine has spent today')
115
142
  .option('--dev', 'Use local control plane (http://localhost:3001)')
116
143
  .option('--watch', 'Continuously print machine-local and workspace spend, one line per tick')
117
144
  .option('--interval <seconds>', 'Tick interval in seconds for --watch', String(5))
@@ -125,7 +152,7 @@ const budgetCmd = program
125
152
  await runBudget(opts);
126
153
  });
127
154
  budgetCmd
128
- .command('set')
155
+ .command('set', needsControlPlane)
129
156
  .description("Set the workspace's daily and monthly caps, alert threshold and whether each cap refuses requests (OWNER, ADMIN)")
130
157
  .option('--daily <usd>', 'Daily cap in USD (0: no daily cap; default: back to the $100 default)')
131
158
  .option('--monthly <usd>', 'Monthly cap in USD (0: no monthly cap; default: back to the $500 default)')
@@ -141,7 +168,7 @@ budgetCmd
141
168
  await runBudgetSet(cmd.optsWithGlobals());
142
169
  });
143
170
  budgetCmd
144
- .command('keys')
171
+ .command('keys', needsControlPlane)
145
172
  .description("Every virtual key's spend budgets, rate limit and spend today and this month")
146
173
  .option('--json', 'Output as JSON')
147
174
  .option('--dev', 'Use local control plane (http://localhost:3001)')
@@ -150,7 +177,7 @@ budgetCmd
150
177
  await runBudgetKeys(cmd.optsWithGlobals());
151
178
  });
152
179
  budgetCmd
153
- .command('key <keyId>')
180
+ .command('key <keyId>', needsControlPlane)
154
181
  .description("Set a virtual key's spend budgets and rate limit (OWNER, ADMIN)")
155
182
  .option('--daily <usd>', 'Day budget in USD, or none to remove it')
156
183
  .option('--monthly <usd>', 'Month budget in USD, or none to remove it')
@@ -165,7 +192,7 @@ budgetCmd
165
192
  await runBudgetKey(keyId, cmd.optsWithGlobals());
166
193
  });
167
194
  budgetCmd
168
- .command('members')
195
+ .command('members', needsControlPlane)
169
196
  .description("The default member budget, and each member's budgets and spend (Biz Org+)")
170
197
  .option('--json', 'Output as JSON')
171
198
  .option('--dev', 'Use local control plane (http://localhost:3001)')
@@ -174,7 +201,7 @@ budgetCmd
174
201
  await runBudgetMembers(cmd.optsWithGlobals());
175
202
  });
176
203
  budgetCmd
177
- .command('member <memberId>')
204
+ .command('member <memberId>', needsControlPlane)
178
205
  .description("Set a member's spend budgets, or the default member budget with `default` (OWNER, ADMIN; Biz Org+)")
179
206
  .option('--daily <usd>', 'Day budget in USD, or none to remove it')
180
207
  .option('--monthly <usd>', 'Month budget in USD, or none to remove it')
@@ -187,7 +214,7 @@ budgetCmd
187
214
  await runBudgetMember(memberId, cmd.optsWithGlobals());
188
215
  });
189
216
  program
190
- .command('predict-cost')
217
+ .command('predict-cost', needsControlPlane)
191
218
  .description('Pre-flight cost estimate for a prompt/task before it runs (requires `intutic login`). ' +
192
219
  "Estimates output tokens from the workspace's recorded usage and prices them with the same " +
193
220
  'rate table as every other cost figure the control plane reports.')
@@ -201,7 +228,7 @@ program
201
228
  await runPredictCost(opts);
202
229
  });
203
230
  const sopsCmd = program
204
- .command('sops')
231
+ .command('sops', needsControlPlane)
205
232
  .description('Manage local and global SOP rules');
206
233
  sopsCmd
207
234
  .command('push <name>')
@@ -247,11 +274,35 @@ sopsCmd
247
274
  const { runSopsOrgRm } = await import('./commands/sops.js');
248
275
  await runSopsOrgRm(orgSopId, opts);
249
276
  });
277
+ // ── SOP Optimizer approval mode ──────────────────────────────────────────
278
+ const optimizerCmd = program
279
+ .command('optimizer', needsControlPlane)
280
+ .description("Read and set the SOP Optimizer's approval mode (whether it may auto-apply strengthening proposals)");
281
+ optimizerCmd
282
+ .command('settings')
283
+ .description('Print the approval mode and the auto-apply threshold')
284
+ .option('--json', 'Output as JSON')
285
+ .option('--dev', 'Use local control plane (http://localhost:3001)')
286
+ .action(async (opts) => {
287
+ const { runOptimizerSettings } = await import('./commands/optimizer.js');
288
+ await runOptimizerSettings(opts);
289
+ });
290
+ optimizerCmd
291
+ .command('set')
292
+ .description('Set the approval mode, and optionally the auto-apply threshold (OWNER, ADMIN)')
293
+ .requiredOption('--mode <mode>', 'manual (nothing is auto-applied) or auto')
294
+ .option('--auto-threshold <n>', 'Score from 0 to 1 a proposal needs to be auto-applied (default: keep the current one)')
295
+ .option('--json', 'Output as JSON')
296
+ .option('--dev', 'Use local control plane (http://localhost:3001)')
297
+ .action(async (opts) => {
298
+ const { runOptimizerSet } = await import('./commands/optimizer.js');
299
+ await runOptimizerSet(opts);
300
+ });
250
301
  const policyCmd = program
251
302
  .command('policy')
252
- .description('WASM rules and the policy snapshot');
303
+ .description(CONTROL_PLANE_PRODUCT_RELEASED ? 'WASM rules and the policy snapshot' : 'WASM rules: compile, test and install them locally');
253
304
  policyCmd
254
- .command('snapshot')
305
+ .command('snapshot', needsControlPlane)
255
306
  .description('Compile workspace policy to ~/.intutic/hooks/policy-snapshot.rules — the file every gate reads.\n' +
256
307
  '\n' +
257
308
  ' Does only that. \'intutic connect\' writes it too, but as one step of\n' +
@@ -274,10 +325,10 @@ policyCmd
274
325
  .command('compile')
275
326
  .description('Compile an AssemblyScript rule to WASM (wraps asc)')
276
327
  .option('--src <path>', 'Rule source entry file (default: assembly/index.ts)')
277
- .option('--out <path>', 'Output .wasm path (default: build/rule.wasm, or build/<candidate>.wasm with --candidate)')
328
+ .option('--out <path>', `Output .wasm path (default: build/rule.wasm${CONTROL_PLANE_PRODUCT_RELEASED ? ', or build/<candidate>.wasm with --candidate' : ''})`)
278
329
  .option('--debug', 'Include debug info and source maps')
279
- .option('--candidate <id>', 'Compile a rule candidate from its source of record on the control plane (fetched and hash-verified; not with --src)')
280
- .option('--upload', 'With --candidate: upload the bundle to the candidate with its source hash and print the gate results')
330
+ .addOption(controlPlaneOption('--candidate <id>', 'Compile a rule candidate from its source of record on the control plane (fetched and hash-verified; not with --src)'))
331
+ .addOption(controlPlaneOption('--upload', 'With --candidate: upload the bundle to the candidate with its source hash and print the gate results'))
281
332
  .option('--dev', 'Use local control plane (http://localhost:3001)')
282
333
  .action(async (opts) => {
283
334
  const { runPolicyCompile } = await import('./commands/policy.js');
@@ -301,7 +352,7 @@ policyCmd
301
352
  await runPolicyListLocal();
302
353
  });
303
354
  policyCmd
304
- .command('replay <ruleId>')
355
+ .command('replay <ruleId>', needsControlPlane)
305
356
  .description("Replay a rule (installed or not) against this workspace's own recent traffic and report " +
306
357
  'what it would have done, without touching enforcement.')
307
358
  .option('--limit <n>', 'Max sampled contexts to replay against (default 500, capped at 5000)')
@@ -347,7 +398,7 @@ rulesCmd
347
398
  // `intutic sops` is files on disk; this namespace is the cited guardrails
348
399
  // extracted from policy documents and their shadow -> promote lifecycle.
349
400
  const guardrailsCmd = program
350
- .command('guardrails')
401
+ .command('guardrails', needsControlPlane)
351
402
  .description('Policy documents, cited clauses and the guardrails compiled from them');
352
403
  const guardrailsSourcesCmd = guardrailsCmd
353
404
  .command('sources')
@@ -590,7 +641,7 @@ guardrailsCmd
590
641
  await runGuardrailsPull(opts);
591
642
  });
592
643
  program
593
- .command('whoami')
644
+ .command('whoami', needsControlPlane)
594
645
  .description('Show current authenticated identity')
595
646
  .option('--dev', 'Use local control plane (http://localhost:3001)')
596
647
  .action(async (opts) => {
@@ -633,7 +684,7 @@ for (const [action, desc] of [
633
684
  });
634
685
  }
635
686
  enforceCmd
636
- .command('report')
687
+ .command('report', needsControlPlane)
637
688
  .description('Report locally recorded enforcement state (firewall/CA-trust/system-hooks) to the control plane. ' +
638
689
  'No privilege needed — for when apply/remove ran elevated and could not reach stored credentials.')
639
690
  .action(async () => {
@@ -646,7 +697,7 @@ enforceCmd
646
697
  // tools/scripts/intutic-enterprise-install.sh, an unrelated air-gapped
647
698
  // docker-compose installer. Does not touch the host firewall; see `enforce`.
648
699
  const enterpriseCmd = program
649
- .command('enterprise')
700
+ .command('enterprise', needsControlPlane)
650
701
  .description('Enterprise fleet rollout: CA trust, Cursor system hooks, MDM manifests');
651
702
  enterpriseCmd
652
703
  .command('install')
@@ -685,7 +736,7 @@ envCmd
685
736
  await runEnvClear();
686
737
  });
687
738
  program
688
- .command('connect')
739
+ .command('connect', needsControlPlane)
689
740
  .description('Start sync daemon — bidirectional config sync with control plane (requires an account)')
690
741
  .option('--dev', 'Use local control plane (http://localhost:3001)')
691
742
  .option('--interval <ms>', 'Poll interval in milliseconds', '30000')
@@ -697,7 +748,7 @@ program
697
748
  await runConnect(opts);
698
749
  });
699
750
  program
700
- .command('sync-context')
751
+ .command('sync-context', needsControlPlane)
701
752
  .description('Record the Git branch and commit in .intutic/git-context.json, which a running `intutic connect` reports')
702
753
  .option('--git', 'Read the branch and commit from the repository (for any not given below)')
703
754
  .option('--branch <name>', 'Current Git branch name')
@@ -707,7 +758,7 @@ program
707
758
  await runSyncContext(opts);
708
759
  });
709
760
  program
710
- .command('decisions-log-refresh')
761
+ .command('decisions-log-refresh', needsControlPlane)
711
762
  .description('One-shot refresh of the governed decisions log (.intutic/DECISIONS.md, and the recent ' +
712
763
  'entries in each governed harness\'s instructions file) — no-ops if decisionsLogEnabled is off. Invoked by the optional ' +
713
764
  'post-merge Git hook `intutic init` installs; safe to run manually.')
@@ -809,7 +860,7 @@ traces
809
860
  });
810
861
  // ── Findings (detector_findings adjudication) ───────────────────────────────
811
862
  const findings = program
812
- .command('findings')
863
+ .command('findings', needsControlPlane)
813
864
  .description('Query and adjudicate detector findings — the TP/FP labeling surface');
814
865
  findings
815
866
  .command('list')
@@ -856,7 +907,7 @@ findings
856
907
  });
857
908
  // ── Governance incidents ───────────────────────────────────────────────────
858
909
  const incidentsCmd = program
859
- .command('incidents')
910
+ .command('incidents', needsControlPlane)
860
911
  .description('Governance incidents, as on Findings › Incidents (OWNER, ADMIN, EM)');
861
912
  incidentsCmd
862
913
  .command('list')
@@ -883,7 +934,7 @@ incidentsCmd
883
934
  });
884
935
  // ── Trace integrity ────────────────────────────────────────────────────────
885
936
  const integrity = program
886
- .command('integrity')
937
+ .command('integrity', needsControlPlane)
887
938
  .description('Verify sealed trace roots — list, re-derive, walk the root chain, and walk the config snapshot chain');
888
939
  integrity
889
940
  .command('roots')
@@ -926,7 +977,7 @@ integrity
926
977
  });
927
978
  // ── Routing reports ─────────────────────────────────────────────────────────
928
979
  const routing = program
929
- .command('routing')
980
+ .command('routing', needsControlPlane)
930
981
  .description('Read-only reports for the routing subsystem');
931
982
  routing
932
983
  .command('adoption-report')
@@ -965,7 +1016,7 @@ function defineDaemonInstall(cmd) {
965
1016
  ' Use \'intutic daemon stop\' or \'intutic daemon uninstall\'.')
966
1017
  .option('--workspace-id <id>', 'Workspace ID (e.g. wk_xxxx) — required unless --proxy')
967
1018
  .option('--api-key <key>', 'Workspace API key (e.g. vk_xxxx) — required unless --proxy')
968
- .option('--control-plane-url <url>', 'Control plane the daemon connects to (default: $INTUTIC_CONTROL_PLANE_URL, then a workspace set up with `intutic init --dev`, then the URL saved by `intutic login`, then https://api.intutic.ai)')
1019
+ .option('--control-plane-url <url>', `Control plane the daemon connects to (default: $INTUTIC_CONTROL_PLANE_URL, then a workspace set up with \`intutic init --dev\`, then the URL saved by \`intutic login\`${CLOUD_RELEASED ? `, then ${HOSTED_CONTROL_PLANE_URL}` : ''})`)
969
1020
  .option('--binary-path <path>', 'Path to intutic CLI binary (defaults to current process); with --proxy, absolute path to intutic-proxy')
970
1021
  .option('--dry-run', 'Print what would be done without writing files')
971
1022
  .option('--system', 'Install as a system-level service (LaunchDaemon on macOS, systemd system unit on Linux)')
@@ -1098,7 +1149,7 @@ skillCmd
1098
1149
  });
1099
1150
  // ── Loop commands ──────────────────────────────────────────────────────────
1100
1151
  const loopCmd = program
1101
- .command('loop')
1152
+ .command('loop', needsControlPlane)
1102
1153
  .description('Manage and execute recursive agent loops with budget limits');
1103
1154
  loopCmd
1104
1155
  .command('start')
@@ -1166,7 +1217,7 @@ loopCmd
1166
1217
  // module doc for why `intutic loop review` was never the right command for
1167
1218
  // this — it addresses a different id space (Loop Runs, not decisions).
1168
1219
  const decisionCmd = program
1169
- .command('decision')
1220
+ .command('decision', needsControlPlane)
1170
1221
  .description('Approve or reject a decision (e.g. a review_before hold) held for human review');
1171
1222
  decisionCmd
1172
1223
  .command('approve <holdId>')
@@ -1192,7 +1243,7 @@ decisionCmd
1192
1243
  // hand-written curl against services/control-plane/src/routes/gateways.ts —
1193
1244
  // see commands/gateway.ts's module doc.
1194
1245
  const gatewayCmd = program
1195
- .command('gateway')
1246
+ .command('gateway', needsControlPlane)
1196
1247
  .description('Manage self-hosted gateway registrations (Docker / Kubernetes / bare metal)');
1197
1248
  gatewayCmd
1198
1249
  .command('register')
@@ -1290,7 +1341,7 @@ gatewayCmd
1290
1341
  // Provision a workspace's own upstream provider keys — the BYO-key wizard's
1291
1342
  // API, previously dashboard-only. See commands/credentials.ts's module doc.
1292
1343
  const credentialsCmd = program
1293
- .command('credentials')
1344
+ .command('credentials', needsControlPlane)
1294
1345
  .description('Manage this workspace\'s own upstream provider API keys (BYO-key)');
1295
1346
  credentialsCmd
1296
1347
  .command('list')
@@ -1334,7 +1385,7 @@ credentialsCmd
1334
1385
  // "CLI mints, dashboard audits": this is the mint/inspect half. See
1335
1386
  // commands/attenuate.ts's module doc.
1336
1387
  const attenuateCmd = program
1337
- .command('attenuate')
1388
+ .command('attenuate', needsControlPlane)
1338
1389
  .description('Attenuate one of your API keys to a child key with narrower scopes (and an optional TTL)')
1339
1390
  .option('--parent-key <keyId>', 'Parent API key ID to attenuate')
1340
1391
  .option('--caps <scopes>', 'Comma-separated API key scopes for the child key (e.g. gateway:use,traces:read); each must be granted by the parent key')
@@ -1352,9 +1403,54 @@ attenuateCmd
1352
1403
  const { runAttenuateChain } = await import('./commands/attenuate.js');
1353
1404
  await runAttenuateChain(chainId, opts);
1354
1405
  });
1406
+ // ── API keys and their scopes ────────────────────────────────────────────
1407
+ const keysCmd = program
1408
+ .command('keys', needsControlPlane)
1409
+ .description('List, create, re-scope and revoke your API keys');
1410
+ keysCmd
1411
+ .command('list')
1412
+ .description('List your API keys with their scopes and expiry')
1413
+ .option('--json', 'Output as JSON')
1414
+ .option('--dev', 'Use local control plane (http://localhost:3001)')
1415
+ .action(async (opts) => {
1416
+ const { runKeysList } = await import('./commands/keys.js');
1417
+ await runKeysList(opts);
1418
+ });
1419
+ keysCmd
1420
+ .command('create')
1421
+ .description('Create an API key and print it once')
1422
+ .requiredOption('--label <label>', 'A name for the key')
1423
+ .option('--scopes <scopes>', 'Comma-separated API key scopes (default: *, everything your role allows)')
1424
+ .option('--expires-in-days <n>', 'Expire the key after this many days (1-365; default: never)')
1425
+ .option('--service-account', 'An automation key: exempt from the SSO idle-key check')
1426
+ .option('--json', 'Output as JSON')
1427
+ .option('--dev', 'Use local control plane (http://localhost:3001)')
1428
+ .action(async (opts) => {
1429
+ const { runKeysCreate } = await import('./commands/keys.js');
1430
+ await runKeysCreate(opts);
1431
+ });
1432
+ keysCmd
1433
+ .command('update <keyId>')
1434
+ .description("Replace a key's scopes (OWNER, ADMIN)")
1435
+ .requiredOption('--scopes <scopes>', 'Comma-separated API key scopes the key will hold')
1436
+ .option('--json', 'Output as JSON')
1437
+ .option('--dev', 'Use local control plane (http://localhost:3001)')
1438
+ .action(async (keyId, opts) => {
1439
+ const { runKeysUpdate } = await import('./commands/keys.js');
1440
+ await runKeysUpdate(keyId, opts);
1441
+ });
1442
+ keysCmd
1443
+ .command('revoke <keyId>')
1444
+ .description('Revoke one of your API keys')
1445
+ .option('--json', 'Output as JSON')
1446
+ .option('--dev', 'Use local control plane (http://localhost:3001)')
1447
+ .action(async (keyId, opts) => {
1448
+ const { runKeysRevoke } = await import('./commands/keys.js');
1449
+ await runKeysRevoke(keyId, opts);
1450
+ });
1355
1451
  // ── Org signup + team management ─────────────────────────────────────────
1356
1452
  const orgCmd = program
1357
- .command('org')
1453
+ .command('org', needsControlPlane)
1358
1454
  .description('Create and manage orgs');
1359
1455
  orgCmd
1360
1456
  .command('create')
@@ -1369,7 +1465,7 @@ orgCmd
1369
1465
  await runOrgCreate(opts);
1370
1466
  });
1371
1467
  const teamCmd = program
1372
- .command('team')
1468
+ .command('team', needsControlPlane)
1373
1469
  .description('Manage teams and workspaces under an org');
1374
1470
  teamCmd
1375
1471
  .command('list')
@@ -1418,7 +1514,7 @@ teamCmd
1418
1514
  // route. Previously dashboard-only. See commands/apiCommand.ts for the shape
1419
1515
  // they share.
1420
1516
  const settingsCmd = program
1421
- .command('settings')
1517
+ .command('settings', needsControlPlane)
1422
1518
  .description('Read and change workspace settings (MCP policy, budgets, group policy, config content upload, ...)');
1423
1519
  settingsCmd
1424
1520
  .command('get [key]')
@@ -1440,7 +1536,7 @@ settingsCmd
1440
1536
  await runSettingsSet(key, value, opts);
1441
1537
  });
1442
1538
  const mcpCmd = program
1443
- .command('mcp')
1539
+ .command('mcp', needsControlPlane)
1444
1540
  .description('Review the MCP servers the workspace\'s MCP proxies have seen, and decide on them');
1445
1541
  mcpCmd
1446
1542
  .command('list')
@@ -1481,7 +1577,7 @@ for (const [action, enabled, desc] of [
1481
1577
  });
1482
1578
  }
1483
1579
  const notificationsCmd = program
1484
- .command('notifications')
1580
+ .command('notifications', needsControlPlane)
1485
1581
  .description('Manage notification rules: which events go to Slack, email, a webhook or PagerDuty');
1486
1582
  function ruleOptions(cmd) {
1487
1583
  return cmd
@@ -1543,7 +1639,7 @@ notificationsCmd
1543
1639
  await runNotificationsRotateSecret(ruleId, opts);
1544
1640
  });
1545
1641
  const siemCmd = program
1546
- .command('siem')
1642
+ .command('siem', needsControlPlane)
1547
1643
  .description('Manage SIEM export destinations and the event sources each receives');
1548
1644
  function destinationOptions(cmd) {
1549
1645
  return cmd
@@ -1617,8 +1713,53 @@ siemCmd
1617
1713
  const { runSiemRotateSecret } = await import('./commands/siem.js');
1618
1714
  await runSiemRotateSecret(destinationId, opts);
1619
1715
  });
1716
+ const tasksCmd = program
1717
+ .command('tasks', needsControlPlane)
1718
+ .description('Manage task-management connections (Jira Cloud, Linear, PagerDuty) that attach ticket context to sessions');
1719
+ tasksCmd
1720
+ .command('list')
1721
+ .description('List task-management connections (tokens are never returned)')
1722
+ .option('--json', 'Output as JSON')
1723
+ .option('--dev', 'Use local control plane (http://localhost:3001)')
1724
+ .action(async (opts) => {
1725
+ const { runTasksList } = await import('./commands/tasks.js');
1726
+ await runTasksList(opts);
1727
+ });
1728
+ tasksCmd
1729
+ .command('add')
1730
+ .description('Connect Jira Cloud, Linear or PagerDuty; the API token is read from --token or --token-file, never echoed')
1731
+ .requiredOption('--provider <provider>', 'jira, linear or pagerduty')
1732
+ .requiredOption('--base-url <url>', 'The provider\'s base URL (e.g. https://acme.atlassian.net)')
1733
+ .requiredOption('--project-key <key>', 'Jira project key, Linear team key or PagerDuty service key')
1734
+ .option('--account-email <email>', 'Jira only (required): the Atlassian account the API token belongs to')
1735
+ .option('--token <token>', 'Provider API token')
1736
+ .option('--token-file <path>', 'Read the API token from a file')
1737
+ .option('--json', 'Output as JSON')
1738
+ .option('--dev', 'Use local control plane (http://localhost:3001)')
1739
+ .action(async (opts) => {
1740
+ const { runTasksAdd } = await import('./commands/tasks.js');
1741
+ await runTasksAdd(opts);
1742
+ });
1743
+ tasksCmd
1744
+ .command('test <connectionId>')
1745
+ .description('Check that the provider still accepts a connection\'s stored credentials')
1746
+ .option('--json', 'Output as JSON')
1747
+ .option('--dev', 'Use local control plane (http://localhost:3001)')
1748
+ .action(async (connectionId, opts) => {
1749
+ const { runTasksTest } = await import('./commands/tasks.js');
1750
+ await runTasksTest(connectionId, opts);
1751
+ });
1752
+ tasksCmd
1753
+ .command('remove <connectionId>')
1754
+ .description('Remove a task-management connection and its stored token')
1755
+ .option('--json', 'Output as JSON')
1756
+ .option('--dev', 'Use local control plane (http://localhost:3001)')
1757
+ .action(async (connectionId, opts) => {
1758
+ const { runTasksRemove } = await import('./commands/tasks.js');
1759
+ await runTasksRemove(connectionId, opts);
1760
+ });
1620
1761
  const complianceCmd = program
1621
- .command('compliance')
1762
+ .command('compliance', needsControlPlane)
1622
1763
  .description('Compliance framework reports and the evidence archive');
1623
1764
  complianceCmd
1624
1765
  .command('coverage <framework_id>')
@@ -1663,7 +1804,7 @@ complianceCmd
1663
1804
  await runComplianceVerify(file, opts);
1664
1805
  });
1665
1806
  const usageCmd = program
1666
- .command('usage')
1807
+ .command('usage', needsControlPlane)
1667
1808
  .description('LLM usage across the fleet, by member, team, branch, commit or pull request');
1668
1809
  for (const [view, handler, desc] of [
1669
1810
  ['members', 'runUsageMembers', 'Usage per member'],
@@ -1694,7 +1835,7 @@ usageCmd
1694
1835
  await runUsagePullRequests(opts);
1695
1836
  });
1696
1837
  const githubWebhookCmd = program
1697
- .command('github')
1838
+ .command('github', needsControlPlane)
1698
1839
  .description('GitHub integration for cost per pull request')
1699
1840
  .command('webhook')
1700
1841
  .description('The pull-request webhook: payload URL and signing secret');
@@ -1717,7 +1858,7 @@ githubWebhookCmd
1717
1858
  await runGithubWebhookRotateSecret(opts);
1718
1859
  });
1719
1860
  const inventoryCmd = program
1720
- .command('inventory')
1861
+ .command('inventory', needsControlPlane)
1721
1862
  .description('AI harnesses, MCP servers and skills on connected developer machines, governed or not');
1722
1863
  inventoryCmd
1723
1864
  .command('summary')
@@ -1779,7 +1920,7 @@ inventoryCmd
1779
1920
  await runInventoryDisconnects(opts);
1780
1921
  });
1781
1922
  program
1782
- .command('gate-liveness')
1923
+ .command('gate-liveness', needsControlPlane)
1783
1924
  .description('Whether each installed harness\'s gate is reporting, and any open silent-gate alert')
1784
1925
  .option('--json', 'Output as JSON')
1785
1926
  .option('--dev', 'Use local control plane (http://localhost:3001)')