From f0b55a23f9ffa9b40977ae0740a4bbd7ffdd61f7 Mon Sep 17 00:00:00 2001 From: Ajax Davis Date: Fri, 23 Jan 2026 05:15:38 +1000 Subject: [PATCH] feat: add collection info command and improve CLI discovery - Add `tpm collection info ` command to list all tools in a collection - Update `run` command examples to show workflow of listing tools first - Fix unsandbox healthCheck tool to use /cluster endpoint instead of /health - Update create-basic-tools template with correct tpmjs field format docs - Change default category from 'ai-ml' to 'utilities' in generator Published: - @tpmjs/cli@0.1.5 - @tpmjs/create-basic-tools@1.0.7 - @tpmjs/tools-unsandbox@0.1.3 --- packages/cli/package.json | 2 +- packages/cli/src/commands/collection/info.ts | 190 ++++++++++++++++++ packages/cli/src/commands/run.ts | 32 ++- packages/tools/create-basic-tools/README.md | 81 ++++++-- .../tools/create-basic-tools/package.json | 2 +- packages/tools/create-basic-tools/src/cli.ts | 2 +- .../tools/official/unsandbox/package.json | 2 +- .../tools/official/unsandbox/src/index.ts | 24 ++- 8 files changed, 308 insertions(+), 27 deletions(-) create mode 100644 packages/cli/src/commands/collection/info.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index 8c18d93..0ffba37 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@tpmjs/cli", - "version": "0.1.4", + "version": "0.1.5", "description": "TPMJS command-line interface for AI tool discovery and execution", "author": "TPMJS", "license": "MIT", diff --git a/packages/cli/src/commands/collection/info.ts b/packages/cli/src/commands/collection/info.ts new file mode 100644 index 0000000..7f51abe --- /dev/null +++ b/packages/cli/src/commands/collection/info.ts @@ -0,0 +1,190 @@ +import { Args, Command, Flags } from '@oclif/core'; + +import { getApiKey, getApiUrl } from '../../lib/config.js'; +import { createOutput } from '../../lib/output.js'; + +interface McpTool { + name: string; + description: string; + inputSchema?: { + type: string; + required?: string[]; + properties?: Record; + }; +} + +interface McpListToolsResponse { + jsonrpc: '2.0'; + id: number; + result?: { + tools: McpTool[]; + }; + error?: { + code: number; + message: string; + }; +} + +export default class CollectionInfo extends Command { + static description = 'Show collection details and list all available tools'; + + static examples = [ + '<%= config.bin %> collection info ajax/unsandbox', + '<%= config.bin %> collection info ajax/unsandbox --json', + '<%= config.bin %> collection info ajax/unsandbox --verbose', + ]; + + static args = { + collection: Args.string({ + description: 'Collection identifier (username/slug)', + required: true, + }), + }; + + static flags = { + json: Flags.boolean({ + description: 'Output in JSON format', + default: false, + }), + verbose: Flags.boolean({ + char: 'v', + description: 'Show tool input schemas', + default: false, + }), + }; + + async run(): Promise { + const { args, flags } = await this.parse(CollectionInfo); + const output = createOutput(flags); + + // Parse collection identifier + const parts = args.collection.split('/'); + if (parts.length !== 2) { + output.error('Invalid collection format. Use: username/slug'); + return; + } + + const [username, slug] = parts; + const baseUrl = getApiUrl().replace(/\/api$/, ''); + const mcpUrl = `${baseUrl}/api/mcp/${username}/${slug}/http`; + + const spinner = output.spinner(`Fetching tools from ${args.collection}...`); + + try { + // Get API key for authentication + const apiKey = getApiKey(); + const headers: Record = { + 'Content-Type': 'application/json', + }; + if (apiKey) { + headers['Authorization'] = `Bearer ${apiKey}`; + } + + // Call MCP tools/list to get all tools + const response = await fetch(mcpUrl, { + method: 'POST', + headers, + body: JSON.stringify({ + jsonrpc: '2.0', + id: 1, + method: 'tools/list', + }), + }); + + if (!response.ok) { + spinner.fail('Failed to fetch collection'); + output.error(`HTTP ${response.status}: ${await response.text()}`); + return; + } + + const data = (await response.json()) as McpListToolsResponse; + + if (data.error) { + spinner.fail('Failed to fetch tools'); + output.error(data.error.message); + return; + } + + const tools = data.result?.tools || []; + spinner.stop(); + + if (flags.json) { + output.json({ + collection: args.collection, + mcpUrl, + toolCount: tools.length, + tools: tools.map((t) => ({ + name: t.name, + description: t.description, + ...(flags.verbose ? { inputSchema: t.inputSchema } : {}), + })), + }); + return; + } + + // Display collection info + output.success(`Collection: ${args.collection}`); + output.text(`MCP URL: ${mcpUrl}`); + output.text(`Tools: ${tools.length}`); + output.divider(); + + if (tools.length === 0) { + output.info('No tools in this collection'); + return; + } + + // Display tools table + output.table( + tools.map((tool) => ({ + name: tool.name, + description: truncate(tool.description, 60), + })), + [ + { key: 'name', header: 'Tool Name', width: 40 }, + { key: 'description', header: 'Description', width: 60 }, + ] + ); + + // Show verbose tool details + if (flags.verbose) { + output.newLine(); + output.divider(); + output.text('Tool Details:'); + output.newLine(); + + for (const tool of tools) { + output.text(`${output.bold(tool.name)}`); + output.text(` ${tool.description}`); + + if (tool.inputSchema?.properties) { + const props = tool.inputSchema.properties; + const required = tool.inputSchema.required || []; + output.text(' Parameters:'); + for (const [name, schema] of Object.entries(props)) { + const req = required.includes(name) ? ' (required)' : ''; + output.text(` - ${name}: ${schema.type}${req}`); + if (schema.description) { + output.text(` ${output.dim(schema.description)}`); + } + } + } + output.newLine(); + } + } + + // Usage hints + output.newLine(); + output.text(output.dim('Usage example:')); + const exampleTool = tools[0]?.name || 'toolName'; + output.text(output.dim(` tpm run -c ${args.collection} -t ${exampleTool} --args '{}'`)); + } catch (error) { + spinner.fail('Failed to fetch collection'); + output.error(error instanceof Error ? error.message : 'Unknown error'); + } + } +} + +function truncate(str: string, maxLen: number): string { + if (str.length <= maxLen) return str; + return str.slice(0, maxLen - 3) + '...'; +} diff --git a/packages/cli/src/commands/run.ts b/packages/cli/src/commands/run.ts index c632240..7107c8b 100644 --- a/packages/cli/src/commands/run.ts +++ b/packages/cli/src/commands/run.ts @@ -52,7 +52,10 @@ function parseToolArgs(argsStr: string): Record | null { /** * Build environment variables from process.env and explicit flags */ -function buildEnvVars(envFlags: string[] | undefined, output: OutputFormatter): Record { +function buildEnvVars( + envFlags: string[] | undefined, + output: OutputFormatter +): Record { const envVars: Record = {}; // Add process env vars that might be relevant @@ -168,11 +171,28 @@ export default class Run extends Command { static description = 'Execute a tool from a collection via MCP'; static examples = [ - '<%= config.bin %> run -c ajax/unsandbox -t execute --args \'{"code":"print(1)","language":"python"}\'', - '<%= config.bin %> run --collection ajax/ajax-collection --tool base64Encode --args \'{"data":"hello"}\'', - 'OPENAI_API_KEY=xxx <%= config.bin %> run -c ajax/my-collection -t myTool', - '<%= config.bin %> run -c ajax/tools -t search --args \'{"query":"test"}\' --json', - '<%= config.bin %> run -c ajax/tools -t search --env API_KEY=xxx --env DEBUG=true', + { + description: 'First, list all tools in a collection', + command: '<%= config.bin %> collection info ajax/unsandbox', + }, + { + description: 'Execute Python code in the unsandbox collection', + command: + '<%= config.bin %> run -c ajax/unsandbox -t unsandbox--execute --args \'{"language":"python","code":"print(42)"}\'', + }, + { + description: 'Pass environment variables for tool authentication', + command: + '<%= config.bin %> run -c ajax/unsandbox -t unsandbox--execute -e UNSANDBOX_PUBLIC_KEY=xxx -e UNSANDBOX_SECRET_KEY=xxx --args \'{"language":"python","code":"print(1)"}\'', + }, + { + description: 'Output result as JSON', + command: '<%= config.bin %> run -c ajax/tools -t search --args \'{"query":"test"}\' --json', + }, + { + description: 'Show verbose output for debugging', + command: "<%= config.bin %> run -c ajax/unsandbox -t unsandbox--healthCheck --args '{}' -v", + }, ]; static flags = { diff --git a/packages/tools/create-basic-tools/README.md b/packages/tools/create-basic-tools/README.md index 9b77f84..e808bd9 100644 --- a/packages/tools/create-basic-tools/README.md +++ b/packages/tools/create-basic-tools/README.md @@ -25,7 +25,7 @@ The CLI asks for just your package name and uses sensible defaults for everythin - **Description**: Auto-generated from package name - **Tools**: 2 example tools you can customize -- **Category**: `ai-ml` (generic) +- **Category**: `utilities` (generic) - **License**: MIT - **Output**: Derived from package name @@ -82,6 +82,56 @@ content-tools/ Simply rename `exampleTool.ts` and `anotherTool.ts` to match your use case, then customize the implementation. +## Generated package.json `tpmjs` Field + +The generator creates a properly formatted `tpmjs` field in your package.json: + +```json +{ + "name": "@myorg/content-tools", + "keywords": ["tpmjs", "ai-sdk", "utilities"], + "tpmjs": { + "category": "utilities", + "tools": [ + { + "name": "exampleTool", + "description": "An example tool - customize this for your use case" + }, + { + "name": "anotherTool", + "description": "Another example tool - add your implementation here" + } + ] + } +} +``` + +**Important:** The `tools` field must be an **array** of objects, not an object map: + +```json +// ✅ Correct - tools is an array +"tools": [ + { "name": "myTool", "description": "Does something useful" } +] + +// ❌ Wrong - tools is an object (will fail validation) +"tools": { + "myTool": { "description": "Does something useful" } +} +``` + +**Minimal format:** If you just want auto-discovery, only `category` is required: + +```json +{ + "tpmjs": { + "category": "web" + } +} +``` + +TPMJS will automatically discover and extract tool metadata from your package exports. + ## Generated Tool File Example Each tool file follows this Zod-first pattern: @@ -165,18 +215,23 @@ Your tools will appear on [tpmjs.com](https://tpmjs.com) within 2-15 minutes aft The generator validates against these official TPMJS categories: -- `web-scraping` -- `data-processing` -- `file-operations` -- `communication` -- `database` -- `api-integration` -- `image-processing` -- `text-analysis` -- `automation` -- `ai-ml` -- `security` -- `monitoring` +**Core categories:** +- `research` - Research and information gathering tools +- `web` - Web scraping, fetching, and browser automation +- `data` - Data processing and transformation +- `documentation` - Documentation generation and management +- `engineering` - Software engineering and development tools +- `security` - Security analysis and vulnerability scanning +- `statistics` - Statistical analysis and calculations +- `ops` - DevOps and infrastructure tools +- `agent` - AI agent orchestration and management +- `sandbox` - Code execution and sandboxing +- `utilities` - General-purpose utility tools +- `html` - HTML generation and manipulation +- `compliance` - Compliance and regulatory tools + +**Legacy categories (still supported):** +- `web-scraping`, `data-processing`, `file-operations`, `communication`, `database`, `api-integration`, `image-processing`, `text-analysis`, `automation`, `ai-ml`, `monitoring` ## Requirements diff --git a/packages/tools/create-basic-tools/package.json b/packages/tools/create-basic-tools/package.json index 113965b..6758112 100644 --- a/packages/tools/create-basic-tools/package.json +++ b/packages/tools/create-basic-tools/package.json @@ -1,6 +1,6 @@ { "name": "@tpmjs/create-basic-tools", - "version": "1.0.5", + "version": "1.0.7", "description": "CLI generator for scaffolding production-ready TPMJS tool packages", "type": "module", "bin": { diff --git a/packages/tools/create-basic-tools/src/cli.ts b/packages/tools/create-basic-tools/src/cli.ts index f61630a..21a8a65 100644 --- a/packages/tools/create-basic-tools/src/cli.ts +++ b/packages/tools/create-basic-tools/src/cli.ts @@ -56,7 +56,7 @@ export async function runInteractiveCLI(): Promise { description: `AI SDK tools for ${packageNameWithoutScope}`, author: '', license: 'MIT', - category: 'ai-ml', + category: 'utilities', }; const tools = [ diff --git a/packages/tools/official/unsandbox/package.json b/packages/tools/official/unsandbox/package.json index ba9b58c..021c287 100644 --- a/packages/tools/official/unsandbox/package.json +++ b/packages/tools/official/unsandbox/package.json @@ -1,6 +1,6 @@ { "name": "@tpmjs/tools-unsandbox", - "version": "0.1.2", + "version": "0.1.3", "description": "Execute code in a secure sandbox environment. Supports 42+ programming languages with async execution, input files, and compiled artifacts.", "type": "module", "keywords": [ diff --git a/packages/tools/official/unsandbox/src/index.ts b/packages/tools/official/unsandbox/src/index.ts index 08dc7a5..a3e83b9 100644 --- a/packages/tools/official/unsandbox/src/index.ts +++ b/packages/tools/official/unsandbox/src/index.ts @@ -2389,21 +2389,37 @@ export const deleteImage = tool({ // ============================================================================ export interface HealthResult { - status: string; + status: 'ok' | 'error'; + version?: string; + available?: number; + allocated?: number; } /** - * Health check. + * Health check - verifies the Unsandbox API is operational. + * Returns a simplified status derived from the cluster endpoint. */ export const healthCheck = tool({ - description: 'Simple health check endpoint to verify the Unsandbox API is operational.', + description: 'Check the health status of the Unsandbox API service.', inputSchema: jsonSchema>({ type: 'object', properties: {}, additionalProperties: false, }), async execute(): Promise { - return apiRequest('GET', '/health', undefined); + // Use /cluster endpoint which is known to work + const cluster = await apiRequest<{ + version?: string; + available?: number; + allocated?: number; + }>('GET', '/cluster', undefined); + + return { + status: 'ok', + version: cluster.version, + available: cluster.available, + allocated: cluster.allocated, + }; }, });