@inneranimalmedia/agentsam-sdk 2.4.0 → 2.4.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inneranimalmedia/agentsam-sdk",
3
- "version": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "description": "Portable AgentSam SDK and CLI kits for local scaffolding, repository intelligence, incremental indexing, identity adapters, and verified dependency maintenance.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -128,7 +128,6 @@
128
128
  },
129
129
  "homepage": "https://github.com/SamPrimeaux/agentsam-sdk#readme",
130
130
  "allowScripts": {
131
- "node-pty@1.0.0": true,
132
131
  "node-pty@1.1.0": true
133
132
  },
134
133
  "devDependencies": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inneranimalmedia/agentsam-sdk-identity",
3
- "version": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "Identity module for @inneranimalmedia/agentsam-sdk (workspace — publish via root SDK)",
@@ -7,6 +7,9 @@ import {
7
7
  blenderRenderPreview,
8
8
  blenderStatus,
9
9
  } from '../lib/cad/index.js';
10
+ import { promptToOpenUrl } from '../lib/open-url.js';
11
+
12
+ const BLENDER_DOWNLOAD_URL = 'https://www.blender.org/download/';
10
13
 
11
14
  function usage() {
12
15
  return `AgentSam programmatic CAD
@@ -24,6 +27,7 @@ Shared options:
24
27
  --cwd <path> Resolve input/output paths from another directory.
25
28
  --json Machine-readable output.
26
29
 
30
+ If Blender is missing, AgentSam explains what is required and can open the official Blender download page.
27
31
  The build command consumes a typed recipe; it never evaluates arbitrary Python.`;
28
32
  }
29
33
 
@@ -49,20 +53,53 @@ function parseArgs(argv) {
49
53
  return opts;
50
54
  }
51
55
 
56
+ function withBlenderInstallGuidance(value) {
57
+ if (value?.available !== false) return value;
58
+ return {
59
+ ...value,
60
+ install: {
61
+ required: true,
62
+ app: 'Blender',
63
+ url: BLENDER_DOWNLOAD_URL,
64
+ message: 'Install Blender, then rerun this command. AgentSam will discover standard installs automatically.',
65
+ alternatives: [
66
+ 'Pass --blender-bin <path> for a custom Blender executable.',
67
+ 'Set AGENTSAM_BLENDER_BIN for a persistent custom executable path.',
68
+ ],
69
+ },
70
+ };
71
+ }
72
+
52
73
  function output(value, json) {
53
74
  if (json) {
54
75
  console.log(JSON.stringify(value));
55
76
  return;
56
77
  }
57
78
  if (value.capability === 'blender.status') {
58
- console.log(value.available
59
- ? `Blender available: ${value.version || 'unknown version'}\n${value.binary}`
60
- : `Blender unavailable${value.error ? `: ${value.error}` : ''}`);
79
+ if (value.available) {
80
+ console.log(`Blender available: ${value.version || 'unknown version'}\n${value.binary}`);
81
+ } else {
82
+ console.log(`Blender unavailable${value.error ? `: ${value.error}` : ''}`);
83
+ if (value.install?.message) console.log(value.install.message);
84
+ if (value.install?.url) console.log(`Download: ${value.install.url}`);
85
+ }
61
86
  return;
62
87
  }
63
88
  console.log(JSON.stringify(value, null, 2));
64
89
  }
65
90
 
91
+ async function presentMissingBlender(value, json) {
92
+ const guided = withBlenderInstallGuidance(value);
93
+ output(guided, json);
94
+ if (!json) {
95
+ await promptToOpenUrl(BLENDER_DOWNLOAD_URL, {
96
+ heading: 'Blender is required for AgentSam programmatic CAD:',
97
+ prompt: 'Press ENTER to open the official Blender download page.',
98
+ });
99
+ }
100
+ return guided;
101
+ }
102
+
66
103
  function required(value, message) {
67
104
  if (!value) throw new Error(message);
68
105
  return value;
@@ -90,10 +127,23 @@ export async function runCad(argv) {
90
127
  cwd,
91
128
  };
92
129
 
93
- let result;
94
130
  if (action === 'status') {
95
- result = await blenderStatus(shared);
96
- } else if (action === 'inspect') {
131
+ const status = withBlenderInstallGuidance(await blenderStatus(shared));
132
+ output(status, opts.json);
133
+ if (!status.available && !opts.json) {
134
+ await promptToOpenUrl(BLENDER_DOWNLOAD_URL, {
135
+ heading: 'Install Blender to enable AgentSam CAD:',
136
+ prompt: 'Press ENTER to open the official Blender download page.',
137
+ });
138
+ }
139
+ return status;
140
+ }
141
+
142
+ const availability = await blenderStatus(shared);
143
+ if (!availability.available) return presentMissingBlender(availability, opts.json);
144
+
145
+ let result;
146
+ if (action === 'inspect') {
97
147
  result = await blenderInspect({ ...shared, input: required(opts.positional[0], 'inspect requires <model.blend>') });
98
148
  } else if (action === 'build') {
99
149
  const recipeFile = path.resolve(cwd, required(opts.positional[0], 'build requires <recipe.json>'));
package/src/lib/auth.js CHANGED
@@ -1,28 +1,15 @@
1
1
  /**
2
2
  * Browser OAuth for SDK init — one click IAM login + Cloudflare connect.
3
3
  */
4
- import http from 'http';
5
- import { randomBytes } from 'crypto';
4
+ import http from 'node:http';
5
+ import { randomBytes } from 'node:crypto';
6
6
  import { postJson } from './core-client.js';
7
+ import { promptToOpenUrl } from './open-url.js';
7
8
 
8
9
  function randomState() {
9
10
  return randomBytes(16).toString('hex');
10
11
  }
11
12
 
12
- function openBrowser(url) {
13
- const start =
14
- process.platform === 'darwin'
15
- ? ['open', url]
16
- : process.platform === 'win32'
17
- ? ['cmd', '/c', 'start', '', url]
18
- : ['xdg-open', url];
19
- import('child_process').then(({ spawn }) => {
20
- spawn(start[0], start.slice(1), { stdio: 'ignore', detached: true }).unref();
21
- }).catch(() => {
22
- console.log(`\n Open in browser:\n ${url}\n`);
23
- });
24
- }
25
-
26
13
  /**
27
14
  * @returns {Promise<{ access_token: string, user_id: string, workspace_id: string, tenant_id: string }>}
28
15
  */
@@ -36,6 +23,8 @@ export async function authenticateViaBrowser() {
36
23
  state,
37
24
  });
38
25
 
26
+ if (!authUrl) throw new Error('IAM auth did not return an authorization URL');
27
+
39
28
  const codePromise = new Promise((resolve, reject) => {
40
29
  const server = http.createServer((req, res) => {
41
30
  try {
@@ -55,7 +44,7 @@ export async function authenticateViaBrowser() {
55
44
  return;
56
45
  }
57
46
  res.writeHead(200, { 'Content-Type': 'text/html' });
58
- res.end('<html><body style="font-family:system-ui"><h1>Agent Sam</h1><p>You can close this tab.</p></body></html>');
47
+ res.end('<html><body style="font-family:system-ui"><h1>Agent Sam</h1><p>Authentication complete. You can close this tab and return to your terminal.</p></body></html>');
59
48
  resolve(code);
60
49
  server.close();
61
50
  } catch (e) {
@@ -67,8 +56,10 @@ export async function authenticateViaBrowser() {
67
56
  server.listen(port, '127.0.0.1');
68
57
  });
69
58
 
70
- console.log('\n Opening browser for IAM sign-in + Cloudflare connect…\n');
71
- openBrowser(authUrl);
59
+ await promptToOpenUrl(authUrl, {
60
+ heading: 'Authenticate your InnerAnimalMedia account at:',
61
+ prompt: 'Press ENTER to open InnerAnimalMedia sign-in in your browser.',
62
+ });
72
63
 
73
64
  const code = await codePromise;
74
65
  const session = await postJson('/api/sdk/auth/exchange', { code, state });
@@ -0,0 +1,66 @@
1
+ import readline from 'node:readline';
2
+ import { spawn } from 'node:child_process';
3
+
4
+ function normalizeHttpUrl(value) {
5
+ const raw = String(value || '').trim();
6
+ if (!raw) throw new Error('URL is required');
7
+ let parsed;
8
+ try { parsed = new URL(raw); }
9
+ catch { throw new Error(`Invalid URL: ${raw}`); }
10
+ if (!['http:', 'https:'].includes(parsed.protocol)) {
11
+ throw new Error(`Unsupported URL protocol: ${parsed.protocol}`);
12
+ }
13
+ return parsed.toString();
14
+ }
15
+
16
+ export function browserCommand(url, platform = process.platform) {
17
+ const normalized = normalizeHttpUrl(url);
18
+ if (platform === 'darwin') return { command: 'open', args: [normalized] };
19
+ if (platform === 'win32') return { command: 'cmd', args: ['/c', 'start', '', normalized] };
20
+ return { command: 'xdg-open', args: [normalized] };
21
+ }
22
+
23
+ export function openExternalUrl(url, {
24
+ platform = process.platform,
25
+ spawnImpl = spawn,
26
+ } = {}) {
27
+ const invocation = browserCommand(url, platform);
28
+ const child = spawnImpl(invocation.command, invocation.args, {
29
+ stdio: 'ignore',
30
+ detached: true,
31
+ });
32
+ child.unref?.();
33
+ return invocation;
34
+ }
35
+
36
+ export async function promptToOpenUrl(url, {
37
+ heading = 'Open in your browser:',
38
+ prompt = 'Press ENTER to open in the browser, or copy the URL above.',
39
+ input = process.stdin,
40
+ output = process.stdout,
41
+ openImpl = openExternalUrl,
42
+ } = {}) {
43
+ const normalized = normalizeHttpUrl(url);
44
+ output.write(`\n${heading}\n${normalized}\n`);
45
+
46
+ if (!input?.isTTY || !output?.isTTY) {
47
+ output.write('Open the URL above in a browser to continue.\n\n');
48
+ return { url: normalized, opened: false, interactive: false };
49
+ }
50
+
51
+ const rl = readline.createInterface({ input, output });
52
+ try {
53
+ await new Promise(resolve => rl.question(`\n${prompt}\n`, resolve));
54
+ } finally {
55
+ rl.close();
56
+ }
57
+
58
+ try {
59
+ openImpl(normalized);
60
+ output.write('\nBrowser opened. Complete the step there, then return here.\n\n');
61
+ return { url: normalized, opened: true, interactive: true };
62
+ } catch (error) {
63
+ output.write(`\nCould not open the browser automatically: ${error.message}\nOpen the URL above manually.\n\n`);
64
+ return { url: normalized, opened: false, interactive: true, error: error.message };
65
+ }
66
+ }
@@ -0,0 +1,64 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { PassThrough } from 'node:stream';
4
+ import { browserCommand, promptToOpenUrl } from '../src/lib/open-url.js';
5
+
6
+ test('browserCommand uses argv-safe platform launchers', () => {
7
+ assert.deepEqual(browserCommand('https://inneranimalmedia.com/auth', 'darwin'), {
8
+ command: 'open',
9
+ args: ['https://inneranimalmedia.com/auth'],
10
+ });
11
+ assert.deepEqual(browserCommand('https://inneranimalmedia.com/auth', 'linux'), {
12
+ command: 'xdg-open',
13
+ args: ['https://inneranimalmedia.com/auth'],
14
+ });
15
+ assert.deepEqual(browserCommand('https://inneranimalmedia.com/auth', 'win32'), {
16
+ command: 'cmd',
17
+ args: ['/c', 'start', '', 'https://inneranimalmedia.com/auth'],
18
+ });
19
+ });
20
+
21
+ test('browserCommand rejects non-http protocols', () => {
22
+ assert.throws(() => browserCommand('file:///tmp/example', 'darwin'), /Unsupported URL protocol/);
23
+ assert.throws(() => browserCommand('javascript:alert(1)', 'darwin'), /Unsupported URL protocol/);
24
+ });
25
+
26
+ test('promptToOpenUrl stays non-interactive when stdin is not a TTY', async () => {
27
+ const input = new PassThrough();
28
+ const output = new PassThrough();
29
+ let text = '';
30
+ output.on('data', chunk => { text += chunk.toString(); });
31
+ let opened = false;
32
+ const result = await promptToOpenUrl('https://www.blender.org/download/', {
33
+ input,
34
+ output,
35
+ openImpl: () => { opened = true; },
36
+ });
37
+ assert.equal(result.interactive, false);
38
+ assert.equal(result.opened, false);
39
+ assert.equal(opened, false);
40
+ assert.match(text, /https:\/\/www\.blender\.org\/download\//);
41
+ assert.match(text, /Open the URL above in a browser/);
42
+ });
43
+
44
+ test('promptToOpenUrl waits for Enter before opening in a TTY', async () => {
45
+ const input = new PassThrough();
46
+ const output = new PassThrough();
47
+ input.isTTY = true;
48
+ output.isTTY = true;
49
+ let text = '';
50
+ output.on('data', chunk => { text += chunk.toString(); });
51
+ let openedUrl = null;
52
+ const pending = promptToOpenUrl('https://inneranimalmedia.com/dashboard', {
53
+ input,
54
+ output,
55
+ openImpl: url => { openedUrl = url; },
56
+ });
57
+ input.write('\n');
58
+ const result = await pending;
59
+ assert.equal(result.interactive, true);
60
+ assert.equal(result.opened, true);
61
+ assert.equal(openedUrl, 'https://inneranimalmedia.com/dashboard');
62
+ assert.match(text, /Press ENTER to open/);
63
+ assert.match(text, /Browser opened/);
64
+ });