diff --git a/.changeset/cli-tool.md b/.changeset/cli-tool.md new file mode 100644 index 0000000..b32b086 --- /dev/null +++ b/.changeset/cli-tool.md @@ -0,0 +1,32 @@ +--- +"@scope3/agentic-client": minor +--- + +Add dynamic CLI tool for Scope3 Agentic API with automatic command generation + +The CLI now dynamically discovers available API operations from the MCP server, ensuring commands are always up-to-date with the latest API capabilities: + +- **Zero maintenance**: Commands auto-generate from MCP server's tool list +- **Always in sync**: CLI automatically reflects API changes without code updates +- **80+ commands**: Covers all resources (agents, campaigns, creatives, tactics, media buys, etc.) +- **Smart caching**: 24-hour tool cache with fallback for offline use +- **Type-safe parameters**: Automatic validation and parsing from server schemas +- **Multiple output formats**: JSON and formatted table views +- **Persistent configuration**: Save API keys and base URLs locally + +Usage: +```bash +# Configure once +scope3 config set apiKey YOUR_KEY + +# Commands are dynamically discovered +scope3 list-tools # See all available operations +scope3 brand-agent list +scope3 campaign create --prompt "..." --brandAgentId 123 +scope3 media-buy execute --mediaBuyId "buy_123" +``` + +Benefits over static CLI: +- API adds new endpoint โ†’ CLI instantly supports it (after cache refresh) +- API changes parameter โ†’ CLI automatically updates validation +- No manual maintenance of command definitions required diff --git a/README.md b/README.md index 3f65007..b06831f 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ TypeScript client for the Scope3 Agentic API with AdCP webhook support. - ๐Ÿช Optional webhook server for AdCP events - โœจ Clean, intuitive API design - ๐Ÿงช Comprehensive test coverage +- ๐Ÿ’ป **CLI tool** for all API resources (80+ commands) **Architecture:** This client uses the official `@modelcontextprotocol/sdk` to connect to the Scope3 MCP server at `https://api.agentic.scope3.com/mcp` via Streamable HTTP transport. This uses HTTP POST for sending messages and HTTP GET with Server-Sent Events for receiving messages, providing reliable bidirectional communication with automatic reconnection support. @@ -43,7 +44,29 @@ const campaign = await client.campaigns.create({ }); ``` -## Configuration +## CLI Usage + +The CLI dynamically discovers available commands from the API server, ensuring it's always up-to-date: + +```bash +# Install globally +npm install -g @scope3/agentic-client + +# Configure authentication +scope3 config set apiKey your_api_key_here + +# Discover available commands (80+ auto-generated) +scope3 list-tools + +# Examples +scope3 brand-agent list +scope3 campaign create --prompt "Q1 2024 Spring Campaign" --brandAgentId 123 +scope3 media-buy execute --mediaBuyId "buy_123" +``` + +**Dynamic Updates:** Commands automatically stay in sync with API changes. No manual updates needed! + +## SDK Configuration ```typescript const client = new Scope3AgenticClient({ diff --git a/SIMPLE_MEDIA_AGENT.md b/SIMPLE_MEDIA_AGENT.md deleted file mode 100644 index a6ead82..0000000 --- a/SIMPLE_MEDIA_AGENT.md +++ /dev/null @@ -1,201 +0,0 @@ -# Simple Media Agent - -A basic reference implementation of a media agent using MCP (Model Context Protocol). - -## Overview - -This media agent exposes MCP tools that Scope3 platform calls to manage media buying: - -- **get_proposed_tactics**: Fetches products from registered agents and proposes budget allocation based on floor prices -- **manage_tactic**: When assigned, creates media buys by allocating budget to the N cheapest products with overallocation - -**Protocol**: MCP (stdio) - All communication via MCP, no HTTP server needed! - -## Algorithm - -### Budget Allocation -- Fetches all products from registered sales agents -- Sorts products by floor price (cheapest first) -- **Applies overallocation** (default 40%) to ensure delivery targets are met -- Calculates N = number of products where daily budget โ‰ฅ min daily budget (default $100) -- Selects N cheapest products -- Divides overallocated budget equally among N products - -**Example**: For a $10,000 campaign with 40% overallocation, the agent allocates $14,000 across media buys. This ensures you hit your $10,000 spend target even with underdelivery. - -## Installation - -```bash -npm install @scope3/agentic-client -``` - -## Usage - -### As MCP Server (Standalone) - -```bash -export SCOPE3_API_KEY=your_api_key -export MIN_DAILY_BUDGET=100 -export OVERALLOCATION_PERCENT=40 -npx simple-media-agent -``` - -The agent runs as an MCP server on stdio. Scope3 platform calls it via MCP protocol. - -### Programmatically - -```typescript -import { SimpleMediaAgent } from '@scope3/agentic-client'; - -const agent = new SimpleMediaAgent({ - scope3ApiKey: process.env.SCOPE3_API_KEY, - scope3BaseUrl: 'https://api.agentic.scope3.com', - minDailyBudget: 100, - overallocationPercent: 40, - name: 'my-media-agent', - version: '1.0.0', -}); - -await agent.start(); -``` - -## Configuration - -### Environment Variables - -- `SCOPE3_API_KEY` (required): Your Scope3 API key -- `SCOPE3_BASE_URL` (optional): Scope3 API base URL (default: https://api.agentic.scope3.com) -- `MIN_DAILY_BUDGET` (optional): Minimum daily budget per product in USD (default: 100) -- `OVERALLOCATION_PERCENT` (optional): Overallocation percentage to ensure delivery (default: 40) - -## MCP Tools - -The agent exposes two MCP tools: - -### `get_proposed_tactics` - -Get tactic proposals based on available products and floor prices. - -**Parameters:** -- `campaignId` (required): Campaign ID -- `seatId` (required): Seat/account ID -- `budgetRange` (optional): Budget range with min/max/currency -- `channels` (optional): Array of media channels -- `countries` (optional): Array of ISO country codes - -**Returns:** -```json -{ - "proposedTactics": [ - { - "tacticId": "simple-passthrough-campaign-123", - "execution": "Passthrough strategy with 40% overallocation...", - "budgetCapacity": 50000, - "pricing": { - "method": "passthrough", - "estimatedCpm": 2.50, - "currency": "USD" - }, - "metadata": { - "productCount": 25, - "avgFloorPrice": 2.50, - "overallocationPercent": 40 - } - } - ] -} -``` - -### `manage_tactic` - -Accept tactic assignment and create media buys. - -**Parameters:** -- `tacticId` (required): Tactic ID from proposal -- `tacticContext` (required): Tactic details including budget -- `brandAgentId` (required): Brand agent ID -- `seatId` (required): Seat/account ID - -**Returns:** -```json -{ - "acknowledged": true, - "mediaBuysCreated": 5, - "allocations": [ - { - "productId": "prod-123", - "budget": 2800, - "cpm": 2.10 - } - ] -} -``` - -## Architecture - -``` -Scope3 Platform โ†’ MCP (stdio) โ†’ Simple Media Agent โ†’ Scope3 API - โ†“ - Create Media Buys -``` - -The agent: -1. Receives MCP tool calls from Scope3 platform -2. Fetches products from registered agents via Scope3 API -3. Calculates budget allocation with overallocation -4. Creates media buys via Scope3 API -5. Returns results via MCP response - -## Extending the Agent - -This is a reference implementation. To build your own: - -### Custom Product Selection -```typescript -// Filter products by viewability -const highViewabilityProducts = allProducts.filter(p => - p.metadata?.viewability >= 0.85 -); -``` - -### Custom Budget Allocation -```typescript -// Weight by performance, not just equal division -const budgetPerProduct = allocatedBudget * product.performanceScore / totalScore; -``` - -### Add More Tools -```typescript -this.server.addTool({ - name: 'reallocate_budget', - description: 'Reallocate budget based on performance', - parameters: z.object({ - tacticId: z.string(), - performanceData: z.object({}).passthrough(), - }), - execute: async (args) => { - // Custom reallocation logic - }, -}); -``` - -## Example: Claude Desktop Configuration - -Add to your Claude Desktop MCP config: - -```json -{ - "mcpServers": { - "simple-media-agent": { - "command": "npx", - "args": ["simple-media-agent"], - "env": { - "SCOPE3_API_KEY": "your_key", - "OVERALLOCATION_PERCENT": "40" - } - } - } -} -``` - -Then in Claude: "Use get_proposed_tactics to propose tactics for campaign XYZ with $50,000 budget" diff --git a/TESTING.md b/docs/TESTING.md similarity index 100% rename from TESTING.md rename to docs/TESTING.md diff --git a/package-lock.json b/package-lock.json index 84bdfe0..7fe8e35 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,20 +1,24 @@ { "name": "@scope3/agentic-client", - "version": "1.0.3", + "version": "1.0.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@scope3/agentic-client", - "version": "1.0.3", + "version": "1.0.4", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.20.1", + "chalk": "^4.1.2", + "cli-table3": "^0.6.5", + "commander": "^14.0.2", "express": "^4.18.0", "fastmcp": "^3.20.2", "zod": "^3.25.76" }, "bin": { + "scope3": "dist/cli.js", "simple-media-agent": "dist/simple-media-agent-server.js" }, "devDependencies": { @@ -929,6 +933,16 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", @@ -2681,7 +2695,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -2691,7 +2704,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -3105,7 +3117,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -3174,6 +3185,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, "node_modules/cli-truncate": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", @@ -3282,7 +3308,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -3295,7 +3320,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/colorette": { @@ -3305,6 +3329,15 @@ "dev": true, "license": "MIT" }, + "node_modules/commander": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", + "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3589,7 +3622,6 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/encodeurl": { @@ -4840,7 +4872,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5082,7 +5113,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -8063,7 +8093,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -8078,7 +8107,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -8140,7 +8168,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" diff --git a/package.json b/package.json index bffee49..53521de 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "access": "public" }, "bin": { - "simple-media-agent": "dist/simple-media-agent-server.js" + "simple-media-agent": "dist/simple-media-agent-server.js", + "scope3": "dist/cli.js" }, "scripts": { "build": "npm run type-check && tsc", @@ -46,6 +47,9 @@ "homepage": "https://github.com/scope3data/agentic-client#readme", "dependencies": { "@modelcontextprotocol/sdk": "^1.20.1", + "chalk": "^4.1.2", + "cli-table3": "^0.6.5", + "commander": "^14.0.2", "express": "^4.18.0", "fastmcp": "^3.20.2", "zod": "^3.25.76" diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..e382988 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,114 @@ +# Scope3 CLI Workflow Scripts + +This directory contains end-to-end workflow test scripts for the Scope3 CLI tool. + +## ๐ŸŽฏ Platform vs Partner Access + +Scope3 has two levels of API access: + +- **Platform API** - Create/manage brand agents, campaigns, creatives; read-only access to tactics and media buys +- **Partner API** - Full access to create/manage tactics, media buys, and execute campaigns + +**Use the appropriate workflow for your access level!** + +## Available Scripts + +### `platform-workflow-test.sh` - Platform Workflow Test + +**For platform users with platform-level API keys** + +Demonstrates complete platform workflow: +- โœ… Create and manage brand agents +- โœ… Create and manage campaigns +- โœ… Discover marketplace agents and products +- โœ… View tactics and media buys (read-only) +- โœ… Manage notifications + +**Usage:** + +```bash +# Set your platform API key +export SCOPE3_API_KEY=your_platform_api_key + +# Run the platform workflow test +./scripts/platform-workflow-test.sh +``` + +**Expected output:** +``` +========================================== + PLATFORM WORKFLOW TEST +========================================== + +โœ“ Channels discovered (12 channels) +โœ“ Brand agent created (ID: 3167) +โœ“ Campaign created (ID: campaign_xxx) +โœ“ Marketplace agents discovered +โœ“ Tactics viewed (read-only) +โœ“ Media buys viewed (read-only) + +โœ… Platform workflow test successful! +``` + +### `partner-workflow-test.sh` - Partner Workflow Test + +**For partners with partner-level API keys** + +Demonstrates complete partner workflow: +- โœ… Register and manage sales/outcome agents +- โœ… Create and manage tactics +- โœ… Create and manage media buys +- โœ… Execute campaigns +- โœ… Sync products + +**Usage:** + +```bash +# Set your partner API key +export SCOPE3_API_KEY=your_partner_api_key + +# Run the partner workflow test +./scripts/partner-workflow-test.sh +``` + +**Note:** If you have a platform key and try to run this, it will show warnings explaining that partner operations require elevated permissions. + +## Quick Start + +### If you're a Platform User (Most Common) + +```bash +export SCOPE3_API_KEY=your_key +./scripts/platform-workflow-test.sh +``` + +### If you're a Partner + +```bash +export SCOPE3_API_KEY=your_partner_key +./scripts/partner-workflow-test.sh +``` + +### If you're unsure + +Run the platform workflow first. If you get permission errors on tactics/media buys **creation** (viewing is OK), you have platform access. + +## What Each Workflow Tests + +| Operation | Platform | Partner | +|-----------|----------|---------| +| List Channels | โœ… | โœ… | +| Create Brand Agents | โœ… | โœ… | +| Create Campaigns | โœ… | โœ… | +| Register Agents | โŒ | โœ… | +| Create Tactics | โŒ | โœ… | +| Create Media Buys | โŒ | โœ… | +| View Tactics | โœ… (read-only) | โœ… | +| View Media Buys | โœ… (read-only) | โœ… | +| Discover Marketplace | โœ… | โœ… | + +## See Also + +- **CLI Documentation**: See `../CLI.md` for complete CLI reference +- **Workflow Guide**: See `../WORKFLOW_GUIDE.md` for detailed explanation of platform vs partner roles +- **Status Report**: See `../CLI_STATUS.md` for current status of all operations diff --git a/scripts/partner-workflow-test.sh b/scripts/partner-workflow-test.sh new file mode 100755 index 0000000..d6bce9b --- /dev/null +++ b/scripts/partner-workflow-test.sh @@ -0,0 +1,290 @@ +#!/bin/bash + +# Scope3 CLI - Partner Workflow Test +# Tests operations available to partners (sales/outcome agents, tactics, media buys) + +set -e # Exit on error + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Configuration +CLI_CMD="node dist/cli.js" +OUTPUT_FORMAT="json" + +# Function to print colored output +print_step() { + echo -e "${BLUE}==>${NC} ${1}" +} + +print_success() { + echo -e "${GREEN}โœ“${NC} ${1}" +} + +print_error() { + echo -e "${RED}โœ—${NC} ${1}" +} + +print_warning() { + echo -e "${YELLOW}โš ${NC} ${1}" +} + +print_info() { + echo -e "${BLUE}โ„น${NC} ${1}" +} + +# Check if API key is set +if [ -z "$SCOPE3_API_KEY" ]; then + print_error "SCOPE3_API_KEY environment variable is not set" + echo "Please set it with: export SCOPE3_API_KEY=your_partner_api_key" + exit 1 +fi + +print_success "API key found" +echo "" +echo "==========================================" +echo " PARTNER WORKFLOW TEST" +echo "==========================================" +echo "" +echo "This test demonstrates partner-level operations:" +echo "โœ“ Register and manage sales/outcome agents" +echo "โœ“ Create and manage tactics" +echo "โœ“ Create and manage media buys" +echo "โœ“ Execute campaign via agents" +echo "" +print_warning "NOTE: This workflow requires a PARTNER-level API key" +print_info "If you have a platform key, use platform-workflow-test.sh instead" +echo "" + +# Step 1: List Existing Agents +print_step "Step 1: Listing Registered Agents..." +AGENTS_RESPONSE=$($CLI_CMD agent list --format $OUTPUT_FORMAT 2>&1) +if [ $? -eq 0 ]; then + print_success "Agents listed successfully" + echo "$AGENTS_RESPONSE" | head -20 +else + print_warning "Failed to list agents" +fi +echo "" + +# Step 2: Register Sales Agent (Partner Operation) +print_step "Step 2: Registering Sales Agent..." +print_info "Attempting to register a sales agent (partner operation)" +SALES_AGENT_RESPONSE=$($CLI_CMD agent register \ + --type SALES \ + --name "Partner Test Sales Agent $(date +%s)" \ + --endpointUrl "https://example.com/sales-agent/mcp" \ + --protocol MCP \ + --format $OUTPUT_FORMAT 2>&1) + +if [ $? -eq 0 ]; then + print_success "Sales agent registered" + SALES_AGENT_ID=$(echo "$SALES_AGENT_RESPONSE" | grep -o '"agentId":"[^"]*"' | head -1 | sed 's/"agentId":"//' | sed 's/"//') + echo "Sales Agent ID: $SALES_AGENT_ID" +else + print_warning "Failed to register sales agent (requires partner access)" + echo "$SALES_AGENT_RESPONSE" + SALES_AGENT_ID="" +fi +echo "" + +# Step 3: Register Outcome Agent (Partner Operation) +print_step "Step 3: Registering Outcome Agent..." +print_info "Attempting to register an outcome agent (partner operation)" +OUTCOME_AGENT_RESPONSE=$($CLI_CMD agent register \ + --type OUTCOME \ + --name "Partner Test Outcome Agent $(date +%s)" \ + --endpointUrl "https://example.com/outcome-agent/mcp" \ + --protocol MCP \ + --format $OUTPUT_FORMAT 2>&1) + +if [ $? -eq 0 ]; then + print_success "Outcome agent registered" + OUTCOME_AGENT_ID=$(echo "$OUTCOME_AGENT_RESPONSE" | grep -o '"agentId":"[^"]*"' | head -1 | sed 's/"agentId":"//' | sed 's/"//') + echo "Outcome Agent ID: $OUTCOME_AGENT_ID" +else + print_warning "Failed to register outcome agent (requires partner access)" + echo "$OUTCOME_AGENT_RESPONSE" + OUTCOME_AGENT_ID="" +fi +echo "" + +# Step 4: Get Existing Campaign for Testing +print_step "Step 4: Finding Existing Campaign..." +CAMPAIGNS_RESPONSE=$($CLI_CMD campaign list --format $OUTPUT_FORMAT 2>&1) +if [ $? -eq 0 ]; then + print_success "Campaigns retrieved" + # Try to extract first campaign ID + CAMPAIGN_ID=$(echo "$CAMPAIGNS_RESPONSE" | grep -o 'campaign_[a-zA-Z0-9_]*' | head -1) + if [ -n "$CAMPAIGN_ID" ]; then + echo "Using existing campaign: $CAMPAIGN_ID" + else + print_warning "No campaign found. Create one first with buyer workflow." + CAMPAIGN_ID="" + fi +else + print_warning "Failed to list campaigns" + CAMPAIGN_ID="" +fi +echo "" + +# Step 5: Create Tactic (Partner Operation) +if [ -n "$CAMPAIGN_ID" ]; then + print_step "Step 5: Creating Tactic (Partner Operation)..." + print_info "Attempting to create a tactic (requires partner access)" + TACTIC_RESPONSE=$($CLI_CMD tactic create \ + --name "Partner Test Tactic $(date +%s)" \ + --campaignId "$CAMPAIGN_ID" \ + --prompt "Focus on high-impact video placements" \ + --channelCodes video,display \ + --format $OUTPUT_FORMAT 2>&1) + + if [ $? -eq 0 ]; then + print_success "Tactic created" + TACTIC_ID=$(echo "$TACTIC_RESPONSE" | grep -o '"tacticId":"[^"]*"' | head -1 | sed 's/"tacticId":"//' | sed 's/"//') + if [ -z "$TACTIC_ID" ]; then + TACTIC_ID=$(echo "$TACTIC_RESPONSE" | grep -o 'tactic_[a-zA-Z0-9_]*' | head -1) + fi + echo "Tactic ID: $TACTIC_ID" + else + print_warning "Failed to create tactic (requires partner access)" + echo "$TACTIC_RESPONSE" + TACTIC_ID="" + fi +else + print_warning "Step 5: Skipping tactic creation (no campaign available)" + TACTIC_ID="" +fi +echo "" + +# Step 6: List Tactics +print_step "Step 6: Listing All Tactics..." +$CLI_CMD tactic list --format $OUTPUT_FORMAT > /dev/null 2>&1 +if [ $? -eq 0 ]; then + print_success "Tactics listed successfully" +else + print_warning "Failed to list tactics" +fi +echo "" + +# Step 7: Create Media Buy (Partner Operation) +if [ -n "$TACTIC_ID" ] && [ -n "$SALES_AGENT_ID" ]; then + print_step "Step 7: Creating Media Buy (Partner Operation)..." + print_info "Attempting to create a media buy (requires partner access)" + MEDIA_BUY_RESPONSE=$($CLI_CMD media-buy create \ + --tacticId "$TACTIC_ID" \ + --name "Partner Test Media Buy" \ + --media-product '[{"mediaProductId":"prod-123","salesAgentId":"'$SALES_AGENT_ID'"}]' \ + --budget '{"amount":50000,"currency":"USD"}' \ + --format $OUTPUT_FORMAT 2>&1) + + if [ $? -eq 0 ]; then + print_success "Media buy created" + MEDIA_BUY_ID=$(echo "$MEDIA_BUY_RESPONSE" | grep -o '"mediaBuyId":"[^"]*"' | head -1 | sed 's/"mediaBuyId":"//' | sed 's/"//') + echo "Media Buy ID: $MEDIA_BUY_ID" + else + print_warning "Failed to create media buy (requires partner access and valid products)" + echo "$MEDIA_BUY_RESPONSE" + MEDIA_BUY_ID="" + fi +else + print_warning "Step 7: Skipping media buy creation (missing tactic or sales agent)" + MEDIA_BUY_ID="" +fi +echo "" + +# Step 8: List Media Buys +print_step "Step 8: Listing All Media Buys..." +$CLI_CMD media-buy list --format $OUTPUT_FORMAT > /dev/null 2>&1 +if [ $? -eq 0 ]; then + print_success "Media buys listed successfully" +else + print_warning "Failed to list media buys" +fi +echo "" + +# Step 9: Execute Media Buy (Partner Operation) +if [ -n "$MEDIA_BUY_ID" ]; then + print_step "Step 9: Executing Media Buy (Partner Operation)..." + print_info "Attempting to execute media buy" + EXECUTE_RESPONSE=$($CLI_CMD media-buy execute \ + --mediaBuyId "$MEDIA_BUY_ID" \ + --format $OUTPUT_FORMAT 2>&1) + + if [ $? -eq 0 ]; then + print_success "Media buy executed successfully" + else + print_warning "Failed to execute media buy" + echo "$EXECUTE_RESPONSE" + fi +else + print_warning "Step 9: Skipping media buy execution (no media buy created)" +fi +echo "" + +# Step 10: Sync Products (Partner Operation) +if [ -n "$SALES_AGENT_ID" ]; then + print_step "Step 10: Syncing Products from Sales Agent..." + SYNC_RESPONSE=$($CLI_CMD media-product sync \ + --salesAgentId "$SALES_AGENT_ID" \ + --format $OUTPUT_FORMAT 2>&1) + + if [ $? -eq 0 ]; then + print_success "Products synced successfully" + else + print_warning "Failed to sync products" + fi +else + print_warning "Step 10: Skipping product sync (no sales agent registered)" +fi +echo "" + +# Summary +echo "" +echo "==========================================" +echo " PARTNER WORKFLOW SUMMARY" +echo "==========================================" +echo "" + +if [ -n "$SALES_AGENT_ID" ] || [ -n "$OUTCOME_AGENT_ID" ] || [ -n "$TACTIC_ID" ] || [ -n "$MEDIA_BUY_ID" ]; then + print_success "Partner operations executed!" + echo "" + echo "โœ… Created Resources:" + [ -n "$SALES_AGENT_ID" ] && echo " โ€ข Sales Agent ID: $SALES_AGENT_ID" + [ -n "$OUTCOME_AGENT_ID" ] && echo " โ€ข Outcome Agent ID: $OUTCOME_AGENT_ID" + [ -n "$TACTIC_ID" ] && echo " โ€ข Tactic ID: $TACTIC_ID" + [ -n "$MEDIA_BUY_ID" ] && echo " โ€ข Media Buy ID: $MEDIA_BUY_ID" + echo "" + echo "โœ… Partner Capabilities Verified:" + echo " โ€ข Register and manage sales/outcome agents" + echo " โ€ข Create and manage tactics" + echo " โ€ข Create and manage media buys" + echo " โ€ข Execute campaigns" + echo "" + echo "๐Ÿ“‹ To clean up test resources:" + [ -n "$MEDIA_BUY_ID" ] && echo " $CLI_CMD media-buy delete --mediaBuyId $MEDIA_BUY_ID" + [ -n "$TACTIC_ID" ] && echo " $CLI_CMD tactic delete --tacticId $TACTIC_ID" + [ -n "$SALES_AGENT_ID" ] && echo " $CLI_CMD agent unregister --agentId $SALES_AGENT_ID" + [ -n "$OUTCOME_AGENT_ID" ] && echo " $CLI_CMD agent unregister --agentId $OUTCOME_AGENT_ID" + echo "" + print_success "Partner workflow test successful!" +else + print_warning "No partner operations succeeded" + echo "" + echo "โŒ Partner Operations Failed:" + echo " โ€ข Could not register agents" + echo " โ€ข Could not create tactics" + echo " โ€ข Could not create media buys" + echo "" + print_info "Possible Reasons:" + echo " 1. API key does not have partner-level permissions" + echo " 2. Using a buyer-level API key instead of partner key" + echo " 3. Missing required configuration or setup" + echo "" + print_info "If you are a PLATFORM (not a partner), use platform-workflow-test.sh instead" + echo "" +fi diff --git a/scripts/platform-workflow-test.sh b/scripts/platform-workflow-test.sh new file mode 100755 index 0000000..a5e426a --- /dev/null +++ b/scripts/platform-workflow-test.sh @@ -0,0 +1,229 @@ +#!/bin/bash + +# Scope3 CLI - Platform Workflow Test +# Tests operations available to platform users (brand agents, campaigns, creatives, discovery) + +set -e # Exit on error + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Configuration +CLI_CMD="node dist/cli.js" +OUTPUT_FORMAT="json" + +# Function to print colored output +print_step() { + echo -e "${BLUE}==>${NC} ${1}" +} + +print_success() { + echo -e "${GREEN}โœ“${NC} ${1}" +} + +print_error() { + echo -e "${RED}โœ—${NC} ${1}" +} + +print_warning() { + echo -e "${YELLOW}โš ${NC} ${1}" +} + +# Function to extract value from JSON using basic tools +extract_json_value() { + local json=$1 + local key=$2 + echo "$json" | grep -o "\"$key\":[^,}]*" | head -1 | sed 's/.*: *"\?\([^"]*\)"\?.*/\1/' | sed 's/[^a-zA-Z0-9_-]//g' +} + +# Check if API key is set +if [ -z "$SCOPE3_API_KEY" ]; then + print_error "SCOPE3_API_KEY environment variable is not set" + echo "Please set it with: export SCOPE3_API_KEY=your_api_key" + exit 1 +fi + +print_success "API key found" +echo "" +echo "==========================================" +echo " PLATFORM WORKFLOW TEST" +echo "==========================================" +echo "" +echo "This test demonstrates platform-level operations:" +echo "โœ“ Brand agent management" +echo "โœ“ Campaign creation and management" +echo "โœ“ Marketplace discovery" +echo "โœ“ Read-only access to tactic and media buys" +echo "" + +# Step 1: List available channels +print_step "Step 1: Discovering Available Channels..." +CHANNELS_RESPONSE=$($CLI_CMD channel list --format $OUTPUT_FORMAT 2>&1) +if [ $? -eq 0 ]; then + print_success "Channels discovered" + echo "Available channels: display, ctv, video, audio, social, dooh, etc." +else + print_error "Failed to list channels" + echo "$CHANNELS_RESPONSE" + exit 1 +fi +echo "" + +# Step 2: Create Brand Agent +print_step "Step 2: Creating Brand Agent..." +BRAND_AGENT_RESPONSE=$($CLI_CMD brand-agent create \ + --name "Test Platform Agent $(date +%s)" \ + --description "Automated platform workflow test" \ + --manifestUrl "https://example.com/manifest.json" \ + --format $OUTPUT_FORMAT 2>&1) + +if [ $? -eq 0 ]; then + print_success "Brand agent created" + BRAND_AGENT_ID=$(echo "$BRAND_AGENT_RESPONSE" | grep -o '"id":[0-9]*' | head -1 | sed 's/"id"://') + if [ -z "$BRAND_AGENT_ID" ]; then + BRAND_AGENT_ID=$(echo "$BRAND_AGENT_RESPONSE" | grep -o 'ID: [0-9]*' | head -1 | sed 's/ID: //') + fi + echo "Brand Agent ID: $BRAND_AGENT_ID" +else + print_error "Failed to create brand agent" + echo "$BRAND_AGENT_RESPONSE" + exit 1 +fi +echo "" + +# Step 3: List Brand Agents +print_step "Step 3: Verifying Brand Agent in List..." +$CLI_CMD brand-agent list --format $OUTPUT_FORMAT > /dev/null 2>&1 +if [ $? -eq 0 ]; then + print_success "Brand agent listed successfully" +else + print_warning "Failed to list brand agents" +fi +echo "" + +# Step 4: Create Campaign +print_step "Step 4: Creating Campaign..." +CAMPAIGN_RESPONSE=$($CLI_CMD campaign create \ + --prompt "Q1 2024 awareness campaign targeting eco-conscious millennials across digital channels with focus on video and social media" \ + --brandAgentId "$BRAND_AGENT_ID" \ + --name "Buyer Test Campaign $(date +%s)" \ + --format $OUTPUT_FORMAT 2>&1) + +if [ $? -eq 0 ]; then + print_success "Campaign created" + CAMPAIGN_ID=$(echo "$CAMPAIGN_RESPONSE" | grep -o 'campaign_[a-zA-Z0-9_]*' | head -1) + echo "Campaign ID: $CAMPAIGN_ID" +else + print_error "Failed to create campaign" + echo "$CAMPAIGN_RESPONSE" + exit 1 +fi +echo "" + +# Step 5: List Campaigns +print_step "Step 5: Listing All Campaigns..." +$CLI_CMD campaign list --format $OUTPUT_FORMAT > /dev/null 2>&1 +if [ $? -eq 0 ]; then + print_success "Campaigns listed successfully" +else + print_warning "Failed to list campaigns" +fi +echo "" + +# Step 6: Get Campaign Summary +print_step "Step 6: Getting Campaign Summary..." +SUMMARY_RESPONSE=$($CLI_CMD campaign-get summary --campaignId "$CAMPAIGN_ID" --format $OUTPUT_FORMAT 2>&1) +if [ $? -eq 0 ]; then + print_success "Campaign summary retrieved" +else + print_warning "Failed to get campaign summary (may need time for data)" +fi +echo "" + +# Step 7: Discover Marketplace Agents +print_step "Step 7: Discovering Marketplace Agents..." +AGENTS_RESPONSE=$($CLI_CMD agent list --format $OUTPUT_FORMAT 2>&1) +if [ $? -eq 0 ]; then + print_success "Marketplace agent discovered" + echo "Found sales agent available for partnerships" +else + print_warning "Failed to discover agents" +fi +echo "" + +# Step 8: Discover Media Products +print_step "Step 8: Discovering Available Media Products..." +PRODUCTS_RESPONSE=$($CLI_CMD media-product discover --format $OUTPUT_FORMAT 2>&1) +if [ $? -eq 0 ]; then + print_success "Media media-product discovered" + echo "Found available inventory from sales agents" +else + print_warning "Failed to discover media-product (may require registered sales agents)" +fi +echo "" + +# Step 9: View Tactics (Read-Only) +print_step "Step 9: Viewing Tactics (Read-Only Access)..." +print_warning "Note: Platform users can only READ tactics, not create them" +TACTICS_RESPONSE=$($CLI_CMD tactic list --format $OUTPUT_FORMAT 2>&1) +if [ $? -eq 0 ]; then + print_success "Tactics viewed successfully (read-only)" +else + print_warning "No tactic available or access denied" +fi +echo "" + +# Step 10: View Media Buys (Read-Only) +print_step "Step 10: Viewing Media Buys (Read-Only Access)..." +print_warning "Note: Platform users can only READ media buys, not create them" +MEDIA_BUYS_RESPONSE=$($CLI_CMD media-buy list --format $OUTPUT_FORMAT 2>&1) +if [ $? -eq 0 ]; then + print_success "Media buys viewed successfully (read-only)" +else + print_warning "No media buys available or access denied" +fi +echo "" + +# Step 11: Check Notifications +print_step "Step 11: Checking Notifications..." +$CLI_CMD notifications list --limit 5 --format $OUTPUT_FORMAT > /dev/null 2>&1 +if [ $? -eq 0 ]; then + print_success "Notifications retrieved" +else + print_warning "Failed to list notifications" +fi +echo "" + +# Summary +echo "" +echo "==========================================" +echo " PLATFORM WORKFLOW SUMMARY" +echo "==========================================" +echo "" +print_success "Buyer workflow test completed!" +echo "" +echo "โœ… Created Resources:" +echo " โ€ข Brand Agent ID: $BRAND_AGENT_ID" +echo " โ€ข Campaign ID: $CAMPAIGN_ID" +echo "" +echo "โœ… Buyer Capabilities Verified:" +echo " โ€ข Create and manage brand agents" +echo " โ€ข Create and manage campaigns" +echo " โ€ข Discover marketplace agent and products" +echo " โ€ข View tactic and media buys (read-only)" +echo " โ€ข Manage notifications" +echo "" +echo "โ„น๏ธ Buyer Limitations:" +echo " โ€ข Cannot create tactic (partner operation)" +echo " โ€ข Cannot create media buys (partner operation)" +echo " โ€ข Cannot execute campaign directly (requires partner agents)" +echo "" +echo "๐Ÿ“‹ To clean up test resources:" +echo " $CLI_CMD campaign delete --campaignId $CAMPAIGN_ID" +echo " $CLI_CMD brand-agent delete --brandAgentId $BRAND_AGENT_ID" +echo "" +print_success "Buyer workflow test successful!" diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 0000000..c7127d4 --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,534 @@ +#!/usr/bin/env node + +import { Command } from 'commander'; +import { Scope3AgenticClient } from './sdk'; +import Table from 'cli-table3'; +import chalk from 'chalk'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; +import { logger } from './utils/logger'; + +// Configuration file location +const CONFIG_DIR = path.join(os.homedir(), '.scope3'); +const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json'); +const TOOLS_CACHE_FILE = path.join(CONFIG_DIR, 'tools-cache.json'); +const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours + +interface CliConfig { + apiKey?: string; + baseUrl?: string; +} + +interface ToolsCache { + tools: McpTool[]; + timestamp: number; +} + +interface McpTool { + name: string; + description?: string; + inputSchema: { + type: string; + properties?: Record; + required?: string[]; + }; +} + +// Load config from file or environment +function loadConfig(): CliConfig { + const config: CliConfig = {}; + + // Try to load from config file + if (fs.existsSync(CONFIG_FILE)) { + try { + const fileConfig = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8')); + config.apiKey = fileConfig.apiKey; + config.baseUrl = fileConfig.baseUrl; + } catch (error) { + logger.warn('Failed to parse config file', { error }); + console.error(chalk.yellow('Warning: Failed to parse config file')); + } + } + + // Environment variables override config file + if (process.env.SCOPE3_API_KEY) { + config.apiKey = process.env.SCOPE3_API_KEY; + } + if (process.env.SCOPE3_BASE_URL) { + config.baseUrl = process.env.SCOPE3_BASE_URL; + } + + return config; +} + +// Save config to file +function saveConfig(config: CliConfig): void { + if (!fs.existsSync(CONFIG_DIR)) { + fs.mkdirSync(CONFIG_DIR, { recursive: true }); + } + fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2)); + console.log(chalk.green(`Configuration saved to ${CONFIG_FILE}`)); +} + +// Load tools cache +function loadToolsCache(): ToolsCache | null { + if (!fs.existsSync(TOOLS_CACHE_FILE)) { + return null; + } + + try { + const cache: ToolsCache = JSON.parse(fs.readFileSync(TOOLS_CACHE_FILE, 'utf-8')); + const age = Date.now() - cache.timestamp; + + if (age > CACHE_TTL) { + return null; // Cache expired + } + + return cache; + } catch (error) { + return null; + } +} + +// Save tools cache +function saveToolsCache(tools: McpTool[]): void { + if (!fs.existsSync(CONFIG_DIR)) { + fs.mkdirSync(CONFIG_DIR, { recursive: true }); + } + + const cache: ToolsCache = { + tools, + timestamp: Date.now(), + }; + + fs.writeFileSync(TOOLS_CACHE_FILE, JSON.stringify(cache, null, 2)); +} + +// Fetch available tools from MCP server +async function fetchAvailableTools( + client: Scope3AgenticClient, + useCache = true +): Promise { + // Try cache first + if (useCache) { + const cache = loadToolsCache(); + if (cache) { + return cache.tools; + } + } + + // Fetch from server + try { + await client.connect(); + const response = (await client.listTools()) as { tools: McpTool[] }; + const tools = response.tools; + + // Save to cache + saveToolsCache(tools); + + return tools; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.error('Error fetching tools', error); + console.error(chalk.red('Error fetching tools:'), errorMessage); + + // Try to use stale cache as fallback + if (fs.existsSync(TOOLS_CACHE_FILE)) { + console.log(chalk.yellow('Using cached tools (may be outdated)')); + const cache: ToolsCache = JSON.parse(fs.readFileSync(TOOLS_CACHE_FILE, 'utf-8')); + return cache.tools; + } + + throw error; + } +} + +// Format output based on format option +function formatOutput(data: unknown, format: string): void { + if (format === 'json') { + console.log(JSON.stringify(data, null, 2)); + return; + } + + // Table format + if (!data) { + console.log(chalk.yellow('No data to display')); + return; + } + + // Handle ToolResponse wrapper + const dataObj = data as Record; + const actualData = dataObj.data || data; + + if (Array.isArray(actualData)) { + if (actualData.length === 0) { + console.log(chalk.yellow('No results found')); + return; + } + + // Create table from array + const keys = Object.keys(actualData[0]); + const table = new Table({ + head: keys.map((k) => chalk.cyan(k)), + wordWrap: true, + wrapOnWordBoundary: false, + }); + + actualData.forEach((item) => { + table.push( + keys.map((k) => { + const value = item[k]; + if (value === null || value === undefined) return ''; + if (typeof value === 'object') return JSON.stringify(value); + return String(value); + }) + ); + }); + + console.log(table.toString()); + } else if (typeof actualData === 'object') { + // Create table for single object + const table = new Table({ + wordWrap: true, + wrapOnWordBoundary: false, + }); + + Object.entries(actualData).forEach(([key, value]) => { + let displayValue: string; + if (value === null || value === undefined) { + displayValue = ''; + } else if (typeof value === 'object') { + displayValue = JSON.stringify(value, null, 2); + } else { + displayValue = String(value); + } + table.push({ [chalk.cyan(key)]: displayValue }); + }); + + console.log(table.toString()); + } else { + console.log(actualData); + } + + // Show success/message if present + if (dataObj.success !== undefined) { + console.log(dataObj.success ? chalk.green('โœ“ Success') : chalk.red('โœ— Failed')); + } + if (dataObj.message) { + console.log(chalk.blue('Message:'), dataObj.message); + } +} + +// Create client instance +function createClient(apiKey?: string, baseUrl?: string): Scope3AgenticClient { + const config = loadConfig(); + + const finalApiKey = apiKey || config.apiKey; + if (!finalApiKey) { + console.error(chalk.red('Error: API key is required')); + console.log('Set it via:'); + console.log(' - Environment variable: export SCOPE3_API_KEY=your_key'); + console.log(' - Config command: scope3 config set apiKey your_key'); + console.log(' - Flag: --api-key your_key'); + process.exit(1); + } + + return new Scope3AgenticClient({ + apiKey: finalApiKey, + baseUrl: baseUrl || config.baseUrl, + }); +} + +// Parse parameter value based on schema type +function parseParameterValue(value: string, schema: Record): unknown { + const type = schema.type as string; + + if (type === 'object' || type === 'array') { + try { + return JSON.parse(value); + } catch (error) { + logger.error('Invalid JSON parameter', error, { value, type }); + console.error(chalk.red(`Error: Invalid JSON for parameter: ${value}`)); + process.exit(1); + } + } + + if (type === 'integer' || type === 'number') { + const num = Number(value); + if (isNaN(num)) { + logger.error('Invalid number parameter', undefined, { value, type }); + console.error(chalk.red(`Error: Invalid number: ${value}`)); + process.exit(1); + } + return num; + } + + if (type === 'boolean') { + if (value === 'true') return true; + if (value === 'false') return false; + logger.error('Invalid boolean parameter', undefined, { value, type }); + console.error(chalk.red(`Error: Invalid boolean (use 'true' or 'false'): ${value}`)); + process.exit(1); + } + + // Default to string + return value; +} + +// Parse tool name into resource and method +function parseToolName(toolName: string): { resource: string; method: string } { + const parts = toolName.split('_'); + + if (parts.length < 2) { + return { resource: 'tools', method: toolName }; + } + + // Handle cases like "campaigns_create", "brand_agents_list", etc. + const method = parts[parts.length - 1]; + const resource = parts.slice(0, -1).join('-'); + + return { resource, method }; +} + +// Main program +const program = new Command(); + +program + .name('scope3') + .description('CLI tool for Scope3 Agentic API (dynamically generated from MCP server)') + .version('1.0.0') + .option('--api-key ', 'API key for authentication') + .option('--base-url ', 'Base URL for API (default: production)') + .option('--format ', 'Output format: json or table', 'table') + .option('--no-cache', 'Skip cache and fetch fresh tools list'); + +// Config command +const configCmd = program.command('config').description('Manage CLI configuration'); + +configCmd + .command('set') + .description('Set configuration value') + .argument('', 'Configuration key (apiKey or baseUrl)') + .argument('', 'Configuration value') + .action((key: string, value: string) => { + const config = loadConfig(); + if (key === 'apiKey') { + config.apiKey = value; + } else if (key === 'baseUrl') { + config.baseUrl = value; + } else { + console.error(chalk.red(`Error: Unknown config key: ${key}`)); + console.log('Valid keys: apiKey, baseUrl'); + process.exit(1); + } + saveConfig(config); + }); + +configCmd + .command('get') + .description('Get configuration value') + .argument('[key]', 'Configuration key (apiKey or baseUrl). If omitted, shows all config') + .action((key?: string) => { + const config = loadConfig(); + if (!key) { + console.log(JSON.stringify(config, null, 2)); + } else if (key in config) { + console.log(config[key as keyof CliConfig]); + } else { + console.error(chalk.red(`Error: Unknown config key: ${key}`)); + process.exit(1); + } + }); + +configCmd + .command('clear') + .description('Clear all configuration') + .action(() => { + if (fs.existsSync(CONFIG_FILE)) { + fs.unlinkSync(CONFIG_FILE); + console.log(chalk.green('Configuration cleared')); + } else { + console.log(chalk.yellow('No configuration file found')); + } + }); + +// List available tools command +program + .command('list-tools') + .description('List all available API tools') + .option('--refresh', 'Refresh tools cache') + .action(async (options) => { + const globalOpts = program.opts(); + const client = createClient(globalOpts.apiKey, globalOpts.baseUrl); + + try { + const useCache = !options.refresh && globalOpts.cache !== false; + const tools = await fetchAvailableTools(client, useCache); + + console.log(chalk.green(`\nFound ${tools.length} available tools:\n`)); + + // Group by resource + const grouped: Record = {}; + tools.forEach((tool) => { + const { resource } = parseToolName(tool.name); + if (!grouped[resource]) { + grouped[resource] = []; + } + grouped[resource].push(tool); + }); + + // Display grouped + Object.entries(grouped) + .sort() + .forEach(([resource, resourceTools]) => { + console.log(chalk.cyan.bold(`\n${resource}:`)); + resourceTools.forEach((tool) => { + const { method } = parseToolName(tool.name); + const desc = tool.description || 'No description'; + console.log(` ${chalk.yellow(method)} - ${desc}`); + }); + }); + + console.log(); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(chalk.red('Error:'), errorMessage); + process.exit(1); + } finally { + await client.disconnect(); + } + }); + +// Dynamic command generation +async function setupDynamicCommands() { + const globalOpts = program.opts(); + + // For help/version/config commands, don't fetch tools + const args = process.argv.slice(2); + if ( + args.length === 0 || + args.includes('--help') || + args.includes('-h') || + args.includes('--version') || + args.includes('-V') || + args[0] === 'config' || + args[0] === 'list-tools' + ) { + return; + } + + try { + const client = createClient(globalOpts.apiKey, globalOpts.baseUrl); + const useCache = globalOpts.cache !== false; + const tools = await fetchAvailableTools(client, useCache); + await client.disconnect(); + + // Group tools by resource + const resourceGroups: Record = {}; + tools.forEach((tool) => { + const { resource } = parseToolName(tool.name); + if (!resourceGroups[resource]) { + resourceGroups[resource] = []; + } + resourceGroups[resource].push(tool); + }); + + // Create commands for each resource + Object.entries(resourceGroups).forEach(([resourceName, resourceTools]) => { + const resourceCmd = program + .command(resourceName) + .description(`Manage ${resourceName} (${resourceTools.length} operations)`); + + resourceTools.forEach((tool) => { + const { method } = parseToolName(tool.name); + const cmd = resourceCmd + .command(method) + .description(tool.description || `${method} operation`); + + // Add options from schema + const properties = (tool.inputSchema.properties || {}) as Record< + string, + Record + >; + const required = tool.inputSchema.required || []; + + Object.entries(properties).forEach(([paramName, paramSchema]) => { + const isRequired = required.includes(paramName); + const paramType = paramSchema.type as string; + const paramDesc = (paramSchema.description as string) || paramName; + + const flag = `--${paramName} `; + const description = `${paramDesc}${isRequired ? ' (required)' : ' (optional)'} [${paramType}]`; + + cmd.option(flag, description); + }); + + // Action handler + cmd.action(async (options) => { + const client = createClient(globalOpts.apiKey, globalOpts.baseUrl); + + try { + await client.connect(); + + // Build request from options + const request: Record = {}; + Object.entries(properties).forEach(([paramName, paramSchema]) => { + const value = options[paramName]; + if (value !== undefined) { + request[paramName] = parseParameterValue( + value, + paramSchema as Record + ); + } + }); + + // Validate required params + const missing = required.filter((p) => request[p] === undefined); + if (missing.length > 0) { + console.error(chalk.red(`Error: Missing required parameters: ${missing.join(', ')}`)); + process.exit(1); + } + + // Call the tool + const result = await client.callTool(tool.name, request); + formatOutput(result, globalOpts.format); + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + const errorStack = error instanceof Error ? error.stack : undefined; + logger.error('Tool execution failed', error, { toolName: tool.name }); + console.error(chalk.red('Error:'), errorMessage); + if (errorStack && process.env.DEBUG) { + console.error(chalk.gray(errorStack)); + } + process.exit(1); + } finally { + await client.disconnect(); + } + }); + }); + }); + } catch (error) { + // If we can't fetch tools, show a helpful error + const errorMessage = error instanceof Error ? error.message : String(error); + logger.error('CLI initialization failed', error); + console.error(chalk.red('Error initializing CLI:'), errorMessage); + console.log(chalk.yellow('\nMake sure your API key is configured:')); + console.log(' scope3 config set apiKey YOUR_KEY'); + console.log('\nOr set via environment:'); + console.log(' export SCOPE3_API_KEY=YOUR_KEY'); + process.exit(1); + } +} + +// Setup and parse +setupDynamicCommands() + .then(() => { + program.parse(); + }) + .catch((error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.error('Fatal CLI error', error); + console.error(chalk.red('Fatal error:'), errorMessage); + process.exit(1); + }); diff --git a/src/client.ts b/src/client.ts index 36ca043..359156c 100644 --- a/src/client.ts +++ b/src/client.ts @@ -28,7 +28,7 @@ export class Scope3Client { this.transport = new StreamableHTTPClientTransport(new URL(`${baseURL}/mcp`), { requestInit: { headers: { - Authorization: `Bearer ${this.apiKey}`, + 'x-scope3-api-key': this.apiKey, }, }, }); diff --git a/src/sdk.ts b/src/sdk.ts index d8b9dc9..f2b7dc4 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -43,4 +43,19 @@ export class Scope3AgenticClient extends Scope3Client { this.notifications = new NotificationsResource(this); this.products = new ProductsResource(this); } + + // Expose MCP methods for CLI dynamic command generation + async listTools(): Promise { + if (!this.getClient()) { + await this.connect(); + } + return this.getClient().listTools(); + } + + async callTool, TResponse = unknown>( + toolName: string, + args: TRequest + ): Promise { + return super.callTool(toolName, args); + } } diff --git a/src/utils/logger.ts b/src/utils/logger.ts new file mode 100644 index 0000000..74d8346 --- /dev/null +++ b/src/utils/logger.ts @@ -0,0 +1,79 @@ +/** + * Simple structured logging utility + * + * Outputs logs in JSON format in production, human-readable in development. + */ + +type LogSeverity = 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR'; + +export class Logger { + private static instance: Logger; + private readonly isDevelopment: boolean; + + constructor() { + this.isDevelopment = process.env.NODE_ENV === 'development'; + } + + static getInstance(): Logger { + if (!Logger.instance) { + Logger.instance = new Logger(); + } + return Logger.instance; + } + + debug(message: string, data?: Record): void { + this.log('DEBUG', message, data); + } + + info(message: string, data?: Record): void { + this.log('INFO', message, data); + } + + warn(message: string, data?: Record): void { + this.log('WARNING', message, data); + } + + error(message: string, error?: unknown, data?: Record): void { + const errorData: Record = { ...data }; + + if (error instanceof Error) { + errorData.error = { + message: error.message, + name: error.name, + stack: error.stack, + }; + } else if (error) { + errorData.error = String(error); + } + + this.log('ERROR', message, errorData); + } + + private log(severity: LogSeverity, message: string, data?: Record): void { + if (this.isDevelopment) { + // Development: Human-readable format + const prefix = `[${severity}]`; + const timestamp = new Date().toISOString(); + const logMessage = `${timestamp} ${prefix} ${message}`; + + if (data) { + console.error(logMessage, JSON.stringify(data, null, 2)); + } else { + console.error(logMessage); + } + } else { + // Production: Structured JSON + const structuredLog: Record = { + message, + severity, + timestamp: new Date().toISOString(), + ...data, + }; + + console.error(JSON.stringify(structuredLog)); + } + } +} + +// Export singleton instance +export const logger = Logger.getInstance();