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:
parent
16c3b0df10
commit
f0b55a23f9
8 changed files with 308 additions and 27 deletions
|
|
@ -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",
|
||||
|
|
|
|||
190
packages/cli/src/commands/collection/info.ts
Normal file
190
packages/cli/src/commands/collection/info.ts
Normal 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) + '...';
|
||||
}
|
||||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue