osuite 2.9.3 → 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.
Files changed (3) hide show
  1. package/README.md +8 -7
  2. package/cli.js +84 -0
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # OSuite SDK (v2.9.2)
1
+ # OSuite SDK (v2.9.4)
2
2
 
3
3
  **Governed action SDK for AI agents.**
4
4
 
@@ -451,11 +451,11 @@ OSuite uses standard HTTP status codes and custom error classes:
451
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 -- codex
455
- # or
456
- curl -fsSL https://studio.osuite.ai/install.sh | bash -s -- claude
454
+ curl -fsSL https://studio.osuite.ai/install.sh | bash
457
455
  ```
458
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
+
459
459
  If you prefer npm directly:
460
460
 
461
461
  ```bash
@@ -464,6 +464,7 @@ npm install -g osuite
464
464
 
465
465
  ```bash
466
466
  osuite # branded welcome and command map
467
+ osuite connect auto # detect the current project and start browser handoff
467
468
  osuite doctor # check env, Studio health, runtime, and signature posture
468
469
  osuite status # show the current Studio connection
469
470
  osuite init codex # install .codex hook files into the current project
@@ -475,7 +476,7 @@ osuite approve <actionId> # approve a specific action
475
476
  osuite deny <actionId> # deny a specific action
476
477
  ```
477
478
 
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:
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:
479
480
  - `doctor` tells a developer what is missing before they connect an agent.
480
481
  - `explain` gives a local CAVA preview before a command reaches OSuite.
481
482
  - `approvals` provides a readable approval inbox instead of raw JSON.
@@ -488,10 +489,10 @@ When an agent calls `waitForApproval()`, it prints the action ID and replay link
488
489
  Govern Claude Code tool calls without any SDK instrumentation. The preferred path is the project-root installer:
489
490
 
490
491
  ```bash
491
- curl -fsSL https://studio.osuite.ai/install.sh | bash -s -- claude
492
+ curl -fsSL https://studio.osuite.ai/install.sh | bash
492
493
  ```
493
494
 
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.
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.
495
496
 
496
497
  ---
497
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
 
@@ -126,6 +127,41 @@ function normalizedBaseUrl(value) {
126
127
  return String(value || DEFAULT_BASE_URL).replace(/\/$/, '');
127
128
  }
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
+
129
165
  function projectFilePath(relativePath) {
130
166
  const clean = String(relativePath || '').replace(/^\/+/, '');
131
167
  const root = path.resolve(process.cwd());
@@ -217,6 +253,7 @@ function printHelp() {
217
253
  write(' osuite review <actionId> Render the decision review card');
218
254
  write(' osuite approve <actionId> --reason "Looks safe"');
219
255
  write(' osuite deny <actionId> --reason "Outside change window"');
256
+ write(' osuite connect [auto|codex|claude|sdk|mcp] Detect runtime and start browser handoff');
220
257
  write(' osuite init [sdk|codex|claude|mcp] Print setup steps for a runtime lane');
221
258
  write('');
222
259
  write(c('Environment', ANSI.bold));
@@ -513,6 +550,52 @@ async function init(target = 'generic', args = {}) {
513
550
  }
514
551
  }
515
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
+
516
599
  async function main() {
517
600
  const { command, args } = parseArgs(process.argv.slice(2));
518
601
 
@@ -525,6 +608,7 @@ async function main() {
525
608
  if (command === 'review') return review(args._[0]);
526
609
  if (command === 'approve') return decide(args._[0], 'allow', args);
527
610
  if (command === 'deny') return decide(args._[0], 'deny', args);
611
+ if (command === 'connect') return connect(args._[0] || 'auto', args);
528
612
  if (command === 'init') return init(args._[0] || 'generic', args);
529
613
 
530
614
  write(`Unknown command: ${command}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osuite",
3
- "version": "2.9.3",
3
+ "version": "2.9.4",
4
4
  "description": "OSuite governed action SDK for AI agents. Approve, replay, prove, and verify runtime decisions.",
5
5
  "type": "module",
6
6
  "publishConfig": {