osuite 2.9.2 → 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.
Files changed (3) hide show
  1. package/README.md +10 -9
  2. package/cli.js +121 -10
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -448,10 +448,12 @@ OSuite uses standard HTTP status codes and custom error classes:
448
448
 
449
449
  ## CLI Approval Channel
450
450
 
451
- Install the OSuite CLI to connect a runtime, preview CAVA interpretation, review Decision V2 output, and approve agent actions from the terminal:
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 -s -- sdk
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
455
457
  ```
456
458
 
457
459
  If you prefer npm directly:
@@ -464,7 +466,8 @@ npm install -g osuite
464
466
  osuite # branded welcome and command map
465
467
  osuite doctor # check env, Studio health, runtime, and signature posture
466
468
  osuite status # show the current Studio connection
467
- osuite init codex # print runtime setup steps
469
+ osuite init codex # install .codex hook files into the current project
470
+ osuite init claude # install .claude hook files into the current project
468
471
  osuite explain "git push origin main"
469
472
  osuite approvals # approval inbox
470
473
  osuite review <actionId> # Decision V2 review surface
@@ -472,7 +475,7 @@ osuite approve <actionId> # approve a specific action
472
475
  osuite deny <actionId> # deny a specific action
473
476
  ```
474
477
 
475
- 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:
476
479
  - `doctor` tells a developer what is missing before they connect an agent.
477
480
  - `explain` gives a local CAVA preview before a command reaches OSuite.
478
481
  - `approvals` provides a readable approval inbox instead of raw JSON.
@@ -482,15 +485,13 @@ When an agent calls `waitForApproval()`, it prints the action ID and replay link
482
485
 
483
486
  ## Claude Code Hooks
484
487
 
485
- Govern Claude Code tool calls without any SDK instrumentation. Copy two files from the `hooks/` directory in the repo into your `.claude/hooks/` folder:
488
+ Govern Claude Code tool calls without any SDK instrumentation. The preferred path is the project-root installer:
486
489
 
487
490
  ```bash
488
- # In your project directory
489
- cp path/to/OSuite/hooks/osuite_pretool.py .claude/hooks/
490
- cp path/to/OSuite/hooks/osuite_posttool.py .claude/hooks/
491
+ curl -fsSL https://studio.osuite.ai/install.sh | bash -s -- claude
491
492
  ```
492
493
 
493
- Then merge the hooks block from `hooks/settings.json` into your `.claude/settings.json`. Set `OSUITE_BASE_URL`, `OSUITE_API_KEY`, and optionally `OSUITE_HOOK_MODE=enforce`.
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.
494
495
 
495
496
  ---
496
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}`),
@@ -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');
@@ -388,14 +507,6 @@ function init(target = 'generic') {
388
507
  write(' export OSUITE_RUNTIME_FAMILY=framework_sdk');
389
508
  write(' export OSUITE_ADAPTER_MODE=inline_sdk');
390
509
  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
510
  } else if (target === 'mcp') {
400
511
  write(c('MCP runtime lane', ANSI.bold));
401
512
  write(' Use OSuite preflight, wait-for-approval, and complete-action tools around consequential actions.');
@@ -414,7 +525,7 @@ async function main() {
414
525
  if (command === 'review') return review(args._[0]);
415
526
  if (command === 'approve') return decide(args._[0], 'allow', args);
416
527
  if (command === 'deny') return decide(args._[0], 'deny', args);
417
- if (command === 'init') return init(args._[0] || 'generic');
528
+ if (command === 'init') return init(args._[0] || 'generic', args);
418
529
 
419
530
  write(`Unknown command: ${command}`);
420
531
  write('');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osuite",
3
- "version": "2.9.2",
3
+ "version": "2.9.3",
4
4
  "description": "OSuite governed action SDK for AI agents. Approve, replay, prove, and verify runtime decisions.",
5
5
  "type": "module",
6
6
  "publishConfig": {