/** * npm.ts — Use any npm package from the browser. * * Two paths, unified: * 1. Browser-compatible packages (pure JS, no Node APIs) → dynamic ESM import * from esm.sh. Works for lodash, date-fns, uuid, marked, mathjs, zod, ramda, * nanoid, js-yaml, papaparse, chroma-js, fuse.js, and thousands more. * * 2. Node-only packages (fs, child_process, native bindings) → install + run * inside the WebContainer (in-browser Node.js runtime). Works for sharp, * sqlite3, express, puppeteer-core, whatever. * * Design: * - pkg_load / pkg_call / pkg_eval: browser ESM path * - pkg_node_run: WebContainer path * - pkg_load tries browser first, reports clearly if it fails (with hint to use * pkg_node_run if it's a Node package) * - Modules cached by spec+version; repeat calls are free * * Runtime security note: this executes arbitrary code from a public CDN (esm.sh). * No worse than javascript_eval, but worth explicit consent for paranoid setups. */ import { tool } from '@strands-agents/sdk' import { z } from 'zod' // --------------------------------------------------------------------------- // CDN resolver // --------------------------------------------------------------------------- type CDN = 'esm.sh' | 'jspm.io' | 'skypack' | 'unpkg' function cdnUrl(cdn: CDN, spec: string): string { // spec forms: "lodash" "lodash@4.17.21" "lodash/chunk" "@scope/pkg" "@scope/pkg@1.2/sub/path" const encodedSpec = encodeURI(spec).replace(/%40/g, '@') switch (cdn) { case 'esm.sh': return `https://esm.sh/${encodedSpec}` case 'jspm.io': return `https://jspm.dev/${encodedSpec}` case 'skypack': return `https://cdn.skypack.dev/${encodedSpec}` case 'unpkg': return `https://unpkg.com/${encodedSpec}?module` } } // Parse a spec into { name, version, subpath }. function parseSpec(spec: string) { // Handle scoped packages: @scope/name[@version][/subpath] let name = '', version = 'latest', subpath = '' if (spec.startsWith('@')) { const firstSlash = spec.indexOf('/') if (firstSlash === -1) return { name: spec, version: 'latest', subpath: '' } const scope = spec.slice(0, firstSlash) const rest = spec.slice(firstSlash + 1) const atIdx = rest.indexOf('@') if (atIdx === -1) { const slashIdx = rest.indexOf('/') if (slashIdx === -1) { name = `${scope}/${rest}` } else { name = `${scope}/${rest.slice(0, slashIdx)}`; subpath = rest.slice(slashIdx) } } else { name = `${scope}/${rest.slice(0, atIdx)}` const after = rest.slice(atIdx + 1) const slashIdx = after.indexOf('/') if (slashIdx === -1) version = after else { version = after.slice(0, slashIdx); subpath = after.slice(slashIdx) } } } else { const atIdx = spec.indexOf('@') if (atIdx === -1) { const slashIdx = spec.indexOf('/') if (slashIdx === -1) name = spec else { name = spec.slice(0, slashIdx); subpath = spec.slice(slashIdx) } } else { name = spec.slice(0, atIdx) const after = spec.slice(atIdx + 1) const slashIdx = after.indexOf('/') if (slashIdx === -1) version = after else { version = after.slice(0, slashIdx); subpath = after.slice(slashIdx) } } } return { name, version, subpath } } // --------------------------------------------------------------------------- // Module cache // --------------------------------------------------------------------------- type LoadedModule = { spec: string cdn: CDN url: string module: any loadedAt: number loadTimeMs: number resolvedVersion?: string } const _modules = new Map() async function loadFromCDNOnce(spec: string, cdn: CDN, timeoutMs: number): Promise<{ entry: LoadedModule; fromCache: boolean }> { const cacheKey = `${cdn}::${spec}` const hit = _modules.get(cacheKey) if (hit) return { entry: hit, fromCache: true } const url = cdnUrl(cdn, spec) const start = performance.now() // Use dynamic import with a timeout race. const importPromise = import(/* @vite-ignore */ url) const module = await Promise.race([ importPromise, new Promise((_, reject) => setTimeout(() => reject(new Error(`Timeout loading ${url} after ${timeoutMs}ms`)), timeoutMs)), ]) const loadTimeMs = performance.now() - start // Try to derive version from module (many packages expose .VERSION, .version, or default.VERSION). let resolvedVersion: string | undefined try { const m: any = module resolvedVersion = m?.VERSION || m?.version || m?.default?.VERSION || m?.default?.version if (typeof resolvedVersion !== 'string') resolvedVersion = undefined } catch {} const entry: LoadedModule = { spec, cdn, url, module, loadedAt: Date.now(), loadTimeMs, resolvedVersion } _modules.set(cacheKey, entry) return { entry, fromCache: false } } // Public wrapper: transparent fallback from esm.sh → jspm.io on network errors (not 404s). async function loadFromCDN(spec: string, cdn: CDN, timeoutMs: number): Promise<{ entry: LoadedModule; fromCache: boolean }> { try { return await loadFromCDNOnce(spec, cdn, timeoutMs) } catch (err: any) { const msg = String(err?.message || err) // Only auto-fallback from esm.sh on network/5xx errors. 404 = real "not found", don't retry. const retryable = cdn === 'esm.sh' && !/\b404\b|not found/i.test(msg) if (!retryable) throw err try { const result = await loadFromCDNOnce(spec, 'jspm.io', timeoutMs) // Annotate that we fell back. result.entry.url = `[fallback:jspm.io] ${result.entry.url}` return result } catch { throw err // keep the original error message } } } // --------------------------------------------------------------------------- // Inspection — safe listing of exports without serializing functions // --------------------------------------------------------------------------- function inspectModule(mod: any, maxDepth: number = 1) { const seen = new WeakSet() const describe = (val: any, depth: number): any => { if (val === null) return { type: 'null' } if (val === undefined) return { type: 'undefined' } const t = typeof val if (t === 'function') { return { type: 'function', name: val.name || '', length: val.length, // arity is_async: val.constructor?.name === 'AsyncFunction', is_class: /^class\s/.test(val.toString?.().slice(0, 64) || ''), } } if (t === 'string') return val.length > 80 ? { type: 'string', preview: val.slice(0, 80) + '…', length: val.length } : val if (t === 'number' || t === 'boolean' || t === 'bigint' || t === 'symbol') return val if (t === 'object') { if (seen.has(val)) return '' seen.add(val) if (Array.isArray(val)) { if (depth >= maxDepth) return { type: 'array', length: val.length } return { type: 'array', length: val.length, items: val.slice(0, 5).map((v) => describe(v, depth + 1)) } } if (depth >= maxDepth) { const keys = Object.keys(val).slice(0, 30) return { type: 'object', keys, key_count: Object.keys(val).length } } const out: Record = {} for (const k of Object.keys(val).slice(0, 40)) { try { out[k] = describe(val[k], depth + 1) } catch { out[k] = '' } } return out } return { type: t } } return describe(mod, 0) } // --------------------------------------------------------------------------- // Dot-path resolver for pkg_call // --------------------------------------------------------------------------- function resolvePath(obj: any, path: string): { target: any; thisArg: any } | null { if (!path) return { target: obj, thisArg: null } const parts = path.split('.') let cur: any = obj let parent: any = null for (const p of parts) { if (cur == null) return null parent = cur cur = cur[p] } if (cur === undefined) return null return { target: cur, thisArg: parent } } // --------------------------------------------------------------------------- // Tools // --------------------------------------------------------------------------- export const pkgLoadTool = tool({ name: 'pkg_load', description: 'Load an npm package via an ESM CDN (esm.sh by default). ' + 'Returns an inspection of the exports so you can see what\'s available. ' + 'Examples: "lodash", "date-fns@4", "uuid", "@scope/pkg@1.2.3", "lodash/chunk". ' + 'If the package needs Node APIs (fs, child_process, native bindings), this will fail — use pkg_node_run instead.', inputSchema: z.object({ spec: z.string().describe('npm spec: "pkg", "pkg@version", "pkg/subpath"'), cdn: z.enum(['esm.sh', 'jspm.io', 'skypack', 'unpkg']).optional().describe('Default esm.sh'), timeout_ms: z.number().optional().describe('Default 15000'), inspect_depth: z.number().optional().describe('How deep to walk exports when describing (default 1)'), }), callback: async (input) => { const cdn = input.cdn ?? 'esm.sh' const timeout = input.timeout_ms ?? 15000 try { const parsed = parseSpec(input.spec) const { entry: loaded, fromCache } = await loadFromCDN(input.spec, cdn, timeout) const inspection = inspectModule(loaded.module, input.inspect_depth ?? 1) // Suggest call paths: walk exports to depth 2, find functions, emit agent-friendly hints. const suggestions: Array<{ path: string; arity: number; is_async: boolean }> = [] try { const walk = (val: any, pathParts: string[]) => { if (pathParts.length > 2) return if (typeof val === 'function') { suggestions.push({ path: pathParts.join('.'), arity: val.length, is_async: val.constructor?.name === 'AsyncFunction', }) return } if (val && typeof val === 'object') { for (const k of Object.keys(val).slice(0, 30)) { try { walk(val[k], [...pathParts, k]) } catch {} } } } for (const k of Object.keys(loaded.module || {}).slice(0, 30)) { walk(loaded.module[k], [k]) } } catch {} return JSON.stringify({ status: 'success', spec: input.spec, parsed, cdn, url: loaded.url, load_time_ms: Number(loaded.loadTimeMs.toFixed(2)), cached: fromCache, resolved_version: loaded.resolvedVersion, exports: Object.keys(loaded.module || {}), has_default: 'default' in (loaded.module || {}), suggested_call_paths: suggestions.slice(0, 40), shape: inspection, }) } catch (err: unknown) { const msg = (err as Error).message || String(err) // Heuristic: detect "needs Node" patterns const looksLikeNode = /require\s*\(|node:|process\.|Buffer|__dirname|module\.exports/.test(msg) || /Cannot find module 'fs|child_process|path|crypto|stream'/.test(msg) return JSON.stringify({ status: 'error', spec: input.spec, cdn, error: msg, likely_needs_node: looksLikeNode, hint: looksLikeNode ? 'This package seems to need a Node runtime. Try pkg_node_run instead (runs in WebContainer).' : `Try a different CDN (jspm.io, skypack, unpkg) or check the package name + version.`, }) } }, }) export const pkgCallTool = tool({ name: 'pkg_call', description: 'Call a function from a previously loaded package. Use dot-path to navigate: ' + '"default.parse", "chunk", "utils.format". Args are passed positionally. ' + 'If the pkg isn\'t loaded, auto-loads it first.', inputSchema: z.object({ spec: z.string().describe('Same spec used with pkg_load'), path: z.string().optional().describe('Dot-path to function. Omit to return the whole module as a value.'), args: z.array(z.any()).optional().describe('Positional args (default [])'), cdn: z.enum(['esm.sh', 'jspm.io', 'skypack', 'unpkg']).optional(), timeout_ms: z.number().optional().describe('Max time for the initial import if not cached (default 15000)'), await_result: z.boolean().optional().describe('If true, await the result (for async fns). Auto-detected when possible.'), return_as: z.enum(['json', 'string', 'inspect']).optional().describe('How to encode the return value. Default "json" (falls back to inspect for unserializable).'), }), callback: async (input) => { const cdn = input.cdn ?? 'esm.sh' try { const { entry: loaded } = await loadFromCDN(input.spec, cdn, input.timeout_ms ?? 15000) const resolved = resolvePath(loaded.module, input.path || '') if (!resolved) { return JSON.stringify({ status: 'error', error: `path "${input.path}" not found in module`, available_exports: Object.keys(loaded.module || {}), }) } const { target, thisArg } = resolved let result: any if (typeof target === 'function') { const args = input.args || [] let called = target.apply(thisArg, args) if (input.await_result || (called && typeof called.then === 'function')) { called = await called } result = called } else { result = target } const returnAs = input.return_as ?? 'json' if (returnAs === 'string') { return JSON.stringify({ status: 'success', result: String(result) }) } if (returnAs === 'inspect') { return JSON.stringify({ status: 'success', inspect: inspectModule(result, 2) }) } // try JSON, fall back to inspect try { // Test JSON-serializability const asJson = JSON.parse(JSON.stringify(result)) return JSON.stringify({ status: 'success', result: asJson }) } catch { return JSON.stringify({ status: 'success', result_type: typeof result, note: 'result was not JSON-serializable; returning inspection', inspect: inspectModule(result, 2), }) } } catch (err: unknown) { return JSON.stringify({ status: 'error', error: (err as Error).message }) } }, }) export const pkgEvalTool = tool({ name: 'pkg_eval', description: 'Evaluate arbitrary JS code with one or more loaded packages bound as identifiers. ' + 'Escape hatch for when pkg_call isn\'t expressive enough (e.g. chained calls, custom logic). ' + 'The code runs as an async function body — you can use await freely.', inputSchema: z.object({ specs: z.array(z.object({ spec: z.string(), as: z.string().describe('Identifier to bind in the eval scope (e.g. "_" for lodash)'), cdn: z.enum(['esm.sh', 'jspm.io', 'skypack', 'unpkg']).optional(), })).describe('Packages to load and bind'), code: z.string().describe('JS code. Use `return value` to get a result. Can use await.'), }), callback: async (input) => { try { // Validate binding identifiers — must be valid JS identifiers, not reserved words. const ID_RE = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/ const RESERVED = new Set(['arguments', 'eval', 'await', 'async', 'yield', 'return', 'this', 'default', 'new', 'class', 'function', 'var', 'let', 'const', 'if', 'else', 'for', 'while', 'break', 'continue', 'do', 'switch', 'case', 'throw', 'try', 'catch', 'finally', 'typeof', 'instanceof', 'in', 'of', 'void', 'null', 'true', 'false', 'undefined']) for (const s of input.specs) { if (!ID_RE.test(s.as)) { return JSON.stringify({ status: 'error', error: `Invalid binding identifier "${s.as}" — must match /^[a-zA-Z_$][a-zA-Z0-9_$]*$/` }) } if (RESERVED.has(s.as)) { return JSON.stringify({ status: 'error', error: `Cannot bind reserved word "${s.as}"` }) } } const bindings: Record = {} for (const s of input.specs) { const { entry: loaded } = await loadFromCDN(s.spec, s.cdn ?? "esm.sh", 15000) // Smart default binding: if module has .default and lots of methods live on it, prefer default. // Heuristic: if mod.default is a function or has more own keys than the namespace. let bound: any = loaded.module const def = loaded.module?.default if (def !== undefined && (typeof def === 'function' || (typeof def === 'object' && def !== null && Object.keys(def).length > Object.keys(loaded.module).length - 2))) { // Still expose the full namespace as .__ns for access to named exports. bound = def try { (bound as any).__ns = loaded.module } catch {} } bindings[s.as] = bound } // Build async function with bindings as args const argNames = Object.keys(bindings) const argValues = argNames.map((k) => bindings[k]) const fn = new Function(...argNames, `"use strict"; return (async () => { ${input.code} })();`) const result = await fn(...argValues) try { return JSON.stringify({ status: 'success', result: JSON.parse(JSON.stringify(result ?? null)) }) } catch { return JSON.stringify({ status: 'success', result_type: typeof result, note: 'not JSON-serializable; returning inspection', inspect: inspectModule(result, 2), }) } } catch (err: unknown) { return JSON.stringify({ status: 'error', error: (err as Error).message }) } }, }) export const pkgListTool = tool({ name: 'pkg_list', description: 'List all packages currently loaded in the browser module cache.', inputSchema: z.object({}), callback: async () => { const entries = Array.from(_modules.values()) return JSON.stringify({ status: 'success', count: entries.length, modules: entries.map((e) => ({ spec: e.spec, cdn: e.cdn, url: e.url, resolved_version: e.resolvedVersion, loaded_at: new Date(e.loadedAt).toISOString(), load_time_ms: Number(e.loadTimeMs.toFixed(2)), exports: Object.keys(e.module || {}), })), }) }, }) export const pkgUnloadTool = tool({ name: 'pkg_unload', description: 'Drop a package from the cache. Note: this does NOT evict the browser\'s module-graph cache — only forces a re-inspect next call.', inputSchema: z.object({ spec: z.string(), cdn: z.enum(['esm.sh', 'jspm.io', 'skypack', 'unpkg']).optional(), }), callback: async (input) => { const cdn = input.cdn ?? 'esm.sh' const key = `${cdn}::${input.spec}` const existed = _modules.delete(key) return JSON.stringify({ status: 'success', removed: existed }) }, }) export const pkgSearchTool = tool({ name: 'pkg_search', description: 'Search the npm registry. Returns top N matching packages with summary info.', inputSchema: z.object({ query: z.string(), size: z.number().optional().describe('Default 10, max 25'), }), callback: async (input) => { try { if (!input.query || !input.query.trim()) { return JSON.stringify({ status: 'error', error: 'query is required and cannot be empty' }) } const size = Math.min(Math.max(input.size ?? 10, 1), 25) const url = `https://registry.npmjs.com/-/v1/search?text=${encodeURIComponent(input.query.trim())}&size=${size}` const resp = await fetch(url) if (!resp.ok) throw new Error(`npm registry ${resp.status}`) const data: any = await resp.json() return JSON.stringify({ status: 'success', total: data.total, returned: data.objects?.length || 0, packages: (data.objects || []).map((o: any) => ({ name: o.package.name, version: o.package.version, description: o.package.description, keywords: o.package.keywords, publisher: o.package.publisher?.username, date: o.package.date, links: o.package.links, score: o.score?.final, })), }) } catch (err: unknown) { return JSON.stringify({ status: 'error', error: (err as Error).message }) } }, }) export const pkgInfoTool = tool({ name: 'pkg_info', description: 'Fetch metadata for a package from the npm registry — latest version, deps, description, types, etc.', inputSchema: z.object({ name: z.string(), version: z.string().optional().describe('Specific version. Default "latest".'), }), callback: async (input) => { try { const version = input.version || 'latest' const url = `https://registry.npmjs.com/${input.name}/${version}` const resp = await fetch(url) if (!resp.ok) throw new Error(`npm registry ${resp.status}: ${input.name}@${version}`) const data: any = await resp.json() return JSON.stringify({ status: 'success', name: data.name, version: data.version, description: data.description, main: data.main, module: data.module, types: data.types || data.typings, exports: data.exports, dependencies: data.dependencies, peerDependencies: data.peerDependencies, keywords: data.keywords, license: data.license, homepage: data.homepage, repository: data.repository, dist: data.dist ? { tarball: data.dist.tarball, unpackedSize: data.dist.unpackedSize, } : undefined, browser_compatible_hint: { has_browser_field: !!data.browser, has_module_field: !!data.module, has_exports_field: !!data.exports, has_native_binding: !!(data.gypfile || data.binary || (data.scripts?.install && /gyp|prebuild|node-gyp/.test(JSON.stringify(data.scripts)))), looks_node_only: /\b(fs|child_process|cluster|dgram|net|tls|http[s]?|dns|os|process|stream|util|zlib|v8|worker_threads)\b/.test( JSON.stringify(data.dependencies || {}) + JSON.stringify(data.peerDependencies || {}), ), }, }) } catch (err: unknown) { return JSON.stringify({ status: 'error', error: (err as Error).message }) } }, }) // --------------------------------------------------------------------------- // Node path via WebContainer // --------------------------------------------------------------------------- export const pkgNodeRunTool = tool({ name: 'pkg_node_run', description: 'Install an npm package in the WebContainer and run a Node.js script that uses it. ' + 'Use this for Node-only packages (sharp, sqlite3, express, puppeteer-core, etc.) that cannot run directly in the browser. ' + 'By default this FIRST probes browser compatibility via esm.sh — if all packages load there, returns a hint to use pkg_call instead (saves ~30-60s install). Pass force=true to skip the probe. ' + 'Dirs are hashed per package set, so repeat calls with the same deps skip the npm install. ' + 'Auto-boots WebContainer if needed. Flow: write package.json + index.mjs → npm install (or reuse) → node index.mjs → capture stdout.', inputSchema: z.object({ packages: z.array(z.string()).describe('npm specs to install (e.g. ["lodash@4", "date-fns"])'), code: z.string().describe('Node.js ESM code. Use import statements referencing the installed packages.'), type: z.enum(['module', 'commonjs']).optional().describe('Default "module" (ESM). Use "commonjs" for packages that only export CJS.'), install_timeout_ms: z.number().optional().describe('Default 180000 (3 min)'), run_timeout_ms: z.number().optional().describe('Default 60000 (1 min)'), force: z.boolean().optional().describe('Skip the browser-compatibility probe. By default we quickly check if ALL packages load via esm.sh and suggest pkg_call if they do (saves ~30-60s install).'), reuse_dir: z.string().optional().describe('Reuse a specific working directory (saves re-install). Default: stable hash of package list.'), }), callback: async (input) => { try { // Lazy import so this file doesn't hard-fail if WebContainer isn't available. const wcModule: any = await import('@webcontainer/api').catch(() => null) if (!wcModule) { return JSON.stringify({ status: 'error', error: '@webcontainer/api not available in this build', }) } // Optional browser-path probe: if ALL packages load via esm.sh, skip the slow install. if (!input.force) { const results = await Promise.all(input.packages.map(async (spec) => { try { await loadFromCDN(spec, 'esm.sh', 8000) return { spec, browser_ok: true } } catch (e: any) { const msg = String(e?.message || e) return { spec, browser_ok: false, error: msg.slice(0, 200) } } })) if (results.every((r) => r.browser_ok)) { return JSON.stringify({ status: 'skip', message: 'All packages load in the browser — no WebContainer needed.', probe: results, hint: 'Use pkg_call or pkg_eval instead. If you specifically want a Node-only runtime, pass force=true.', }) } } // Get/boot WebContainer. We reuse the same singleton pattern as webcontainer.ts. // We can't import from webcontainer.ts directly without coupling, so we boot our own // reference if needed — WebContainer API is a per-page singleton, so the same instance // is returned by boot() or throws if another boot already happened. let wc: any try { wc = await wcModule.WebContainer.boot() } catch (err: any) { // If it errors because "another instance already booted", we need to cheat — // the user should have booted via webcontainer_boot. Surface that clearly. if (String(err?.message).includes('already booted') || String(err?.message).includes('another')) { return JSON.stringify({ status: 'error', error: 'WebContainer already booted elsewhere. Run pkg_node_run AFTER webcontainer_boot (they share the page singleton).', original: String(err?.message), }) } throw err } const ext = input.type === 'commonjs' ? 'cjs' : 'mjs' // Stable dir per package set → avoid re-installing same deps across calls. const stableHash = (() => { const key = [...input.packages].sort().join('|') + '::' + (input.type ?? 'module') let h = 0 for (let i = 0; i < key.length; i++) h = ((h << 5) - h + key.charCodeAt(i)) | 0 return Math.abs(h).toString(36) })() const dir = input.reuse_dir ?? `pkg_run_${stableHash}` const packageJson = { name: 'pkg-run', version: '1.0.0', type: input.type ?? 'module', dependencies: Object.fromEntries(input.packages.map((spec) => { const atIdx = spec.lastIndexOf('@') if (atIdx <= 0) return [spec, 'latest'] // starts-with-@ (scoped) or no version if (spec.startsWith('@') && spec.indexOf('@', 1) === -1) return [spec, 'latest'] return [spec.slice(0, atIdx > 0 ? atIdx : spec.length), atIdx > 0 ? spec.slice(atIdx + 1) : 'latest'] })), } // Mount files await wc.fs.mkdir(dir, { recursive: true }) await wc.fs.writeFile(`${dir}/package.json`, JSON.stringify(packageJson, null, 2)) await wc.fs.writeFile(`${dir}/index.${ext}`, input.code) // npm install const installTimeout = input.install_timeout_ms ?? 180_000 const runTimeout = input.run_timeout_ms ?? 60_000 const runProc = async (cmd: string, args: string[], timeoutMs: number) => { const proc = await wc.spawn(cmd, args, { cwd: dir }) let out = '' const reader = proc.output.getReader() let killed = false const killTimer = setTimeout(() => { killed = true; try { proc.kill() } catch {} }, timeoutMs) const drain = (async () => { try { while (true) { const { value, done } = await reader.read() if (done) break out += typeof value === 'string' ? value : String(value ?? '') } } catch {} })() const code: number = await Promise.race([ proc.exit, new Promise((r) => setTimeout(() => { try { proc.kill() } catch {}; r(-1) }, timeoutMs)), ]) clearTimeout(killTimer) await Promise.race([drain, new Promise((r) => setTimeout(r, 150))]) return { code, out, killed } } // Skip npm install if node_modules exists in this dir (dep set hasn't changed). let needsInstall = true try { const entries = await wc.fs.readdir(`${dir}/node_modules`) if (Array.isArray(entries) && entries.length > 0) needsInstall = false } catch {} const install = needsInstall ? await runProc('npm', ['install'], installTimeout) : { code: 0, out: '[cached: node_modules exists, skipped install]', killed: false } if (install.code !== 0) { return JSON.stringify({ status: 'error', phase: 'install', exitCode: install.code, timed_out: install.code === -1, output: install.out.slice(-4000), packages: input.packages, }) } const run = await runProc('node', [`index.${ext}`], runTimeout) return JSON.stringify({ status: run.code === 0 ? 'success' : 'error', phase: 'run', exitCode: run.code, timed_out: run.code === -1, output: run.out.slice(-4000), output_length: run.out.length, truncated: run.out.length > 4000, packages: input.packages, install_output_preview: install.out.slice(-1500), cached_install: !needsInstall, working_dir: dir, }) } catch (err: unknown) { return JSON.stringify({ status: 'error', error: (err as Error).message }) } }, }) export const pkgGuideTool = tool({ name: 'pkg_guide', description: 'Quick decision guide: tells you whether to use the browser path or WebContainer for a given package, with concrete examples.', inputSchema: z.object({ package_name: z.string().optional(), }), callback: async (input) => { const guide = { status: 'success', decision_tree: [ '1. pkg_info({ name }) — inspect metadata; browser_compatible_hint.looks_node_only gives a fast signal', '2. Try pkg_load({ spec }) — esm.sh transforms most things; if it works, use pkg_call / pkg_eval', '3. If pkg_load fails with likely_needs_node=true → pkg_node_run', '4. For native bindings (sharp, better-sqlite3, canvas-native) → pkg_node_run', ], browser_safe_examples: [ { pkg: 'lodash', use: "pkg_call({ spec: 'lodash', path: 'chunk', args: [[1,2,3,4,5], 2] })" }, { pkg: 'date-fns', use: "pkg_call({ spec: 'date-fns', path: 'formatDistanceToNow', args: [new Date('2026-01-01')] })" }, { pkg: 'uuid', use: "pkg_call({ spec: 'uuid', path: 'v4' })" }, { pkg: 'marked', use: "pkg_call({ spec: 'marked', path: 'parse', args: ['# hi'] })" }, { pkg: 'js-yaml', use: "pkg_call({ spec: 'js-yaml', path: 'load', args: ['foo: bar'] })" }, { pkg: 'mathjs', use: "pkg_call({ spec: 'mathjs', path: 'evaluate', args: ['sqrt(2)'] })" }, { pkg: 'nanoid', use: "pkg_call({ spec: 'nanoid', path: 'nanoid' })" }, { pkg: 'zod', use: "pkg_eval({ specs: [{spec:'zod', as:'z'}], code: 'return z.string().min(3).safeParse(\"hi\")' })" }, ], node_required_examples: [ { pkg: 'sharp', why: 'native image processing via libvips' }, { pkg: 'better-sqlite3', why: 'native SQLite binding' }, { pkg: 'bcrypt', why: 'native crypto binding' }, { pkg: 'express', why: 'needs node http/net' }, { pkg: 'puppeteer', why: 'launches Chromium (downloads in-container is iffy; try puppeteer-core + remote WS)' }, ], notes: [ "esm.sh auto-shims require() and bundles deps, so even many CJS-only packages work in the browser.", "Subpaths work: lodash/chunk resolves correctly via any of the CDNs.", "Version pinning: use 'pkg@1.2.3' in the spec.", "Cached by spec+cdn for the page lifetime — repeat calls are free.", ], } if (input.package_name) { // Best-effort hint for a specific package const name = input.package_name.toLowerCase() const nodeOnlyList = ['sharp', 'better-sqlite3', 'sqlite3', 'bcrypt', 'canvas-native', 'express', 'koa', 'fastify', 'puppeteer', 'playwright', 'fs-extra', 'glob', 'chokidar'] const browserSafeList = ['lodash', 'date-fns', 'uuid', 'nanoid', 'marked', 'mathjs', 'zod', 'js-yaml', 'papaparse', 'chroma-js', 'chalk', 'ramda', 'fuse.js', 'dayjs'] const suggestion = nodeOnlyList.some((n) => name.includes(n)) ? 'likely_node_only: use pkg_node_run' : browserSafeList.some((n) => name.includes(n)) ? 'likely_browser_safe: use pkg_load + pkg_call' : 'unknown: try pkg_load first; if it fails with likely_needs_node, fall through to pkg_node_run' return JSON.stringify({ ...guide, package_name: input.package_name, suggestion }) } return JSON.stringify(guide) }, }) export const NPM_TOOLS = [ pkgGuideTool, // START HERE when exploring pkgSearchTool, // find a package pkgInfoTool, // read registry metadata pkgLoadTool, // bring it into the browser pkgCallTool, // call one of its functions pkgEvalTool, // escape hatch: arbitrary JS with packages bound pkgListTool, // what's loaded pkgUnloadTool, // drop from cache pkgNodeRunTool, // fallback for Node-only packages ]