osuite 2.9.4 → 2.9.5

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 -1
  2. package/cli.js +167 -5
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # OSuite SDK (v2.9.4)
1
+ # OSuite SDK (v2.9.5)
2
2
 
3
3
  **Governed action SDK for AI agents.**
4
4
 
@@ -11,6 +11,13 @@ The OSuite SDK helps teams route approvals, record governed decisions, and produ
11
11
  npm install osuite
12
12
  ```
13
13
 
14
+ ### One-command runtime connect
15
+ ```bash
16
+ curl -fsSL https://studio.osuite.ai/install.sh | bash
17
+ ```
18
+
19
+ The installer detects Codex, Claude Code, SDK, or MCP-style projects, opens a browser handoff, and writes the approved runtime environment into the project (`.codex/.env`, `.claude/.env`, or `.osuite/.env`). API keys remain available as a fallback, but the normal path is terminal start -> Studio approval -> terminal auto-finish.
20
+
14
21
  ### Python
15
22
  ```bash
16
23
  pip install requests
package/cli.js CHANGED
@@ -162,6 +162,124 @@ function buildConnectHandoffUrl({ baseUrl, runtime, code }) {
162
162
  return url.toString();
163
163
  }
164
164
 
165
+ function defaultAgentIdForRuntime(runtime) {
166
+ if (runtime === 'codex') return 'codex-runtime';
167
+ if (runtime === 'claude') return 'claude-code-runtime';
168
+ if (runtime === 'mcp') return 'mcp-runtime';
169
+ return 'sdk-runtime';
170
+ }
171
+
172
+ function envPathForRuntime(runtime) {
173
+ if (runtime === 'codex') return '.codex/.env';
174
+ if (runtime === 'claude') return '.claude/.env';
175
+ return '.osuite/.env';
176
+ }
177
+
178
+ function orderedEnvEntries(env = {}) {
179
+ const preferred = [
180
+ 'OSUITE_BASE_URL',
181
+ 'OSUITE_API_KEY',
182
+ 'OSUITE_AGENT_ID',
183
+ 'OSUITE_RUNTIME_ADAPTER_ID',
184
+ 'OSUITE_SIGNATURE_MODE',
185
+ ];
186
+ const seen = new Set();
187
+ const entries = [];
188
+ preferred.forEach((key) => {
189
+ if (env[key] !== undefined && env[key] !== null && env[key] !== '') {
190
+ seen.add(key);
191
+ entries.push([key, env[key]]);
192
+ }
193
+ });
194
+ Object.keys(env)
195
+ .sort()
196
+ .forEach((key) => {
197
+ if (!seen.has(key) && env[key] !== undefined && env[key] !== null && env[key] !== '') {
198
+ entries.push([key, env[key]]);
199
+ }
200
+ });
201
+ return entries;
202
+ }
203
+
204
+ function formatEnvBody(env = {}) {
205
+ return `${orderedEnvEntries(env)
206
+ .map(([key, value]) => `${key}=${String(value).replace(/\n/g, '')}`)
207
+ .join('\n')}\n`;
208
+ }
209
+
210
+ function parsePositiveInt(value, fallback) {
211
+ const parsed = Number.parseInt(value, 10);
212
+ if (!Number.isFinite(parsed) || parsed < 0) return fallback;
213
+ return parsed;
214
+ }
215
+
216
+ function sleep(ms) {
217
+ return new Promise((resolve) => setTimeout(resolve, ms));
218
+ }
219
+
220
+ async function postJson(url, body) {
221
+ const response = await fetch(url, {
222
+ method: 'POST',
223
+ headers: { 'content-type': 'application/json' },
224
+ body: JSON.stringify(body || {}),
225
+ });
226
+ const text = await response.text();
227
+ let payload = {};
228
+ if (text) {
229
+ try {
230
+ payload = JSON.parse(text);
231
+ } catch {
232
+ payload = { raw: text };
233
+ }
234
+ }
235
+ return { response, payload };
236
+ }
237
+
238
+ async function startDeviceConnect({ baseUrl, runtime, agentId }) {
239
+ const { response, payload } = await postJson(`${normalizedBaseUrl(baseUrl)}/api/onboarding/device-connect/start`, {
240
+ runtime,
241
+ agent_id: agentId,
242
+ project_root: process.cwd(),
243
+ });
244
+ if (!response.ok) {
245
+ throw new Error(payload.error || `Unable to start browser handoff: HTTP ${response.status}`);
246
+ }
247
+ if (!payload.device_code || !payload.poll_token || !payload.user_code) {
248
+ throw new Error('Studio did not return a complete device-connect payload.');
249
+ }
250
+ return payload;
251
+ }
252
+
253
+ async function pollDeviceConnect({ baseUrl, deviceCode, pollToken, timeoutMs, intervalMs }) {
254
+ const deadline = Date.now() + timeoutMs;
255
+ while (Date.now() <= deadline) {
256
+ const { response, payload } = await postJson(`${normalizedBaseUrl(baseUrl)}/api/onboarding/device-connect/poll`, {
257
+ device_code: deviceCode,
258
+ poll_token: pollToken,
259
+ });
260
+
261
+ if (response.status === 200 && payload.status === 'approved' && payload.credential?.env) {
262
+ return payload.credential;
263
+ }
264
+ if (response.status === 202 || payload.status === 'pending') {
265
+ const serverInterval = Number(payload.interval_seconds || 0) * 1000;
266
+ await sleep(serverInterval > 0 ? Math.max(intervalMs, serverInterval) : intervalMs);
267
+ continue;
268
+ }
269
+ if (response.status === 410 || payload.status === 'expired') {
270
+ throw new Error('Browser handoff expired. Run osuite connect again.');
271
+ }
272
+ if (response.status === 409 || payload.status === 'consumed') {
273
+ throw new Error(payload.error || 'Browser handoff was already consumed.');
274
+ }
275
+ if (response.status === 404) {
276
+ throw new Error('Browser handoff was not found. Run osuite connect again.');
277
+ }
278
+ throw new Error(payload.error || `Unable to finish browser handoff: HTTP ${response.status}`);
279
+ }
280
+ return null;
281
+ }
282
+
165
283
  function projectFilePath(relativePath) {
166
284
  const clean = String(relativePath || '').replace(/^\/+/, '');
167
285
  const root = path.resolve(process.cwd());
@@ -554,21 +672,32 @@ async function connect(target = 'auto', args = {}) {
554
672
  const config = readConfig();
555
673
  const runtime = detectRuntimeTarget(target);
556
674
  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 });
675
+ const configuredAgentId = envValue('OSUITE_AGENT_ID', 'DASHCLAW_AGENT_ID');
676
+ const agentId = String(args['agent-id'] || configuredAgentId || defaultAgentIdForRuntime(runtime));
677
+ const dryRunCode = String(args.code || generateConnectCode()).trim() || generateConnectCode();
678
+ let connectPayload = null;
679
+ let code = dryRunCode;
680
+ let handoffUrl = buildConnectHandoffUrl({ baseUrl, runtime, code });
559
681
  const hasApiKey = Boolean(config.apiKey);
560
682
 
683
+ if (!args['dry-run']) {
684
+ connectPayload = await startDeviceConnect({ baseUrl, runtime, agentId });
685
+ code = connectPayload.user_code;
686
+ handoffUrl = connectPayload.verification_uri_complete || buildConnectHandoffUrl({ baseUrl, runtime, code });
687
+ }
688
+
561
689
  write(c('OSuite Connect', `${ANSI.bold}${ANSI.cyan}`));
562
690
  write(c('Project-root setup for governed agent actions.', ANSI.dim));
563
691
  write('');
564
692
  write(`Detected runtime ${runtime}`);
565
693
  write(`Project root ${process.cwd()}`);
566
694
  write(`Browser handoff ${handoffUrl}`);
695
+ write(`One-time code ${code}`);
567
696
  write('');
568
697
  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.');
698
+ write(' 1. Open the browser handoff URL and approve this terminal connection in Studio.');
699
+ write(' 2. OSuite prepares the runtime identity, project hook lane, and signed env for this project.');
700
+ write(' 3. Return here; the CLI will finish automatically and then you can run osuite doctor.');
572
701
  write('');
573
702
 
574
703
  if (PROJECT_BOOTSTRAPS[runtime]) {
@@ -584,6 +713,39 @@ async function connect(target = 'auto', args = {}) {
584
713
  if (runtime === 'mcp') write(' Wrap consequential tool calls with OSuite preflight, wait-for-approval, and complete-action steps.');
585
714
  }
586
715
 
716
+ if (!args['dry-run'] && connectPayload) {
717
+ const timeoutMs = parsePositiveInt(args['poll-timeout-ms'], 180000);
718
+ const intervalMs = parsePositiveInt(
719
+ args['poll-interval-ms'],
720
+ Math.max(1000, Number(connectPayload.interval_seconds || 2) * 1000),
721
+ );
722
+
723
+ write('');
724
+ write(c('Waiting for browser approval', ANSI.bold));
725
+ write(` Code ${code} expires in about ${Math.round(Number(connectPayload.expires_in || 600) / 60)} minutes.`);
726
+
727
+ const credential = await pollDeviceConnect({
728
+ baseUrl,
729
+ deviceCode: connectPayload.device_code,
730
+ pollToken: connectPayload.poll_token,
731
+ timeoutMs,
732
+ intervalMs,
733
+ });
734
+
735
+ if (credential?.env) {
736
+ const envPath = envPathForRuntime(runtime);
737
+ const result = writeProjectFile(envPath, formatEnvBody(credential.env), { force: true });
738
+ write('');
739
+ write(c('Connected to OSuite', ANSI.green));
740
+ write(` ${result.status === 'written' ? 'wrote' : 'kept'} ${result.relativePath}`);
741
+ write(' Run osuite doctor, then start the agent from this project root.');
742
+ } else {
743
+ write('');
744
+ write(c('Browser approval not completed before the timeout.', ANSI.yellow));
745
+ write(' Keep the browser handoff open or run osuite connect again.');
746
+ }
747
+ }
748
+
587
749
  write('');
588
750
  write(c('API key fallback', ANSI.bold));
589
751
  if (hasApiKey) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osuite",
3
- "version": "2.9.4",
3
+ "version": "2.9.5",
4
4
  "description": "OSuite governed action SDK for AI agents. Approve, replay, prove, and verify runtime decisions.",
5
5
  "type": "module",
6
6
  "publishConfig": {