osuite 2.9.1 → 2.9.3
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 +16 -9
- package/cli.js +127 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# OSuite SDK (v2.9.
|
|
1
|
+
# OSuite SDK (v2.9.2)
|
|
2
2
|
|
|
3
3
|
**Governed action SDK for AI agents.**
|
|
4
4
|
|
|
@@ -448,7 +448,15 @@ 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
|
+
|
|
453
|
+
```bash
|
|
454
|
+
curl -fsSL https://studio.osuite.ai/install.sh | bash -s -- codex
|
|
455
|
+
# or
|
|
456
|
+
curl -fsSL https://studio.osuite.ai/install.sh | bash -s -- claude
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
If you prefer npm directly:
|
|
452
460
|
|
|
453
461
|
```bash
|
|
454
462
|
npm install -g osuite
|
|
@@ -458,7 +466,8 @@ npm install -g osuite
|
|
|
458
466
|
osuite # branded welcome and command map
|
|
459
467
|
osuite doctor # check env, Studio health, runtime, and signature posture
|
|
460
468
|
osuite status # show the current Studio connection
|
|
461
|
-
osuite init codex #
|
|
469
|
+
osuite init codex # install .codex hook files into the current project
|
|
470
|
+
osuite init claude # install .claude hook files into the current project
|
|
462
471
|
osuite explain "git push origin main"
|
|
463
472
|
osuite approvals # approval inbox
|
|
464
473
|
osuite review <actionId> # Decision V2 review surface
|
|
@@ -466,7 +475,7 @@ osuite approve <actionId> # approve a specific action
|
|
|
466
475
|
osuite deny <actionId> # deny a specific action
|
|
467
476
|
```
|
|
468
477
|
|
|
469
|
-
The CLI is intentionally more than an API wrapper:
|
|
478
|
+
For local CLI agents, 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:
|
|
470
479
|
- `doctor` tells a developer what is missing before they connect an agent.
|
|
471
480
|
- `explain` gives a local CAVA preview before a command reaches OSuite.
|
|
472
481
|
- `approvals` provides a readable approval inbox instead of raw JSON.
|
|
@@ -476,15 +485,13 @@ When an agent calls `waitForApproval()`, it prints the action ID and replay link
|
|
|
476
485
|
|
|
477
486
|
## Claude Code Hooks
|
|
478
487
|
|
|
479
|
-
Govern Claude Code tool calls without any SDK instrumentation.
|
|
488
|
+
Govern Claude Code tool calls without any SDK instrumentation. The preferred path is the project-root installer:
|
|
480
489
|
|
|
481
490
|
```bash
|
|
482
|
-
|
|
483
|
-
cp path/to/OSuite/hooks/osuite_pretool.py .claude/hooks/
|
|
484
|
-
cp path/to/OSuite/hooks/osuite_posttool.py .claude/hooks/
|
|
491
|
+
curl -fsSL https://studio.osuite.ai/install.sh | bash -s -- claude
|
|
485
492
|
```
|
|
486
493
|
|
|
487
|
-
|
|
494
|
+
It creates `.claude/hooks/*`, `.claude/settings.json`, `.claude/settings.local.json`, and `.claude/.env.example`. Copy the env example to `.claude/.env`, fill `OSUITE_API_KEY`, then run Claude Code from that project root.
|
|
488
495
|
|
|
489
496
|
---
|
|
490
497
|
|
package/cli.js
CHANGED
|
@@ -24,6 +24,25 @@ const PROTECTED_REFS = new Set(['main', 'master', 'prod', 'production', 'stable'
|
|
|
24
24
|
const OBSERVE_OPS = new Set(['get', 'describe', 'logs', 'top', 'version', 'diff']);
|
|
25
25
|
const PLAN_OPS = new Set(['plan', 'show', 'validate', 'output', 'fmt', 'providers', 'graph']);
|
|
26
26
|
const MUTATION_OPS = new Set(['apply', 'destroy', 'delete', 'create', 'replace', 'taint', 'untaint', 'import', 'upgrade', 'rollback', 'install', 'uninstall']);
|
|
27
|
+
const DEFAULT_BASE_URL = 'https://studio.osuite.ai';
|
|
28
|
+
const PROJECT_BOOTSTRAPS = {
|
|
29
|
+
codex: {
|
|
30
|
+
label: 'Codex hook runtime',
|
|
31
|
+
endpoint: '/api/runtimes/codex/bootstrap',
|
|
32
|
+
envPath: '.codex/.env.example',
|
|
33
|
+
readmePath: '.codex/README.osuite.md',
|
|
34
|
+
runLine: 'Run Codex from this project root so .codex/hooks.json can govern Bash/Edit/Write actions.',
|
|
35
|
+
smokeLine: 'Try: osuite explain "git push origin main", then ask Codex to run a low-risk command such as git status.',
|
|
36
|
+
},
|
|
37
|
+
claude: {
|
|
38
|
+
label: 'Claude Code hook runtime',
|
|
39
|
+
endpoint: '/api/runtimes/claude-code/bootstrap',
|
|
40
|
+
envPath: '.claude/.env.example',
|
|
41
|
+
readmePath: '.claude/README.osuite.md',
|
|
42
|
+
runLine: 'Run Claude Code from this project root so .claude/settings.json can govern tool calls.',
|
|
43
|
+
smokeLine: 'Try: osuite explain "git push origin main", then ask Claude Code to run a low-risk command such as git status.',
|
|
44
|
+
},
|
|
45
|
+
};
|
|
27
46
|
|
|
28
47
|
function useColor() {
|
|
29
48
|
return !process.env.NO_COLOR && process.stdout.isTTY !== false;
|
|
@@ -37,6 +56,25 @@ function write(value = '') {
|
|
|
37
56
|
process.stdout.write(`${value}\n`);
|
|
38
57
|
}
|
|
39
58
|
|
|
59
|
+
function loadDotenvFile(relativePath) {
|
|
60
|
+
const absolute = path.join(process.cwd(), relativePath);
|
|
61
|
+
if (!fs.existsSync(absolute)) return;
|
|
62
|
+
const body = fs.readFileSync(absolute, 'utf8');
|
|
63
|
+
body.split(/\r?\n/).forEach((line) => {
|
|
64
|
+
const trimmed = line.trim();
|
|
65
|
+
if (!trimmed || trimmed.startsWith('#') || !trimmed.includes('=')) return;
|
|
66
|
+
const [rawKey, ...rawValue] = trimmed.split('=');
|
|
67
|
+
const key = rawKey.trim();
|
|
68
|
+
let value = rawValue.join('=').trim().replace(/^['"]|['"]$/g, '');
|
|
69
|
+
if (value.includes(' #')) value = value.slice(0, value.indexOf(' #')).trim();
|
|
70
|
+
if (key && !process.env[key]) process.env[key] = value;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function loadProjectEnvironment() {
|
|
75
|
+
['.codex/.env', '.claude/.env', '.osuite/.env', '.env'].forEach(loadDotenvFile);
|
|
76
|
+
}
|
|
77
|
+
|
|
40
78
|
function parseArgs(argv) {
|
|
41
79
|
const [command = 'help', ...rest] = argv;
|
|
42
80
|
const args = { _: [] };
|
|
@@ -57,6 +95,8 @@ function parseArgs(argv) {
|
|
|
57
95
|
return { command, args };
|
|
58
96
|
}
|
|
59
97
|
|
|
98
|
+
loadProjectEnvironment();
|
|
99
|
+
|
|
60
100
|
function envValue(name, fallback) {
|
|
61
101
|
return process.env[name] || process.env[fallback] || '';
|
|
62
102
|
}
|
|
@@ -82,6 +122,80 @@ function requireClient() {
|
|
|
82
122
|
return new OSuite(config);
|
|
83
123
|
}
|
|
84
124
|
|
|
125
|
+
function normalizedBaseUrl(value) {
|
|
126
|
+
return String(value || DEFAULT_BASE_URL).replace(/\/$/, '');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function projectFilePath(relativePath) {
|
|
130
|
+
const clean = String(relativePath || '').replace(/^\/+/, '');
|
|
131
|
+
const root = path.resolve(process.cwd());
|
|
132
|
+
const absolute = path.resolve(root, clean);
|
|
133
|
+
if (absolute !== root && !absolute.startsWith(`${root}${path.sep}`)) {
|
|
134
|
+
throw new Error(`Refusing to write outside the project root: ${relativePath}`);
|
|
135
|
+
}
|
|
136
|
+
return absolute;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function writeProjectFile(relativePath, body, { force = false } = {}) {
|
|
140
|
+
const absolute = projectFilePath(relativePath);
|
|
141
|
+
if (fs.existsSync(absolute) && !force) return { relativePath, status: 'skipped' };
|
|
142
|
+
fs.mkdirSync(path.dirname(absolute), { recursive: true });
|
|
143
|
+
fs.writeFileSync(absolute, String(body || ''));
|
|
144
|
+
return { relativePath, status: 'written' };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function remapBootstrapPath(spec, relativePath) {
|
|
148
|
+
if (relativePath === 'README.md') return spec.readmePath;
|
|
149
|
+
return relativePath;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function loadRuntimeBootstrap(spec, args, config) {
|
|
153
|
+
if (args.from) {
|
|
154
|
+
const fixturePath = path.resolve(process.cwd(), String(args.from));
|
|
155
|
+
return JSON.parse(fs.readFileSync(fixturePath, 'utf8'));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const baseUrl = normalizedBaseUrl(args['base-url'] || config.baseUrl || DEFAULT_BASE_URL);
|
|
159
|
+
const response = await fetch(`${baseUrl}${spec.endpoint}`);
|
|
160
|
+
if (!response.ok) {
|
|
161
|
+
throw new Error(`Unable to download ${spec.label} bootstrap: HTTP ${response.status}`);
|
|
162
|
+
}
|
|
163
|
+
return response.json();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function installRuntimeProjectBootstrap(target, args = {}) {
|
|
167
|
+
const spec = PROJECT_BOOTSTRAPS[target];
|
|
168
|
+
const config = readConfig();
|
|
169
|
+
const bootstrap = await loadRuntimeBootstrap(spec, args, config);
|
|
170
|
+
const force = Boolean(args.force);
|
|
171
|
+
const results = [];
|
|
172
|
+
|
|
173
|
+
Object.entries(bootstrap.files || {}).forEach(([relativePath, body]) => {
|
|
174
|
+
results.push(writeProjectFile(remapBootstrapPath(spec, relativePath), body, { force }));
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
if (bootstrap.recommended_env) {
|
|
178
|
+
results.push(writeProjectFile(spec.envPath, `${bootstrap.recommended_env.trim()}\n`, { force }));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const written = results.filter((item) => item.status === 'written');
|
|
182
|
+
const skipped = results.filter((item) => item.status === 'skipped');
|
|
183
|
+
|
|
184
|
+
write(c(spec.label, ANSI.bold));
|
|
185
|
+
write(`Installing project files into ${process.cwd()}`);
|
|
186
|
+
write('');
|
|
187
|
+
written.forEach((item) => write(` ${c('created', ANSI.green)} ${item.relativePath}`));
|
|
188
|
+
skipped.forEach((item) => write(` ${c('kept ', ANSI.yellow)} ${item.relativePath} (use --force to replace)`));
|
|
189
|
+
write('');
|
|
190
|
+
write(c('Next steps', ANSI.bold));
|
|
191
|
+
write(` 1. Copy ${spec.envPath} to ${spec.envPath.replace(/\.example$/, '')} and fill OSUITE_API_KEY.`);
|
|
192
|
+
write(' 2. Run osuite doctor.');
|
|
193
|
+
write(` 3. ${spec.runLine}`);
|
|
194
|
+
write(` 4. ${spec.smokeLine}`);
|
|
195
|
+
write('');
|
|
196
|
+
write(c('OSuite CLI is the control surface; the project hook files are what intercept agent actions.', ANSI.dim));
|
|
197
|
+
}
|
|
198
|
+
|
|
85
199
|
function banner() {
|
|
86
200
|
return [
|
|
87
201
|
c('OSUITE', `${ANSI.bold}${ANSI.cyan}`),
|
|
@@ -103,7 +217,7 @@ function printHelp() {
|
|
|
103
217
|
write(' osuite review <actionId> Render the decision review card');
|
|
104
218
|
write(' osuite approve <actionId> --reason "Looks safe"');
|
|
105
219
|
write(' osuite deny <actionId> --reason "Outside change window"');
|
|
106
|
-
write(' osuite init [codex|claude|mcp]
|
|
220
|
+
write(' osuite init [sdk|codex|claude|mcp] Print setup steps for a runtime lane');
|
|
107
221
|
write('');
|
|
108
222
|
write(c('Environment', ANSI.bold));
|
|
109
223
|
write(' OSUITE_BASE_URL Studio URL, for example https://studio.osuite.ai');
|
|
@@ -373,8 +487,13 @@ async function decide(actionId, decision, args) {
|
|
|
373
487
|
if (result?.action?.status) write(`New status ${result.action.status}`);
|
|
374
488
|
}
|
|
375
489
|
|
|
376
|
-
function init(target = 'generic') {
|
|
490
|
+
async function init(target = 'generic', args = {}) {
|
|
377
491
|
write(c('OSuite init', `${ANSI.bold}${ANSI.cyan}`));
|
|
492
|
+
if (PROJECT_BOOTSTRAPS[target]) {
|
|
493
|
+
await installRuntimeProjectBootstrap(target, args);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
|
|
378
497
|
write('Paste these into your shell, then run `osuite doctor`:');
|
|
379
498
|
write('');
|
|
380
499
|
write('export OSUITE_BASE_URL=https://studio.osuite.ai');
|
|
@@ -382,14 +501,12 @@ function init(target = 'generic') {
|
|
|
382
501
|
write('export OSUITE_AGENT_ID=<agent-or-runtime-id>');
|
|
383
502
|
write('');
|
|
384
503
|
|
|
385
|
-
if (target === '
|
|
386
|
-
write(c('
|
|
504
|
+
if (target === 'sdk') {
|
|
505
|
+
write(c('Embedded SDK runtime lane', ANSI.bold));
|
|
506
|
+
write(' npm install osuite');
|
|
507
|
+
write(' export OSUITE_RUNTIME_FAMILY=framework_sdk');
|
|
508
|
+
write(' export OSUITE_ADAPTER_MODE=inline_sdk');
|
|
387
509
|
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
510
|
} else if (target === 'mcp') {
|
|
394
511
|
write(c('MCP runtime lane', ANSI.bold));
|
|
395
512
|
write(' Use OSuite preflight, wait-for-approval, and complete-action tools around consequential actions.');
|
|
@@ -408,7 +525,7 @@ async function main() {
|
|
|
408
525
|
if (command === 'review') return review(args._[0]);
|
|
409
526
|
if (command === 'approve') return decide(args._[0], 'allow', args);
|
|
410
527
|
if (command === 'deny') return decide(args._[0], 'deny', args);
|
|
411
|
-
if (command === 'init') return init(args._[0] || 'generic');
|
|
528
|
+
if (command === 'init') return init(args._[0] || 'generic', args);
|
|
412
529
|
|
|
413
530
|
write(`Unknown command: ${command}`);
|
|
414
531
|
write('');
|