osuite 2.9.2 → 2.9.4
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/README.md +12 -10
- package/cli.js +205 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# OSuite SDK (v2.9.
|
|
1
|
+
# OSuite SDK (v2.9.4)
|
|
2
2
|
|
|
3
3
|
**Governed action SDK for AI agents.**
|
|
4
4
|
|
|
@@ -448,12 +448,14 @@ OSuite uses standard HTTP status codes and custom error classes:
|
|
|
448
448
|
|
|
449
449
|
## CLI Approval Channel
|
|
450
450
|
|
|
451
|
-
Install the OSuite CLI
|
|
451
|
+
Install the OSuite CLI from the project root where your agent works. For Codex and Claude Code, the installer adds project-scoped hook files (`.codex` or `.claude`) while the CLI remains the control surface for doctor, explain, review, and approval:
|
|
452
452
|
|
|
453
453
|
```bash
|
|
454
|
-
curl -fsSL https://studio.osuite.ai/install.sh | bash
|
|
454
|
+
curl -fsSL https://studio.osuite.ai/install.sh | bash
|
|
455
455
|
```
|
|
456
456
|
|
|
457
|
+
The default installer auto-detects common project lanes and starts a browser handoff. If auto-detection is wrong, force a lane with `OSUITE_INSTALL_TARGET=codex`, `OSUITE_INSTALL_TARGET=claude`, `OSUITE_INSTALL_TARGET=sdk`, or `OSUITE_INSTALL_TARGET=mcp`.
|
|
458
|
+
|
|
457
459
|
If you prefer npm directly:
|
|
458
460
|
|
|
459
461
|
```bash
|
|
@@ -462,9 +464,11 @@ npm install -g osuite
|
|
|
462
464
|
|
|
463
465
|
```bash
|
|
464
466
|
osuite # branded welcome and command map
|
|
467
|
+
osuite connect auto # detect the current project and start browser handoff
|
|
465
468
|
osuite doctor # check env, Studio health, runtime, and signature posture
|
|
466
469
|
osuite status # show the current Studio connection
|
|
467
|
-
osuite init codex #
|
|
470
|
+
osuite init codex # install .codex hook files into the current project
|
|
471
|
+
osuite init claude # install .claude hook files into the current project
|
|
468
472
|
osuite explain "git push origin main"
|
|
469
473
|
osuite approvals # approval inbox
|
|
470
474
|
osuite review <actionId> # Decision V2 review surface
|
|
@@ -472,7 +476,7 @@ osuite approve <actionId> # approve a specific action
|
|
|
472
476
|
osuite deny <actionId> # deny a specific action
|
|
473
477
|
```
|
|
474
478
|
|
|
475
|
-
The CLI is intentionally more than an API wrapper:
|
|
479
|
+
For local CLI agents, `osuite connect auto` prints a browser handoff URL and installs the project hook lane when it can detect Codex or Claude Code. If browser handoff is unavailable, copy `.codex/.env.example` or `.claude/.env.example` to `.env`, fill `OSUITE_API_KEY`, and run the agent from the same project root. The CLI is intentionally more than an API wrapper:
|
|
476
480
|
- `doctor` tells a developer what is missing before they connect an agent.
|
|
477
481
|
- `explain` gives a local CAVA preview before a command reaches OSuite.
|
|
478
482
|
- `approvals` provides a readable approval inbox instead of raw JSON.
|
|
@@ -482,15 +486,13 @@ When an agent calls `waitForApproval()`, it prints the action ID and replay link
|
|
|
482
486
|
|
|
483
487
|
## Claude Code Hooks
|
|
484
488
|
|
|
485
|
-
Govern Claude Code tool calls without any SDK instrumentation.
|
|
489
|
+
Govern Claude Code tool calls without any SDK instrumentation. The preferred path is the project-root installer:
|
|
486
490
|
|
|
487
491
|
```bash
|
|
488
|
-
|
|
489
|
-
cp path/to/OSuite/hooks/osuite_pretool.py .claude/hooks/
|
|
490
|
-
cp path/to/OSuite/hooks/osuite_posttool.py .claude/hooks/
|
|
492
|
+
curl -fsSL https://studio.osuite.ai/install.sh | bash
|
|
491
493
|
```
|
|
492
494
|
|
|
493
|
-
|
|
495
|
+
It detects `.claude`, creates `.claude/hooks/*`, `.claude/settings.json`, `.claude/settings.local.json`, and `.claude/.env.example`, then prints the browser handoff. Copy the env example to `.claude/.env` and fill `OSUITE_API_KEY` only when the browser flow is not available.
|
|
494
496
|
|
|
495
497
|
---
|
|
496
498
|
|
package/cli.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import crypto from 'node:crypto';
|
|
5
6
|
import { fileURLToPath } from 'node:url';
|
|
6
7
|
import { OSuite, buildReviewSurface } from './osuite.js';
|
|
7
8
|
|
|
@@ -24,6 +25,25 @@ const PROTECTED_REFS = new Set(['main', 'master', 'prod', 'production', 'stable'
|
|
|
24
25
|
const OBSERVE_OPS = new Set(['get', 'describe', 'logs', 'top', 'version', 'diff']);
|
|
25
26
|
const PLAN_OPS = new Set(['plan', 'show', 'validate', 'output', 'fmt', 'providers', 'graph']);
|
|
26
27
|
const MUTATION_OPS = new Set(['apply', 'destroy', 'delete', 'create', 'replace', 'taint', 'untaint', 'import', 'upgrade', 'rollback', 'install', 'uninstall']);
|
|
28
|
+
const DEFAULT_BASE_URL = 'https://studio.osuite.ai';
|
|
29
|
+
const PROJECT_BOOTSTRAPS = {
|
|
30
|
+
codex: {
|
|
31
|
+
label: 'Codex hook runtime',
|
|
32
|
+
endpoint: '/api/runtimes/codex/bootstrap',
|
|
33
|
+
envPath: '.codex/.env.example',
|
|
34
|
+
readmePath: '.codex/README.osuite.md',
|
|
35
|
+
runLine: 'Run Codex from this project root so .codex/hooks.json can govern Bash/Edit/Write actions.',
|
|
36
|
+
smokeLine: 'Try: osuite explain "git push origin main", then ask Codex to run a low-risk command such as git status.',
|
|
37
|
+
},
|
|
38
|
+
claude: {
|
|
39
|
+
label: 'Claude Code hook runtime',
|
|
40
|
+
endpoint: '/api/runtimes/claude-code/bootstrap',
|
|
41
|
+
envPath: '.claude/.env.example',
|
|
42
|
+
readmePath: '.claude/README.osuite.md',
|
|
43
|
+
runLine: 'Run Claude Code from this project root so .claude/settings.json can govern tool calls.',
|
|
44
|
+
smokeLine: 'Try: osuite explain "git push origin main", then ask Claude Code to run a low-risk command such as git status.',
|
|
45
|
+
},
|
|
46
|
+
};
|
|
27
47
|
|
|
28
48
|
function useColor() {
|
|
29
49
|
return !process.env.NO_COLOR && process.stdout.isTTY !== false;
|
|
@@ -37,6 +57,25 @@ function write(value = '') {
|
|
|
37
57
|
process.stdout.write(`${value}\n`);
|
|
38
58
|
}
|
|
39
59
|
|
|
60
|
+
function loadDotenvFile(relativePath) {
|
|
61
|
+
const absolute = path.join(process.cwd(), relativePath);
|
|
62
|
+
if (!fs.existsSync(absolute)) return;
|
|
63
|
+
const body = fs.readFileSync(absolute, 'utf8');
|
|
64
|
+
body.split(/\r?\n/).forEach((line) => {
|
|
65
|
+
const trimmed = line.trim();
|
|
66
|
+
if (!trimmed || trimmed.startsWith('#') || !trimmed.includes('=')) return;
|
|
67
|
+
const [rawKey, ...rawValue] = trimmed.split('=');
|
|
68
|
+
const key = rawKey.trim();
|
|
69
|
+
let value = rawValue.join('=').trim().replace(/^['"]|['"]$/g, '');
|
|
70
|
+
if (value.includes(' #')) value = value.slice(0, value.indexOf(' #')).trim();
|
|
71
|
+
if (key && !process.env[key]) process.env[key] = value;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function loadProjectEnvironment() {
|
|
76
|
+
['.codex/.env', '.claude/.env', '.osuite/.env', '.env'].forEach(loadDotenvFile);
|
|
77
|
+
}
|
|
78
|
+
|
|
40
79
|
function parseArgs(argv) {
|
|
41
80
|
const [command = 'help', ...rest] = argv;
|
|
42
81
|
const args = { _: [] };
|
|
@@ -57,6 +96,8 @@ function parseArgs(argv) {
|
|
|
57
96
|
return { command, args };
|
|
58
97
|
}
|
|
59
98
|
|
|
99
|
+
loadProjectEnvironment();
|
|
100
|
+
|
|
60
101
|
function envValue(name, fallback) {
|
|
61
102
|
return process.env[name] || process.env[fallback] || '';
|
|
62
103
|
}
|
|
@@ -82,6 +123,115 @@ function requireClient() {
|
|
|
82
123
|
return new OSuite(config);
|
|
83
124
|
}
|
|
84
125
|
|
|
126
|
+
function normalizedBaseUrl(value) {
|
|
127
|
+
return String(value || DEFAULT_BASE_URL).replace(/\/$/, '');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function sanitizeRuntimeTarget(value, fallback = 'auto') {
|
|
131
|
+
const normalized = String(value || fallback)
|
|
132
|
+
.trim()
|
|
133
|
+
.toLowerCase()
|
|
134
|
+
.replace(/[^a-z0-9_-]/g, '');
|
|
135
|
+
if (!normalized) return fallback;
|
|
136
|
+
if (normalized === 'claude-code' || normalized === 'claudecode') return 'claude';
|
|
137
|
+
if (normalized === 'generic') return 'auto';
|
|
138
|
+
return normalized;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function detectRuntimeTarget(preferred = 'auto') {
|
|
142
|
+
const target = sanitizeRuntimeTarget(preferred);
|
|
143
|
+
if (target !== 'auto') return target;
|
|
144
|
+
|
|
145
|
+
const cwd = process.cwd();
|
|
146
|
+
if (fs.existsSync(path.join(cwd, '.codex'))) return 'codex';
|
|
147
|
+
if (fs.existsSync(path.join(cwd, '.claude'))) return 'claude';
|
|
148
|
+
if (fs.existsSync(path.join(cwd, 'mcp.json')) || fs.existsSync(path.join(cwd, '.mcp.json'))) return 'mcp';
|
|
149
|
+
if (fs.existsSync(path.join(cwd, 'package.json'))) return 'sdk';
|
|
150
|
+
return 'sdk';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function generateConnectCode() {
|
|
154
|
+
return `OS-${crypto.randomBytes(3).toString('hex').toUpperCase()}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function buildConnectHandoffUrl({ baseUrl, runtime, code }) {
|
|
158
|
+
const url = new URL('/connect', normalizedBaseUrl(baseUrl));
|
|
159
|
+
url.searchParams.set('runtime', runtime);
|
|
160
|
+
url.searchParams.set('source', 'cli');
|
|
161
|
+
url.searchParams.set('code', code);
|
|
162
|
+
return url.toString();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function projectFilePath(relativePath) {
|
|
166
|
+
const clean = String(relativePath || '').replace(/^\/+/, '');
|
|
167
|
+
const root = path.resolve(process.cwd());
|
|
168
|
+
const absolute = path.resolve(root, clean);
|
|
169
|
+
if (absolute !== root && !absolute.startsWith(`${root}${path.sep}`)) {
|
|
170
|
+
throw new Error(`Refusing to write outside the project root: ${relativePath}`);
|
|
171
|
+
}
|
|
172
|
+
return absolute;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function writeProjectFile(relativePath, body, { force = false } = {}) {
|
|
176
|
+
const absolute = projectFilePath(relativePath);
|
|
177
|
+
if (fs.existsSync(absolute) && !force) return { relativePath, status: 'skipped' };
|
|
178
|
+
fs.mkdirSync(path.dirname(absolute), { recursive: true });
|
|
179
|
+
fs.writeFileSync(absolute, String(body || ''));
|
|
180
|
+
return { relativePath, status: 'written' };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function remapBootstrapPath(spec, relativePath) {
|
|
184
|
+
if (relativePath === 'README.md') return spec.readmePath;
|
|
185
|
+
return relativePath;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function loadRuntimeBootstrap(spec, args, config) {
|
|
189
|
+
if (args.from) {
|
|
190
|
+
const fixturePath = path.resolve(process.cwd(), String(args.from));
|
|
191
|
+
return JSON.parse(fs.readFileSync(fixturePath, 'utf8'));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const baseUrl = normalizedBaseUrl(args['base-url'] || config.baseUrl || DEFAULT_BASE_URL);
|
|
195
|
+
const response = await fetch(`${baseUrl}${spec.endpoint}`);
|
|
196
|
+
if (!response.ok) {
|
|
197
|
+
throw new Error(`Unable to download ${spec.label} bootstrap: HTTP ${response.status}`);
|
|
198
|
+
}
|
|
199
|
+
return response.json();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function installRuntimeProjectBootstrap(target, args = {}) {
|
|
203
|
+
const spec = PROJECT_BOOTSTRAPS[target];
|
|
204
|
+
const config = readConfig();
|
|
205
|
+
const bootstrap = await loadRuntimeBootstrap(spec, args, config);
|
|
206
|
+
const force = Boolean(args.force);
|
|
207
|
+
const results = [];
|
|
208
|
+
|
|
209
|
+
Object.entries(bootstrap.files || {}).forEach(([relativePath, body]) => {
|
|
210
|
+
results.push(writeProjectFile(remapBootstrapPath(spec, relativePath), body, { force }));
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
if (bootstrap.recommended_env) {
|
|
214
|
+
results.push(writeProjectFile(spec.envPath, `${bootstrap.recommended_env.trim()}\n`, { force }));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const written = results.filter((item) => item.status === 'written');
|
|
218
|
+
const skipped = results.filter((item) => item.status === 'skipped');
|
|
219
|
+
|
|
220
|
+
write(c(spec.label, ANSI.bold));
|
|
221
|
+
write(`Installing project files into ${process.cwd()}`);
|
|
222
|
+
write('');
|
|
223
|
+
written.forEach((item) => write(` ${c('created', ANSI.green)} ${item.relativePath}`));
|
|
224
|
+
skipped.forEach((item) => write(` ${c('kept ', ANSI.yellow)} ${item.relativePath} (use --force to replace)`));
|
|
225
|
+
write('');
|
|
226
|
+
write(c('Next steps', ANSI.bold));
|
|
227
|
+
write(` 1. Copy ${spec.envPath} to ${spec.envPath.replace(/\.example$/, '')} and fill OSUITE_API_KEY.`);
|
|
228
|
+
write(' 2. Run osuite doctor.');
|
|
229
|
+
write(` 3. ${spec.runLine}`);
|
|
230
|
+
write(` 4. ${spec.smokeLine}`);
|
|
231
|
+
write('');
|
|
232
|
+
write(c('OSuite CLI is the control surface; the project hook files are what intercept agent actions.', ANSI.dim));
|
|
233
|
+
}
|
|
234
|
+
|
|
85
235
|
function banner() {
|
|
86
236
|
return [
|
|
87
237
|
c('OSUITE', `${ANSI.bold}${ANSI.cyan}`),
|
|
@@ -103,6 +253,7 @@ function printHelp() {
|
|
|
103
253
|
write(' osuite review <actionId> Render the decision review card');
|
|
104
254
|
write(' osuite approve <actionId> --reason "Looks safe"');
|
|
105
255
|
write(' osuite deny <actionId> --reason "Outside change window"');
|
|
256
|
+
write(' osuite connect [auto|codex|claude|sdk|mcp] Detect runtime and start browser handoff');
|
|
106
257
|
write(' osuite init [sdk|codex|claude|mcp] Print setup steps for a runtime lane');
|
|
107
258
|
write('');
|
|
108
259
|
write(c('Environment', ANSI.bold));
|
|
@@ -373,8 +524,13 @@ async function decide(actionId, decision, args) {
|
|
|
373
524
|
if (result?.action?.status) write(`New status ${result.action.status}`);
|
|
374
525
|
}
|
|
375
526
|
|
|
376
|
-
function init(target = 'generic') {
|
|
527
|
+
async function init(target = 'generic', args = {}) {
|
|
377
528
|
write(c('OSuite init', `${ANSI.bold}${ANSI.cyan}`));
|
|
529
|
+
if (PROJECT_BOOTSTRAPS[target]) {
|
|
530
|
+
await installRuntimeProjectBootstrap(target, args);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
|
|
378
534
|
write('Paste these into your shell, then run `osuite doctor`:');
|
|
379
535
|
write('');
|
|
380
536
|
write('export OSUITE_BASE_URL=https://studio.osuite.ai');
|
|
@@ -388,20 +544,58 @@ function init(target = 'generic') {
|
|
|
388
544
|
write(' export OSUITE_RUNTIME_FAMILY=framework_sdk');
|
|
389
545
|
write(' export OSUITE_ADAPTER_MODE=inline_sdk');
|
|
390
546
|
write(' osuite doctor');
|
|
391
|
-
} else if (target === 'codex') {
|
|
392
|
-
write(c('Codex runtime lane', ANSI.bold));
|
|
393
|
-
write(' osuite doctor');
|
|
394
|
-
write(' npm run runtime:install:codex-plugin');
|
|
395
|
-
} else if (target === 'claude') {
|
|
396
|
-
write(c('Claude Code runtime lane', ANSI.bold));
|
|
397
|
-
write(' export OSUITE_RUNTIME_ADAPTER_ID=claude_code_hooks');
|
|
398
|
-
write(' export OSUITE_SIGNATURE_MODE=required');
|
|
399
547
|
} else if (target === 'mcp') {
|
|
400
548
|
write(c('MCP runtime lane', ANSI.bold));
|
|
401
549
|
write(' Use OSuite preflight, wait-for-approval, and complete-action tools around consequential actions.');
|
|
402
550
|
}
|
|
403
551
|
}
|
|
404
552
|
|
|
553
|
+
async function connect(target = 'auto', args = {}) {
|
|
554
|
+
const config = readConfig();
|
|
555
|
+
const runtime = detectRuntimeTarget(target);
|
|
556
|
+
const baseUrl = normalizedBaseUrl(args['base-url'] || config.baseUrl || DEFAULT_BASE_URL);
|
|
557
|
+
const code = String(args.code || generateConnectCode()).trim() || generateConnectCode();
|
|
558
|
+
const handoffUrl = buildConnectHandoffUrl({ baseUrl, runtime, code });
|
|
559
|
+
const hasApiKey = Boolean(config.apiKey);
|
|
560
|
+
|
|
561
|
+
write(c('OSuite Connect', `${ANSI.bold}${ANSI.cyan}`));
|
|
562
|
+
write(c('Project-root setup for governed agent actions.', ANSI.dim));
|
|
563
|
+
write('');
|
|
564
|
+
write(`Detected runtime ${runtime}`);
|
|
565
|
+
write(`Project root ${process.cwd()}`);
|
|
566
|
+
write(`Browser handoff ${handoffUrl}`);
|
|
567
|
+
write('');
|
|
568
|
+
write(c('What happens next', ANSI.bold));
|
|
569
|
+
write(' 1. Open the browser handoff URL and choose the workspace that should own this runtime.');
|
|
570
|
+
write(' 2. OSuite prepares the runtime identity and project hook lane for the detected agent.');
|
|
571
|
+
write(' 3. Run osuite doctor, then trigger one low-risk governed action from the same project root.');
|
|
572
|
+
write('');
|
|
573
|
+
|
|
574
|
+
if (PROJECT_BOOTSTRAPS[runtime]) {
|
|
575
|
+
if (args['dry-run']) {
|
|
576
|
+
write(`Dry run: would install project hook files for ${runtime}.`);
|
|
577
|
+
} else {
|
|
578
|
+
await installRuntimeProjectBootstrap(runtime, args);
|
|
579
|
+
}
|
|
580
|
+
} else {
|
|
581
|
+
write(c('Runtime setup', ANSI.bold));
|
|
582
|
+
write(' This runtime uses the embedded SDK/MCP lane. Install the SDK in the app code path that dispatches actions.');
|
|
583
|
+
if (runtime === 'sdk') write(' npm install osuite');
|
|
584
|
+
if (runtime === 'mcp') write(' Wrap consequential tool calls with OSuite preflight, wait-for-approval, and complete-action steps.');
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
write('');
|
|
588
|
+
write(c('API key fallback', ANSI.bold));
|
|
589
|
+
if (hasApiKey) {
|
|
590
|
+
write(' OSUITE_API_KEY is already present in this shell or project env. Browser handoff can be skipped for this project.');
|
|
591
|
+
} else if (PROJECT_BOOTSTRAPS[runtime]) {
|
|
592
|
+
const envPath = PROJECT_BOOTSTRAPS[runtime].envPath.replace(/\.example$/, '');
|
|
593
|
+
write(` If browser handoff is unavailable, copy ${PROJECT_BOOTSTRAPS[runtime].envPath} to ${envPath} and paste a workspace API key.`);
|
|
594
|
+
} else {
|
|
595
|
+
write(' If browser handoff is unavailable, export OSUITE_BASE_URL and OSUITE_API_KEY before running the agent.');
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
405
599
|
async function main() {
|
|
406
600
|
const { command, args } = parseArgs(process.argv.slice(2));
|
|
407
601
|
|
|
@@ -414,7 +608,8 @@ async function main() {
|
|
|
414
608
|
if (command === 'review') return review(args._[0]);
|
|
415
609
|
if (command === 'approve') return decide(args._[0], 'allow', args);
|
|
416
610
|
if (command === 'deny') return decide(args._[0], 'deny', args);
|
|
417
|
-
if (command === '
|
|
611
|
+
if (command === 'connect') return connect(args._[0] || 'auto', args);
|
|
612
|
+
if (command === 'init') return init(args._[0] || 'generic', args);
|
|
418
613
|
|
|
419
614
|
write(`Unknown command: ${command}`);
|
|
420
615
|
write('');
|