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",
"version": "0.1.4",
"version": "0.1.5",
"description": "TPMJS command-line interface for AI tool discovery and execution",
"author": "TPMJS",
"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
*/
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> = {};
// 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 = {

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
- **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

View file

@ -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": {

View file

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

View file

@ -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": [

View file

@ -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<Record<string, never>>({
type: 'object',
properties: {},
additionalProperties: false,
}),
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,
};
},
});