feat: add collection info command and improve CLI discovery

- Add `tpm collection info <collection>` 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
This commit is contained in:
Ajax Davis 2026-01-23 05:15:38 +10:00
parent 16c3b0df10
commit f0b55a23f9
8 changed files with 308 additions and 27 deletions

View file

@ -1,6 +1,6 @@
{ {
"name": "@tpmjs/cli", "name": "@tpmjs/cli",
"version": "0.1.4", "version": "0.1.5",
"description": "TPMJS command-line interface for AI tool discovery and execution", "description": "TPMJS command-line interface for AI tool discovery and execution",
"author": "TPMJS", "author": "TPMJS",
"license": "MIT", "license": "MIT",

View file

@ -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<string, { type: string; description?: string }>;
};
}
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<void> {
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<string, string> = {
'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) + '...';
}

View file

@ -52,7 +52,10 @@ function parseToolArgs(argsStr: string): Record<string, unknown> | null {
/** /**
* Build environment variables from process.env and explicit flags * Build environment variables from process.env and explicit flags
*/ */
function buildEnvVars(envFlags: string[] | undefined, output: OutputFormatter): Record<string, string> { function buildEnvVars(
envFlags: string[] | undefined,
output: OutputFormatter
): Record<string, string> {
const envVars: Record<string, string> = {}; const envVars: Record<string, string> = {};
// Add process env vars that might be relevant // 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 description = 'Execute a tool from a collection via MCP';
static examples = [ 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"}\'', description: 'First, list all tools in a collection',
'OPENAI_API_KEY=xxx <%= config.bin %> run -c ajax/my-collection -t myTool', command: '<%= config.bin %> collection info ajax/unsandbox',
'<%= 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: '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 = { static flags = {

View file

@ -25,7 +25,7 @@ The CLI asks for just your package name and uses sensible defaults for everythin
- **Description**: Auto-generated from package name - **Description**: Auto-generated from package name
- **Tools**: 2 example tools you can customize - **Tools**: 2 example tools you can customize
- **Category**: `ai-ml` (generic) - **Category**: `utilities` (generic)
- **License**: MIT - **License**: MIT
- **Output**: Derived from package name - **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. 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 ## Generated Tool File Example
Each tool file follows this Zod-first pattern: 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: The generator validates against these official TPMJS categories:
- `web-scraping` **Core categories:**
- `data-processing` - `research` - Research and information gathering tools
- `file-operations` - `web` - Web scraping, fetching, and browser automation
- `communication` - `data` - Data processing and transformation
- `database` - `documentation` - Documentation generation and management
- `api-integration` - `engineering` - Software engineering and development tools
- `image-processing` - `security` - Security analysis and vulnerability scanning
- `text-analysis` - `statistics` - Statistical analysis and calculations
- `automation` - `ops` - DevOps and infrastructure tools
- `ai-ml` - `agent` - AI agent orchestration and management
- `security` - `sandbox` - Code execution and sandboxing
- `monitoring` - `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 ## Requirements

View file

@ -1,6 +1,6 @@
{ {
"name": "@tpmjs/create-basic-tools", "name": "@tpmjs/create-basic-tools",
"version": "1.0.5", "version": "1.0.7",
"description": "CLI generator for scaffolding production-ready TPMJS tool packages", "description": "CLI generator for scaffolding production-ready TPMJS tool packages",
"type": "module", "type": "module",
"bin": { "bin": {

View file

@ -56,7 +56,7 @@ export async function runInteractiveCLI(): Promise<GenerationResult> {
description: `AI SDK tools for ${packageNameWithoutScope}`, description: `AI SDK tools for ${packageNameWithoutScope}`,
author: '', author: '',
license: 'MIT', license: 'MIT',
category: 'ai-ml', category: 'utilities',
}; };
const tools = [ const tools = [

View file

@ -1,6 +1,6 @@
{ {
"name": "@tpmjs/tools-unsandbox", "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.", "description": "Execute code in a secure sandbox environment. Supports 42+ programming languages with async execution, input files, and compiled artifacts.",
"type": "module", "type": "module",
"keywords": [ "keywords": [

View file

@ -2389,21 +2389,37 @@ export const deleteImage = tool({
// ============================================================================ // ============================================================================
export interface HealthResult { 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({ 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<Record<string, never>>({ inputSchema: jsonSchema<Record<string, never>>({
type: 'object', type: 'object',
properties: {}, properties: {},
additionalProperties: false, additionalProperties: false,
}), }),
async execute(): Promise<HealthResult> { async execute(): Promise<HealthResult> {
return apiRequest<HealthResult>('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,
};
}, },
}); });