@tangle-network/sandbox-cli 0.22.64 → 0.22.65
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/dist/index.mjs +1 -1
- package/package.json +5 -5
package/dist/index.mjs
CHANGED
|
@@ -123,7 +123,7 @@ await connect(new StdioServerTransport());`;case`claude-desktop`:return`// Add t
|
|
|
123
123
|
}
|
|
124
124
|
}`}}function Zr(e){return{anthropic:`Anthropic Messages API (Claude)`,openai:`OpenAI Chat Completions (function calling)`,"vercel-ai":`Vercel AI SDK (generateText / streamText)`,mastra:`Mastra agent framework`,"mcp-local":`Local MCP server bridge (stdio)`,"claude-desktop":`Claude Desktop config (uses local MCP)`,cursor:`Cursor config (uses local MCP)`,zed:`Zed config (uses local MCP)`}[e]}function Qr(){return new x(`connect`).description(`Print a copy-paste SDK integration snippet for one of: ${Yr.join(`, `)} (to stream a local agent's telemetry to Intelligence, see 'tangle connect')`).argument(`<framework>`,Yr.join(` | `)).option(`-i, --sandbox <id>`,`Sandbox ID to embed in the snippet`,`<SANDBOX_ID>`).option(`-s, --session <id>`,`Session ID to embed in the snippet`,`default`).action((e,t)=>{Yr.includes(e)||(console.error(b.red(`unknown framework: ${e}`),`\nsupported: ${Yr.join(`, `)}`),process.exit(2));let n=e;process.stdout.write(b.cyan(`# ${Zr(n)}\n\n`)),process.stdout.write(`${Xr(n,t.sandbox,t.session)}\n`)})}async function $r(e,t=!1,n){n?.throwIfAborted();let r=t?new Oe({write(e,t,n){n()}}):process.stderr,i=De({input:process.stdin,signal:n,output:r,...t?{terminal:!0}:{}});n&&i.once(`SIGINT`,()=>{process.emit(`SIGINT`),i.close()}),t&&process.stderr.write(e);try{return await new Promise(n=>{i.once(`close`,()=>n(void 0)),i.question(t?``:e,e=>{n(e.trim()),i.close()})})}finally{i.close(),t&&process.stderr.write(`
|
|
125
125
|
`)}}async function ei(e){let t=await $r(e,!0);if(t===void 0)throw Error(`No answer on stdin.`);return t}async function ti(e,t){let n=await $r(e);if(n===void 0)throw process.stderr.write(`
|
|
126
|
-
`),Error(t?`No answer on stdin. Pass ${t} to skip the confirmation.`:`No answer on stdin.`);return[`y`,`yes`].includes(n.toLowerCase())}async function ni(e,t,n){let r=await $r(`${e}${t?` [${t}]`:``}: `,!1,n);if(r===void 0)throw Error(`No answer on stdin.`);return r||t||``}async function ri(e,t=`secret`){if(process.stdin.isTTY)throw Error(e?`${e} reads ${t} from stdin, but stdin is a terminal; pipe it in`:`Cannot read ${t} from stdin when stdin is a TTY`);return(await ke(process.stdin)).toString(`utf8`).replace(/\r?\n$/,``)}function ii(){let e=new x(`profile`).description(`Create, inspect, and validate portable agent profiles`);return e.command(`create [output]`).description(`Create a profile — interactively (a wizard), or fully via flags`).option(`--name <name>`,`Profile name (when set, skips the wizard)`).option(`--description <text>`,`Profile description`).option(`--model <model>`,`Model (provider/model)`).option(`--system-prompt <text>`,`System prompt`).option(`--skills <ref...>`,`Skill refs: local path or github:<owner>/<repo>/<path>[@<ref>]`).option(`--tools <ref...>`,`Tool refs: local path or github:<owner>/<repo>/<path>[@<ref>]`).option(`--harness <name>`,`Harness hint shown in the run command`).action(async(e,t)=>{let n=t.name,r=t.description,i=t.harness,a=t.model,o=t.systemPrompt,s=t.skills??[],c=t.tools??[];if(!n&&process.stdin.isTTY&&(n=await ni(`Profile name`,`my-agent`),r=await ni(`Description (optional)`)||void 0,i=await ni(`Harness`,`claude-code`)||void 0,a=await ni(`Model (optional, provider/model)`)||void 0,o=await ni(`System prompt (optional)`)||void 0,s=await ai(`Skill ref (local path or github:..., blank to finish)`),c=await ai(`Tool ref (blank to finish)`)),!n)throw Error(process.stdin.isTTY?`Profile name is required.`:"`agent profile create` needs at least --name in a non-interactive shell (no TTY to prompt). Pass --name <name> (and optionally --model/--skills/--tools/--system-prompt).");let l=tn({model:a,systemPrompt:o,skills:s,tools:c}),u=rn({...n?{name:n}:{},...r?{description:r}:{},...l??{},...i?{harness:i}:{}},`new profile`);if(t.json){I(u);return}let d=e??`agent-profile.json`;fe(d,`${JSON.stringify(u,null,2)}\n`),R(`Wrote ${d}`),R(`Run it: tangle agent prompt <box> --profile ${d}${i?` --harness ${i}`:``}`)}),e.command(`show <source>`).description(`Load and print a profile (local path, https URL, or github:owner/repo/path[@ref])`).action(async e=>{try{I(await nn(e))}catch(e){U(e,!0)}}),e.command(`validate <source>`).description(`Validate a profile against the canonical AgentProfile schema`).action(async(e,t)=>{let n=await nn(e),r=typeof n.name==`string`?n.name:`(unnamed)`;t.json?I({ok:!0,name:r}):R(`Profile valid: "${r}"`)}),e}async function ai(e){let t=[];for(;;){let n=await ni(e);if(!n)break;t.push(n)}return t}function oi(e){return(process.env.TANGLE_SANDBOX_WEB_URL?.trim()||e).replace(/\/+$/,``)}function si(e,t){if(process.exitCode=1,e.status===`blocked_on_approval`){console.error(b.yellow(`⚠ Blocked on a hub approval.`));let t=e.approval?.approvalId;console.error(t?`Approve it: tangle hub approvals approve ${t}`:"Approve the pending request (`tangle hub approvals`), then re-run.");return}if(e.status===`awaiting_question`){console.error(b.yellow(`⚠ Agent is awaiting an answer to a question.`)),console.error(`Re-run with --interactive to answer it, or answer programmatically via box.session(<id>).answer(<questionId>, { ... }).`);return}if(e.status===`awaiting_interaction`){console.error(b.yellow(`⚠ Agent is awaiting a response.`)),console.error(`Open an interactive session, or answer the canonical interaction through the Sandbox SDK.`);return}if(e.status===`awaiting_plan_decision`){console.error(b.yellow(`⚠ Agent is awaiting a plan decision.`)),console.error(`Approve or reject the pending plan before continuing.`);return}console.error(b.red(`✗ ${e.error&&e.error.length>0?e.error:t}`))}function ci(e){let t=e.part;if(t?.type===`text`)return t.text??t.content??void 0}function li(e){return e.finalText??e.response??e.output??e.content??e.text??void 0}function ui(e){if(!e.result.success){si(e.result,`Agent run failed`);return}let t=Date.now()-e.startedAt,n=e.tokenUsage,r=[`${t}ms`,n?.inputTokens===void 0?void 0:`${n.inputTokens} in`,n?.outputTokens===void 0?void 0:`${n.outputTokens} out`].filter(e=>!!e);console.log(b.green(`✓ Done`),b.dim(`(${r.join(`, `)})`))}async function di(e,t,n=!0){let r=``,i,a=m(),o=e=>{if(e.startsWith(r)){let t=e.slice(r.length);t&&n&&process.stdout.write(t)}else n&&process.stdout.write(e);r=e};try{for await(let t of e)switch(a.observe(t),t.type){case`token`:{let e=t.data?.value;typeof e==`string`&&(n&&process.stdout.write(e),r+=e);break}case`message.part.updated`:{let e=ci(t.data);typeof e==`string`&&e.length>0&&o(e);break}case`raw`:break;case`result`:i=t.data;break}}catch(e){a.observe({type:`error`,data:{message:e instanceof Error?e.message:String(e)}})}if(r.length===0&&i){let e=li(i);typeof e==`string`&&e.length>0&&(n&&process.stdout.write(e),r=e)}return{outcome:{result:a.finish(),startedAt:t,tokenUsage:i?.tokenUsage},resultData:i,printed:r.length,...r.length>0?{response:r}:{}}}function fi(e){let t=kr(e??[]);if(t.length!==0)return{profile:{connections:t}}}function pi(e,t,n,r,i,a){let o={...r??{},harness:n,...i?{model:{...r?.model??{},default:i}}:{}},s={profile:o,requestedProfileDigest:ne(o),initialPrompt:a};return{...s,run:D({provider:`tangle-sandbox`,environmentId:e,sessionId:t,executionId:`interactive-execution-${t}`},s)}}async function mi(e){let t=kr(e.connection??[]);if(t.length===0)throw Error(`--allow-writes needs at least one --connection to grant writes for.`);if(t.some(e=>e.connectionId===`*`))throw Error("--allow-writes needs explicit --connection ids; the `*:*` wildcard can't be bulk-granted. Grant per connection, e.g. --connection conn_x:* --allow-writes.");let n=await q({hubKey:e.hubKey});for(let r of t){let t=await n.permissions.allowWrites(r.connectionId);e.json||R(`Allowed ${t.writeActions} write action${t.writeActions===1?``:`s`} for connection ${r.connectionId} (${t.granted} newly granted).`)}e.json||R(`Revert anytime: ${t.map(e=>`tangle hub permissions revert-writes --connection ${e.connectionId}`).join(` · `)}`)}async function hi(e){!e.connection||e.connection.length===0||(e.connection=await Or(e.connection,()=>q({hubKey:e.hubKey})))}async function gi(e,t){let n=process.stdin,r=process.stdout,i=null,a=!1,o=!1,s,c=new Promise(e=>{s=e}),l=()=>{try{n.setRawMode(!1)}catch{}n.pause()},u=()=>{a||(a=!0,l(),n.off(`data`,p),process.off(`SIGWINCH`,f),process.off(`SIGINT`,d),process.off(`SIGTERM`,d),process.off(`exit`,l),s())},d=e=>{(i?.close()??Promise.resolve()).finally(()=>{u(),process.exit(e===`SIGTERM`?143:130)})},f=()=>{i?.isOpen&&i.resize(r.columns||80,r.rows||24)},p=e=>{if(!i)return;let t=e.indexOf(29);if(t===-1){i.write(e);return}t>0&&i.write(e.subarray(0,t)),i.close().finally(u)};if(i=await e.attach({cols:r.columns||80,rows:r.rows||24,handlers:{onData:e=>{r.write(Buffer.from(e.buffer,e.byteOffset,e.byteLength))},onExit:e=>{o=!0;let t=e.exitCode===void 0?``:` (code ${e.exitCode})`;r.write(`\r\n[interactive session exited${t}]\r\n`),i?.close().finally(u)},onError:e=>{r.write(`\r\n[terminal stream error: ${e.message}]\r\n`)},onClose:()=>u()}}),n.setRawMode(!0),n.resume(),n.on(`data`,p),process.on(`SIGWINCH`,f),process.on(`SIGINT`,d),process.on(`SIGTERM`,d),process.on(`exit`,l),await c,console.log(),o){R(`Interactive session ended.`);return}R(`Detached — the session keeps running.`),B({"Watch & drive":t})}function _i(){let e=new x(`agent`).description(`Interact with AI agent`);return e.command(`prompt <idOrMessage> [message]`).description("Send a single prompt to the agent. With one arg it's the prompt and the box comes from `tangle use` / --box / TANGLE_SANDBOX; with two it's <id> <message>.").option(`--box <id>`,`Target sandbox (overrides the active one)`).option(`--session <id>`,`Continue existing session`).option(`--model <model>`,`Model to use`).option(`-t, --timeout <ms>`,`Timeout in milliseconds`,`300000`).option(`--stream`,`Stream response in real-time`).option(`--interactive`,`Spawn the harness's interactive TUI in the sandbox, inject this prompt, and print a link to watch + drive the live session in the browser (omit for the default headless run)`).option(`--harness <name>`,`Harness that executes this run (claude-code, codex, kimi-code, opencode, pi, amp, hermes). Defaults to the sandbox's configured backend. The box ships all harnesses, so this is a per-run choice.`).option(`--profile <source>`,`Agent profile to run: a local JSON path, an https URL, or github:<owner>/<repo>/<path>[@<ref>]`).option(`--skills <ref...>`,`Attach skills (repeatable): a local file path, or github:<owner>/<repo>/<path>[@<ref>] to pull from a repo`).option(`--tools <ref...>`,`Attach tools (repeatable): a local file path, or github:<owner>/<repo>/<path>[@<ref>]`).option(`--system-prompt <text>`,`Override the agent's system prompt for this run`).option(`--connection <spec>`,"Grant a hub connection to the agent as MCP tools (repeatable). The connection is a connection id or a provider name. Modes: <connection>:<cap1>,<cap2> (least privilege, recommended), <connection>:* (all of one connection), or *:* (all connections). List connections with `tangle hub connections`; paths with `tangle hub tools search <keyword>`.",Dr,[]).option(`--allow-writes`,"Pre-approve every WRITE action of each granted --connection so the agent can write unattended (reads are already allowed; destructive actions still prompt). Revert with `tangle hub permissions revert-writes`.").option(`--no-hub-key`,`Do not auto-mint a platform Hub key for --allow-writes; use the stored credential as-is`).action(async(e,t,n)=>{let r=t===void 0?e:t,i=t===void 0?$n(n.box):e,{context:a,client:o}=K(n),s=await on({profileSource:n.profile,systemPrompt:n.systemPrompt,skills:n.skills,tools:n.tools});await hi(n);let c=fi(n.connection);n.allowWrites&&await mi(n);let l=s||c?{...s,...c?.profile}:void 0,u=n.harness,d=l||u||n.model?{...u?{type:u}:{},...l?{profile:l}:{},...n.model?{model:{model:n.model}}:{}}:void 0,f=await J(o,a,i);if(f=await Y(f),n.interactive){let e=n.session??`int-${w()}`,t=u??(await f.backend.status()).type,o=pi(i,e,t,l,n.model,r),s=f.session(e).interactive(),c=z(`Starting interactive ${t} session...`);c.start();let d;try{d=await s.start(o)}finally{c.stop()}let p=d.ref.preparationReceipt.harness,m=d.state===`running`?d.streamUrl:void 0,h=`${oi(a.baseUrl)}/dashboard/sandbox/${encodeURIComponent(i)}?interactive=${encodeURIComponent(d.ref.run.sessionId)}`;if(n.json){I({state:d.state,sessionId:d.ref.run.sessionId,harness:p,...m?{streamUrl:m}:{},webUrl:h});return}if(R(`Interactive ${p} session ${d.state}.`),console.log(),B({...d.state===`running`?{"Watch & drive":h}:{},Session:d.ref.run.sessionId}),console.log(),d.state===`exited`){R(`The initial prompt completed with reason: ${d.reason}.`);return}R(`Open the link to watch the agent and type to take over.`),process.stdin.isTTY===!0&&process.stdout.isTTY===!0&&(console.log(),R(`Attaching to the live session — press Ctrl-] to detach.`),await gi(s,h));return}if(n.stream){n.json||(R(`Streaming response...`),console.log());let e=Date.now(),t=await di(f.streamPrompt(r,{sessionId:n.session,timeoutMs:Number.parseInt(n.timeout,10),...d?{backend:d}:{}}),e,!n.json);n.json?(I({...t.outcome.result,...t.response?{response:t.response}:{},...t.outcome.tokenUsage?{tokenUsage:t.outcome.tokenUsage}:{},durationMs:Date.now()-e}),t.outcome.result.success||(process.exitCode=1)):(t.printed>0&&console.log(),ui(t.outcome))}else{let e=z(`Processing prompt...`);e.start();let t=await f.prompt(r,{sessionId:n.session,timeoutMs:Number.parseInt(n.timeout,10),...d?{backend:d}:{}});e.stop(),n.json?(I(t),t.success===!1&&(process.exitCode=1)):t.success===!1?si(t,`Agent prompt failed`):(console.log(t.response),console.log(),B({Duration:`${t.durationMs}ms`,"Input Tokens":t.usage?.inputTokens,"Output Tokens":t.usage?.outputTokens}))}}),e.addCommand(Qr()),e.addCommand(ii()),e}async function vi(e){let t=e.timeoutMs??1e4,n=e.baseUrl.replace(/\/$/,``),a=`${n}/v1/account/me`;try{let n=await fetch(a,{headers:{Accept:`application/json`,Authorization:`Bearer ${e.apiKey}`},signal:AbortSignal.timeout(t)});if(!n.ok){let e=await bi(n);throw n.status===401||n.status===403?new r(e||`Invalid API key`):n.status>=500?new u(e||`Sandbox API returned an unexpected error`,{status:n.status}):Error(e||`Credential validation failed with status ${n.status}`)}let i=await n.json();if(!i.success||!i.data)throw Error(`Sandbox API returned an invalid account response`);return{customerId:i.data.customer_id,billingOwnerId:typeof i.data.billing_owner_id==`string`?i.data.billing_owner_id:void 0,apiKeyId:typeof i.data.api_key_id==`string`||i.data.api_key_id===null?i.data.api_key_id:void 0,billingDelegationAuthorized:typeof i.data.billing_delegation_authorized==`boolean`?i.data.billing_delegation_authorized:void 0,email:i.data.email,name:i.data.name,tier:i.data.tier,createdAt:i.data.created_at}}catch(e){throw e instanceof r||e instanceof u||e instanceof f?e:e instanceof Error&&e.name===`AbortError`?new f(`Timed out validating credentials against ${n}`,{timeoutMs:t}):e instanceof Error&&!(e instanceof TypeError)?e:new i(`Failed to reach ${n}`,{cause:yi(e)})}}function yi(e){return e instanceof Error?e:void 0}async function bi(e){let t=await e.text();if(t)try{let e=JSON.parse(t);return e.error?.message??e.message??t}catch{return t}}function xi(e){return e.replace(/&/g,`&`).replace(/</g,`<`).replace(/>/g,`>`).replace(/"/g,`"`).replace(/'/g,`'`)}function Si(e){if(!wi(e.hostHeader))return{kind:`host-mismatch`};let t;try{t=new URL(e.requestUrl??`/`,`http://127.0.0.1`)}catch{return{kind:`not-found`}}if(t.pathname!==`/callback`)return{kind:`not-found`};if(t.searchParams.get(`state`)!==e.expectedState)return{kind:`state-mismatch`};let n=t.searchParams.get(`error`);if(n)return{kind:`error`,reason:n};let r=t.searchParams.get(`grant_token`);return r?{kind:`ok`,token:r}:{kind:`missing-token`}}async function Ci(e){e.signal?.throwIfAborted();let t=e.timeoutMs??12e4,n=e.baseUrl.replace(/\/$/,``),r=await import(`node:http`),i=te(32).toString(`hex`),a=null,o=null,s=new Promise((e,t)=>{a=e,o=t}),c=r.createServer((e,t)=>{try{let n=Si({hostHeader:e.headers.host,requestUrl:e.url,expectedState:i});switch(n.kind){case`host-mismatch`:t.writeHead(421,{"content-type":`text/plain; charset=utf-8`}),t.end(`Misdirected request`);return;case`not-found`:t.writeHead(404,{"content-type":`text/plain; charset=utf-8`}),t.end(`Not found`);return;case`state-mismatch`:t.writeHead(400,{"content-type":`text/html; charset=utf-8`}),t.end(Oi(`State mismatch — refusing login`)),o?.(Error(`Browser login state mismatch — refusing potentially hijacked callback`));return;case`error`:t.writeHead(400,{"content-type":`text/html; charset=utf-8`}),t.end(Oi(n.reason)),o?.(Error(`Browser login failed: ${n.reason}`));return;case`missing-token`:t.writeHead(400,{"content-type":`text/html; charset=utf-8`}),t.end(Oi(null)),o?.(Error(`Browser login did not return a grant token`));return;case`ok`:t.writeHead(200,{"content-type":`text/html; charset=utf-8`}),t.end(Oi(null)),a?.(n.token);return}}catch(e){o?.(e instanceof Error?e:Error(`Browser login callback failed`))}});await new Promise((e,t)=>{c.once(`error`,t),c.listen(0,`127.0.0.1`,()=>e())});try{let r=c.address();if(!r||typeof r==`string`)throw Error(`Failed to bind local callback server`);let a=new URL(`http://127.0.0.1:${r.port}/callback`);a.searchParams.set(`state`,i);let o=new URL(`${n}/auth/cli/login`);o.searchParams.set(`callback_url`,a.toString()),e.provider&&o.searchParams.set(`provider`,e.provider);let l=await Di(o.toString());e.onLoginUrl?.({loginUrl:o.toString(),browserOpened:l});let u=await Ei({baseUrl:n,grantToken:await Ti(s,t,e.signal),timeoutMs:t,signal:e.signal});return{apiKey:u.apiKey,email:u.email,name:u.name,tier:u.tier}}finally{e.signal?.aborted&&c.closeAllConnections(),await new Promise((e,t)=>{c.close(n=>{if(n){t(n);return}e()})}).catch(()=>void 0)}}function wi(e){if(!e)return!1;let t=e.toLowerCase().match(/^(\[[^\]]+\]|[^:]+)(?::\d+)?$/);if(!t)return!1;let n=t[1];return n===`127.0.0.1`||n===`localhost`||n===`[::1]`}async function Ti(e,t,n){let r=new AbortController;try{return await Promise.race([e,Ae(t,void 0,{signal:AbortSignal.any([r.signal,...n?[n]:[]])}).then(()=>{throw new f(`Timed out waiting for browser login to complete`,{timeoutMs:t})})])}finally{r.abort()}}async function Ei(e){let t=await fetch(`${e.baseUrl}/auth/cli/exchange`,{method:`POST`,headers:{Accept:`application/json`,"Content-Type":`application/json`},body:JSON.stringify({grant_token:e.grantToken}),signal:AbortSignal.any([AbortSignal.timeout(e.timeoutMs),...e.signal?[e.signal]:[]])}).catch(t=>{throw new i(`Failed to reach ${e.baseUrl}`,{cause:t instanceof Error?t:void 0})}),n=await t.json().catch(()=>null);if(!t.ok||!n?.success||!n.data?.api_key)throw Error(n?.error?.message||`Failed to exchange browser login grant`);return{apiKey:n.data.api_key,email:n.data.email,name:n.data.name,tier:n.data.tier}}async function Di(e){let{spawn:t}=await import(`node:child_process`),n=process.platform===`darwin`?[`open`,e]:process.platform===`win32`?[`cmd`,`/c`,`start`,``,e]:[`xdg-open`,e];return await new Promise(e=>{let r=t(n[0],n.slice(1),{detached:!0,stdio:`ignore`});r.once(`error`,()=>e(!1)),r.once(`spawn`,()=>{r.unref(),e(!0)})})}function Oi(e){return`<!doctype html>
|
|
126
|
+
`),Error(t?`No answer on stdin. Pass ${t} to skip the confirmation.`:`No answer on stdin.`);return[`y`,`yes`].includes(n.toLowerCase())}async function ni(e,t,n){let r=await $r(`${e}${t?` [${t}]`:``}: `,!1,n);if(r===void 0)throw Error(`No answer on stdin.`);return r||t||``}async function ri(e,t=`secret`){if(process.stdin.isTTY)throw Error(e?`${e} reads ${t} from stdin, but stdin is a terminal; pipe it in`:`Cannot read ${t} from stdin when stdin is a TTY`);return(await ke(process.stdin)).toString(`utf8`).replace(/\r?\n$/,``)}function ii(){let e=new x(`profile`).description(`Create, inspect, and validate portable agent profiles`);return e.command(`create [output]`).description(`Create a profile — interactively (a wizard), or fully via flags`).option(`--name <name>`,`Profile name (when set, skips the wizard)`).option(`--description <text>`,`Profile description`).option(`--model <model>`,`Model (provider/model)`).option(`--system-prompt <text>`,`System prompt`).option(`--skills <ref...>`,`Skill refs: local path or github:<owner>/<repo>/<path>[@<ref>]`).option(`--tools <ref...>`,`Tool refs: local path or github:<owner>/<repo>/<path>[@<ref>]`).option(`--harness <name>`,`Harness hint shown in the run command`).action(async(e,t)=>{let n=t.name,r=t.description,i=t.harness,a=t.model,o=t.systemPrompt,s=t.skills??[],c=t.tools??[];if(!n&&process.stdin.isTTY&&(n=await ni(`Profile name`,`my-agent`),r=await ni(`Description (optional)`)||void 0,i=await ni(`Harness`,`claude-code`)||void 0,a=await ni(`Model (optional, provider/model)`)||void 0,o=await ni(`System prompt (optional)`)||void 0,s=await ai(`Skill ref (local path or github:..., blank to finish)`),c=await ai(`Tool ref (blank to finish)`)),!n)throw Error(process.stdin.isTTY?`Profile name is required.`:"`agent profile create` needs at least --name in a non-interactive shell (no TTY to prompt). Pass --name <name> (and optionally --model/--skills/--tools/--system-prompt).");let l=tn({model:a,systemPrompt:o,skills:s,tools:c}),u=rn({...n?{name:n}:{},...r?{description:r}:{},...l??{},...i?{harness:i}:{}},`new profile`);if(t.json){I(u);return}let d=e??`agent-profile.json`;fe(d,`${JSON.stringify(u,null,2)}\n`),R(`Wrote ${d}`),R(`Run it: tangle agent prompt <box> --profile ${d}${i?` --harness ${i}`:``}`)}),e.command(`show <source>`).description(`Load and print a profile (local path, https URL, or github:owner/repo/path[@ref])`).action(async e=>{try{I(await nn(e))}catch(e){U(e,!0)}}),e.command(`validate <source>`).description(`Validate a profile against the canonical AgentProfile schema`).action(async(e,t)=>{let n=await nn(e),r=typeof n.name==`string`?n.name:`(unnamed)`;t.json?I({ok:!0,name:r}):R(`Profile valid: "${r}"`)}),e}async function ai(e){let t=[];for(;;){let n=await ni(e);if(!n)break;t.push(n)}return t}function oi(e){return(process.env.TANGLE_SANDBOX_WEB_URL?.trim()||e).replace(/\/+$/,``)}function si(e,t){if(process.exitCode=1,e.status===`blocked_on_approval`){console.error(b.yellow(`⚠ Blocked on a hub approval.`));let t=e.approval?.approvalId;console.error(t?`Approve it: tangle hub approvals approve ${t}`:"Approve the pending request (`tangle hub approvals`), then re-run.");return}if(e.status===`awaiting_question`){console.error(b.yellow(`⚠ Agent is awaiting an answer to a question.`)),console.error(`Re-run with --interactive to answer it, or answer programmatically via box.session(<id>).answer(<questionId>, { ... }).`);return}if(e.status===`awaiting_interaction`){console.error(b.yellow(`⚠ Agent is awaiting a response.`)),console.error(`Open an interactive session, or answer the canonical interaction through the Sandbox SDK.`);return}if(e.status===`awaiting_plan_decision`){console.error(b.yellow(`⚠ Agent is awaiting a plan decision.`)),console.error(`Approve or reject the pending plan before continuing.`);return}console.error(b.red(`✗ ${e.error&&e.error.length>0?e.error:t}`))}function ci(e){let t=e.part;if(t?.type===`text`)return t.text??t.content??void 0}function li(e){return e.finalText??e.response??e.output??e.content??e.text??void 0}function ui(e){if(!e.result.success){si(e.result,`Agent run failed`);return}let t=Date.now()-e.startedAt,n=e.tokenUsage,r=[`${t}ms`,n?.inputTokens===void 0?void 0:`${n.inputTokens} in`,n?.outputTokens===void 0?void 0:`${n.outputTokens} out`].filter(e=>!!e);console.log(b.green(`✓ Done`),b.dim(`(${r.join(`, `)})`))}async function di(e,t,n=!0){let r=``,i,a=m(),o=e=>{if(e.startsWith(r)){let t=e.slice(r.length);t&&n&&process.stdout.write(t)}else n&&process.stdout.write(e);r=e};try{for await(let t of e)switch(a.observe(t),t.type){case`token`:{let e=t.data?.value;typeof e==`string`&&(n&&process.stdout.write(e),r+=e);break}case`message.part.updated`:{let e=ci(t.data);typeof e==`string`&&e.length>0&&o(e);break}case`raw`:break;case`result`:i=t.data;break}}catch(e){a.observe({type:`error`,data:{message:e instanceof Error?e.message:String(e)}})}if(r.length===0&&i){let e=li(i);typeof e==`string`&&e.length>0&&(n&&process.stdout.write(e),r=e)}return{outcome:{result:a.finish(),startedAt:t,tokenUsage:i?.tokenUsage},resultData:i,printed:r.length,...r.length>0?{response:r}:{}}}function fi(e){let t=kr(e??[]);if(t.length!==0)return{profile:{connections:t}}}function pi(e,t,n,r,i,a){let o={...r??{},harness:n,...i?{model:{...r?.model??{},default:i}}:{}},s={profile:o,requestedProfileDigest:ne(o),initialPrompt:a};return{...s,run:D({provider:`tangle-sandbox`,environmentId:e,sessionId:t,executionId:`interactive-execution-${t}`},s)}}async function mi(e){let t=kr(e.connection??[]);if(t.length===0)throw Error(`--allow-writes needs at least one --connection to grant writes for.`);if(t.some(e=>e.connectionId===`*`))throw Error("--allow-writes needs explicit --connection ids; the `*:*` wildcard can't be bulk-granted. Grant per connection, e.g. --connection conn_x:* --allow-writes.");let n=await q({hubKey:e.hubKey});for(let r of t){let t=await n.permissions.allowWrites(r.connectionId);e.json||R(`Allowed ${t.writeActions} write action${t.writeActions===1?``:`s`} for connection ${r.connectionId} (${t.granted} newly granted).`)}e.json||R(`Revert anytime: ${t.map(e=>`tangle hub permissions revert-writes --connection ${e.connectionId}`).join(` · `)}`)}async function hi(e){!e.connection||e.connection.length===0||(e.connection=await Or(e.connection,()=>q({hubKey:e.hubKey})))}async function gi(e,t){let n=process.stdin,r=process.stdout,i=null,a=!1,o=!1,s,c=new Promise(e=>{s=e}),l=()=>{try{n.setRawMode(!1)}catch{}n.pause()},u=()=>{a||(a=!0,l(),n.off(`data`,p),process.off(`SIGWINCH`,f),process.off(`SIGINT`,d),process.off(`SIGTERM`,d),process.off(`exit`,l),s())},d=e=>{(i?.close()??Promise.resolve()).finally(()=>{u(),process.exit(e===`SIGTERM`?143:130)})},f=()=>{i?.isOpen&&i.resize(r.columns||80,r.rows||24)},p=e=>{if(!i)return;let t=e.indexOf(29);if(t===-1){i.write(e);return}t>0&&i.write(e.subarray(0,t)),i.close().finally(u)};if(i=await e.attach({cols:r.columns||80,rows:r.rows||24,handlers:{onData:e=>{r.write(Buffer.from(e.buffer,e.byteOffset,e.byteLength))},onExit:e=>{o=!0;let t=e.exitCode===void 0?``:` (code ${e.exitCode})`;r.write(`\r\n[interactive session exited${t}]\r\n`),i?.close().finally(u)},onError:e=>{r.write(`\r\n[terminal stream error: ${e.message}]\r\n`)},onClose:()=>u()}}),n.setRawMode(!0),n.resume(),n.on(`data`,p),process.on(`SIGWINCH`,f),process.on(`SIGINT`,d),process.on(`SIGTERM`,d),process.on(`exit`,l),await c,console.log(),o){R(`Interactive session ended.`);return}R(`Detached — the session keeps running.`),B({"Watch & drive":t})}function _i(){let e=new x(`agent`).description(`Interact with AI agent`);return e.command(`prompt <idOrMessage> [message]`).description("Send a single prompt to the agent. With one arg it's the prompt and the box comes from `tangle use` / --box / TANGLE_SANDBOX; with two it's <id> <message>.").option(`--box <id>`,`Target sandbox (overrides the active one)`).option(`--session <id>`,`Continue existing session`).option(`--model <model>`,`Model to use`).option(`-t, --timeout <ms>`,`Timeout in milliseconds`,`300000`).option(`--stream`,`Stream response in real-time`).option(`--interactive`,`Spawn the harness's interactive TUI in the sandbox, inject this prompt, and print a link to watch + drive the live session in the browser (omit for the default headless run)`).option(`--harness <name>`,`Harness that executes this run (claude-code, codex, kimi-code, opencode, pi, amp, hermes). Defaults to the sandbox's configured backend. The box ships all harnesses, so this is a per-run choice.`).option(`--profile <source>`,`Agent profile to run: a local JSON path, an https URL, or github:<owner>/<repo>/<path>[@<ref>]`).option(`--skills <ref...>`,`Attach skills (repeatable): a local file path, or github:<owner>/<repo>/<path>[@<ref>] to pull from a repo`).option(`--tools <ref...>`,`Attach tools (repeatable): a local file path, or github:<owner>/<repo>/<path>[@<ref>]`).option(`--system-prompt <text>`,`Override the agent's system prompt for this run`).option(`--connection <spec>`,"Grant a hub connection to the agent as MCP tools (repeatable). The connection is a connection id or a provider name. Modes: <connection>:<cap1>,<cap2> (least privilege, recommended), <connection>:* (all of one connection), or *:* (all connections). List connections with `tangle hub connections`; paths with `tangle hub tools search <keyword>`.",Dr,[]).option(`--allow-writes`,"Pre-approve every WRITE action of each granted --connection so the agent can write unattended (reads are already allowed; destructive actions still prompt). Revert with `tangle hub permissions revert-writes`.").option(`--no-hub-key`,`Do not auto-mint a platform Hub key for --allow-writes; use the stored credential as-is`).action(async(e,t,n)=>{let r=t===void 0?e:t,i=t===void 0?$n(n.box):e,{context:a,client:o}=K(n),s=await on({profileSource:n.profile,systemPrompt:n.systemPrompt,skills:n.skills,tools:n.tools});await hi(n);let c=fi(n.connection);n.allowWrites&&await mi(n);let l=s||c?{...s,...c?.profile}:void 0,u=n.harness,d=l||u||n.model?{...u?{type:u}:{},...l?{profile:l}:{},...n.model?{model:{model:n.model}}:{}}:void 0,f=await J(o,a,i);if(f=await Y(f),n.interactive){let e=n.session??`int-${w()}`,t=u??(await f.backend.status()).type,i=pi(f.id,e,t,l,n.model,r),o=f.session(e).interactive(),s=z(`Starting interactive ${t} session...`);s.start();let c;try{c=await o.start(i)}finally{s.stop()}let d=c.ref.preparationReceipt.harness,p=c.state===`running`?c.streamUrl:void 0,m=`${oi(a.baseUrl)}/dashboard/sandbox/${encodeURIComponent(f.id)}?interactive=${encodeURIComponent(c.ref.run.sessionId)}`;if(n.json){I({state:c.state,sessionId:c.ref.run.sessionId,harness:d,...p?{streamUrl:p}:{},webUrl:m});return}if(R(`Interactive ${d} session ${c.state}.`),console.log(),B({...c.state===`running`?{"Watch & drive":m}:{},Session:c.ref.run.sessionId}),console.log(),c.state===`exited`){R(`The initial prompt completed with reason: ${c.reason}.`);return}R(`Open the link to watch the agent and type to take over.`),process.stdin.isTTY===!0&&process.stdout.isTTY===!0&&(console.log(),R(`Attaching to the live session — press Ctrl-] to detach.`),await gi(o,m));return}if(n.stream){n.json||(R(`Streaming response...`),console.log());let e=Date.now(),t=await di(f.streamPrompt(r,{sessionId:n.session,timeoutMs:Number.parseInt(n.timeout,10),...d?{backend:d}:{}}),e,!n.json);n.json?(I({...t.outcome.result,...t.response?{response:t.response}:{},...t.outcome.tokenUsage?{tokenUsage:t.outcome.tokenUsage}:{},durationMs:Date.now()-e}),t.outcome.result.success||(process.exitCode=1)):(t.printed>0&&console.log(),ui(t.outcome))}else{let e=z(`Processing prompt...`);e.start();let t=await f.prompt(r,{sessionId:n.session,timeoutMs:Number.parseInt(n.timeout,10),...d?{backend:d}:{}});e.stop(),n.json?(I(t),t.success===!1&&(process.exitCode=1)):t.success===!1?si(t,`Agent prompt failed`):(console.log(t.response),console.log(),B({Duration:`${t.durationMs}ms`,"Input Tokens":t.usage?.inputTokens,"Output Tokens":t.usage?.outputTokens}))}}),e.addCommand(Qr()),e.addCommand(ii()),e}async function vi(e){let t=e.timeoutMs??1e4,n=e.baseUrl.replace(/\/$/,``),a=`${n}/v1/account/me`;try{let n=await fetch(a,{headers:{Accept:`application/json`,Authorization:`Bearer ${e.apiKey}`},signal:AbortSignal.timeout(t)});if(!n.ok){let e=await bi(n);throw n.status===401||n.status===403?new r(e||`Invalid API key`):n.status>=500?new u(e||`Sandbox API returned an unexpected error`,{status:n.status}):Error(e||`Credential validation failed with status ${n.status}`)}let i=await n.json();if(!i.success||!i.data)throw Error(`Sandbox API returned an invalid account response`);return{customerId:i.data.customer_id,billingOwnerId:typeof i.data.billing_owner_id==`string`?i.data.billing_owner_id:void 0,apiKeyId:typeof i.data.api_key_id==`string`||i.data.api_key_id===null?i.data.api_key_id:void 0,billingDelegationAuthorized:typeof i.data.billing_delegation_authorized==`boolean`?i.data.billing_delegation_authorized:void 0,email:i.data.email,name:i.data.name,tier:i.data.tier,createdAt:i.data.created_at}}catch(e){throw e instanceof r||e instanceof u||e instanceof f?e:e instanceof Error&&e.name===`AbortError`?new f(`Timed out validating credentials against ${n}`,{timeoutMs:t}):e instanceof Error&&!(e instanceof TypeError)?e:new i(`Failed to reach ${n}`,{cause:yi(e)})}}function yi(e){return e instanceof Error?e:void 0}async function bi(e){let t=await e.text();if(t)try{let e=JSON.parse(t);return e.error?.message??e.message??t}catch{return t}}function xi(e){return e.replace(/&/g,`&`).replace(/</g,`<`).replace(/>/g,`>`).replace(/"/g,`"`).replace(/'/g,`'`)}function Si(e){if(!wi(e.hostHeader))return{kind:`host-mismatch`};let t;try{t=new URL(e.requestUrl??`/`,`http://127.0.0.1`)}catch{return{kind:`not-found`}}if(t.pathname!==`/callback`)return{kind:`not-found`};if(t.searchParams.get(`state`)!==e.expectedState)return{kind:`state-mismatch`};let n=t.searchParams.get(`error`);if(n)return{kind:`error`,reason:n};let r=t.searchParams.get(`grant_token`);return r?{kind:`ok`,token:r}:{kind:`missing-token`}}async function Ci(e){e.signal?.throwIfAborted();let t=e.timeoutMs??12e4,n=e.baseUrl.replace(/\/$/,``),r=await import(`node:http`),i=te(32).toString(`hex`),a=null,o=null,s=new Promise((e,t)=>{a=e,o=t}),c=r.createServer((e,t)=>{try{let n=Si({hostHeader:e.headers.host,requestUrl:e.url,expectedState:i});switch(n.kind){case`host-mismatch`:t.writeHead(421,{"content-type":`text/plain; charset=utf-8`}),t.end(`Misdirected request`);return;case`not-found`:t.writeHead(404,{"content-type":`text/plain; charset=utf-8`}),t.end(`Not found`);return;case`state-mismatch`:t.writeHead(400,{"content-type":`text/html; charset=utf-8`}),t.end(Oi(`State mismatch — refusing login`)),o?.(Error(`Browser login state mismatch — refusing potentially hijacked callback`));return;case`error`:t.writeHead(400,{"content-type":`text/html; charset=utf-8`}),t.end(Oi(n.reason)),o?.(Error(`Browser login failed: ${n.reason}`));return;case`missing-token`:t.writeHead(400,{"content-type":`text/html; charset=utf-8`}),t.end(Oi(null)),o?.(Error(`Browser login did not return a grant token`));return;case`ok`:t.writeHead(200,{"content-type":`text/html; charset=utf-8`}),t.end(Oi(null)),a?.(n.token);return}}catch(e){o?.(e instanceof Error?e:Error(`Browser login callback failed`))}});await new Promise((e,t)=>{c.once(`error`,t),c.listen(0,`127.0.0.1`,()=>e())});try{let r=c.address();if(!r||typeof r==`string`)throw Error(`Failed to bind local callback server`);let a=new URL(`http://127.0.0.1:${r.port}/callback`);a.searchParams.set(`state`,i);let o=new URL(`${n}/auth/cli/login`);o.searchParams.set(`callback_url`,a.toString()),e.provider&&o.searchParams.set(`provider`,e.provider);let l=await Di(o.toString());e.onLoginUrl?.({loginUrl:o.toString(),browserOpened:l});let u=await Ei({baseUrl:n,grantToken:await Ti(s,t,e.signal),timeoutMs:t,signal:e.signal});return{apiKey:u.apiKey,email:u.email,name:u.name,tier:u.tier}}finally{e.signal?.aborted&&c.closeAllConnections(),await new Promise((e,t)=>{c.close(n=>{if(n){t(n);return}e()})}).catch(()=>void 0)}}function wi(e){if(!e)return!1;let t=e.toLowerCase().match(/^(\[[^\]]+\]|[^:]+)(?::\d+)?$/);if(!t)return!1;let n=t[1];return n===`127.0.0.1`||n===`localhost`||n===`[::1]`}async function Ti(e,t,n){let r=new AbortController;try{return await Promise.race([e,Ae(t,void 0,{signal:AbortSignal.any([r.signal,...n?[n]:[]])}).then(()=>{throw new f(`Timed out waiting for browser login to complete`,{timeoutMs:t})})])}finally{r.abort()}}async function Ei(e){let t=await fetch(`${e.baseUrl}/auth/cli/exchange`,{method:`POST`,headers:{Accept:`application/json`,"Content-Type":`application/json`},body:JSON.stringify({grant_token:e.grantToken}),signal:AbortSignal.any([AbortSignal.timeout(e.timeoutMs),...e.signal?[e.signal]:[]])}).catch(t=>{throw new i(`Failed to reach ${e.baseUrl}`,{cause:t instanceof Error?t:void 0})}),n=await t.json().catch(()=>null);if(!t.ok||!n?.success||!n.data?.api_key)throw Error(n?.error?.message||`Failed to exchange browser login grant`);return{apiKey:n.data.api_key,email:n.data.email,name:n.data.name,tier:n.data.tier}}async function Di(e){let{spawn:t}=await import(`node:child_process`),n=process.platform===`darwin`?[`open`,e]:process.platform===`win32`?[`cmd`,`/c`,`start`,``,e]:[`xdg-open`,e];return await new Promise(e=>{let r=t(n[0],n.slice(1),{detached:!0,stdio:`ignore`});r.once(`error`,()=>e(!1)),r.once(`spawn`,()=>{r.unref(),e(!0)})})}function Oi(e){return`<!doctype html>
|
|
127
127
|
<html lang="en">
|
|
128
128
|
<head>
|
|
129
129
|
<meta charset="utf-8" />
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/sandbox-cli",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.65",
|
|
4
4
|
"description": "CLI for Tangle Sandbox operations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -59,8 +59,8 @@
|
|
|
59
59
|
"tsx": "^4.23.15",
|
|
60
60
|
"typescript": "^6.0.3",
|
|
61
61
|
"vitest": "^4.1.12",
|
|
62
|
-
"@
|
|
63
|
-
"@
|
|
62
|
+
"@repo/shared": "0.0.0",
|
|
63
|
+
"@tangle-network/runtime-contracts": "0.20.0"
|
|
64
64
|
},
|
|
65
65
|
"publishConfig": {
|
|
66
66
|
"access": "public",
|
|
@@ -80,8 +80,8 @@
|
|
|
80
80
|
"author": "Tangle Network",
|
|
81
81
|
"license": "MIT",
|
|
82
82
|
"tangleRelease": {
|
|
83
|
-
"sourceSha": "
|
|
84
|
-
"mergedAt": "2026-11-09T08:
|
|
83
|
+
"sourceSha": "7045bc279c8e24e29f5d36b9c992979b02fe9d2b",
|
|
84
|
+
"mergedAt": "2026-11-09T08:35:29-08:00"
|
|
85
85
|
},
|
|
86
86
|
"scripts": {
|
|
87
87
|
"build": "tsdown",
|