osuite 2.9.0 → 2.9.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.
package/cli.js CHANGED
@@ -1,129 +1,417 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import pkg from './package.json' with { type: 'json' };
4
- import { OSuite } from './osuite.js';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { OSuite, buildReviewSurface } from './osuite.js';
5
7
 
6
- function printHelp() {
7
- process.stdout.write(`OSuite CLI v${pkg.version}
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
11
+
12
+ const ANSI = {
13
+ reset: '\x1b[0m',
14
+ bold: '\x1b[1m',
15
+ dim: '\x1b[2m',
16
+ red: '\x1b[31m',
17
+ green: '\x1b[32m',
18
+ yellow: '\x1b[33m',
19
+ blue: '\x1b[34m',
20
+ cyan: '\x1b[36m',
21
+ };
22
+
23
+ const PROTECTED_REFS = new Set(['main', 'master', 'prod', 'production', 'stable']);
24
+ const OBSERVE_OPS = new Set(['get', 'describe', 'logs', 'top', 'version', 'diff']);
25
+ const PLAN_OPS = new Set(['plan', 'show', 'validate', 'output', 'fmt', 'providers', 'graph']);
26
+ const MUTATION_OPS = new Set(['apply', 'destroy', 'delete', 'create', 'replace', 'taint', 'untaint', 'import', 'upgrade', 'rollback', 'install', 'uninstall']);
8
27
 
9
- Usage:
10
- osuite help
11
- osuite version
12
- osuite approvals [--limit 20] [--offset 0]
13
- osuite approve <actionId> [--reason "Approved by operator"]
14
- osuite deny <actionId> [--reason "Outside change window"]
28
+ function useColor() {
29
+ return !process.env.NO_COLOR && process.stdout.isTTY !== false;
30
+ }
31
+
32
+ function c(value, color) {
33
+ return useColor() ? `${color}${value}${ANSI.reset}` : value;
34
+ }
15
35
 
16
- Environment:
17
- OSUITE_BASE_URL Required, your OSuite base URL
18
- OSUITE_URL Optional fallback for hosted/base URL
19
- NEXTAUTH_URL Optional fallback when reusing app env files
20
- OSUITE_API_KEY Required, admin API key for approval operations
21
- OSUITE_AGENT_ID Optional, defaults to "osuite-cli"
22
- `);
36
+ function write(value = '') {
37
+ process.stdout.write(`${value}\n`);
23
38
  }
24
39
 
25
40
  function parseArgs(argv) {
26
41
  const [command = 'help', ...rest] = argv;
27
42
  const args = { _: [] };
28
-
29
- for (let i = 0; i < rest.length; i += 1) {
30
- const token = rest[i];
43
+ for (let index = 0; index < rest.length; index += 1) {
44
+ const token = rest[index];
31
45
  if (token.startsWith('--')) {
32
46
  const key = token.slice(2);
33
- const next = rest[i + 1];
34
- if (!next || next.startsWith('--')) {
35
- args[key] = true;
36
- } else {
47
+ const next = rest[index + 1];
48
+ if (!next || next.startsWith('--')) args[key] = true;
49
+ else {
37
50
  args[key] = next;
38
- i += 1;
51
+ index += 1;
39
52
  }
40
- continue;
53
+ } else {
54
+ args._.push(token);
41
55
  }
42
- args._.push(token);
43
56
  }
44
-
45
57
  return { command, args };
46
58
  }
47
59
 
48
- function getClient() {
49
- const baseUrl = process.env.OSUITE_BASE_URL
50
- || process.env.DASHCLAW_BASE_URL
51
- || process.env.OSUITE_URL
52
- || process.env.DASHCLAW_URL
53
- || process.env.NEXTAUTH_URL;
54
- const apiKey = process.env.OSUITE_API_KEY || process.env.DASHCLAW_API_KEY;
55
- const agentId = process.env.OSUITE_AGENT_ID || process.env.DASHCLAW_AGENT_ID || 'osuite-cli';
60
+ function envValue(name, fallback) {
61
+ return process.env[name] || process.env[fallback] || '';
62
+ }
63
+
64
+ function readConfig() {
65
+ return {
66
+ baseUrl: envValue('OSUITE_BASE_URL', 'DASHCLAW_BASE_URL'),
67
+ apiKey: envValue('OSUITE_API_KEY', 'DASHCLAW_API_KEY'),
68
+ agentId: envValue('OSUITE_AGENT_ID', 'DASHCLAW_AGENT_ID') || 'osuite-cli',
69
+ runtimeAdapterId: process.env.OSUITE_RUNTIME_ADAPTER_ID || '',
70
+ signatureMode: process.env.OSUITE_SIGNATURE_MODE || '',
71
+ };
72
+ }
56
73
 
57
- if (!baseUrl || !apiKey) {
58
- process.stderr.write('Missing a base URL env (OSUITE_BASE_URL / OSUITE_URL / NEXTAUTH_URL) or OSUITE_API_KEY.\n');
74
+ function requireClient() {
75
+ const config = readConfig();
76
+ if (!config.baseUrl || !config.apiKey) {
77
+ write(c('OSuite is not connected yet.', ANSI.yellow));
78
+ write('Set OSUITE_BASE_URL and OSUITE_API_KEY, or run:');
79
+ write(' osuite init');
59
80
  process.exit(1);
60
81
  }
82
+ return new OSuite(config);
83
+ }
61
84
 
62
- return new OSuite({ baseUrl, apiKey, agentId });
85
+ function banner() {
86
+ return [
87
+ c('OSUITE', `${ANSI.bold}${ANSI.cyan}`),
88
+ c('Governed action layer for AI agents', ANSI.bold),
89
+ c('PCAA authority | CAVA action understanding | Decision V2 review', ANSI.dim),
90
+ ].join('\n');
63
91
  }
64
92
 
65
- async function listApprovals(args) {
66
- const client = getClient();
67
- const limit = Number.parseInt(args.limit || '20', 10);
68
- const offset = Number.parseInt(args.offset || '0', 10);
69
- const result = await client.getPendingApprovals(limit, offset);
70
- const actions = result.actions || [];
93
+ function printHelp() {
94
+ write(banner());
95
+ write('');
96
+ write(c(`OSuite CLI v${pkg.version}`, ANSI.dim));
97
+ write('');
98
+ write(c('Useful commands', ANSI.bold));
99
+ write(' osuite doctor Check connection, runtime, signatures, package health');
100
+ write(' osuite status Ping Studio and show current workspace connection');
101
+ write(' osuite explain "git push origin main" Explain how CAVA/Decision V2 will read a command');
102
+ write(' osuite approvals --limit 20 Show pending approval inbox');
103
+ write(' osuite review <actionId> Render the decision review card');
104
+ write(' osuite approve <actionId> --reason "Looks safe"');
105
+ write(' osuite deny <actionId> --reason "Outside change window"');
106
+ write(' osuite init [codex|claude|mcp] Print setup steps for a runtime lane');
107
+ write('');
108
+ write(c('Environment', ANSI.bold));
109
+ write(' OSUITE_BASE_URL Studio URL, for example https://studio.osuite.ai');
110
+ write(' OSUITE_API_KEY Workspace or admin API key');
111
+ write(' OSUITE_AGENT_ID Optional actor label for CLI actions');
112
+ write('');
113
+ write(c('Old DASHCLAW_* environment names are still accepted for compatibility.', ANSI.dim));
114
+ }
71
115
 
72
- if (actions.length === 0) {
73
- process.stdout.write('No pending approvals.\n');
74
- return;
116
+ function tokenize(command) {
117
+ return String(command || '').match(/"[^"]*"|'[^']*'|\S+/g)?.map((token) => token.replace(/^['"]|['"]$/g, '')) || [];
118
+ }
119
+
120
+ function protectedGitTarget(tokens) {
121
+ if (tokens[0] !== 'git' || tokens[1] !== 'push') return null;
122
+ const refs = tokens.slice(2).filter((token) => !token.startsWith('-') && token !== 'origin' && !token.includes(':'));
123
+ const target = refs[0] || 'unknown';
124
+ const normalized = target.replace(/^refs\/heads\//, '');
125
+ if (PROTECTED_REFS.has(normalized) || normalized.startsWith('release/') || normalized.includes('prod')) return normalized;
126
+ return null;
127
+ }
128
+
129
+ function classifyCommand(command) {
130
+ const tokens = tokenize(command);
131
+ const executable = tokens[0] || '';
132
+ const operation = tokens[1] || '';
133
+
134
+ if (!tokens.length) {
135
+ return { category: 'unknown', operation: 'unknown', risk: 20, reversible: true, summary: 'No command provided.' };
75
136
  }
76
137
 
77
- for (const action of actions) {
78
- process.stdout.write(
79
- `${action.action_id}\t${action.agent_name || action.agent_id}\t${action.action_type}\t${action.declared_goal || '-'}\n`
80
- );
138
+ if (executable === 'git' && operation === 'push') {
139
+ const protectedTarget = protectedGitTarget(tokens);
140
+ const target = protectedTarget || tokens.filter((token) => !token.startsWith('-')).at(-1) || 'remote';
141
+ return {
142
+ category: 'deployment',
143
+ operation: 'push',
144
+ risk: protectedTarget ? 82 : 58,
145
+ reversible: false,
146
+ systems: ['git', 'remote_repository'],
147
+ summary: protectedTarget
148
+ ? `Git push to protected target ${protectedTarget}.`
149
+ : `Git push to ${target}.`,
150
+ notes: protectedTarget ? ['protected target', 'approval recommended before side effects'] : ['feature branch target'],
151
+ };
81
152
  }
82
- }
83
153
 
84
- async function decide(actionId, decision, args) {
85
- if (!actionId) {
86
- process.stderr.write('Missing actionId.\n');
87
- process.exit(1);
154
+ if (executable === 'npm' && operation === 'run' && tokens[2] === 'build') {
155
+ return {
156
+ category: 'build',
157
+ operation: 'build',
158
+ risk: 30,
159
+ reversible: true,
160
+ systems: ['local_workspace'],
161
+ summary: 'Local build or compilation step.',
162
+ notes: ['usually does not require approval unless chained with deployment'],
163
+ };
88
164
  }
89
165
 
90
- const client = getClient();
91
- const result = await client.approveAction(actionId, decision, args.reason || args.reasoning);
92
- process.stdout.write(`${decision === 'allow' ? 'Approved' : 'Denied'} ${actionId}\n`);
93
- if (result?.action?.status) {
94
- process.stdout.write(`New status: ${result.action.status}\n`);
166
+ if (['kubectl', 'helm', 'terraform', 'pulumi', 'ansible'].includes(executable)) {
167
+ const op = operation || 'unknown';
168
+ if (OBSERVE_OPS.has(op)) {
169
+ return {
170
+ category: 'observation',
171
+ operation: op,
172
+ risk: 28,
173
+ reversible: true,
174
+ systems: [executable],
175
+ summary: `${executable} observation command.`,
176
+ notes: ['read-oriented runtime action'],
177
+ };
178
+ }
179
+ if (PLAN_OPS.has(op)) {
180
+ return {
181
+ category: 'infrastructure_plan',
182
+ operation: op,
183
+ risk: 38,
184
+ reversible: true,
185
+ systems: [executable],
186
+ summary: `${executable} planning command.`,
187
+ notes: ['plan evidence can improve approval confidence'],
188
+ };
189
+ }
190
+ if (MUTATION_OPS.has(op)) {
191
+ return {
192
+ category: 'infrastructure_change',
193
+ operation: op,
194
+ risk: op === 'apply' || op === 'destroy' ? 90 : 78,
195
+ reversible: false,
196
+ systems: [executable],
197
+ summary: `${executable} infrastructure mutation.`,
198
+ notes: ['approval recommended', 'capture plan/output as evidence'],
199
+ };
200
+ }
201
+ }
202
+
203
+ if (executable === 'az' && tokens[1] === 'keyvault' && tokens[2] === 'secret') {
204
+ return {
205
+ category: 'secret_access',
206
+ operation: tokens[3] || 'secret',
207
+ risk: 74,
208
+ reversible: true,
209
+ systems: ['azure_key_vault'],
210
+ summary: 'Azure Key Vault secret access.',
211
+ notes: ['sensitive read', 'approval may be required by policy'],
212
+ };
213
+ }
214
+
215
+ if (['rm', 'drop', 'truncate'].some((prefix) => String(command).toLowerCase().includes(prefix))) {
216
+ return {
217
+ category: 'destructive_operation',
218
+ operation: executable,
219
+ risk: 92,
220
+ reversible: false,
221
+ systems: ['workspace'],
222
+ summary: 'Potentially destructive operation.',
223
+ notes: ['approval strongly recommended'],
224
+ };
95
225
  }
226
+
227
+ return {
228
+ category: 'shell_command',
229
+ operation: executable,
230
+ risk: 45,
231
+ reversible: null,
232
+ systems: ['shell'],
233
+ summary: 'General shell command. OSuite will use runtime context and policy to decide.',
234
+ notes: ['provide declared_goal and evidence for better scoring'],
235
+ };
96
236
  }
97
237
 
98
- async function main() {
99
- const { command, args } = parseArgs(process.argv.slice(2));
238
+ function riskBand(score) {
239
+ if (score >= 75) return 'High';
240
+ if (score >= 45) return 'Moderate';
241
+ return 'Low';
242
+ }
100
243
 
101
- if (command === 'help' || command === '--help' || command === '-h') {
102
- printHelp();
103
- return;
244
+ function printExplanation(command) {
245
+ const result = classifyCommand(command);
246
+ const band = riskBand(result.risk);
247
+ const bandColor = band === 'High' ? ANSI.red : band === 'Moderate' ? ANSI.yellow : ANSI.green;
248
+
249
+ write(c('OSuite CAVA explanation', `${ANSI.bold}${ANSI.cyan}`));
250
+ write(c('Local preview. No server call was made.', ANSI.dim));
251
+ write('');
252
+ write(`Command ${command}`);
253
+ write(`Category ${result.category}`);
254
+ write(`Operation ${result.operation}`);
255
+ write(`Risk ${c(`${band} (${result.risk}/100)`, bandColor)}`);
256
+ write(`Reversible ${result.reversible === null ? 'unknown' : String(result.reversible)}`);
257
+ write(`Systems touched ${(result.systems || []).join(', ') || 'unknown'}`);
258
+ write(`OSuite understood ${result.summary}`);
259
+ if (result.notes?.length) {
260
+ write('');
261
+ write(c('Decision hints', ANSI.bold));
262
+ result.notes.forEach((note) => write(` - ${note}`));
104
263
  }
264
+ }
105
265
 
106
- if (command === 'version' || command === '--version' || command === '-v') {
107
- process.stdout.write(`${pkg.version}\n`);
108
- return;
266
+ async function doctor() {
267
+ const config = readConfig();
268
+ let ok = true;
269
+
270
+ write(c('OSuite Doctor', `${ANSI.bold}${ANSI.cyan}`));
271
+ write(c('Checking the local developer entry point.', ANSI.dim));
272
+ write('');
273
+
274
+ const checks = [
275
+ ['OSUITE_BASE_URL', Boolean(config.baseUrl), config.baseUrl || 'missing'],
276
+ ['OSUITE_API_KEY', Boolean(config.apiKey), config.apiKey ? 'present' : 'missing'],
277
+ ['OSUITE_AGENT_ID', Boolean(config.agentId), config.agentId || 'osuite-cli'],
278
+ ['Runtime adapter', Boolean(config.runtimeAdapterId), config.runtimeAdapterId || 'not declared'],
279
+ ['Signature mode', Boolean(config.signatureMode), config.signatureMode || 'not declared'],
280
+ ];
281
+
282
+ checks.forEach(([label, passed, detail]) => {
283
+ if (!passed && (label === 'OSUITE_BASE_URL' || label === 'OSUITE_API_KEY')) ok = false;
284
+ write(`${passed ? c('OK ', ANSI.green) : c('MISS', ANSI.yellow)} ${label.padEnd(18)} ${detail}`);
285
+ });
286
+
287
+ if (config.baseUrl) {
288
+ try {
289
+ const response = await fetch(`${config.baseUrl.replace(/\/$/, '')}/api/health`);
290
+ const body = await response.text();
291
+ const healthy = response.ok && body.includes('healthy');
292
+ ok = ok && healthy;
293
+ write(`${healthy ? c('OK ', ANSI.green) : c('WARN', ANSI.yellow)} ${'Studio health'.padEnd(18)} HTTP ${response.status}`);
294
+ } catch (error) {
295
+ ok = false;
296
+ write(`${c('FAIL', ANSI.red)} ${'Studio health'.padEnd(18)} ${error.message}`);
297
+ }
109
298
  }
110
299
 
111
- if (command === 'approvals') {
112
- await listApprovals(args);
113
- return;
300
+ write('');
301
+ if (!ok) {
302
+ write(c('Next step', ANSI.bold));
303
+ write(' osuite init');
304
+ write(' export OSUITE_BASE_URL=https://studio.osuite.ai');
305
+ write(' export OSUITE_API_KEY=<workspace-api-key>');
306
+ process.exit(1);
114
307
  }
115
308
 
116
- if (command === 'approve') {
117
- await decide(args._[0], 'allow', args);
309
+ write(c('Your OSuite CLI is ready.', ANSI.green));
310
+ }
311
+
312
+ async function status() {
313
+ const config = readConfig();
314
+ write(c('OSuite Status', `${ANSI.bold}${ANSI.cyan}`));
315
+ write(`Base URL ${config.baseUrl || 'not configured'}`);
316
+ write(`Agent ID ${config.agentId}`);
317
+ write(`Runtime adapter ${config.runtimeAdapterId || 'not declared'}`);
318
+ if (!config.baseUrl) return;
319
+
320
+ const response = await fetch(`${config.baseUrl.replace(/\/$/, '')}/api/health`);
321
+ write(`Studio health HTTP ${response.status}`);
322
+ }
323
+
324
+ function renderActionRow(action) {
325
+ const id = action.action_id || action.id || '-';
326
+ const score = action.risk_score ?? action.agent_risk_score ?? '-';
327
+ const type = action.action_type || '-';
328
+ const agent = action.agent_name || action.agent_id || '-';
329
+ const goal = action.declared_goal || action.input_summary || '-';
330
+ return `${id.padEnd(18)} ${String(score).padStart(3)} ${type.padEnd(24)} ${agent.padEnd(18)} ${goal}`;
331
+ }
332
+
333
+ async function approvals(args) {
334
+ const client = requireClient();
335
+ const limit = Number.parseInt(args.limit || '20', 10);
336
+ const offset = Number.parseInt(args.offset || '0', 10);
337
+ const payload = await client.getPendingApprovals(limit, offset);
338
+ const actions = payload.actions || [];
339
+
340
+ write(c('OSuite Approval Inbox', `${ANSI.bold}${ANSI.cyan}`));
341
+ write(c('Actions waiting for human authority.', ANSI.dim));
342
+ write('');
343
+
344
+ if (!actions.length) {
345
+ write('No pending approvals.');
118
346
  return;
119
347
  }
120
348
 
121
- if (command === 'deny') {
122
- await decide(args._[0], 'deny', args);
123
- return;
349
+ write(`${'Action ID'.padEnd(18)} ${'Risk'.padStart(3)} ${'Type'.padEnd(24)} ${'Agent'.padEnd(18)} Goal`);
350
+ actions.forEach((action) => write(renderActionRow(action)));
351
+ }
352
+
353
+ async function review(actionId) {
354
+ if (!actionId) {
355
+ write('Missing actionId. Usage: osuite review <actionId>');
356
+ process.exit(1);
357
+ }
358
+ const client = requireClient();
359
+ const payload = await client.getAction(actionId);
360
+ const action = payload.action || payload;
361
+ write(buildReviewSurface(action, { baseUrl: client.baseUrl }));
362
+ }
363
+
364
+ async function decide(actionId, decision, args) {
365
+ if (!actionId) {
366
+ write(`Missing actionId. Usage: osuite ${decision === 'allow' ? 'approve' : 'deny'} <actionId>`);
367
+ process.exit(1);
124
368
  }
369
+ const client = requireClient();
370
+ const result = await client.approveAction(actionId, decision, args.reason || args.reasoning);
371
+ const label = decision === 'allow' ? 'Approved' : 'Denied';
372
+ write(c(`${label} ${actionId}`, decision === 'allow' ? ANSI.green : ANSI.red));
373
+ if (result?.action?.status) write(`New status ${result.action.status}`);
374
+ }
375
+
376
+ function init(target = 'generic') {
377
+ write(c('OSuite init', `${ANSI.bold}${ANSI.cyan}`));
378
+ write('Paste these into your shell, then run `osuite doctor`:');
379
+ write('');
380
+ write('export OSUITE_BASE_URL=https://studio.osuite.ai');
381
+ write('export OSUITE_API_KEY=<workspace-api-key>');
382
+ write('export OSUITE_AGENT_ID=<agent-or-runtime-id>');
383
+ write('');
384
+
385
+ if (target === 'codex') {
386
+ write(c('Codex runtime lane', ANSI.bold));
387
+ write(' osuite doctor');
388
+ write(' npm run runtime:install:codex-plugin');
389
+ } else if (target === 'claude') {
390
+ write(c('Claude Code runtime lane', ANSI.bold));
391
+ write(' export OSUITE_RUNTIME_ADAPTER_ID=claude_code_hooks');
392
+ write(' export OSUITE_SIGNATURE_MODE=required');
393
+ } else if (target === 'mcp') {
394
+ write(c('MCP runtime lane', ANSI.bold));
395
+ write(' Use OSuite preflight, wait-for-approval, and complete-action tools around consequential actions.');
396
+ }
397
+ }
398
+
399
+ async function main() {
400
+ const { command, args } = parseArgs(process.argv.slice(2));
401
+
402
+ if (['help', '--help', '-h'].includes(command)) return printHelp();
403
+ if (['version', '--version', '-v'].includes(command)) return write(pkg.version);
404
+ if (command === 'doctor') return doctor();
405
+ if (command === 'status') return status();
406
+ if (command === 'explain') return printExplanation(args._.join(' '));
407
+ if (command === 'approvals') return approvals(args);
408
+ if (command === 'review') return review(args._[0]);
409
+ if (command === 'approve') return decide(args._[0], 'allow', args);
410
+ if (command === 'deny') return decide(args._[0], 'deny', args);
411
+ if (command === 'init') return init(args._[0] || 'generic');
125
412
 
126
- process.stderr.write(`Unknown command: ${command}\n\n`);
413
+ write(`Unknown command: ${command}`);
414
+ write('');
127
415
  printHelp();
128
416
  process.exit(1);
129
417
  }