From a47ee8b517345bc19d1b21ba7bb2ac351520acba Mon Sep 17 00:00:00 2001 From: Kostiantyn Pshenychnyi Date: Wed, 22 Apr 2026 11:29:39 +0300 Subject: [PATCH 01/13] feat(skills): add codemie-sdk skill --- .../claude/plugin/skills/codemie-sdk/SKILL.md | 170 ++++++++ .../skills/codemie-sdk/examples/assistants.md | 256 ++++++++++++ .../codemie-sdk/examples/datasources.md | 309 ++++++++++++++ .../codemie-sdk/examples/integrations.md | 149 +++++++ .../skills/codemie-sdk/examples/workflows.md | 126 ++++++ src/cli/commands/sdk/assistants.ts | 300 ++++++++++++++ src/cli/commands/sdk/datasources.ts | 392 ++++++++++++++++++ src/cli/commands/sdk/index.ts | 22 + src/cli/commands/sdk/integrations.ts | 315 ++++++++++++++ src/cli/commands/sdk/llm.ts | 72 ++++ src/cli/commands/sdk/services/assistants.ts | 100 +++++ src/cli/commands/sdk/services/datasources.ts | 306 ++++++++++++++ src/cli/commands/sdk/services/index.ts | 5 + src/cli/commands/sdk/services/integrations.ts | 106 +++++ src/cli/commands/sdk/services/llm.ts | 13 + src/cli/commands/sdk/services/workflows.ts | 68 +++ src/cli/commands/sdk/utils/cli-utils.ts | 141 +++++++ .../commands/sdk/utils/datasource-types.ts | 74 ++++ src/cli/commands/sdk/utils/file-utils.ts | 25 ++ src/cli/commands/sdk/utils/render.ts | 190 +++++++++ src/cli/commands/sdk/workflows.ts | 250 +++++++++++ src/cli/index.ts | 2 + 22 files changed, 3391 insertions(+) create mode 100644 src/agents/plugins/claude/plugin/skills/codemie-sdk/SKILL.md create mode 100644 src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/assistants.md create mode 100644 src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/datasources.md create mode 100644 src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/integrations.md create mode 100644 src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/workflows.md create mode 100644 src/cli/commands/sdk/assistants.ts create mode 100644 src/cli/commands/sdk/datasources.ts create mode 100644 src/cli/commands/sdk/index.ts create mode 100644 src/cli/commands/sdk/integrations.ts create mode 100644 src/cli/commands/sdk/llm.ts create mode 100644 src/cli/commands/sdk/services/assistants.ts create mode 100644 src/cli/commands/sdk/services/datasources.ts create mode 100644 src/cli/commands/sdk/services/index.ts create mode 100644 src/cli/commands/sdk/services/integrations.ts create mode 100644 src/cli/commands/sdk/services/llm.ts create mode 100644 src/cli/commands/sdk/services/workflows.ts create mode 100644 src/cli/commands/sdk/utils/cli-utils.ts create mode 100644 src/cli/commands/sdk/utils/datasource-types.ts create mode 100644 src/cli/commands/sdk/utils/file-utils.ts create mode 100644 src/cli/commands/sdk/utils/render.ts create mode 100644 src/cli/commands/sdk/workflows.ts diff --git a/src/agents/plugins/claude/plugin/skills/codemie-sdk/SKILL.md b/src/agents/plugins/claude/plugin/skills/codemie-sdk/SKILL.md new file mode 100644 index 00000000..2c5ad060 --- /dev/null +++ b/src/agents/plugins/claude/plugin/skills/codemie-sdk/SKILL.md @@ -0,0 +1,170 @@ +--- +name: codemie-sdk +description: >- + Manage CodeMie platform assets (assistants, workflows, datasources, integrations) directly from CLI + using CodeMie SDK. Use when user says "create assistant", "list workflows", "update datasource", + "delete assistant", "show my assistants", "get workflow details", "manage integrations", + "create integration", "list integrations", "list llm models", "list embedding models", + or any request to manage CodeMie platform resources. +--- + +# CodeMie SDK Asset Management + +Manage CodeMie platform assets from the CLI. + +**Asset Types:** `assistants`, `workflows`, `datasources`, `integrations` + +**Operations:** `list`, `get`, `create`, `update`, `delete` + +--- + +## 🚨 Project Clarification (MANDATORY) + +**Before doing any work**, check if the user has specified a project. + +- All asset types use `project_name` except assistants which use `project`. +- If the project is **not specified** → **ask the user** before running any commands. +- If the project **is specified** → proceed directly. + +Example prompt: *"Which CodeMie project should I use for this operation?"* + +This applies to **all asset types**: assistants, workflows, datasources, and integrations. + +--- + +## 📖 Consult Examples Before Working on an Asset (MANDATORY) + +**Before creating, updating, or querying any asset**, read the corresponding example file for complete field references, schemas, and commands to fetch referenced assets. + +| Asset | Example file | +|-------|-------------| +| Assistants | [examples/assistants.md](examples/assistants.md) | +| Workflows | [examples/workflows.md](examples/workflows.md) | +| Datasources | [examples/datasources.md](examples/datasources.md) | +| Integrations | [examples/integrations.md](examples/integrations.md) | + +Do **not** guess field names or skip this step — all required/optional fields, nested schemas, and asset cross-reference commands are documented there. + +--- + +## Input / Output + +**Two ways to pass data:** +- Inline JSON: `--data '{"key":"value"}'` +- From file: `--json path/to/config.json` + +**IDs are UUIDs**, e.g. `bc1a4b75-955c-48a5-b26d-bf702c1fee5d` + +**Create does not return the new ID** in the output. After creating, use `list --search` to find the new asset's ID. + +**Update replaces non-primitive values in full** — arrays and objects are not merged with existing values; the value you provide replaces the entire field. To preserve existing entries, either do not provide the value at all or fetch the current state first (`get --json`), merge locally, then send the full updated value. + +--- + +## Assistants + +> See [examples/assistants.md](examples/assistants.md) for full field reference and examples. + +```bash +codemie sdk assistants list [--scope visible_to_user|marketplace] [--search ] [--projects ] [--page ] [--per-page ] [--full-response] [--json] +codemie sdk assistants get [--json] +codemie sdk assistants get-tools [--json] +codemie sdk assistants create --data '' | --json +codemie sdk assistants update --data '' | --json +codemie sdk assistants delete +``` + +**Required on create:** `name`, `project`, `system_prompt` + +**Important notes:** +- Use `context` (not `skill_ids`) to attach datasources. Get datasource IDs: `codemie sdk datasources list --json` +- Use `toolkits` to attach integrations. Get exact structure: `codemie sdk assistants get-tools --json` +- Use `base_name` from `codemie sdk llm list --json` when setting `llm_model_type` +- `skill_ids` holds built-in platform skills, not datasources + +**Responses:** `✓ Specified assistant saved` / `✓ Specified assistant updated` / `✓ Assistant deleted.` + +--- + +## Workflows + +> See [examples/workflows.md](examples/workflows.md) for full field reference and examples. + +```bash +codemie sdk workflows list [--search ] [--projects ] [--page ] [--per-page ] [--json] +codemie sdk workflows get [--json] +codemie sdk workflows create --data '' --config '' | --config path/to/config.yaml +codemie sdk workflows update --data '' [--config '' | --config path/to/config.yaml] +codemie sdk workflows delete +``` + +**Required on create:** `name`, `project`, `mode` (`"Sequential"`), `shared` (boolean), plus `--config` with YAML graph definition + +**Important notes:** +- `--config` is required on create and optional on update +- `mode` and `shared` are required on create; both are optional on update +- Reference assistant IDs in YAML: `codemie sdk assistants list --json` + +**Responses:** `✓ Workflow created successfully` / `✓ Workflow updated successfully` / `✓ Workflow deleted.` + +--- + +## Datasources + +> See [examples/datasources.md](examples/datasources.md) for full field reference and examples. + +Datasources use **type subcommands** for create/update: `confluence`, `jira`, `file`, `code`, `google`, `json`, `provider`, `summary`, `chunk-summary` + +```bash +codemie sdk datasources list [--search ] [--projects ] [--status ] [--datasource-types ] [--sort-key date|update_date] [--sort-order asc|desc] [--page ] [--per-page ] [--json] +codemie sdk datasources get [--json] +codemie sdk datasources create --data '' | --json +codemie sdk datasources update --data '' | --json +codemie sdk datasources delete +# file type only: --file ./doc.pdf (repeatable, max 10) +``` + +**Required on create (all types):** `name` (no spaces, use hyphens), `project_name`, plus type-specific required fields + +**Important notes:** +- `confluence` and `jira` require a pre-configured integration. Get integration IDs: `codemie sdk integrations list --json` +- `code` type triggers background indexing — response is `✓ Indexing of datasource has been started in the background` +- Update supports reindex control flags: `full_reindex`, `incremental_reindex`, `resume_indexing`, `skip_reindex` +- Status values: `completed`, `failed`, `fetching`, `in_progress` + +--- + +## Integrations + +> See [examples/integrations.md](examples/integrations.md) for full field reference and examples. + +```bash +codemie sdk integrations list [--setting-type user|project] [--search ] [--projects ] [--page ] [--per-page ] [--json] +codemie sdk integrations get [--setting-type user|project] [--json] +codemie sdk integrations get-by-alias [--setting-type user|project] [--json] +codemie sdk integrations create --data '' | --json +codemie sdk integrations update --data '' | --json +codemie sdk integrations delete [--setting-type user|project] +``` + +**Required on create:** `credential_type`, `project_name`, `credential_values` + +**Important notes:** +- `credential_values` **must include** `{"key":"alias","value":""}` matching the top-level `alias` field +- `--setting-type` defaults to `user`; use `project` for team-shared integrations +- Sensitive values are masked as `**********` in all output + +**Responses:** `✓ Specified credentials saved` / `✓ Specified credentials updated` / `✓ Integration deleted.` + +--- + +## LLM Models + +```bash +codemie sdk llm list [--json] +codemie sdk llm list --embeddings [--json] +``` + +Returns `LLMModel` objects. Key fields: `base_name`, `label`, `provider`, `default`, `enabled`. + +Use `base_name` when setting `llm_model_type` on an assistant or `embeddings_model`/`summarization_model` on a datasource. diff --git a/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/assistants.md b/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/assistants.md new file mode 100644 index 00000000..79e082a7 --- /dev/null +++ b/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/assistants.md @@ -0,0 +1,256 @@ +# Assistants Examples + +## List + +```bash +# Basic list +codemie sdk assistants list + +# Search by name +codemie sdk assistants list --search 'Code Review' + +# Filter by project +codemie sdk assistants list --projects MyProject + +# Marketplace assistants +codemie sdk assistants list --scope marketplace + +# JSON output with pagination +codemie sdk assistants list --page 0 --per-page 20 --json + +# Full assistant details in list +codemie sdk assistants list --full-response --json +``` + +**JSON output fields (list):** `id`, `name`, `slug`, `type`, `description`, `shared`, `is_global`, `categories`, `created_by` + +## Get + +```bash +codemie sdk assistants get bc1a4b75-955c-48a5-b26d-bf702c1fee5d +codemie sdk assistants get bc1a4b75-955c-48a5-b26d-bf702c1fee5d --json +``` + +**Additional fields in get:** `project`, `llm_model_type`, `system_prompt`, `temperature`, `toolkits`, `skill_ids` + +## Create + +```bash +# Minimal (required fields only) +codemie sdk assistants create --data '{"name":"My Assistant","project":"MyProject","system_prompt":"You are a helpful assistant."}' + +# Full example from file +codemie sdk assistants create --json assistant.json +``` + +**Field reference:** + +| Field | Required | Type | Description | +|-------|----------|------|-------------| +| `name` | ✅ | string | Display name of the assistant | +| `project` | ✅ | string | Project to create the assistant in | +| `system_prompt` | ✅ | string | Instruction text that defines the assistant's persona and behavior | +| `description` | — | string | Short description shown in list and marketplace views | +| `icon_url` | — | string | URL to an image used as the assistant's avatar | +| `shared` | — | boolean | `true` = visible to all project members; `false` = private (default) | +| `is_global` | — | boolean | `true` = available across all projects platform-wide | +| `is_react` | — | boolean | Use ReAct (reasoning + acting) agent pattern; requires a model with `react_agent: true` | +| `slug` | — | string | URL-friendly identifier for deep-linking to this assistant | +| `llm_model_type` | — | string | Model `base_name` — if omitted, the platform default is used | +| `temperature` | — | number | Sampling temperature 0.0–1.0; lower = more deterministic outputs | +| `top_p` | — | number | Nucleus sampling 0.0–1.0; use either `temperature` or `top_p`, not both | +| `conversation_starters` | — | string[] | Suggested prompts displayed to users as quick-start buttons | +| `context` | — | array | Datasources attached as knowledge — see schema below | +| `toolkits` | — | array | Integration toolkits (Git, Jira, etc.) — see schema below | +| `mcp_servers` | — | array | MCP server connections for additional tools — see schema below | +| `assistant_ids` | — | string[] | Sub-assistant IDs for orchestration (multi-agent workflows) | +| `prompt_variables` | — | array | Dynamic `{{variable}}` placeholders in the system prompt — see schema below | +| `categories` | — | string[] | Labels for marketplace classification (e.g. `["DevOps", "Code Review"]`) | +| `skill_ids` | — | string[] | Built-in platform skill IDs — **not** datasource IDs | +| `skip_integration_validation` | — | boolean | Skip credential validation when attaching toolkits (useful with test credentials) | + +**Get available models:** +```bash +codemie sdk llm list --json | jq -r '.[] | "\(.base_name) (\(.label))"' +``` + +**`context` entry schema** — attach datasources: +```json +{ "id": "", "context_type": "knowledge_base", "name": "" } +``` +Valid `context_type`: `"knowledge_base"` (file/confluence/jira/google), `"code"` (code repository). +```bash +# Get datasource IDs +codemie sdk datasources list --projects MyProject --json | jq -r '.[] | "\(.id) \(.name)"' +``` + +**`toolkits` entry schema** — get the exact structure from the platform: +```bash +codemie sdk assistants get-tools --json +``` +Then pick the desired toolkit object(s) and include them in your payload. Each entry looks like: +```json +{ + "toolkit": "Jira", + "label": "Jira", + "settings_config": false, + "is_external": false, + "tools": [ + { "name": "jira_get_issue", "settings_config": false }, + { "name": "jira_search", "settings_config": false } + ] +} +``` + +**`mcp_servers` entry schema:** +```json +{ + "name": "my-mcp-server", + "description": "Custom tool server", + "enabled": true, + "config": { + "url": "https://mcp.example.com", + "auth_token": "", + "env": {} + }, + "tools_tokens_size_limit": 4096 +} +``` + +**`assistant_ids`** — for multi-agent orchestration, reference other assistant IDs: +```bash +codemie sdk assistants list --projects MyProject --json | jq -r '.[] | "\(.id) \(.name)"' +``` + +**`prompt_variables` entry schema:** +```json +{ "key": "language", "description": "Programming language to focus on", "default_value": "TypeScript" } +``` +Reference in system prompt as `{{language}}`. + +**Full `assistant.json` example:** +```json +{ + "name": "Code Reviewer", + "project": "Engineering", + "description": "Reviews code for best practices and security", + "system_prompt": "You are a {{language}} code review assistant. Focus on {{focus_area}}.", + "shared": true, + "llm_model_type": "claude-3-7-sonnet", + "temperature": 0.3, + "conversation_starters": [ + "Review my latest PR", + "Check this function for security issues" + ], + "context": [ + { "id": "", "context_type": "knowledge_base", "name": "Engineering Docs" } + ], + "toolkits": [ + { + "toolkit": "Jira", + "label": "Jira", + "settings_config": false, + "is_external": false, + "tools": [ + { "name": "jira_get_issue", "settings_config": false }, + { "name": "jira_search", "settings_config": false } + ] + } + ], + "prompt_variables": [ + { "key": "language", "description": "Primary language", "default_value": "TypeScript" }, + { "key": "focus_area", "description": "Review focus", "default_value": "security and performance" } + ], + "categories": ["Code Review", "Engineering"] +} +``` + +Output: `✓ Specified assistant saved` — **no ID is returned**. Find the new ID via: +```bash +codemie sdk assistants list --search 'My Assistant' --json | jq -r '.[0].id' +``` + +## Update + +```bash +codemie sdk assistants update --data '{"name":"Updated Name","shared":true}' +codemie sdk assistants update --json updates.json +``` + +## Delete + +```bash +# Always verify before deleting +codemie sdk assistants get +codemie sdk assistants delete +``` + +## Linking a Datasource + +Datasources are attached via the `context` field — **not** `skill_ids`. Each entry requires `id`, `context_type`, and `name`. + +Valid `context_type` values: `"knowledge_base"` (file/confluence/jira datasources), `"code"` (code repository datasources). + +```bash +# Get the datasource ID first +codemie sdk datasources list --search 'my-docs' --json + +# Attach to assistant +codemie sdk assistants update --data '{ + "context": [ + { + "id": "", + "context_type": "knowledge_base", + "name": "" + } + ] +}' +``` + +> **Note:** `skill_ids` holds built-in platform skills, not datasources. Do not use it to attach datasources. + +## Linking a Toolkit (Integration) + +Use the `toolkits` array to attach integrations (Git, Jira, etc.) to an assistant. + +Known toolkit names: `"Git"`, `"Jira"`, `"Confluence"`, `"Access Management"`, `"Codebase Tools"`, `"Project Management"`, `"Research"`. + +```bash +# Attach Git toolkit +codemie sdk assistants update --data '{ + "toolkits": [ + { + "toolkit": "Git", + "label": "Git", + "settings_config": true, + "is_external": false, + "tools": [{ "name": "git_tools", "settings_config": false }] + } + ] +}' +``` + +If the integration credentials can't be validated (test/fake credentials), add `"skip_integration_validation": true` at the top level: + +```bash +codemie sdk assistants update --data '{ + "toolkits": [...], + "skip_integration_validation": true +}' +``` + +## Scripting + +```bash +# Create then immediately fetch the new ID +codemie sdk assistants create --data '{"name":"My Bot","project":"Eng","system_prompt":"You are helpful."}' +ID=$(codemie sdk assistants list --search 'My Bot' --json | jq -r '.[0].id') + +# Find assistant by name +codemie sdk assistants list --search 'My Bot' --json | jq -r '.[].id' + +# Update all assistants in a project to shared +codemie sdk assistants list --projects Engineering --json | jq -r '.[].id' | while read id; do + codemie sdk assistants update "$id" --data '{"shared":true}' +done +``` diff --git a/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/datasources.md b/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/datasources.md new file mode 100644 index 00000000..cf662c86 --- /dev/null +++ b/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/datasources.md @@ -0,0 +1,309 @@ +# Datasources Examples + +> **Important:** `create` and `update` require a **type subcommand**: `confluence`, `jira`, `file`, `code`, `google` +> +> **Name constraint:** must match `^[a-zA-Z0-9][\w-]*$` — no spaces, use hyphens (e.g. `my-wiki`, not `My Wiki`) + +## List + +```bash +# Basic list +codemie sdk datasources list + +# Filter +codemie sdk datasources list --search 'Wiki' --projects Documentation +codemie sdk datasources list --status completed +codemie sdk datasources list --datasource-types confluence,jira +codemie sdk datasources list --sort-key update_date --sort-order desc --json +``` + +**Status values:** `completed`, `failed`, `fetching`, `in_progress` + +**List columns:** ID, Name, Project, Type, Status + +## Get + +```bash +codemie sdk datasources get ebfe842a-07af-4a8e-8790-7213834068e9 +codemie sdk datasources get ebfe842a-07af-4a8e-8790-7213834068e9 --json +``` + +## Create + +**Base fields (all types):** + +| Field | Required | Description | +|-------|----------|-------------| +| `name` | ✅ | Must match `^[a-zA-Z0-9][\w-]*$` — no spaces, use hyphens | +| `project_name` | ✅ | Project to create the datasource in | +| `description` | — | Human-readable description of this datasource | +| `shared_with_project` | — | `true` = visible to all project members | +| `setting_id` | — | Integration ID to authenticate fetching — get IDs: `codemie sdk integrations list --json` | + +### Confluence + +> Requires a **Confluence integration** already configured in the project. +> Get the integration ID: `codemie sdk integrations list --setting-type project --json | jq -r '.[] | select(.credential_type=="Confluence") | "\(.id) \(.alias)"'` + +```bash +codemie sdk datasources create confluence --data '{ + "name": "company-wiki", + "project_name": "Documentation", + "cql": "space=TEAM AND type=page", + "description": "Company-wide wiki", + "shared_with_project": true +}' +``` + +**Confluence-specific fields:** + +| Field | Required | Description | +|-------|----------|-------------| +| `cql` | ✅ | Confluence Query Language — e.g. `space=TEAM`, `space=TEAM AND type=page` | +| `include_restricted_content` | — | Include pages that are restricted but accessible by the integration user | +| `include_archived_content` | — | Include archived/historic pages | +| `include_attachments` | — | Include file attachments from pages | +| `include_comments` | — | Include page comments in indexed content | +| `keep_markdown_format` | — | Preserve Markdown formatting in fetched content | +| `keep_newlines` | — | Preserve newlines (prevents content collapsing) | +| `max_pages` | — | Maximum number of pages to fetch (caps the index size) | +| `pages_per_request` | — | Batch size per API call — tune lower to avoid Confluence rate limits | + +**Full Confluence example:** +```json +{ + "name": "engineering-wiki", + "project_name": "Engineering", + "cql": "space=ENG AND type=page AND ancestor=12345", + "description": "Engineering team wiki pages", + "shared_with_project": true, + "include_attachments": false, + "include_comments": false, + "keep_markdown_format": true, + "max_pages": 500, + "pages_per_request": 25 +} +``` + +### Jira + +> Requires a **Jira integration** already configured in the project. +> Get the integration ID: `codemie sdk integrations list --setting-type project --json | jq -r '.[] | select(.credential_type=="Jira") | "\(.id) \(.alias)"'` + +```bash +codemie sdk datasources create jira --data '{ + "name": "support-tickets", + "project_name": "Support", + "jql": "project=SUP AND status != Done", + "description": "Open support tickets", + "shared_with_project": true +}' +``` + +**Jira-specific fields:** + +| Field | Required | Description | +|-------|----------|-------------| +| `jql` | ✅ | Jira Query Language — e.g. `project=SUP AND status=Open ORDER BY created DESC` | + +### File (local files) + +```bash +# One or more files (max 10), plus metadata +codemie sdk datasources create file --file ./doc1.pdf --file ./doc2.docx --data '{"name":"team-docs","project_name":"Engineering"}' + +# From metadata file +codemie sdk datasources create file --file ./report.pdf --json metadata.json +``` + +**`metadata.json`:** +```json +{ + "name": "project-docs", + "project_name": "Engineering", + "description": "Key project documents", + "shared_with_project": true +} +``` + +Supported file formats: PDF, DOCX, XLSX, TXT, MD, and other common document types. Max 10 files per create call. + +### Code Repository + +```bash +codemie sdk datasources create code --data '{ + "name": "main-repo", + "project_name": "Engineering", + "link": "https://github.com/org/repo", + "branch": "main", + "index_type": "code", + "description": "Main application codebase" +}' +``` + +Output: `✓ Indexing of datasource main-repo has been started in the background` + +**Code-specific fields:** + +| Field | Required | Description | +|-------|----------|-------------| +| `link` | ✅ | Repository HTTPS URL — e.g. `https://github.com/org/repo` | +| `branch` | ✅ | Branch to index — e.g. `main`, `develop` | +| `index_type` | ✅ | `"code"` = searchable code snippets; `"summary"` = LLM-generated summaries | +| `files_filter` | — | Glob pattern to limit indexed files — e.g. `src/**/*.ts`, `**/*.py` | +| `embeddings_model` | — | Embedding model `base_name` — get values: `codemie sdk llm list --embeddings --json` | +| `summarization_model` | — | LLM model `base_name` for summary generation — get values: `codemie sdk llm list --json` | +| `prompt` | — | Custom prompt template for summarization (overrides platform default) | +| `docs_generation` | — | `true` = auto-generate documentation from code during indexing | + +**Full Code example:** +```json +{ + "name": "backend-api", + "project_name": "Engineering", + "link": "https://github.com/org/backend", + "branch": "main", + "index_type": "code", + "description": "Backend API codebase", + "files_filter": "src/**/*.ts", + "docs_generation": true +} +``` + +### Google Docs + +```bash +codemie sdk datasources create google --data '{ + "name": "team-docs", + "project_name": "Product", + "google_doc": "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms", + "description": "Team documentation", + "shared_with_project": true +}' +``` + +**Google-specific fields:** + +| Field | Required | Description | +|-------|----------|-------------| +| `google_doc` | ✅ | Google Doc ID (from URL) or full Google Docs URL | + +## Update + +Same subcommands as create, but takes `` after the type: + +```bash +codemie sdk datasources update confluence --data '{"name":"updated-wiki","project_name":"Documentation","cql":"space=NEWTEAM"}' +codemie sdk datasources update jira --data '{"name":"support-tickets","project_name":"Support","jql":"project=SUP AND status=Open"}' +codemie sdk datasources update file --data '{"name":"team-docs","project_name":"Engineering","description":"Updated docs"}' +codemie sdk datasources update code --json updates.json +# Output: ✓ Incremental reindexing of datasource has been started in the background +``` + +**Update-only reindex flags** (add to any update payload): + +| Flag | Description | +|------|-------------| +| `full_reindex` | Discard existing index and re-fetch everything from scratch | +| `incremental_reindex` | Only fetch and index content changed since last run (default for most types) | +| `resume_indexing` | Resume a previously interrupted indexing job | +| `skip_reindex` | Update metadata (name, description, etc.) without triggering any reindex | + +```bash +# Rename without reindexing +codemie sdk datasources update confluence --data '{"name":"new-name","project_name":"Documentation","cql":"space=TEAM","skip_reindex":true}' + +# Force full re-fetch +codemie sdk datasources update code --data '{"name":"main-repo","project_name":"Engineering","link":"https://github.com/org/repo","branch":"main","index_type":"code","full_reindex":true}' +``` + +## Delete + +```bash +codemie sdk datasources get +codemie sdk datasources delete +``` + +## Linking to an Assistant + +After creating a datasource, attach it to an assistant via the assistant's `context` field. + +- Use `context_type: "knowledge_base"` for file, Confluence, Jira, and Google datasources. +- Use `context_type: "code"` for code repository datasources. + +```bash +# Get datasource ID +DS_ID=$(codemie sdk datasources list --search 'my-docs' --json | jq -r '.[0].id') +DS_NAME=$(codemie sdk datasources list --search 'my-docs' --json | jq -r '.[0].name') + +# Attach to assistant +codemie sdk assistants update --data "{ + \"context\": [{\"id\": \"$DS_ID\", \"context_type\": \"knowledge_base\", \"name\": \"$DS_NAME\"}] +}" +``` + +> See [Assistants — Linking a Datasource](assistants.md#linking-a-datasource) for full details. + +### JSON Knowledge Base + +```bash +codemie sdk datasources create json --data '{ + "name": "json-data", + "project_name": "Team", + "description": "JSON knowledge base", + "shared_with_project": true +}' +``` + +### Provider + +```bash +codemie sdk datasources create provider --data '{ + "name": "my-provider", + "project_name": "Team", + "description": "Provider datasource", + "shared_with_project": true +}' +``` + +### Summary + +```bash +codemie sdk datasources create summary --data '{ + "name": "my-summary", + "project_name": "Team", + "description": "Summary datasource", + "shared_with_project": true +}' +``` + +### Chunk Summary + +```bash +codemie sdk datasources create chunk-summary --data '{ + "name": "my-chunk-summary", + "project_name": "Team", + "description": "Chunk summary datasource", + "shared_with_project": true +}' +``` + +**Fields for json, provider, summary, chunk-summary (base fields only):** + +| Field | Required | Description | +|-------|----------|-------------| +| `name` | ✅ | Must match `^[a-zA-Z0-9][\w-]*$` — no spaces, use hyphens | +| `project_name` | ✅ | Project to create the datasource in | +| `description` | — | Human-readable description | +| `shared_with_project` | — | `true` = visible to all project members | +| `setting_id` | — | Integration ID for authentication | + +## Scripting + +```bash +# Check for failed datasources +codemie sdk datasources list --status failed --json | jq -r '.[] | "\(.name) (\(.project_name)): \(.error_message)"' + +# List by type +codemie sdk datasources list --datasource-types knowledge_base_confluence --json +``` diff --git a/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/integrations.md b/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/integrations.md new file mode 100644 index 00000000..5396909f --- /dev/null +++ b/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/integrations.md @@ -0,0 +1,149 @@ +# Integrations Examples + +## List + +```bash +# User-level integrations (default) +codemie sdk integrations list + +# Project-level integrations +codemie sdk integrations list --setting-type project + +# Search and filter +codemie sdk integrations list --search 'jira' +codemie sdk integrations list --projects Engineering +codemie sdk integrations list --page 0 --per-page 25 --json +``` + +**setting-type:** `user` (default) or `project` + +**List columns:** ID, Alias, Type, Project + +## Get + +```bash +# By ID +codemie sdk integrations get 6fcbb938-239c-40c4-b304-b1f3cec3d501 +codemie sdk integrations get 6fcbb938-239c-40c4-b304-b1f3cec3d501 --setting-type project --json + +# By alias (more convenient) +codemie sdk integrations get-by-alias jira-main +codemie sdk integrations get-by-alias jira-main --json +``` + +**JSON fields:** `id`, `alias`, `credential_type`, `project_name`, `setting_type`, `default`, `is_global`, `credential_values`, `date`, `update_date`, `user_id`, `created_by` + +> Note: Sensitive credential values are masked as `**********` in output. + +## Create + +```bash +# Inline JSON +codemie sdk integrations create --data '{ + "credential_type": "Jira", + "project_name": "Engineering", + "alias": "jira-main", + "setting_type": "user", + "credential_values": [ + {"key": "url", "value": "https://company.atlassian.net"}, + {"key": "token", "value": "your-api-token"}, + {"key": "username", "value": "bot@company.com"}, + {"key": "alias", "value": "jira-main"} + ] +}' + +# From file +codemie sdk integrations create --json jira-integration.json +``` + +**Field reference:** + +| Field | Required | Description | +|-------|----------|-------------| +| `credential_type` | ✅ | Integration type — e.g. `"Jira"`, `"Confluence"`, `"Git"`, `"LiteLLM"` | +| `project_name` | ✅ | Project to associate the integration with | +| `credential_values` | ✅ | Array of `{"key": "...", "value": "..."}` credential pairs — **must include `alias` key** | +| `setting_type` | — | `"user"` (default, personal) or `"project"` (team-shared) | +| `alias` | — | Human-readable identifier used with `get-by-alias` | +| `default` | — | `true` = mark as the default integration of this type for the project | +| `enabled` | — | `false` = disable the integration without deleting it (default: `true`) | +| `external_id` | — | External system identifier for cross-referencing with other tools | + +**Common `credential_type` values:** `Jira`, `Confluence`, `Git`, `AWS`, `GCP`, `Azure`, `Keycloak`, `Elastic`, `OpenAPI`, `Webhook`, `SQL`, `MCP`, `LiteLLM`, `AzureDevOps`, `ServiceNow`, `Telegram` + +> **Important:** `credential_values` **must include an `alias` key** with the same value as the top-level `alias` field, otherwise the API returns an error. Always add `{"key": "alias", "value": ""}` to the array. + +### Type examples + +**Confluence:** +```json +{ + "credential_type": "Confluence", + "project_name": "Documentation", + "alias": "confluence-main", + "setting_type": "user", + "credential_values": [ + {"key": "url", "value": "https://company.atlassian.net/wiki"}, + {"key": "token", "value": "api-token"}, + {"key": "username", "value": "admin@company.com"}, + {"key": "alias", "value": "confluence-main"} + ] +} +``` + +**Git:** +```json +{ + "credential_type": "Git", + "project_name": "Engineering", + "alias": "github-main", + "setting_type": "project", + "credential_values": [ + {"key": "url", "value": "https://github.com/org/repo"}, + {"key": "token", "value": "ghp_yourToken"}, + {"key": "alias", "value": "github-main"} + ] +} +``` + +**LiteLLM:** +```json +{ + "credential_type": "LiteLLM", + "project_name": "AI", + "alias": "litellm-proxy", + "setting_type": "user", + "credential_values": [ + {"key": "base_url", "value": "http://localhost:4000"}, + {"key": "api_key", "value": "sk-master-key"} + ] +} +``` + +Output: `✓ Specified credentials saved` + +## Update + +```bash +codemie sdk integrations update --data '{ + "credential_type": "Jira", + "project_name": "Engineering", + "alias": "jira-main", + "credential_values": [ + {"key": "url", "value": "https://company.atlassian.net"}, + {"key": "token", "value": "new-rotated-token"}, + {"key": "username", "value": "bot@company.com"} + ] +}' +``` + +Output: `✓ Specified credentials updated` + +## Delete + +```bash +codemie sdk integrations delete +codemie sdk integrations delete --setting-type project +``` + +Output: `✓ Integration deleted.` diff --git a/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/workflows.md b/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/workflows.md new file mode 100644 index 00000000..dccf27b5 --- /dev/null +++ b/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/workflows.md @@ -0,0 +1,126 @@ +# Workflows Examples + +## List + +```bash +# Basic list +codemie sdk workflows list + +# Search and filter +codemie sdk workflows list --search 'Pipeline' +codemie sdk workflows list --projects Engineering +codemie sdk workflows list --page 0 --per-page 25 --json +``` + +**List columns:** ID, Name, Project, Mode, Shared + +## Get + +```bash +codemie sdk workflows get 1d3d69bb-3a53-495b-b0e7-61826d10a947 +codemie sdk workflows get 1d3d69bb-3a53-495b-b0e7-61826d10a947 --json +``` + +**JSON fields:** `id`, `project`, `name`, `description`, `yaml_config`, `mode`, `shared`, `created_by`, `created_date`, `update_date` + +## Create + +Workflows require both a `--data` JSON payload (metadata) and a `--config` YAML (graph definition). + +```bash +# Minimal required fields +codemie sdk workflows create \ + --data '{"name":"My Workflow","project":"Engineering","mode":"Sequential","shared":true}' \ + --config path/to/workflow.yaml + +# All metadata inline +codemie sdk workflows create \ + --data '{"name":"My Workflow","project":"Engineering","mode":"Sequential","shared":true,"description":"Automates deployment","icon_url":"https://example.com/icon.png"}' \ + --config path/to/workflow.yaml + +# From JSON file + YAML config +codemie sdk workflows create --json workflow-meta.json --config path/to/workflow.yaml +``` + +**Field reference:** + +| Field | Required | Type | Description | +|-------|----------|------|-------------| +| `name` | ✅ | string | Display name of the workflow | +| `project` | ✅ | string | Project the workflow belongs to | +| `yaml_config` | ✅ (via `--config`) | string | Workflow graph definition — pass as `--config` file path or inline YAML string | +| `mode` | ✅ | string | Execution mode: `"Sequential"` (step-by-step) | +| `shared` | ✅ | boolean | `true` = visible to all project members; `false` = private | +| `description` | — | string | Short description of the workflow's purpose | +| `icon_url` | — | string | URL to an image used as the workflow's icon | + +> **`mode` values:** +> - `"Sequential"` — nodes execute in a defined order; each step waits for the previous + +**`workflow-meta.json` example:** +```json +{ + "name": "Data Pipeline", + "project": "Analytics", + "description": "Processes incoming data streams", + "mode": "Sequential", + "shared": true +} +``` + +**`workflow.yaml` example:** +```yaml +assistants: + - id: processor + assistant_id: + system_prompt: You are a data processing assistant +states: + - id: start + assistant_id: processor + next: + state_id: end +``` + +Get assistant IDs to reference in YAML: +```bash +codemie sdk assistants list --projects Engineering --json | jq -r '.[] | "\(.id) \(.name)"' +``` + +## Update + +```bash +# Update metadata only (no reconfig of graph) +codemie sdk workflows update --data '{"name":"Updated Pipeline","project":"Engineering","shared":false}' + +# Update metadata and graph definition +codemie sdk workflows update --json updates.json --config path/to/new-config.yaml +``` + +**Update field reference:** + +| Field | Required | Description | +|-------|----------|-------------| +| `name` | ✅ | Workflow display name | +| `project` | ✅ | Project the workflow belongs to | +| `yaml_config` | optional | New graph definition — only if changing the workflow structure | +| `mode` | optional | Change execution mode: `"Sequential"` | +| `shared` | optional | Change visibility | +| `description` | optional | Update description | +| `icon_url` | optional | Update icon URL | + +## Delete + +```bash +codemie sdk workflows get +codemie sdk workflows delete +``` + +## Scripting + +```bash +# Export workflow config +codemie sdk workflows get --json > workflow-backup.json + +# List workflows by project, get IDs +codemie sdk workflows list --projects DataPipeline --json | jq -r '.[].id' +``` diff --git a/src/cli/commands/sdk/assistants.ts b/src/cli/commands/sdk/assistants.ts new file mode 100644 index 00000000..3144897c --- /dev/null +++ b/src/cli/commands/sdk/assistants.ts @@ -0,0 +1,300 @@ +import { Command } from "commander"; +import chalk from "chalk"; +import ora from "ora"; +import type { + Assistant, + AssistantBase, + AssistantCreateParams, + AssistantUpdateParams, + ToolKitDetails, +} from "codemie-sdk"; +import { + listAssistants, + getAssistant, + createAssistant, + updateAssistant, + deleteAssistant, + getAssistantTools, +} from "./services/assistants.js"; +import { + getSdkClient, + parseDataOrJsonFile, + outputJson, + handleSdkError, + getResponseMessage, +} from "./utils/cli-utils.js"; +import { + printTable, + printDetail, + printEmpty, + printListHeader, + printSuccess, + printInfo, + optional, + yesNo, + type TableColumn, + type DetailRow, +} from "./utils/render.js"; + +export function createAssistantsSubcommand(): Command { + const cmd = new Command("assistants").description( + "Manage CodeMie assistants", + ); + + cmd + .command("list") + .description( + "List assistants visible to the current user\n" + + "Examples:\n" + + " $ codemie assistants list\n" + + " $ codemie assistants list --scope marketplace --page 2 --per-page 25\n" + + " $ codemie assistants list --search 'Notification sender' --project MyProject --json", + ) + .option("--json", "Output in JSON format") + .option( + "--scope ", + "Scope: 'visible_to_user' or 'marketplace'", + "visible_to_user", + ) + .option("--page ", "Page number (starts at 0)", "0") + .option("--per-page ", "Results per page (1-100)", "10") + .option("--search ", "Search by name or description") + .option("--projects ", "Filter by project name (comma-separated)") + .option("--full-response", "Include all assistant properties") + .action(async (opts) => { + const client = await getSdkClient(); + const spinner = ora("Fetching assistants...").start(); + + try { + const filters: Record = {}; + if (opts.search) { + filters.search = opts.search; + } + if (opts.projects) { + filters.project = opts.projects.trim().split(","); + } + + const items = await listAssistants(client, { + scope: opts.scope, + page: parseInt(opts.page, 10), + per_page: parseInt(opts.perPage, 10), + minimal_response: !opts.fullResponse, + filters: Object.keys(filters).length > 0 ? filters : undefined, + }); + + spinner.stop(); + + if (opts.json) { + outputJson(items); + return; + } + + if (items.length === 0) { + printEmpty("assistants"); + return; + } + + printListHeader("Assistants", items.length); + + const columns: TableColumn[] = [ + { header: "ID", width: 40, getValue: (a) => chalk.cyan(a.id) }, + { header: "Name", width: 28, getValue: (a) => a.name }, + ]; + printTable(items, columns); + } catch (error) { + spinner.stop(); + handleSdkError(error, "list assistants"); + } + }); + + cmd + .command("get ") + .description("Get detailed information about a specific assistant") + .option("--json", "Output in JSON format") + .action(async (id: string, opts) => { + const client = await getSdkClient(); + const spinner = ora("Fetching assistant...").start(); + + try { + const item = await getAssistant(client, id); + spinner.stop(); + + if (opts.json) { + outputJson(item); + return; + } + + const rows: DetailRow[] = [ + { label: "ID", value: chalk.cyan(item.id) }, + { label: "Name", value: item.name }, + { label: "Project", value: optional(item.project) }, + { label: "Description", value: optional(item.description) }, + { label: "Shared", value: yesNo(item.shared) }, + { label: "Global", value: yesNo(item.is_global) }, + { + label: "Creator", + value: optional(item.created_by?.name ?? item.creator), + }, + ]; + + if (item.llm_model_type) { + rows.push({ label: "Model", value: item.llm_model_type }); + } + + if (item.updated_date) { + rows.push({ label: "Updated", value: item.updated_date }); + } + + printDetail(rows); + } catch (error) { + spinner.stop(); + handleSdkError(error, "get assistant"); + } + }); + + cmd + .command("create") + .description( + "Create a new assistant with the specified configuration\n" + + "Examples:\n" + + ' $ codemie assistants create --data \'{"name":"My Assistant","description":"Helpful bot"}\'\n' + + ' $ codemie assistants create --json path/to/assistant.json\n', + ) + .option( + "--data ", + "Assistant configuration as inline JSON string", + ) + .option( + "--json ", + "Path to JSON file with assistant configuration", + ) + .action(async (opts) => { + const client = await getSdkClient(); + const spinner = ora("Creating assistant...").start(); + + try { + const data = await parseDataOrJsonFile(opts.data, opts.json); + const result = await createAssistant( + client, + data as AssistantCreateParams, + ); + spinner.stop(); + + printSuccess(getResponseMessage(result)); + if (result.assistant_id) { + printInfo(`ID: ${result.assistant_id}`); + } + } catch (error) { + spinner.stop(); + handleSdkError(error, "create assistant"); + } + }); + + cmd + .command("update ") + .description( + "Update an existing assistant's configuration\n" + + "Examples:\n" + + ' $ codemie assistants update ast_abc123 --data \'{"name":"Updated Name"}\'\n' + + ' $ codemie assistants update ast_abc123 --json path/to/update.json\n', + ) + .option( + "--data ", + "Fields to update as inline JSON string", + ) + .option( + "--json ", + "Path to JSON file with fields to update", + ) + .action(async (id: string, opts) => { + const client = await getSdkClient(); + const spinner = ora("Updating assistant...").start(); + try { + const data = await parseDataOrJsonFile(opts.data, opts.json); + const result = await updateAssistant( + client, + id, + data as AssistantUpdateParams, + ); + spinner.stop(); + + printSuccess(getResponseMessage(result)); + } catch (error) { + spinner.stop(); + handleSdkError(error, "update assistant"); + } + }); + + cmd + .command("delete ") + .description("Permanently delete an assistant") + .action(async (id: string) => { + const client = await getSdkClient(); + const spinner = ora("Deleting assistant...").start(); + try { + await deleteAssistant(client, id); + spinner.stop(); + printSuccess(`✓ Assistant ${chalk.cyan(id)} deleted.`); + } catch (error) { + spinner.stop(); + handleSdkError(error, "delete assistant"); + } + }); + + cmd + .command("get-tools") + .description( + "List available toolkits that can be assigned to an assistant\n" + + "Use toolkit names from this list in create/update --data toolkits field.\n" + + "Examples:\n" + + " $ codemie sdk assistants get-tools\n" + + " $ codemie sdk assistants get-tools --json", + ) + .option("--json", "Output in JSON format") + .action(async (opts) => { + const client = await getSdkClient(); + const spinner = ora("Fetching available toolkits...").start(); + + try { + const tools = await getAssistantTools(client); + spinner.stop(); + + if (opts.json) { + outputJson(tools); + return; + } + + if (tools.length === 0) { + printEmpty("toolkits"); + return; + } + + printListHeader("Available Toolkits", tools.length); + + const columns: TableColumn[] = [ + { + header: "Toolkit", + width: 28, + getValue: (t) => chalk.cyan(t.toolkit), + }, + { header: "Label", width: 30, getValue: (t) => t.label }, + { + header: "Tools", + width: 10, + getValue: (t) => String(t.tools.length), + }, + { + header: "External", + width: 10, + getValue: (t) => (t.is_external ? chalk.dim("yes") : "no"), + }, + ]; + printTable(tools, columns); + } catch (error) { + spinner.stop(); + handleSdkError(error, "get toolkits"); + } + }); + + return cmd; +} diff --git a/src/cli/commands/sdk/datasources.ts b/src/cli/commands/sdk/datasources.ts new file mode 100644 index 00000000..5bd2a6f3 --- /dev/null +++ b/src/cli/commands/sdk/datasources.ts @@ -0,0 +1,392 @@ +import { Command } from "commander"; +import chalk from "chalk"; +import ora from "ora"; +import type { DataSource, FileDataSourceCreateParams } from "codemie-sdk"; +import * as datasourceService from "./services/datasources.js"; +import { + getSdkClient, + parseDataOrJsonFile, + outputJson, + handleSdkError, + getResponseMessage, +} from "./utils/cli-utils.js"; +import { + printTable, + printDetail, + printEmpty, + printListHeader, + printSuccess, + optional, + yesNo, + statusColor, + type TableColumn, + type DetailRow, +} from "./utils/render.js"; +import { DATASOURCE_TYPES } from "./utils/datasource-types.js"; + +export function createDatasourcesSubcommand(): Command { + const cmd = new Command("datasources").description( + "Manage CodeMie datasources", + ); + + cmd + .command("list") + .description( + "List datasources visible to the current user\n" + + "Examples:\n" + + " $ codemie datasources list\n" + + " $ codemie datasources list --page 2 --per-page 25\n" + + " $ codemie datasources list --search 'My Datasource' --project MyProject --status active --json\n" + + " $ codemie datasources list --datasource-types confluence,jira --sort-key update_date --sort-order desc", + ) + .option("--json", "Output in JSON format") + .option("--page ", "Page number (starts at 0)", "0") + .option("--per-page ", "Results per page (1-100)", "20") + .option("--search ", "Search by name or description") + .option("--projects ", "Filter by project name (comma-separated)") + .option( + "--status ", + "Filter by status (completed, failed, fetching, in_progress)", + ) + .option("--sort-key ", "Sort by field (date, update_date)") + .option("--sort-order ", "Sort order (asc, desc)") + .option( + "--datasource-types ", + "Filter by datasource types (comma-separated)", + ) + .action(async (opts) => { + const client = await getSdkClient(); + const spinner = ora("Fetching datasources...").start(); + + try { + const params: Record = { + page: parseInt(opts.page, 10), + per_page: parseInt(opts.perPage, 10), + }; + + if (opts.search) { + params.search = opts.search; + } + if (opts.projects) { + params.projects = opts.projects.trim().split(","); + } + if (opts.status) { + params.status = opts.status; + } + if (opts.sortKey) { + params.sort_key = opts.sortKey; + } + if (opts.sortOrder) { + params.sort_order = opts.sortOrder; + } + if (opts.datasourceTypes) { + params.datasource_types = opts.datasourceTypes + .split(",") + .map((s: string) => s.trim()); + } + + const items = await datasourceService.listDatasources(client, params); + + spinner.stop(); + + if (opts.json) { + outputJson(items); + return; + } + + if (items.length === 0) { + printEmpty("datasources"); + return; + } + + printListHeader("Datasources", items.length); + + const columns: TableColumn[] = [ + { header: "ID", width: 40, getValue: (d) => chalk.cyan(d.id) }, + { header: "Name", width: 26, getValue: (d) => d.name }, + { + header: "Project", + width: 20, + getValue: (d) => optional(d.project_name), + }, + { header: "Type", width: 30, getValue: (d) => optional(d.type) }, + { + header: "Status", + width: 14, + getValue: (d) => statusColor(d.status), + }, + ]; + printTable(items, columns); + } catch (error) { + spinner.stop(); + handleSdkError(error, "list datasources"); + } + }); + + cmd + .command("get ") + .description("Get detailed information about a specific datasource") + .option("--json", "Output in JSON format") + .action(async (id: string, opts) => { + const client = await getSdkClient(); + const spinner = ora("Fetching datasource...").start(); + + try { + const item = await datasourceService.getDatasource(client, id); + spinner.stop(); + + if (opts.json) { + outputJson(item); + return; + } + + const rows: DetailRow[] = [ + { label: "ID", value: chalk.cyan(item.id) }, + { label: "Name", value: item.name }, + { label: "Project", value: optional(item.project_name) }, + { label: "Type", value: optional(item.type) }, + { label: "Status", value: statusColor(item.status) }, + { label: "Description", value: optional(item.description) }, + { label: "Shared", value: yesNo(item.shared_with_project) }, + { + label: "Creator", + value: optional(item.created_by?.name), + }, + ]; + + if (item.embeddings_model) { + rows.push({ label: "Embeddings", value: item.embeddings_model }); + } + + if (item.error_message) { + rows.push({ label: "Error", value: chalk.red(item.error_message) }); + } + + if (item.update_date) { + rows.push({ label: "Updated", value: item.update_date }); + } + + printDetail(rows); + } catch (error) { + spinner.stop(); + handleSdkError(error, "get datasource"); + } + }); + + const createCmd = new Command("create").description( + "Create a new datasource (use subcommand for specific type)", + ); + + // Collector function for multiple --file flags + function collectFiles(value: string, previous: string[]): string[] { + return previous.concat([value]); + } + + for (const typeConfig of DATASOURCE_TYPES) { + const serviceKey = typeConfig.serviceKey ?? typeConfig.command; + const serviceFnName = `create${serviceKey.charAt(0).toUpperCase() + serviceKey.slice(1)}Datasource`; + + // Special handling for file datasource + if (typeConfig.command === "file") { + createCmd + .command("file") + .description( + `Create ${typeConfig.description}\n` + + `Examples:\n` + + ` $ codemie sdk datasources create file --file ./doc1.pdf --file ./doc2.docx --data '{"name":"Docs","project_name":"Team"}'\n` + + ` $ codemie sdk datasources create file --file ./report.pdf --json path/to/metadata.json`, + ) + .requiredOption( + "--file ", + "File path (can be specified multiple times, max 10)", + collectFiles, + [], + ) + .option( + "--data ", + "Datasource metadata as inline JSON string (name, project_name, description, etc.)", + ) + .option( + "--json ", + "Path to JSON file with datasource metadata", + ) + .action(async (opts) => { + const client = await getSdkClient(); + const spinner = ora("Creating file datasource...").start(); + + try { + if (!opts.file || opts.file.length === 0) { + throw new Error("At least one --file is required"); + } + + if (opts.file.length > 10) { + throw new Error( + `Maximum 10 files allowed, received ${opts.file.length}`, + ); + } + + const data = (await parseDataOrJsonFile( + opts.data, + opts.json, + )) as FileDataSourceCreateParams; + const result = await datasourceService.createFileDatasource( + client, + data, + opts.file, + ); + spinner.stop(); + + printSuccess(getResponseMessage(result)); + } catch (error) { + spinner.stop(); + handleSdkError(error, "create file datasource"); + } + }); + } else { + createCmd + .command(typeConfig.command) + .description( + `Create ${typeConfig.description}\n` + + `Examples:\n` + + ` $ codemie sdk datasources create ${typeConfig.command} --data '${typeConfig.example}'\n` + + ` $ codemie sdk datasources create ${typeConfig.command} --json path/to/config.json`, + ) + .option( + "--data ", + "Datasource configuration as inline JSON string", + ) + .option( + "--json ", + "Path to JSON file with datasource configuration", + ) + .action(async (opts) => { + const client = await getSdkClient(); + const spinner = ora( + `Creating ${typeConfig.command} datasource...`, + ).start(); + + try { + const data = await parseDataOrJsonFile(opts.data, opts.json); + const serviceFn = datasourceService[ + serviceFnName as keyof typeof datasourceService + ] as (client: any, data: any) => Promise; + const result = await serviceFn(client, data); + spinner.stop(); + + printSuccess(getResponseMessage(result)); + } catch (error) { + spinner.stop(); + handleSdkError(error, `create ${typeConfig.command} datasource`); + } + }); + } + } + + cmd.addCommand(createCmd); + + const updateCmd = new Command("update").description( + "Update an existing datasource (use subcommand for specific type)", + ); + + for (const typeConfig of DATASOURCE_TYPES) { + const serviceKey = typeConfig.serviceKey ?? typeConfig.command; + const serviceFnName = `update${serviceKey.charAt(0).toUpperCase() + serviceKey.slice(1)}Datasource`; + + // Special handling for file datasource + if (typeConfig.command === "file") { + updateCmd + .command("file ") + .description( + `Update ${typeConfig.description}\n` + + `Examples:\n` + + ` $ codemie sdk datasources update file ds_123 --data '{"description":"Updated docs"}'\n` + + ` $ codemie sdk datasources update file ds_123 --json path/to/update.json`, + ) + .option( + "--data ", + "Fields to update as inline JSON string", + ) + .option( + "--json ", + "Path to JSON file with fields to update", + ) + .action(async (id: string, opts) => { + const client = await getSdkClient(); + const spinner = ora("Updating file datasource...").start(); + + try { + const data = await parseDataOrJsonFile(opts.data, opts.json); + const result = await datasourceService.updateFileDatasource( + client, + id, + data as any, + ); + spinner.stop(); + + printSuccess(getResponseMessage(result)); + } catch (error) { + spinner.stop(); + handleSdkError(error, "update file datasource"); + } + }); + } else { + updateCmd + .command(`${typeConfig.command} `) + .description( + `Update ${typeConfig.description}\n` + + `Examples:\n` + + ` $ codemie sdk datasources update ${typeConfig.command} ds_123 --data '${typeConfig.example}'\n` + + ` $ codemie sdk datasources update ${typeConfig.command} ds_123 --json path/to/update.json`, + ) + .option( + "--data ", + "Fields to update as inline JSON string", + ) + .option( + "--json ", + "Path to JSON file with fields to update", + ) + .action(async (id: string, opts) => { + const client = await getSdkClient(); + const spinner = ora( + `Updating ${typeConfig.command} datasource...`, + ).start(); + + try { + const data = await parseDataOrJsonFile(opts.data, opts.json); + const serviceFn = datasourceService[ + serviceFnName as keyof typeof datasourceService + ] as (client: any, id: string, data: any) => Promise; + const result = await serviceFn(client, id, data); + spinner.stop(); + + printSuccess(getResponseMessage(result)); + } catch (error) { + spinner.stop(); + handleSdkError(error, `update ${typeConfig.command} datasource`); + } + }); + } + } + + cmd.addCommand(updateCmd); + + cmd + .command("delete ") + .description("Permanently delete a datasource") + .action(async (id: string) => { + const client = await getSdkClient(); + const spinner = ora("Deleting datasource...").start(); + + try { + await datasourceService.deleteDatasource(client, id); + spinner.stop(); + printSuccess(`✓ Datasource ${chalk.cyan(id)} deleted.`); + } catch (error) { + spinner.stop(); + handleSdkError(error, "delete datasource"); + } + }); + + return cmd; +} diff --git a/src/cli/commands/sdk/index.ts b/src/cli/commands/sdk/index.ts new file mode 100644 index 00000000..a1e2cdc0 --- /dev/null +++ b/src/cli/commands/sdk/index.ts @@ -0,0 +1,22 @@ +import { Command } from 'commander'; +import { createAssistantsSubcommand } from './assistants.js'; +import { createWorkflowsSubcommand } from './workflows.js'; +import { createDatasourcesSubcommand } from './datasources.js'; +import { createIntegrationsSubcommand } from './integrations.js'; +import { createLlmModelsSubcommand } from './llm.js'; + +export function createSdkCommand(): Command { + const cmd = new Command('sdk'); + + cmd.description( + 'Manage CodeMie platform assets (assistants, workflows, datasources, integrations) via the SDK' + ); + + cmd.addCommand(createAssistantsSubcommand()); + cmd.addCommand(createWorkflowsSubcommand()); + cmd.addCommand(createDatasourcesSubcommand()); + cmd.addCommand(createIntegrationsSubcommand()); + cmd.addCommand(createLlmModelsSubcommand()); + + return cmd; +} diff --git a/src/cli/commands/sdk/integrations.ts b/src/cli/commands/sdk/integrations.ts new file mode 100644 index 00000000..7464438a --- /dev/null +++ b/src/cli/commands/sdk/integrations.ts @@ -0,0 +1,315 @@ +import { Command } from "commander"; +import chalk from "chalk"; +import ora from "ora"; +import type { + Integration, + IntegrationCreateParams, + IntegrationUpdateParams, + IntegrationTypeType, +} from "codemie-sdk"; +import { + listIntegrations, + getIntegration, + getIntegrationByAlias, + createIntegration, + updateIntegration, + deleteIntegration, +} from "./services/integrations.js"; +import { + getSdkClient, + parseDataOrJsonFile, + outputJson, + handleSdkError, + getResponseMessage, +} from "./utils/cli-utils.js"; +import { + printTable, + printDetail, + printEmpty, + printListHeader, + printSuccess, + optional, + type TableColumn, + type DetailRow, +} from "./utils/render.js"; + +export function createIntegrationsSubcommand(): Command { + const cmd = new Command("integrations").description( + "Manage CodeMie integrations", + ); + + cmd + .command("list") + .description( + "List integrations visible to the current user\n" + + "Examples:\n" + + " $ codemie integrations list\n" + + " $ codemie integrations list --setting-type project --page 2 --per-page 25\n" + + ' $ codemie integrations list --filters \'{"credential_type":"Jira"}\' --json', + ) + .option("--json", "Output in JSON format") + .option( + "--setting-type ", + "Setting type: 'user' or 'project'", + "user", + ) + .option("--page ", "Page number (starts at 0)", "0") + .option("--per-page ", "Results per page (1-100)", "10") + .option("--search ", "Search by name or description") + .option("--projects ", "Filter by project name (comma-separated)") + .action(async (opts) => { + const client = await getSdkClient(); + const spinner = ora("Fetching integrations...").start(); + + try { + const items = await listIntegrations(client, { + setting_type: opts.settingType, + page: parseInt(opts.page, 10), + per_page: parseInt(opts.perPage, 10), + filters: { + ...(opts.projects + ? { project: opts.projects.trim().split(",") } + : {}), + ...(opts.search ? { search: opts.search } : {}), + }, + }); + + spinner.stop(); + + if (opts.json) { + outputJson(items); + return; + } + + if (items.length === 0) { + printEmpty("integrations"); + return; + } + + printListHeader("Integrations", items.length); + + const columns: TableColumn[] = [ + { header: "ID", width: 25, getValue: (i) => chalk.cyan(i.id) }, + { + header: "Alias", + width: 20, + getValue: (i) => optional(i.alias), + }, + { + header: "Type", + width: 15, + getValue: (i) => i.credential_type, + }, + { + header: "Project", + width: 25, + getValue: (i) => i.project_name, + }, + ]; + printTable(items, columns); + } catch (error) { + spinner.stop(); + handleSdkError(error, "list integrations"); + } + }); + + cmd + .command("get ") + .description("Get detailed information about a specific integration by ID") + .option("--json", "Output in JSON format") + .option( + "--setting-type ", + "Setting type: 'user' or 'project'", + "user", + ) + .action(async (id: string, opts) => { + const client = await getSdkClient(); + const spinner = ora("Fetching integration...").start(); + + try { + const item = await getIntegration(client, { + integration_id: id, + setting_type: opts.settingType, + }); + spinner.stop(); + + if (opts.json) { + outputJson(item); + return; + } + + const rows: DetailRow[] = [ + { label: "ID", value: chalk.cyan(item.id) }, + { label: "Alias", value: optional(item.alias) }, + { label: "Credential Type", value: item.credential_type }, + { label: "Project Name", value: item.project_name }, + { label: "Setting Type", value: item.setting_type }, + { label: "Default", value: item.default ? "Yes" : "No" }, + ]; + + if (item.date) { + rows.push({ label: "Created", value: item.date }); + } + + if (item.update_date) { + rows.push({ label: "Updated", value: item.update_date }); + } + + if (item.user_id) { + rows.push({ label: "User ID", value: item.user_id }); + } + + printDetail(rows); + } catch (error) { + spinner.stop(); + handleSdkError(error, "get integration"); + } + }); + + cmd + .command("get-by-alias ") + .description("Get integration by alias") + .option("--json", "Output in JSON format") + .option( + "--setting-type ", + "Setting type: 'user' or 'project'", + "user", + ) + .action(async (alias: string, opts) => { + const client = await getSdkClient(); + const spinner = ora("Fetching integration...").start(); + + try { + const item = await getIntegrationByAlias(client, { + alias, + setting_type: opts.settingType, + }); + spinner.stop(); + + if (opts.json) { + outputJson(item); + return; + } + + const rows: DetailRow[] = [ + { label: "ID", value: chalk.cyan(item.id) }, + { label: "Alias", value: optional(item.alias) }, + { label: "Credential Type", value: item.credential_type }, + { label: "Project Name", value: item.project_name }, + { label: "Setting Type", value: item.setting_type }, + { label: "Default", value: item.default ? "Yes" : "No" }, + ]; + + if (item.date) { + rows.push({ label: "Created", value: item.date }); + } + + if (item.update_date) { + rows.push({ label: "Updated", value: item.update_date }); + } + + if (item.user_id) { + rows.push({ label: "User ID", value: item.user_id }); + } + + printDetail(rows); + } catch (error) { + spinner.stop(); + handleSdkError(error, "get integration by alias"); + } + }); + + cmd + .command("create") + .description( + "Create a new integration with the specified configuration\n" + + "Examples:\n" + + ' $ codemie integrations create --data \'{"credential_type":"Jira","project_name":"MyProject","alias":"jira-main","credential_values":[{"key":"url","value":"https://jira.example.com"},{"key":"token","value":"secret"}]}\'\n' + + " $ codemie integrations create --json path/to/integration.json\n", + ) + .option( + "--data ", + "Integration configuration as inline JSON string", + ) + .option("--json ", "Path to JSON file with integration configuration") + .action(async (opts) => { + const client = await getSdkClient(); + const spinner = ora("Creating integration...").start(); + + try { + const data = await parseDataOrJsonFile(opts.data, opts.json); + const result = await createIntegration( + client, + data as IntegrationCreateParams, + ); + spinner.stop(); + + printSuccess(getResponseMessage(result)); + } catch (error) { + spinner.stop(); + handleSdkError(error, "create integration"); + } + }); + + cmd + .command("update ") + .description( + "Update an existing integration's configuration\n" + + "Examples:\n" + + ' $ codemie integrations update int_abc123 --data \'{"alias":"jira-updated"}\'\n' + + " $ codemie integrations update int_abc123 --json path/to/update.json\n", + ) + .option("--data ", "Fields to update as inline JSON string") + .option("--json ", "Path to JSON file with fields to update") + .option( + "--setting-type ", + "Setting type: 'user' or 'project'", + "user", + ) + .action(async (id: string, opts) => { + const client = await getSdkClient(); + const spinner = ora("Updating integration...").start(); + try { + const data = await parseDataOrJsonFile(opts.data, opts.json); + const result = await updateIntegration( + client, + id, + opts.settingType, + data as IntegrationUpdateParams, + ); + spinner.stop(); + + printSuccess(getResponseMessage(result)); + } catch (error) { + spinner.stop(); + handleSdkError(error, "update integration"); + } + }); + + cmd + .command("delete ") + .description("Permanently delete an integration") + .option( + "--setting-type ", + "Setting type: 'user' or 'project'", + "user", + ) + .action(async (id: string, opts) => { + const client = await getSdkClient(); + const spinner = ora("Deleting integration...").start(); + try { + await deleteIntegration( + client, + id, + opts.settingType as IntegrationTypeType, + ); + spinner.stop(); + printSuccess(`✓ Integration ${chalk.cyan(id)} deleted.`); + } catch (error) { + spinner.stop(); + handleSdkError(error, "delete integration"); + } + }); + + return cmd; +} diff --git a/src/cli/commands/sdk/llm.ts b/src/cli/commands/sdk/llm.ts new file mode 100644 index 00000000..efe727bd --- /dev/null +++ b/src/cli/commands/sdk/llm.ts @@ -0,0 +1,72 @@ +import { Command } from "commander"; +import chalk from "chalk"; +import ora from "ora"; +import type { LLMModel } from "codemie-sdk"; +import { listLlmModels, listEmbeddingModels } from "./services/llm.js"; +import { + getSdkClient, + outputJson, + handleSdkError, +} from "./utils/cli-utils.js"; +import { + printTable, + printListHeader, + printEmpty, + yesNo, + optional, + type TableColumn, +} from "./utils/render.js"; + +const LLM_COLUMNS: TableColumn[] = [ + { header: "Base Name", width: 36, getValue: (m) => chalk.cyan(m.base_name) }, + { header: "Label", width: 36, getValue: (m) => optional(m.label) }, + { header: "Provider", width: 18, getValue: (m) => optional(m.provider) }, + { header: "Default", width: 10, getValue: (m) => yesNo(m.default) }, + { header: "Enabled", width: 10, getValue: (m) => yesNo(m.enabled) }, +]; + +export function createLlmModelsSubcommand(): Command { + const cmd = new Command("llm").description( + "List available LLM models", + ); + + cmd + .command("list") + .description("List available LLM models") + .option("--embeddings", "List embedding models instead of chat models") + .option("--json", "Output in JSON format") + .action(async (opts) => { + const client = await getSdkClient(); + const label = opts.embeddings ? "embedding models" : "LLM models"; + const spinner = ora(`Fetching ${label}...`).start(); + + try { + const items = opts.embeddings + ? await listEmbeddingModels(client) + : await listLlmModels(client); + + spinner.stop(); + + if (opts.json) { + outputJson(items); + return; + } + + if (items.length === 0) { + printEmpty(label); + return; + } + + printListHeader( + opts.embeddings ? "Embedding Models" : "LLM Models", + items.length, + ); + printTable(items, LLM_COLUMNS); + } catch (error) { + spinner.stop(); + handleSdkError(error, `list ${label}`); + } + }); + + return cmd; +} diff --git a/src/cli/commands/sdk/services/assistants.ts b/src/cli/commands/sdk/services/assistants.ts new file mode 100644 index 00000000..7f26e723 --- /dev/null +++ b/src/cli/commands/sdk/services/assistants.ts @@ -0,0 +1,100 @@ +import type { + CodeMieClient, + Assistant, + AssistantBase, + AssistantCreateParams, + AssistantUpdateParams, + AssistantListParams, + ToolKitDetails, +} from "codemie-sdk"; +import { listLlmModels } from "./llm.js"; + +type Writeable = { -readonly [P in keyof T]: T[P] }; + +export async function getAssistantTools( + client: CodeMieClient, +): Promise { + return client.assistants.getTools(); +} + +export async function listAssistants( + client: CodeMieClient, + params?: AssistantListParams, +): Promise<(Assistant | AssistantBase)[]> { + return client.assistants.list(params); +} + +export async function getAssistant( + client: CodeMieClient, + assistantId: string, +): Promise { + return client.assistants.get(assistantId); +} + +export async function createAssistant( + client: CodeMieClient, + params: Partial, +): Promise<{ message: string; assistant_id?: string }> { + const llmModels = await listLlmModels(client); + const defaultLlmModel = + llmModels.find((m) => m.default)?.base_name ?? llmModels[0].base_name; + + const mergedParams: Partial = { + context: [], + toolkits: [], + conversation_starters: [], + mcp_servers: [], + assistant_ids: [], + llm_model_type: defaultLlmModel, + ...params, + }; + + return client.assistants.create(mergedParams as AssistantCreateParams); +} + +export async function updateAssistant( + client: CodeMieClient, + assistantId: string, + params: Partial, +): Promise<{ message: string }> { + const [existing, llmModels] = await Promise.all([ + client.assistants.get(assistantId), + listLlmModels(client), + ]); + const defaultLlmModel = + llmModels.find((m) => m.default)?.base_name ?? llmModels[0].base_name; + + const mergedParams: Writeable> = { + ...existing, + ...params, + icon_url: existing.icon_url ?? "", + slug: existing.slug ?? "", + llm_model_type: + params.llm_model_type ?? existing.llm_model_type ?? defaultLlmModel, + categories: + params.categories ?? existing.categories?.map((c) => c.id) ?? [], + toolkits: params.toolkits ?? existing.toolkits ?? [], + }; + + if (params.temperature) mergedParams.temperature = params.temperature; + else if (existing.temperature === null) { + delete mergedParams.temperature; + } + + if (params.top_p) mergedParams.top_p = params.top_p; + else if (existing.top_p === null) { + delete mergedParams.top_p; + } + + return client.assistants.update( + assistantId, + mergedParams as AssistantUpdateParams, + ); +} + +export async function deleteAssistant( + client: CodeMieClient, + assistantId: string, +): Promise { + await client.assistants.delete(assistantId); +} diff --git a/src/cli/commands/sdk/services/datasources.ts b/src/cli/commands/sdk/services/datasources.ts new file mode 100644 index 00000000..9b873255 --- /dev/null +++ b/src/cli/commands/sdk/services/datasources.ts @@ -0,0 +1,306 @@ +import type { + CodeMieClient, + ConfluenceDataSourceCreateParams, + ConfluenceDataSourceUpdateParams, + DataSource, + DataSourceListParams, + FileDataSourceCreateParams, + FileDataSourceUpdateDto, + GoogleDataSourceCreateParams, + JiraDataSourceCreateParams, + JiraDataSourceUpdateParams, + OtherDataSourceCreateParams, + OtherDataSourceUpdateParams, +} from "codemie-sdk"; +import { readFilesFromPaths } from "../utils/file-utils.js"; + +export async function listDatasources( + client: CodeMieClient, + params?: DataSourceListParams, +): Promise { + return client.datasources.list(params); +} + +export async function getDatasource( + client: CodeMieClient, + datasourceId: string, +): Promise { + return client.datasources.get(datasourceId); +} + +// CONFLUENCE +export async function createConfluenceDatasource( + client: CodeMieClient, + data: ConfluenceDataSourceCreateParams, +): Promise { + const params: ConfluenceDataSourceCreateParams = { + type: "knowledge_base_confluence", + cql: data.cql, + description: data.description, + name: data.name, + project_name: data.project_name, + setting_id: data.setting_id, + shared_with_project: data.shared_with_project, + }; + + return client.datasources.create(params); +} + +export async function updateConfluenceDatasource( + client: CodeMieClient, + id: string, + data: Partial, +): Promise { + const existing = await client.datasources.get(id); + + const params: ConfluenceDataSourceUpdateParams = { + type: "knowledge_base_confluence", + cql: data.cql, + description: data.description, + name: existing.name, + project_name: data.project_name ?? existing.project_name, + setting_id: data.setting_id, + shared_with_project: data.shared_with_project, + }; + + return client.datasources.update(params); +} + +// JIRA +export async function createJiraDatasource( + client: CodeMieClient, + data: JiraDataSourceCreateParams, +): Promise { + const params: JiraDataSourceCreateParams = { + ...data, + type: "knowledge_base_jira", + name: data.name, + description: data.description, + jql: data.jql, + project_name: data.project_name, + setting_id: data.setting_id, + shared_with_project: data.shared_with_project, + }; + + return client.datasources.create(params); +} + +export async function updateJiraDatasource( + client: CodeMieClient, + id: string, + data: Partial, +): Promise { + const existing = await client.datasources.get(id); + + const params: JiraDataSourceUpdateParams = { + type: "knowledge_base_jira", + name: existing.name, + project_name: data.project_name ?? existing.project_name, + description: data.description ?? existing.description, + jql: data.jql ?? existing.jira?.jql, + setting_id: data.setting_id ?? existing.setting_id, + shared_with_project: + data.shared_with_project ?? existing.shared_with_project, + }; + + return client.datasources.update(params); +} + +// FILE +export async function createFileDatasource( + client: CodeMieClient, + data: FileDataSourceCreateParams, + filePaths: string[], +): Promise { + if (!filePaths || !Array.isArray(filePaths)) { + throw new Error("files array is required for file datasources"); + } + + const files = await readFilesFromPaths(filePaths); + + return client.datasources.create({ + ...data, + type: "knowledge_base_file", + files, + }); +} + +export async function updateFileDatasource( + client: CodeMieClient, + id: string, + data: Partial, +): Promise { + const existing = await client.datasources.get(id); + + const updateParams: FileDataSourceUpdateDto = { + type: "knowledge_base_file", + name: existing.name, + project_name: existing.project_name, + ...data, + }; + + return client.datasources.update(updateParams); +} + +// CODE +export async function createCodeDatasource( + client: CodeMieClient, + data: any, +): Promise { + return client.datasources.create({ + ...data, + type: "code", + }); +} + +export async function updateCodeDatasource( + client: CodeMieClient, + id: string, + data: any, +): Promise { + const existing = await client.datasources.get(id); + return client.datasources.update({ + id, + type: "code", + name: existing.name, + project_name: existing.project_name, + ...data, + }); +} + +// GOOGLE +export async function createGoogleDatasource( + client: CodeMieClient, + data: GoogleDataSourceCreateParams, +): Promise { + return client.datasources.create({ + ...data, + type: "llm_routing_google", + }); +} + +export async function updateGoogleDatasource( + client: CodeMieClient, + id: string, + data: any, +): Promise { + const existing = await client.datasources.get(id); + return client.datasources.update({ + id, + type: "llm_routing_google", + name: existing.name, + project_name: existing.project_name, + ...data, + }); +} + +// JSON +export async function createJsonDatasource( + client: CodeMieClient, + data: Omit, +): Promise { + return client.datasources.create({ + ...data, + type: "knowledge_base_json", + }); +} + +export async function updateJsonDatasource( + client: CodeMieClient, + id: string, + data: Partial>, +): Promise { + const existing = await client.datasources.get(id); + const params: OtherDataSourceUpdateParams = { + type: "knowledge_base_json", + name: existing.name, + project_name: existing.project_name, + ...data, + }; + return client.datasources.update(params); +} + +// PROVIDER +export async function createProviderDatasource( + client: CodeMieClient, + data: Omit, +): Promise { + return client.datasources.create({ + ...data, + type: "provider", + }); +} + +export async function updateProviderDatasource( + client: CodeMieClient, + id: string, + data: Partial>, +): Promise { + const existing = await client.datasources.get(id); + const params: OtherDataSourceUpdateParams = { + type: "provider", + name: existing.name, + project_name: existing.project_name, + ...data, + }; + return client.datasources.update(params); +} + +// SUMMARY +export async function createSummaryDatasource( + client: CodeMieClient, + data: Omit, +): Promise { + return client.datasources.create({ + ...data, + type: "summary", + }); +} + +export async function updateSummaryDatasource( + client: CodeMieClient, + id: string, + data: Partial>, +): Promise { + const existing = await client.datasources.get(id); + const params: OtherDataSourceUpdateParams = { + type: "summary", + name: existing.name, + project_name: existing.project_name, + ...data, + }; + return client.datasources.update(params); +} + +// CHUNK SUMMARY +export async function createChunkSummaryDatasource( + client: CodeMieClient, + data: Omit, +): Promise { + return client.datasources.create({ + ...data, + type: "chunk-summary", + }); +} + +export async function updateChunkSummaryDatasource( + client: CodeMieClient, + id: string, + data: Partial>, +): Promise { + const existing = await client.datasources.get(id); + const params: OtherDataSourceUpdateParams = { + type: "chunk-summary", + name: existing.name, + project_name: existing.project_name, + ...data, + }; + return client.datasources.update(params); +} + +export async function deleteDatasource( + client: CodeMieClient, + datasourceId: string, +): Promise { + await client.datasources.delete(datasourceId); +} diff --git a/src/cli/commands/sdk/services/index.ts b/src/cli/commands/sdk/services/index.ts new file mode 100644 index 00000000..bd74dc89 --- /dev/null +++ b/src/cli/commands/sdk/services/index.ts @@ -0,0 +1,5 @@ +export * from "./assistants.js"; +export * from "./workflows.js"; +export * from "./datasources.js"; +export * from "./integrations.js"; +export * from "./llm.js"; diff --git a/src/cli/commands/sdk/services/integrations.ts b/src/cli/commands/sdk/services/integrations.ts new file mode 100644 index 00000000..fe2d3935 --- /dev/null +++ b/src/cli/commands/sdk/services/integrations.ts @@ -0,0 +1,106 @@ +import type { CodeMieClient } from "codemie-sdk"; +import type { + Integration, + IntegrationListParams, + IntegrationGetParams, + IntegrationGetByAliasParams, + IntegrationCreateParams, + IntegrationUpdateParams, + IntegrationTypeType, +} from "codemie-sdk"; + +/** + * List integrations with pagination and filters + */ +export async function listIntegrations( + client: CodeMieClient, + params: IntegrationListParams = {}, +): Promise { + return client.integrations.list(params); +} + +/** + * Get integration by ID + */ +export async function getIntegration( + client: CodeMieClient, + params: IntegrationGetParams, +): Promise { + return client.integrations.get(params); +} + +/** + * Get integration by alias + */ +export async function getIntegrationByAlias( + client: CodeMieClient, + params: IntegrationGetByAliasParams, +): Promise { + return client.integrations.getByAlias(params); +} + +/** + * Create a new integration + */ +export async function createIntegration( + client: CodeMieClient, + params: IntegrationCreateParams, +): Promise { + return client.integrations.create(params); +} + +function mergeCredentialValues( + existing: { key: string }[], + params: { key: string }[], +) { + const map = new Map(); + existing.forEach((item) => { + map.set(item.key, item); + }); + + params.forEach((item) => { + map.set(item.key, item); + }); + + return Array.from(map.values()); +} + +/** + * Update an existing integration + */ +export async function updateIntegration( + client: CodeMieClient, + settingId: string, + settingType: "user" | "project", + params: Partial, +): Promise { + const existing = await client.integrations.get({ + integration_id: settingId, + setting_type: settingType, + }); + + const data: IntegrationUpdateParams = { + project_name: existing.project_name, + credential_type: existing.credential_type, + alias: params.alias ?? existing.alias, + credential_values: mergeCredentialValues( + existing.credential_values, + params.credential_values ?? [], + ), + setting_type: existing.setting_type, + default: params.default ?? existing.default, + }; + + return client.integrations.update(settingId, data); +} + +/** + * Delete an integration + */ +export async function deleteIntegration( + client: CodeMieClient, + settingId: string, + settingType?: IntegrationTypeType, +): Promise { + return client.integrations.delete(settingId, settingType); +} diff --git a/src/cli/commands/sdk/services/llm.ts b/src/cli/commands/sdk/services/llm.ts new file mode 100644 index 00000000..88438bc5 --- /dev/null +++ b/src/cli/commands/sdk/services/llm.ts @@ -0,0 +1,13 @@ +import { CodeMieClient, LLMModel } from "codemie-sdk"; + +export async function listLlmModels( + client: CodeMieClient, +): Promise { + return client.llms.list(); +} + +export async function listEmbeddingModels( + client: CodeMieClient, +): Promise { + return client.llms.listEmbeddings(); +} diff --git a/src/cli/commands/sdk/services/workflows.ts b/src/cli/commands/sdk/services/workflows.ts new file mode 100644 index 00000000..475f1b08 --- /dev/null +++ b/src/cli/commands/sdk/services/workflows.ts @@ -0,0 +1,68 @@ +import type { + CodeMieClient, + Workflow, + WorkflowCreateParams, + WorkflowUpdateParams, + WorkflowListParams, +} from "codemie-sdk"; + +export async function listWorkflows( + client: CodeMieClient, + params?: WorkflowListParams, +): Promise { + return client.workflows.list(params); +} + +export async function getWorkflow( + client: CodeMieClient, + workflowId: string, +): Promise { + return client.workflows.get(workflowId); +} + +export async function createWorkflow( + client: CodeMieClient, + params: WorkflowCreateParams, + yamlConfig?: string, +): Promise { + const paramsWithDefaults: WorkflowCreateParams = { + mode: "Sequential", + description: params.description ?? "", + shared: params.shared ?? false, + ...(params as Partial), + } as WorkflowCreateParams; + + if (yamlConfig) { + (paramsWithDefaults as Record).yaml_config = yamlConfig; + } + + return client.workflows.create(paramsWithDefaults); +} + +export async function updateWorkflow( + client: CodeMieClient, + workflowId: string, + params: WorkflowUpdateParams, + yamlConfig?: string, +): Promise { + const existing = await client.workflows.get(workflowId); + + const mergedParams: WorkflowUpdateParams = { + ...existing, + ...params, + icon_url: params.icon_url ?? existing.icon_url ?? "", + }; + + if (yamlConfig) { + (mergedParams as Record).yaml_config = yamlConfig; + } + + return client.workflows.update(workflowId, mergedParams); +} + +export async function deleteWorkflow( + client: CodeMieClient, + workflowId: string, +): Promise { + await client.workflows.delete(workflowId); +} diff --git a/src/cli/commands/sdk/utils/cli-utils.ts b/src/cli/commands/sdk/utils/cli-utils.ts new file mode 100644 index 00000000..567bcf21 --- /dev/null +++ b/src/cli/commands/sdk/utils/cli-utils.ts @@ -0,0 +1,141 @@ +import { readFile } from "node:fs/promises"; +import chalk from "chalk"; +import type { CodeMieClient } from "codemie-sdk"; +import { ApiError } from "codemie-sdk"; +import { ConfigLoader } from "@/utils/config.js"; +import { getAuthenticatedClient } from "@/utils/auth.js"; +import z, { ZodError } from "zod"; + +/** + * Get an authenticated CodeMie SDK client + */ +export async function getSdkClient(): Promise { + const config = await ConfigLoader.load(); + return getAuthenticatedClient(config); +} + +/** + * Parse --data flag value: inline JSON string only + */ +export async function parseDataInput( + dataFlag: string | undefined, +): Promise { + if (!dataFlag) { + throw new Error('No data provided. Use --data \'{"key":"value"}\''); + } + + return JSON.parse(dataFlag); +} + +/** + * Parse --json flag value: path to JSON file + */ +export async function parseJsonFileInput( + jsonFlag: string | undefined, +): Promise { + if (!jsonFlag) { + throw new Error("No JSON file provided. Use --json path/to/file.json"); + } + + const content = await readFile(jsonFlag, "utf-8"); + return JSON.parse(content); +} + +/** + * Parse data from either --data (inline JSON string) or --json (file path) + * They are mutually exclusive + */ +export async function parseDataOrJsonFile( + dataFlag: string | undefined, + jsonFlag: string | undefined, +): Promise { + if (dataFlag && jsonFlag) { + throw new Error( + "Cannot use both --data and --json. Use --data for inline JSON string or --json for JSON file path.", + ); + } + + if (!dataFlag && !jsonFlag) { + throw new Error( + 'Either --data or --json is required. Use --data \'{"key":"value"}\' or --json path/to/file.json', + ); + } + + if (dataFlag) { + return parseDataInput(dataFlag); + } + + return parseJsonFileInput(jsonFlag); +} + +/** + * Output data as formatted JSON + */ +export function outputJson(data: unknown): void { + console.log(JSON.stringify(data, null, 2)); +} + +/** + * Handle API errors with user-friendly messages and exit + */ +export function handleSdkError(error: unknown, operation: string): never { + if (error instanceof ApiError) { + const status = (error as ApiError & { status?: number }).status; + if (status === 401 || status === 403) { + console.error( + chalk.red( + `❌ Authorization error: You do not have permission to ${operation}.`, + ), + ); + console.error( + chalk.dim( + ' Run "codemie setup" to re-authenticate if your session expired.', + ), + ); + } else if (status === 404) { + console.error( + chalk.red(`❌ Not found: The requested resource does not exist.`), + ); + } else { + console.error(chalk.red(`❌ API error: ${error.message}`)); + } + } else if (error instanceof ZodError) { + console.error(chalk.red(`❌ Operation failed:`)); + console.error(chalk.red(z.prettifyError(error))); + } else { + const msg = error instanceof Error ? error.message : String(error); + console.error(chalk.red(`❌ ${msg}`)); + } + process.exit(1); +} + +/** + * Read a single string property safely from an unknown API response + */ +export function getResponseMessage(response: unknown): string { + if (response && typeof response === "object" && "message" in response) { + return String((response as Record).message); + } + return "Done."; +} + +/** + * Parse --config flag value: YAML string or @file path + */ +export async function parseConfigInput( + configFlag: string | undefined, +): Promise { + if (!configFlag) { + throw new Error( + "No config provided. Use --config 'yaml string' or --config @file.yaml", + ); + } + + if (configFlag.startsWith("@")) { + const filePath = configFlag.slice(1); + const content = await readFile(filePath, "utf-8"); + return content; + } + + return configFlag; +} diff --git a/src/cli/commands/sdk/utils/datasource-types.ts b/src/cli/commands/sdk/utils/datasource-types.ts new file mode 100644 index 00000000..3f4680cf --- /dev/null +++ b/src/cli/commands/sdk/utils/datasource-types.ts @@ -0,0 +1,74 @@ +export interface DatasourceTypeConfig { + command: string; + serviceKey?: string; // Override for service function name when command contains invalid identifier chars + type: string; // SDK type value + description: string; + example: string; +} + +export const DATASOURCE_TYPES: DatasourceTypeConfig[] = [ + { + command: "confluence", + type: "knowledge_base_confluence", + description: "Confluence datasource", + example: + '{"name":"Wiki","project_name":"Docs","cql":"space=TEAM","description":"Company wiki","shared_with_project":true}', + }, + { + command: "jira", + type: "knowledge_base_jira", + description: "Jira datasource", + example: + '{"name":"Tickets","project_name":"Support","jql":"project=SUP","description":"Support tickets","shared_with_project":true}', + }, + { + command: "file", + type: "knowledge_base_file", + description: "File datasource (use --file flags for local files)", + example: + '{"name":"Docs","project_name":"Team","description":"Team documents","shared_with_project":true}', + }, + { + command: "code", + type: "code", + description: "Code repository datasource", + example: + '{"name":"Repo","project_name":"Eng","link":"https://github.com/org/repo","branch":"main","index_type":"code","description":"Main codebase"}', + }, + { + command: "google", + type: "llm_routing_google", + description: "Google Docs datasource", + example: + '{"name":"Docs","project_name":"Team","google_doc":"doc-id-or-url","description":"Team docs","shared_with_project":true}', + }, + { + command: "json", + type: "knowledge_base_json", + description: "JSON knowledge base datasource", + example: + '{"name":"json-data","project_name":"Team","description":"JSON knowledge base","shared_with_project":true}', + }, + { + command: "provider", + type: "provider", + description: "Provider datasource", + example: + '{"name":"my-provider","project_name":"Team","description":"Provider datasource","shared_with_project":true}', + }, + { + command: "summary", + type: "summary", + description: "Summary datasource", + example: + '{"name":"my-summary","project_name":"Team","description":"Summary datasource","shared_with_project":true}', + }, + { + command: "chunk-summary", + serviceKey: "chunkSummary", + type: "chunk-summary", + description: "Chunk summary datasource", + example: + '{"name":"my-chunk-summary","project_name":"Team","description":"Chunk summary datasource","shared_with_project":true}', + }, +]; diff --git a/src/cli/commands/sdk/utils/file-utils.ts b/src/cli/commands/sdk/utils/file-utils.ts new file mode 100644 index 00000000..3923eb20 --- /dev/null +++ b/src/cli/commands/sdk/utils/file-utils.ts @@ -0,0 +1,25 @@ +import { readFile } from "fs/promises"; +import { basename } from "path"; +import { lookup } from "mime-types"; +import type { File } from "codemie-sdk"; + +/** + * Read files from local paths and convert to SDK File format + * @param filePaths Array of local file paths + * @returns Array of File objects with content and metadata + */ +export async function readFilesFromPaths( + filePaths: string[], +): Promise { + const files: File[] = []; + + for (const filePath of filePaths) { + const content = await readFile(filePath); + const name = basename(filePath); + const mime_type = lookup(filePath) || "application/octet-stream"; + + files.push({ name, content, mime_type }); + } + + return files; +} diff --git a/src/cli/commands/sdk/utils/render.ts b/src/cli/commands/sdk/utils/render.ts new file mode 100644 index 00000000..7b924733 --- /dev/null +++ b/src/cli/commands/sdk/utils/render.ts @@ -0,0 +1,190 @@ +import chalk from "chalk"; +import Table from "cli-table3"; + +export interface TableColumn { + header: string; + width: number; + getValue: (item: T) => string; +} + +/** + * Generic table renderer for list views + * + * @example + * ```ts + * const columns: TableColumn[] = [ + * { header: "ID", width: 40, getValue: (a) => chalk.cyan(a.id) }, + * { header: "Name", width: 28, getValue: (a) => a.name }, + * ]; + * printTable(assistants, columns); + * ``` + */ +export function printTable(items: T[], columns: TableColumn[]): void { + const table = new Table({ + head: columns.map((col) => chalk.bold(col.header)), + colWidths: columns.map((col) => col.width), + wordWrap: true, + }); + + for (const item of items) { + table.push(columns.map((col) => col.getValue(item))); + } + + console.log(table.toString()); +} + +export interface DetailRow { + label: string; + value: string; +} + +/** + * Generic detail renderer for entity detail views + * + * @example + * ```ts + * const rows: DetailRow[] = [ + * { label: "ID", value: chalk.cyan(assistant.id) }, + * { label: "Name", value: assistant.name }, + * ]; + * printDetail(rows); + * ``` + */ +export function printDetail( + rows: DetailRow[], + options: { labelWidth?: number; valueWidth?: number } = {}, +): void { + const { labelWidth = 18, valueWidth = 60 } = options; + + const table = new Table({ + colWidths: [labelWidth, valueWidth], + wordWrap: true, + }); + + for (const row of rows) { + table.push([chalk.bold(row.label), row.value]); + } + + console.log("\n" + table.toString()); +} + +/** + * Format optional field with fallback to dim dash + */ +export function optional(value: string | null | undefined): string { + return value ?? chalk.dim("—"); +} + +/** + * Format boolean as yes/no with color + */ +export function yesNo(value: boolean | null | undefined): string { + if (value === true) return chalk.green("yes"); + if (value === false) return chalk.dim("no"); + return chalk.dim("—"); +} + +/** + * Format status with color coding + */ +export function statusColor( + status: string | null | undefined, + colorMap: Record string> = {}, +): string { + if (!status) return chalk.dim("—"); + + const defaultMap: Record string> = { + completed: chalk.green, + success: chalk.green, + failed: chalk.red, + error: chalk.red, + pending: chalk.yellow, + in_progress: chalk.yellow, + ...colorMap, + }; + + const colorFn = defaultMap[status.toLowerCase()] || ((s: string) => s); + return colorFn(status); +} + +export interface EmptyStateInstructions { + message?: string; + commands?: string[]; + locations?: string[]; + helpUrl?: string; +} + +/** + * Generic empty state message with optional helpful instructions + * + * @example + * ```ts + * printEmpty('assistants', { + * message: 'Create assistants to get started', + * commands: ['codemie sdk assistants create --data @assistant.json'], + * helpUrl: 'https://docs.codemie.ai/assistants' + * }); + * ``` + */ +export function printEmpty( + entityType: string, + instructions?: EmptyStateInstructions, +): void { + console.log(chalk.yellow(`\nNo ${entityType} found.`)); + + if (!instructions) { + return; + } + + // Optional custom message + if (instructions.message) { + console.log(chalk.white(instructions.message)); + console.log(""); + } + + // Commands to run + if (instructions.commands && instructions.commands.length > 0) { + console.log(chalk.white("Create new with:")); + for (const cmd of instructions.commands) { + console.log(` ${chalk.cyan(cmd)}`); + } + console.log(""); + } + + // Locations/paths + if (instructions.locations && instructions.locations.length > 0) { + console.log(chalk.white("Or manage via:")); + for (const loc of instructions.locations) { + console.log(` • ${chalk.cyan(loc)}`); + } + console.log(""); + } + + // Help URL + if (instructions.helpUrl) { + console.log(chalk.white("Learn more:")); + console.log(` ${chalk.cyan(instructions.helpUrl)}`); + console.log(""); + } +} + +/** + * Generic list header + */ +export function printListHeader(entityType: string, count: number): void { + console.log(chalk.bold(`\n${entityType} (${count})`)); +} + +/** + * Generic success message for create/update/delete operations + */ +export function printSuccess(message: string): void { + console.log(chalk.green(`✓ ${message}`)); +} + +/** + * Print additional info line (dimmed) + */ +export function printInfo(message: string): void { + console.log(chalk.dim(` ${message}`)); +} diff --git a/src/cli/commands/sdk/workflows.ts b/src/cli/commands/sdk/workflows.ts new file mode 100644 index 00000000..b4421102 --- /dev/null +++ b/src/cli/commands/sdk/workflows.ts @@ -0,0 +1,250 @@ +import { Command } from "commander"; +import chalk from "chalk"; +import ora from "ora"; +import type { + Workflow, + WorkflowCreateParams, + WorkflowUpdateParams, +} from "codemie-sdk"; +import { + listWorkflows, + getWorkflow, + createWorkflow, + updateWorkflow, + deleteWorkflow, +} from "./services/workflows.js"; +import { + getSdkClient, + parseDataOrJsonFile, + parseConfigInput, + outputJson, + handleSdkError, + getResponseMessage, +} from "./utils/cli-utils.js"; +import { + printTable, + printDetail, + printEmpty, + printListHeader, + printSuccess, + optional, + yesNo, + type TableColumn, + type DetailRow, +} from "./utils/render.js"; + +export function createWorkflowsSubcommand(): Command { + const cmd = new Command("workflows").description("Manage CodeMie workflows"); + + cmd + .command("list") + .description( + "List workflows visible to the current user\n" + + "Examples:\n" + + " $ codemie workflows list\n" + + " $ codemie workflows list --page 2 --per-page 25\n" + + " $ codemie workflows list --search 'My Workflow' --project MyProject --json", + ) + .option("--json", "Output in JSON format") + .option("--page ", "Page number (starts at 0)", "0") + .option("--per-page ", "Results per page (1-100)", "10") + .option("--search ", "Search by name or description") + .option("--projects ", "Filter by project name (comma-separated)") + .action(async (opts) => { + const client = await getSdkClient(); + const spinner = ora("Fetching workflows...").start(); + + try { + const params: Record = { + page: parseInt(opts.page, 10), + per_page: parseInt(opts.perPage, 10), + }; + + if (opts.search) { + params.search = opts.search; + } + if (opts.projects) { + params.projects = opts.projects.trim().split(","); + } + + const items = await listWorkflows(client, params); + + spinner.stop(); + + if (opts.json) { + outputJson(items); + return; + } + + if (items.length === 0) { + printEmpty("workflows"); + return; + } + + printListHeader("Workflows", items.length); + + const columns: TableColumn[] = [ + { header: "ID", width: 40, getValue: (w) => chalk.cyan(w.id) }, + { header: "Name", width: 26, getValue: (w) => w.name }, + { + header: "Project", + width: 20, + getValue: (w) => optional(w.project), + }, + { header: "Mode", width: 14, getValue: (w) => optional(w.mode) }, + { header: "Shared", width: 8, getValue: (w) => yesNo(w.shared) }, + ]; + printTable(items, columns); + } catch (error) { + spinner.stop(); + handleSdkError(error, "list workflows"); + } + }); + + cmd + .command("get ") + .description("Get detailed information about a specific workflow") + .option("--json", "Output in JSON format") + .action(async (id: string, opts) => { + const client = await getSdkClient(); + const spinner = ora("Fetching workflow...").start(); + + try { + const item = await getWorkflow(client, id); + spinner.stop(); + + if (opts.json) { + outputJson(item); + return; + } + + const rows: DetailRow[] = [ + { label: "ID", value: chalk.cyan(item.id) }, + { label: "Name", value: item.name }, + { label: "Project", value: optional(item.project) }, + { label: "Mode", value: optional(item.mode) }, + { label: "Description", value: optional(item.description) }, + { label: "Shared", value: yesNo(item.shared) }, + { + label: "Creator", + value: optional(item.created_by?.name), + }, + ]; + + if (item.update_date) { + rows.push({ label: "Updated", value: item.update_date }); + } + + printDetail(rows); + } catch (error) { + spinner.stop(); + handleSdkError(error, "get workflow"); + } + }); + + cmd + .command("create") + .description( + "Create a new workflow with the specified configuration\n" + + "Examples:\n" + + ' $ codemie workflows create --data \'{"name":"My Workflow","description":"Custom workflow"}\' --config @workflow.yaml\n' + + ' $ codemie workflows create --json path/to/workflow.json --config @workflow.yaml\n', + ) + .option( + "--data ", + "Workflow configuration as inline JSON string", + ) + .option( + "--json ", + "Path to JSON file with workflow configuration", + ) + .option( + "--config ", + "Workflow YAML config as string or @file.yaml path", + ) + .action(async (opts) => { + const client = await getSdkClient(); + const spinner = ora("Creating workflow...").start(); + + try { + const data = await parseDataOrJsonFile(opts.data, opts.json); + const config = opts.config + ? await parseConfigInput(opts.config) + : undefined; + const result = await createWorkflow( + client, + data as WorkflowCreateParams, + config, + ); + spinner.stop(); + + printSuccess(getResponseMessage(result)); + } catch (error) { + spinner.stop(); + handleSdkError(error, "create workflow"); + } + }); + + cmd + .command("update ") + .description( + "Update an existing workflow's configuration\n" + + "Examples:\n" + + ' $ codemie workflows update wfl_abc123 --data \'{"name":"Updated Name"}\' --config @workflow.yaml\n' + + ' $ codemie workflows update wfl_abc123 --json path/to/update.json --config @workflow.yaml\n', + ) + .option( + "--data ", + "Fields to update as inline JSON string", + ) + .option( + "--json ", + "Path to JSON file with fields to update", + ) + .option( + "--config ", + "Workflow YAML config as string or @file.yaml path", + ) + .action(async (id: string, opts) => { + const client = await getSdkClient(); + const spinner = ora("Updating workflow...").start(); + + try { + const data = await parseDataOrJsonFile(opts.data, opts.json); + const config = opts.config + ? await parseConfigInput(opts.config) + : undefined; + const result = await updateWorkflow( + client, + id, + data as WorkflowUpdateParams, + config, + ); + spinner.stop(); + + printSuccess(getResponseMessage(result)); + } catch (error) { + spinner.stop(); + handleSdkError(error, "update workflow"); + } + }); + + cmd + .command("delete ") + .description("Permanently delete a workflow") + .action(async (id: string) => { + const client = await getSdkClient(); + const spinner = ora("Deleting workflow...").start(); + + try { + await deleteWorkflow(client, id); + spinner.stop(); + printSuccess(`✓ Workflow ${chalk.cyan(id)} deleted.`); + } catch (error) { + spinner.stop(); + handleSdkError(error, "delete workflow"); + } + }); + + return cmd; +} diff --git a/src/cli/index.ts b/src/cli/index.ts index 9278c2e8..7e735c41 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -32,6 +32,7 @@ import { createOpencodeMetricsCommand } from './commands/opencode-metrics.js'; import { createTestMetricsCommand } from './commands/test-metrics.js'; import { createModelsCommand } from './commands/models.js'; import { createAssistantsCommand } from './commands/assistants/index.js'; +import { createSdkCommand } from './commands/sdk/index.js'; import { createMcpCommand } from './commands/mcp/index.js'; import { createMcpProxyCommand } from './commands/mcp-proxy.js'; import { createProxyCommand } from './commands/proxy/index.js'; @@ -90,6 +91,7 @@ program.addCommand(createPluginCommand()); program.addCommand(createOpencodeMetricsCommand()); program.addCommand(createTestMetricsCommand()); program.addCommand(createModelsCommand()); +program.addCommand(createSdkCommand()); program.addCommand(createMcpCommand()); program.addCommand(createMcpProxyCommand()); program.addCommand(createProxyCommand()); From 550ff0f5841cceac1b594159362632b17cdaeee8 Mon Sep 17 00:00:00 2001 From: Kostiantyn Pshenychnyi Date: Thu, 23 Apr 2026 10:26:25 +0300 Subject: [PATCH 02/13] feat: add new datasource types --- .../claude/plugin/skills/codemie-sdk/SKILL.md | 2 +- .../codemie-sdk/examples/datasources.md | 141 +++++++++++++++++- .../codemie-sdk/examples/integrations.md | 97 +++++++++++- src/cli/commands/sdk/services/datasources.ts | 138 +++++++++++++++++ .../commands/sdk/utils/datasource-types.ts | 37 +++++ 5 files changed, 409 insertions(+), 6 deletions(-) diff --git a/src/agents/plugins/claude/plugin/skills/codemie-sdk/SKILL.md b/src/agents/plugins/claude/plugin/skills/codemie-sdk/SKILL.md index 2c5ad060..745f82c7 100644 --- a/src/agents/plugins/claude/plugin/skills/codemie-sdk/SKILL.md +++ b/src/agents/plugins/claude/plugin/skills/codemie-sdk/SKILL.md @@ -113,7 +113,7 @@ codemie sdk workflows delete > See [examples/datasources.md](examples/datasources.md) for full field reference and examples. -Datasources use **type subcommands** for create/update: `confluence`, `jira`, `file`, `code`, `google`, `json`, `provider`, `summary`, `chunk-summary` +Datasources use **type subcommands** for create/update: `confluence`, `jira`, `file`, `code`, `google`, `json`, `provider`, `summary`, `chunk-summary`, `azure-devops-wiki`, `azure-devops-work-item`, `xray`, `sharepoint`, `platform` ```bash codemie sdk datasources list [--search ] [--projects ] [--status ] [--datasource-types ] [--sort-key date|update_date] [--sort-order asc|desc] [--page ] [--per-page ] [--json] diff --git a/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/datasources.md b/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/datasources.md index cf662c86..2057986e 100644 --- a/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/datasources.md +++ b/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/datasources.md @@ -1,6 +1,6 @@ # Datasources Examples -> **Important:** `create` and `update` require a **type subcommand**: `confluence`, `jira`, `file`, `code`, `google` +> **Important:** `create` and `update` require a **type subcommand**: `confluence`, `jira`, `file`, `code`, `google`, `azure-devops-wiki`, `azure-devops-work-item`, `xray`, `sharepoint`, `platform` > > **Name constraint:** must match `^[a-zA-Z0-9][\w-]*$` — no spaces, use hyphens (e.g. `my-wiki`, not `My Wiki`) @@ -198,6 +198,10 @@ codemie sdk datasources update jira --data '{"name":"support-tickets","proj codemie sdk datasources update file --data '{"name":"team-docs","project_name":"Engineering","description":"Updated docs"}' codemie sdk datasources update code --json updates.json # Output: ✓ Incremental reindexing of datasource has been started in the background +codemie sdk datasources update azure-devops-wiki --data '{"organization":"my-org","project":"my-project","wiki_name":"updated-wiki"}' +codemie sdk datasources update azure-devops-work-item --data '{"wiql_query":"SELECT [Id],[Title] FROM WorkItems WHERE [System.State]!='\''Closed'\''"}' +codemie sdk datasources update xray --data '{"jql":"project=QA AND issuetype in testExecutions()"}' +codemie sdk datasources update sharepoint --data '{"include_lists":true,"skip_reindex":true}' ``` **Update-only reindex flags** (add to any update payload): @@ -228,7 +232,7 @@ codemie sdk datasources delete After creating a datasource, attach it to an assistant via the assistant's `context` field. -- Use `context_type: "knowledge_base"` for file, Confluence, Jira, and Google datasources. +- Use `context_type: "knowledge_base"` for file, Confluence, Jira, Google, Azure DevOps Wiki, Azure DevOps Work Item, Xray, SharePoint, and JSON datasources. - Use `context_type: "code"` for code repository datasources. ```bash @@ -244,6 +248,126 @@ codemie sdk assistants update --data "{ > See [Assistants — Linking a Datasource](assistants.md#linking-a-datasource) for full details. +### Azure DevOps Wiki + +> Requires an **AzureDevOps integration** already configured in the project. +> Get the integration ID: `codemie sdk integrations list --setting-type project --json | jq -r '.[] | select(.credential_type=="AzureDevOps") | "\(.id) \(.alias)"'` + +```bash +codemie sdk datasources create azure-devops-wiki --data '{ + "name": "ado-wiki", + "project_name": "Engineering", + "organization": "my-org", + "project": "my-project", + "wiki_name": "my-wiki", + "description": "Azure DevOps team wiki", + "shared_with_project": true +}' +``` + +**Azure DevOps Wiki-specific fields:** + +| Field | Required | Description | +|-------|----------|-------------| +| `organization` | — | Azure DevOps organization name | +| `project` | — | Azure DevOps project name | +| `wiki_name` | — | Name of the wiki to index | +| `wiki_query` | — | Query to filter wiki pages | + +### Azure DevOps Work Item + +> Requires an **AzureDevOps integration** already configured in the project. + +```bash +codemie sdk datasources create azure-devops-work-item --data '{ + "name": "ado-work-items", + "project_name": "Engineering", + "organization": "my-org", + "project": "my-project", + "wiql_query": "SELECT [Id],[Title],[State] FROM WorkItems WHERE [System.TeamProject]=@project AND [System.State]!='\''Closed'\''", + "description": "Azure DevOps work items", + "shared_with_project": true +}' +``` + +**Azure DevOps Work Item-specific fields:** + +| Field | Required | Description | +|-------|----------|-------------| +| `organization` | — | Azure DevOps organization name | +| `project` | — | Azure DevOps project name | +| `wiql_query` | — | WIQL query to filter work items — e.g. `SELECT [Id],[Title] FROM WorkItems WHERE [System.State]!='Closed'` | + +### Xray + +> Requires a **Jira integration** already configured (Xray is a Jira plugin). +> Get the integration ID: `codemie sdk integrations list --setting-type project --json | jq -r '.[] | select(.credential_type=="Jira") | "\(.id) \(.alias)"'` + +```bash +codemie sdk datasources create xray --data '{ + "name": "xray-tests", + "project_name": "QA", + "jql": "project=QA AND issuetype in testExecutions()", + "description": "Xray test execution data", + "shared_with_project": true +}' +``` + +**Xray-specific fields:** + +| Field | Required | Description | +|-------|----------|-------------| +| `jql` | ✅ | Jira Query Language to filter Xray test issues | + +### SharePoint + +> Requires a **SharePoint integration** or OAuth credentials. + +```bash +codemie sdk datasources create sharepoint --data '{ + "name": "sharepoint-docs", + "project_name": "Engineering", + "site_url": "https://company.sharepoint.com/sites/team", + "include_pages": true, + "include_documents": true, + "description": "SharePoint team site", + "shared_with_project": true +}' +``` + +**SharePoint-specific fields:** + +| Field | Required | Description | +|-------|----------|-------------| +| `site_url` | ✅ | SharePoint site URL (must start with `https://`) | +| `include_pages` | — | Index SharePoint site pages | +| `include_documents` | — | Index document libraries | +| `include_lists` | — | Index list items | +| `max_file_size_mb` | — | Maximum file size in MB to index (1–500, default 50) | +| `files_filter` | — | Gitignore-style file/path filter | +| `auth_type` | — | Authentication method: `integration`, `oauth_codemie`, or `oauth_custom` | +| `access_token` | — | OAuth access token (required for `oauth_codemie` and `oauth_custom`) | +| `oauth_client_id` | — | Azure app client ID (`oauth_custom` only) | +| `oauth_tenant_id` | — | Azure AD tenant ID (`oauth_custom` only) | +| `embedding_model` | — | Override default embedding model | +| `cron_expression` | — | Cron expression for scheduled reindexing | + +**Full SharePoint example:** +```json +{ + "name": "team-sharepoint", + "project_name": "Engineering", + "site_url": "https://company.sharepoint.com/sites/engineering", + "include_pages": true, + "include_documents": true, + "include_lists": false, + "max_file_size_mb": 50, + "auth_type": "integration", + "description": "Engineering SharePoint site", + "shared_with_project": true +} +``` + ### JSON Knowledge Base ```bash @@ -288,7 +412,18 @@ codemie sdk datasources create chunk-summary --data '{ }' ``` -**Fields for json, provider, summary, chunk-summary (base fields only):** +### Platform + +```bash +codemie sdk datasources create platform --data '{ + "name": "platform-assistant", + "project_name": "Team", + "description": "Platform marketplace assistant", + "shared_with_project": true +}' +``` + +**Fields for json, provider, summary, chunk-summary, platform (base fields only):** | Field | Required | Description | |-------|----------|-------------| diff --git a/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/integrations.md b/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/integrations.md index 5396909f..68c56bad 100644 --- a/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/integrations.md +++ b/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/integrations.md @@ -69,7 +69,7 @@ codemie sdk integrations create --json jira-integration.json | `enabled` | — | `false` = disable the integration without deleting it (default: `true`) | | `external_id` | — | External system identifier for cross-referencing with other tools | -**Common `credential_type` values:** `Jira`, `Confluence`, `Git`, `AWS`, `GCP`, `Azure`, `Keycloak`, `Elastic`, `OpenAPI`, `Webhook`, `SQL`, `MCP`, `LiteLLM`, `AzureDevOps`, `ServiceNow`, `Telegram` +**All supported `credential_type` values:** `Jira`, `Confluence`, `Git`, `Kubernetes`, `AWS`, `GCP`, `Azure`, `Keycloak`, `Elastic`, `OpenAPI`, `Plugin`, `FileSystem`, `Scheduler`, `Webhook`, `Email`, `AzureDevOps`, `Sonar`, `SQL`, `Telegram`, `ZephyrScale`, `ZephyrSquad`, `ServiceNow`, `DIAL`, `A2A`, `MCP`, `LiteLLM`, `ReportPortal`, `Xray`, `SharePoint` > **Important:** `credential_values` **must include an `alias` key** with the same value as the top-level `alias` field, otherwise the API returns an error. Always add `{"key": "alias", "value": ""}` to the array. @@ -115,7 +115,100 @@ codemie sdk integrations create --json jira-integration.json "setting_type": "user", "credential_values": [ {"key": "base_url", "value": "http://localhost:4000"}, - {"key": "api_key", "value": "sk-master-key"} + {"key": "api_key", "value": "sk-master-key"}, + {"key": "alias", "value": "litellm-proxy"} + ] +} +``` + +**AzureDevOps:** +```json +{ + "credential_type": "AzureDevOps", + "project_name": "Engineering", + "alias": "ado-main", + "setting_type": "project", + "credential_values": [ + {"key": "url", "value": "https://dev.azure.com/my-org"}, + {"key": "token", "value": "your-pat-token"}, + {"key": "alias", "value": "ado-main"} + ] +} +``` + +**SharePoint:** +```json +{ + "credential_type": "SharePoint", + "project_name": "Engineering", + "alias": "sharepoint-main", + "setting_type": "project", + "credential_values": [ + {"key": "site_url", "value": "https://company.sharepoint.com/sites/team"}, + {"key": "client_id", "value": "your-client-id"}, + {"key": "client_secret", "value": "your-client-secret"}, + {"key": "tenant_id", "value": "your-tenant-id"}, + {"key": "alias", "value": "sharepoint-main"} + ] +} +``` + +**Xray:** +```json +{ + "credential_type": "Xray", + "project_name": "QA", + "alias": "xray-main", + "setting_type": "project", + "credential_values": [ + {"key": "client_id", "value": "your-xray-client-id"}, + {"key": "client_secret", "value": "your-xray-client-secret"}, + {"key": "alias", "value": "xray-main"} + ] +} +``` + +**ZephyrScale:** +```json +{ + "credential_type": "ZephyrScale", + "project_name": "QA", + "alias": "zephyr-scale", + "setting_type": "user", + "credential_values": [ + {"key": "url", "value": "https://company.atlassian.net"}, + {"key": "token", "value": "your-zephyr-api-token"}, + {"key": "alias", "value": "zephyr-scale"} + ] +} +``` + +**ServiceNow:** +```json +{ + "credential_type": "ServiceNow", + "project_name": "ITSM", + "alias": "servicenow-main", + "setting_type": "project", + "credential_values": [ + {"key": "url", "value": "https://company.service-now.com"}, + {"key": "username", "value": "api-user"}, + {"key": "password", "value": "api-password"}, + {"key": "alias", "value": "servicenow-main"} + ] +} +``` + +**MCP:** +```json +{ + "credential_type": "MCP", + "project_name": "AI", + "alias": "mcp-server", + "setting_type": "user", + "credential_values": [ + {"key": "url", "value": "http://localhost:3000"}, + {"key": "alias", "value": "mcp-server"} ] } ``` diff --git a/src/cli/commands/sdk/services/datasources.ts b/src/cli/commands/sdk/services/datasources.ts index 9b873255..8ce7373e 100644 --- a/src/cli/commands/sdk/services/datasources.ts +++ b/src/cli/commands/sdk/services/datasources.ts @@ -1,5 +1,9 @@ import type { CodeMieClient, + AzureDevOpsWikiDataSourceCreateParams, + AzureDevOpsWikiDataSourceUpdateParams, + AzureDevOpsWorkItemDataSourceCreateParams, + AzureDevOpsWorkItemDataSourceUpdateParams, ConfluenceDataSourceCreateParams, ConfluenceDataSourceUpdateParams, DataSource, @@ -11,6 +15,10 @@ import type { JiraDataSourceUpdateParams, OtherDataSourceCreateParams, OtherDataSourceUpdateParams, + SharePointDataSourceCreateParams, + SharePointDataSourceUpdateParams, + XrayDataSourceCreateParams, + XrayDataSourceUpdateParams, } from "codemie-sdk"; import { readFilesFromPaths } from "../utils/file-utils.js"; @@ -298,6 +306,136 @@ export async function updateChunkSummaryDatasource( return client.datasources.update(params); } +// AZURE DEVOPS WIKI +export async function createAzureDevOpsWikiDatasource( + client: CodeMieClient, + data: Omit, +): Promise { + return client.datasources.create({ + ...data, + type: "knowledge_base_azure_devops_wiki", + }); +} + +export async function updateAzureDevOpsWikiDatasource( + client: CodeMieClient, + id: string, + data: Partial>, +): Promise { + const existing = await client.datasources.get(id); + const params: AzureDevOpsWikiDataSourceUpdateParams = { + type: "knowledge_base_azure_devops_wiki", + name: existing.name, + project_name: existing.project_name, + ...data, + }; + return client.datasources.update(params); +} + +// AZURE DEVOPS WORK ITEM +export async function createAzureDevOpsWorkItemDatasource( + client: CodeMieClient, + data: Omit, +): Promise { + return client.datasources.create({ + ...data, + type: "knowledge_base_azure_devops_work_item", + }); +} + +export async function updateAzureDevOpsWorkItemDatasource( + client: CodeMieClient, + id: string, + data: Partial>, +): Promise { + const existing = await client.datasources.get(id); + const params: AzureDevOpsWorkItemDataSourceUpdateParams = { + type: "knowledge_base_azure_devops_work_item", + name: existing.name, + project_name: existing.project_name, + ...data, + }; + return client.datasources.update(params); +} + +// XRAY +export async function createXrayDatasource( + client: CodeMieClient, + data: Omit, +): Promise { + return client.datasources.create({ + ...data, + type: "knowledge_base_xray", + }); +} + +export async function updateXrayDatasource( + client: CodeMieClient, + id: string, + data: Partial>, +): Promise { + const existing = await client.datasources.get(id); + const params: XrayDataSourceUpdateParams = { + type: "knowledge_base_xray", + name: existing.name, + project_name: existing.project_name, + ...data, + }; + return client.datasources.update(params); +} + +// SHAREPOINT +export async function createSharepointDatasource( + client: CodeMieClient, + data: Omit, +): Promise { + return client.datasources.create({ + ...data, + type: "knowledge_base_sharepoint", + }); +} + +export async function updateSharepointDatasource( + client: CodeMieClient, + id: string, + data: Partial>, +): Promise { + const existing = await client.datasources.get(id); + const params: SharePointDataSourceUpdateParams = { + type: "knowledge_base_sharepoint", + name: existing.name, + project_name: existing.project_name, + ...data, + }; + return client.datasources.update(params); +} + +// PLATFORM +export async function createPlatformDatasource( + client: CodeMieClient, + data: Omit, +): Promise { + return client.datasources.create({ + ...data, + type: "platform_marketplace_assistant", + }); +} + +export async function updatePlatformDatasource( + client: CodeMieClient, + id: string, + data: Partial>, +): Promise { + const existing = await client.datasources.get(id); + const params: OtherDataSourceUpdateParams = { + type: "platform_marketplace_assistant", + name: existing.name, + project_name: existing.project_name, + ...data, + }; + return client.datasources.update(params); +} + export async function deleteDatasource( client: CodeMieClient, datasourceId: string, diff --git a/src/cli/commands/sdk/utils/datasource-types.ts b/src/cli/commands/sdk/utils/datasource-types.ts index 3f4680cf..24a33d1f 100644 --- a/src/cli/commands/sdk/utils/datasource-types.ts +++ b/src/cli/commands/sdk/utils/datasource-types.ts @@ -71,4 +71,41 @@ export const DATASOURCE_TYPES: DatasourceTypeConfig[] = [ example: '{"name":"my-chunk-summary","project_name":"Team","description":"Chunk summary datasource","shared_with_project":true}', }, + { + command: "azure-devops-wiki", + serviceKey: "azureDevOpsWiki", + type: "knowledge_base_azure_devops_wiki", + description: "Azure DevOps Wiki datasource", + example: + '{"name":"ado-wiki","project_name":"Engineering","organization":"my-org","project":"my-project","wiki_name":"my-wiki","description":"Team wiki","shared_with_project":true}', + }, + { + command: "azure-devops-work-item", + serviceKey: "azureDevOpsWorkItem", + type: "knowledge_base_azure_devops_work_item", + description: "Azure DevOps Work Item datasource", + example: + '{"name":"ado-work-items","project_name":"Engineering","organization":"my-org","project":"my-project","wiql_query":"SELECT [Id],[Title] FROM WorkItems WHERE [System.TeamProject]=@project","description":"ADO work items","shared_with_project":true}', + }, + { + command: "xray", + type: "knowledge_base_xray", + description: "Xray test management datasource", + example: + '{"name":"xray-tests","project_name":"QA","jql":"project=QA AND issuetype in testExecutions()","description":"Xray test data","shared_with_project":true}', + }, + { + command: "sharepoint", + type: "knowledge_base_sharepoint", + description: "SharePoint datasource", + example: + '{"name":"sharepoint-docs","project_name":"Engineering","site_url":"https://company.sharepoint.com/sites/team","include_pages":true,"include_documents":true,"description":"SharePoint team site","shared_with_project":true}', + }, + { + command: "platform", + type: "platform_marketplace_assistant", + description: "Platform marketplace assistant datasource", + example: + '{"name":"platform-assistant","project_name":"Team","description":"Platform marketplace assistant","shared_with_project":true}', + }, ]; From 3dcb061afe5290e0c47f6d0ef38afbe45cd1681f Mon Sep 17 00:00:00 2001 From: Kostiantyn Pshenychnyi Date: Thu, 23 Apr 2026 16:09:23 +0300 Subject: [PATCH 03/13] feat: add skills, users --- .../claude/plugin/skills/codemie-sdk/SKILL.md | 37 ++++- .../skills/codemie-sdk/examples/skills.md | 49 +++++++ .../skills/codemie-sdk/examples/users.md | 32 +++++ src/cli/commands/sdk/index.ts | 6 +- src/cli/commands/sdk/services/skills.ts | 17 +++ src/cli/commands/sdk/services/users.ts | 11 ++ src/cli/commands/sdk/skills.ts | 134 ++++++++++++++++++ src/cli/commands/sdk/users.ts | 90 ++++++++++++ 8 files changed, 373 insertions(+), 3 deletions(-) create mode 100644 src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/skills.md create mode 100644 src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/users.md create mode 100644 src/cli/commands/sdk/services/skills.ts create mode 100644 src/cli/commands/sdk/services/users.ts create mode 100644 src/cli/commands/sdk/skills.ts create mode 100644 src/cli/commands/sdk/users.ts diff --git a/src/agents/plugins/claude/plugin/skills/codemie-sdk/SKILL.md b/src/agents/plugins/claude/plugin/skills/codemie-sdk/SKILL.md index 745f82c7..b6f24c3e 100644 --- a/src/agents/plugins/claude/plugin/skills/codemie-sdk/SKILL.md +++ b/src/agents/plugins/claude/plugin/skills/codemie-sdk/SKILL.md @@ -1,10 +1,11 @@ --- name: codemie-sdk description: >- - Manage CodeMie platform assets (assistants, workflows, datasources, integrations) directly from CLI + Manage CodeMie platform assets (assistants, workflows, datasources, integrations, skills, users) directly from CLI using CodeMie SDK. Use when user says "create assistant", "list workflows", "update datasource", "delete assistant", "show my assistants", "get workflow details", "manage integrations", "create integration", "list integrations", "list llm models", "list embedding models", + "list skills", "get skill", "who am i", "current user", "my profile", "user info", or any request to manage CodeMie platform resources. --- @@ -12,7 +13,7 @@ description: >- Manage CodeMie platform assets from the CLI. -**Asset Types:** `assistants`, `workflows`, `datasources`, `integrations` +**Asset Types:** `assistants`, `workflows`, `datasources`, `integrations`, `skills`, `users` **Operations:** `list`, `get`, `create`, `update`, `delete` @@ -42,6 +43,8 @@ This applies to **all asset types**: assistants, workflows, datasources, and int | Workflows | [examples/workflows.md](examples/workflows.md) | | Datasources | [examples/datasources.md](examples/datasources.md) | | Integrations | [examples/integrations.md](examples/integrations.md) | +| Skills | [examples/skills.md](examples/skills.md) | +| Users | [examples/users.md](examples/users.md) | Do **not** guess field names or skip this step — all required/optional fields, nested schemas, and asset cross-reference commands are documented there. @@ -168,3 +171,33 @@ codemie sdk llm list --embeddings [--json] Returns `LLMModel` objects. Key fields: `base_name`, `label`, `provider`, `default`, `enabled`. Use `base_name` when setting `llm_model_type` on an assistant or `embeddings_model`/`summarization_model` on a datasource. + +--- + +## Skills + +> See [examples/skills.md](examples/skills.md) for full field reference and examples. + +```bash +codemie sdk skills list [--scope marketplace|project|project_with_marketplace] [--page ] [--per-page ] [--json] +codemie sdk skills get [--json] +``` + +**Key fields:** `id`, `name`, `project`, `visibility`, `description`, `content`, `created_by`, `created_date` + +**`--scope` values:** `marketplace`, `project`, `project_with_marketplace` + +--- + +## Users + +> See [examples/users.md](examples/users.md) for full field reference and examples. + +```bash +codemie sdk users me [--json] +codemie sdk users data [--json] +``` + +**`users me`** — current user profile. Fields: `name`, `username`, `applications`, `picture` + +**`users data`** — user preferences and metadata. Fields: `id`, `user_id`, `date`, `update_date`, `sidebar_view`, `stt_support` diff --git a/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/skills.md b/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/skills.md new file mode 100644 index 00000000..8912c442 --- /dev/null +++ b/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/skills.md @@ -0,0 +1,49 @@ +# Skills Examples + +## List + +```bash +# Default list (first 10) +codemie sdk skills list + +# Paginate +codemie sdk skills list --page 2 --per-page 25 + +# Filter by scope +codemie sdk skills list --scope marketplace +codemie sdk skills list --scope project +codemie sdk skills list --scope project_with_marketplace + +# JSON output +codemie sdk skills list --json +``` + +**`--scope` values:** `marketplace`, `project`, `project_with_marketplace` (default returns all accessible) + +**List columns:** ID, Name, Project, Visibility + +**JSON fields (list):** `id`, `name`, `project`, `visibility` + +## Get + +```bash +codemie sdk skills get 3d5b188f-185b-48df-b4b3-e608e4efb1ad +codemie sdk skills get 3d5b188f-185b-48df-b4b3-e608e4efb1ad --json +``` + +**Detail fields:** `id`, `name`, `project`, `visibility`, `description`, `created_by` (with `name`), `created_date`, `updated_date`, `content` + +> Note: `content` contains the full skill markdown text (SKILL.md body). + +## Scripting + +```bash +# Find skill ID by name +codemie sdk skills list --json | jq -r '.[] | select(.name == "my-skill") | .id' + +# List all public skills +codemie sdk skills list --scope marketplace --json | jq -r '.[] | "\(.id) \(.name)"' + +# Get skill content +codemie sdk skills get --json | jq -r '.content' +``` diff --git a/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/users.md b/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/users.md new file mode 100644 index 00000000..fd7cccb3 --- /dev/null +++ b/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/users.md @@ -0,0 +1,32 @@ +# Users Examples + +## Get current user profile + +```bash +codemie sdk users me +codemie sdk users me --json +``` + +**JSON fields:** `name`, `username`, `applications`, `picture` + +## Get current user data + +```bash +codemie sdk users data +codemie sdk users data --json +``` + +**JSON fields:** `id`, `user_id`, `date`, `update_date`, `sidebar_view`, `stt_support` + +## Scripting + +```bash +# Get your username +codemie sdk users me --json | jq -r '.username' + +# Get your user UUID (from user data, not profile) +codemie sdk users data --json | jq -r '.user_id' + +# Get list of projects you have access to +codemie sdk users me --json | jq -r '.applications[]' +``` diff --git a/src/cli/commands/sdk/index.ts b/src/cli/commands/sdk/index.ts index a1e2cdc0..4297a8f4 100644 --- a/src/cli/commands/sdk/index.ts +++ b/src/cli/commands/sdk/index.ts @@ -4,12 +4,14 @@ import { createWorkflowsSubcommand } from './workflows.js'; import { createDatasourcesSubcommand } from './datasources.js'; import { createIntegrationsSubcommand } from './integrations.js'; import { createLlmModelsSubcommand } from './llm.js'; +import { createSkillsSubcommand } from './skills.js'; +import { createUsersSubcommand } from './users.js'; export function createSdkCommand(): Command { const cmd = new Command('sdk'); cmd.description( - 'Manage CodeMie platform assets (assistants, workflows, datasources, integrations) via the SDK' + 'Manage CodeMie platform assets (assistants, workflows, datasources, integrations, skills, users, executions) via the SDK' ); cmd.addCommand(createAssistantsSubcommand()); @@ -17,6 +19,8 @@ export function createSdkCommand(): Command { cmd.addCommand(createDatasourcesSubcommand()); cmd.addCommand(createIntegrationsSubcommand()); cmd.addCommand(createLlmModelsSubcommand()); + cmd.addCommand(createSkillsSubcommand()); + cmd.addCommand(createUsersSubcommand()); return cmd; } diff --git a/src/cli/commands/sdk/services/skills.ts b/src/cli/commands/sdk/services/skills.ts new file mode 100644 index 00000000..12d25acc --- /dev/null +++ b/src/cli/commands/sdk/services/skills.ts @@ -0,0 +1,17 @@ +import type { CodeMieClient, SkillListItem, SkillDetail } from "codemie-sdk"; + +type SkillListParams = Parameters[0]; + +export async function listSkills( + client: CodeMieClient, + params?: SkillListParams, +): Promise { + return client.skills.list(params); +} + +export async function getSkill( + client: CodeMieClient, + skillId: string, +): Promise { + return client.skills.get(skillId); +} diff --git a/src/cli/commands/sdk/services/users.ts b/src/cli/commands/sdk/services/users.ts new file mode 100644 index 00000000..48e5d8dd --- /dev/null +++ b/src/cli/commands/sdk/services/users.ts @@ -0,0 +1,11 @@ +import type { CodeMieClient, AboutUser, UserData } from "codemie-sdk"; + +export async function getUserProfile( + client: CodeMieClient, +): Promise { + return client.users.aboutMe(); +} + +export async function getUserData(client: CodeMieClient): Promise { + return client.users.getData(); +} diff --git a/src/cli/commands/sdk/skills.ts b/src/cli/commands/sdk/skills.ts new file mode 100644 index 00000000..cb646512 --- /dev/null +++ b/src/cli/commands/sdk/skills.ts @@ -0,0 +1,134 @@ +import { Command } from "commander"; +import chalk from "chalk"; +import ora from "ora"; +import type { SkillListItem, SkillDetail } from "codemie-sdk"; +import { listSkills, getSkill } from "./services/skills.js"; +import { + getSdkClient, + outputJson, + handleSdkError, +} from "./utils/cli-utils.js"; +import { + printTable, + printDetail, + printEmpty, + printListHeader, + optional, + type TableColumn, + type DetailRow, +} from "./utils/render.js"; + +export function createSkillsSubcommand(): Command { + const cmd = new Command("skills").description("Manage CodeMie skills"); + + cmd + .command("list") + .description( + "List skills accessible to the current user\n" + + "Examples:\n" + + " $ codemie sdk skills list\n" + + " $ codemie sdk skills list --page 2 --per-page 25\n" + + " $ codemie sdk skills list --scope marketplace --json", + ) + .option("--json", "Output in JSON format") + .option("--page ", "Page number (starts at 0)", "0") + .option("--per-page ", "Results per page (1-100)", "10") + .option( + "--scope ", + "Scope filter: 'marketplace', 'project', or 'project_with_marketplace'", + ) + .action(async (opts) => { + const client = await getSdkClient(); + const spinner = ora("Fetching skills...").start(); + + try { + const params: Record = { + page: parseInt(opts.page, 10), + per_page: parseInt(opts.perPage, 10), + }; + + if (opts.scope) { + params.scope = opts.scope; + } + + const items = await listSkills(client, params); + + spinner.stop(); + + if (opts.json) { + outputJson(items); + return; + } + + if (items.length === 0) { + printEmpty("skills"); + return; + } + + printListHeader("Skills", items.length); + + const columns: TableColumn[] = [ + { header: "ID", width: 40, getValue: (s) => chalk.cyan(s.id) }, + { header: "Name", width: 30, getValue: (s) => s.name }, + { header: "Project", width: 20, getValue: (s) => optional(s.project) }, + { + header: "Visibility", + width: 12, + getValue: (s) => s.visibility, + }, + ]; + printTable(items, columns); + } catch (error) { + spinner.stop(); + handleSdkError(error, "list skills"); + } + }); + + cmd + .command("get ") + .description("Get detailed information about a specific skill") + .option("--json", "Output in JSON format") + .action(async (id: string, opts) => { + const client = await getSdkClient(); + const spinner = ora("Fetching skill...").start(); + + try { + const item = await getSkill(client, id); + spinner.stop(); + + if (opts.json) { + outputJson(item); + return; + } + + const rows: DetailRow[] = [ + { label: "ID", value: chalk.cyan(item.id) }, + { label: "Name", value: item.name }, + { label: "Project", value: optional(item.project) }, + { label: "Visibility", value: item.visibility }, + { label: "Description", value: optional(item.description) }, + { + label: "Creator", + value: optional(item.created_by?.name), + }, + { label: "Created", value: item.created_date }, + ]; + + if (item.updated_date) { + rows.push({ label: "Updated", value: item.updated_date }); + } + + const detailItem = item as SkillDetail; + if (detailItem.content) { + rows.push({ label: "Content", value: detailItem.content }); + } + + printDetail(rows); + } catch (error) { + spinner.stop(); + handleSdkError(error, "get skill"); + } + }); + + return cmd; +} diff --git a/src/cli/commands/sdk/users.ts b/src/cli/commands/sdk/users.ts new file mode 100644 index 00000000..45e5711c --- /dev/null +++ b/src/cli/commands/sdk/users.ts @@ -0,0 +1,90 @@ +import { Command } from "commander"; +import chalk from "chalk"; +import ora from "ora"; +import { getUserProfile, getUserData } from "./services/users.js"; +import { + getSdkClient, + outputJson, + handleSdkError, +} from "./utils/cli-utils.js"; +import { printDetail, optional, type DetailRow } from "./utils/render.js"; + +export function createUsersSubcommand(): Command { + const cmd = new Command("users").description( + "Manage CodeMie user information", + ); + + cmd + .command("me") + .description( + "Get current user profile\n" + + "Examples:\n" + + " $ codemie sdk users me\n" + + " $ codemie sdk users me --json", + ) + .option("--json", "Output in JSON format") + .action(async (opts) => { + const client = await getSdkClient(); + const spinner = ora("Fetching user profile...").start(); + + try { + const user = await getUserProfile(client); + spinner.stop(); + + if (opts.json) { + outputJson(user); + return; + } + + const rows: DetailRow[] = [ + { label: "ID", value: chalk.cyan(user.user_id) }, + { label: "Username", value: optional(user.username) }, + { label: "Name", value: optional(user.name) }, + { label: "Admin", value: user.is_admin ? chalk.green("yes") : chalk.dim("no") }, + ]; + + printDetail(rows); + } catch (error) { + spinner.stop(); + handleSdkError(error, "get user profile"); + } + }); + + cmd + .command("data") + .description( + "Get current user data and preferences\n" + + "Examples:\n" + + " $ codemie sdk users data\n" + + " $ codemie sdk users data --json", + ) + .option("--json", "Output in JSON format") + .action(async (opts) => { + const client = await getSdkClient(); + const spinner = ora("Fetching user data...").start(); + + try { + const data = await getUserData(client); + spinner.stop(); + + if (opts.json) { + outputJson(data); + return; + } + + const rows: DetailRow[] = [ + { label: "ID", value: optional(data.id) }, + { label: "User ID", value: optional(data.user_id) }, + { label: "Created", value: optional(data.date) }, + { label: "Updated", value: optional(data.update_date) }, + ]; + + printDetail(rows); + } catch (error) { + spinner.stop(); + handleSdkError(error, "get user data"); + } + }); + + return cmd; +} From 27dc61287b90e73f9c673befc4cc92957be416cb Mon Sep 17 00:00:00 2001 From: Kostiantyn Pshenychnyi Date: Fri, 24 Apr 2026 10:43:22 +0300 Subject: [PATCH 04/13] feat: add new skills service methods, update users service response --- .../claude/plugin/skills/codemie-sdk/SKILL.md | 105 +++- .../skills/codemie-sdk/examples/assistants.md | 4 +- .../skills/codemie-sdk/examples/categories.md | 101 ++++ .../skills/codemie-sdk/examples/skills.md | 148 ++++- .../skills/codemie-sdk/examples/users.md | 14 +- src/cli/commands/sdk/categories.ts | 255 ++++++++ src/cli/commands/sdk/index.ts | 4 +- src/cli/commands/sdk/services/categories.ts | 51 ++ src/cli/commands/sdk/services/skills.ts | 148 ++++- src/cli/commands/sdk/skills.ts | 569 +++++++++++++++++- src/cli/commands/sdk/users.ts | 15 + 11 files changed, 1377 insertions(+), 37 deletions(-) create mode 100644 src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/categories.md create mode 100644 src/cli/commands/sdk/categories.ts create mode 100644 src/cli/commands/sdk/services/categories.ts diff --git a/src/agents/plugins/claude/plugin/skills/codemie-sdk/SKILL.md b/src/agents/plugins/claude/plugin/skills/codemie-sdk/SKILL.md index b6f24c3e..ec39d6fa 100644 --- a/src/agents/plugins/claude/plugin/skills/codemie-sdk/SKILL.md +++ b/src/agents/plugins/claude/plugin/skills/codemie-sdk/SKILL.md @@ -1,11 +1,13 @@ --- name: codemie-sdk description: >- - Manage CodeMie platform assets (assistants, workflows, datasources, integrations, skills, users) directly from CLI + Manage CodeMie platform assets (assistants, workflows, datasources, integrations, skills, users, categories) directly from CLI using CodeMie SDK. Use when user says "create assistant", "list workflows", "update datasource", "delete assistant", "show my assistants", "get workflow details", "manage integrations", "create integration", "list integrations", "list llm models", "list embedding models", - "list skills", "get skill", "who am i", "current user", "my profile", "user info", + "list skills", "get skill", "create skill", "update skill", "delete skill", "publish skill", + "import skill", "export skill", "attach skill", "list categories", "get category", + "create category", "delete category", "who am i", "current user", "my profile", "user info", or any request to manage CodeMie platform resources. --- @@ -13,7 +15,7 @@ description: >- Manage CodeMie platform assets from the CLI. -**Asset Types:** `assistants`, `workflows`, `datasources`, `integrations`, `skills`, `users` +**Asset Types:** `assistants`, `workflows`, `datasources`, `integrations`, `skills`, `users`, `categories` **Operations:** `list`, `get`, `create`, `update`, `delete` @@ -21,15 +23,47 @@ Manage CodeMie platform assets from the CLI. ## 🚨 Project Clarification (MANDATORY) -**Before doing any work**, check if the user has specified a project. +**Before proceeding with any work, you must determine which project to use by following these steps:** -- All asset types use `project_name` except assistants which use `project`. -- If the project is **not specified** → **ask the user** before running any commands. -- If the project **is specified** → proceed directly. +### Step 1 — Fetch the User Profile -Example prompt: *"Which CodeMie project should I use for this operation?"* +```bash +codemie sdk users me --json +``` + +This command returns `user_id`, `username`, `email`, `is_admin`, `applications`, and `applications_admin`. + +### Step 2 — Identify Available Projects + +- If `is_admin = true`: the user can access **all** projects on the platform, not just those listed in `applications`. +- If `is_admin = false`: the user can only work with projects listed in `applications`. +- The **default project** is the one matching the user's email (e.g., for `alice@acme.com`, the default project is `alice@acme.com`). + +### Step 3 — Confirm the Project Selection + +- If the user **explicitly** states a project name, or uses phrases like **"use my project"** or **"in my project"**, proceed with the identified project (use the default project for phrases like "my project" or "my default project"). +- In **all other cases**, ALWAYS ask the user which project to use. + +Aks the user what project to use with the options as follows: + +1. **Default project** — `` *(personal default project)* +2. **Choose a different project** — let the user to manually type the project name. + +**Example prompt:** +> *Which project should I use?* +> *1. alice@acme.com (your default project)* +> *2. Choose a different project* + +--- + +**Note:** +Only select a project automatically if the user has explicitly named it, or used clear phrases indicating the default (e.g., "my project", "my default project"). In all other situations, always ask for project clarification. + +### Step 4 — Proceed -This applies to **all asset types**: assistants, workflows, datasources, and integrations. +Once the project is known, use it in all subsequent commands: +- Assistants, skills, categories: `"project": ""` +- Workflows, datasources, integrations: `"project_name": ""` --- @@ -45,6 +79,7 @@ This applies to **all asset types**: assistants, workflows, datasources, and int | Integrations | [examples/integrations.md](examples/integrations.md) | | Skills | [examples/skills.md](examples/skills.md) | | Users | [examples/users.md](examples/users.md) | +| Categories | [examples/categories.md](examples/categories.md) | Do **not** guess field names or skip this step — all required/optional fields, nested schemas, and asset cross-reference commands are documented there. @@ -181,12 +216,32 @@ Use `base_name` when setting `llm_model_type` on an assistant or `embeddings_mod ```bash codemie sdk skills list [--scope marketplace|project|project_with_marketplace] [--page ] [--per-page ] [--json] codemie sdk skills get [--json] +codemie sdk skills create --data '' | --json +codemie sdk skills update --data '' | --json +codemie sdk skills delete +codemie sdk skills import --project [--visibility private|project|public] [--json] +codemie sdk skills export +codemie sdk skills attach +codemie sdk skills detach +codemie sdk skills list-assistant-skills [--json] +codemie sdk skills bulk-attach --assistant-ids ,,... +codemie sdk skills get-assistants [--json] +codemie sdk skills publish [--categories ,] +codemie sdk skills unpublish +codemie sdk skills list-categories [--json] +codemie sdk skills get-users [--json] +codemie sdk skills react --reaction like|dislike +codemie sdk skills remove-reactions ``` -**Key fields:** `id`, `name`, `project`, `visibility`, `description`, `content`, `created_by`, `created_date` +**Required on create:** `name` (kebab-case, 3–64 chars), `description` (10–1000 chars), `content` (markdown, min 100 chars), `project` + +**Key fields:** `id`, `name`, `project`, `visibility`, `description`, `content`, `created_by`, `createdDate`, `assistants_count`, `categories` **`--scope` values:** `marketplace`, `project`, `project_with_marketplace` +**`visibility` values:** `private`, `project`, `public` + --- ## Users @@ -198,6 +253,32 @@ codemie sdk users me [--json] codemie sdk users data [--json] ``` -**`users me`** — current user profile. Fields: `name`, `username`, `applications`, `picture` +**`users me`** — current user profile. Fields: `user_id`, `name`, `username`, `email`, `is_admin`, `applications`, `applications_admin`, `picture`, `knowledge_bases` + +**`users data`** — user preferences and metadata. Fields: `id`, `user_id`, `date`, `update_date` + +--- + +## Categories + +> See [examples/categories.md](examples/categories.md) for full field reference and examples. + +**Note:** Categories can only be used for assistants (set via the `categories` field on create/update). + +```bash +codemie sdk categories list [--paginated] [--page ] [--per-page ] [--json] +codemie sdk categories get [--json] +codemie sdk categories create --data '' | --json +codemie sdk categories update --data '' | --json +codemie sdk categories delete +``` + +**Required on create:** `name` (1–255 chars) + +**Key fields:** `id`, `name`, `description`, `marketplaceAssistantCount`, `projectAssistantCount`, `createdAt` -**`users data`** — user preferences and metadata. Fields: `id`, `user_id`, `date`, `update_date`, `sidebar_view`, `stt_support` +**Important notes:** +- `list` without `--paginated` calls the public endpoint (no admin required) — returns `id`, `name`, `description` +- `list --paginated`, `get`, `create`, `update`, `delete` all require **admin access** +- `delete` fails with 409 if any assistants are still assigned to the category +- Use the category `id` in the assistant `categories` field when creating/updating assistants diff --git a/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/assistants.md b/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/assistants.md index 79e082a7..0ca86bc5 100644 --- a/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/assistants.md +++ b/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/assistants.md @@ -65,7 +65,7 @@ codemie sdk assistants create --json assistant.json | `mcp_servers` | — | array | MCP server connections for additional tools — see schema below | | `assistant_ids` | — | string[] | Sub-assistant IDs for orchestration (multi-agent workflows) | | `prompt_variables` | — | array | Dynamic `{{variable}}` placeholders in the system prompt — see schema below | -| `categories` | — | string[] | Labels for marketplace classification (e.g. `["DevOps", "Code Review"]`) | +| `categories` | — | string[] | Label IDs for marketplace classification (e.g. `["devops", "code-review"]`). You can manage assistants categories via codemie sdk categories command (get, create, update and so on). Only categories listed via this command can be used in this field | | `skill_ids` | — | string[] | Built-in platform skill IDs — **not** datasource IDs | | `skip_integration_validation` | — | boolean | Skip credential validation when attaching toolkits (useful with test credentials) | @@ -161,7 +161,7 @@ Reference in system prompt as `{{language}}`. { "key": "language", "description": "Primary language", "default_value": "TypeScript" }, { "key": "focus_area", "description": "Review focus", "default_value": "security and performance" } ], - "categories": ["Code Review", "Engineering"] + "categories": ["code-review", "engineering"] } ``` diff --git a/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/categories.md b/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/categories.md new file mode 100644 index 00000000..757c9c5c --- /dev/null +++ b/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/categories.md @@ -0,0 +1,101 @@ +# Categories Examples + +> **Note:** Categories can only be used for **assistants**. Set a category on an assistant via the `categories` field (array of category IDs) when creating or updating. + +## List + +```bash +# List all categories (public, no admin required) +codemie sdk categories list +codemie sdk categories list --json + +# Paginated list with assistant counts (admin required) +codemie sdk categories list --paginated +codemie sdk categories list --paginated --page 0 --per-page 25 --json +``` + +**Non-paginated JSON fields:** `id`, `name`, `description` + +**Paginated JSON fields:** `categories[]` (with `id`, `name`, `description`, `marketplaceAssistantCount`, `projectAssistantCount`, `createdAt`, `updatedAt`), `page`, `per_page`, `total`, `pages` + +## Get + +```bash +codemie sdk categories get +codemie sdk categories get --json +``` + +Admin access required. Returns `id`, `name`, `description`, `marketplaceAssistantCount`, `projectAssistantCount`, `createdAt`, `updatedAt`. + +## Create + +```bash +# Minimal (name only) +codemie sdk categories create --data '{"name":"DevOps"}' + +# With description +codemie sdk categories create --data '{"name":"Code Review","description":"Skills for reviewing code quality and security"}' + +# From file +codemie sdk categories create --json category.json +``` + +**Field reference:** + +| Field | Required | Type | Description | +|-------|----------|------|-------------| +| `name` | ✅ | string | 1–255 chars | +| `description` | — | string | Optional, max 1000 chars | + +Admin access required. + +## Update + +```bash +codemie sdk categories update --data '{"name":"Updated Name"}' +codemie sdk categories update --data '{"name":"DevOps","description":"Updated description"}' +``` + +Admin access required. + +## Delete + +```bash +# Verify before deleting +codemie sdk categories get +codemie sdk categories delete +``` + +Admin access required. Fails with **409** if any assistants are still assigned to this category — reassign or remove those assistants first. + +## Using Categories with Assistants + +Categories are referenced by their `id` in the assistant `categories` field. + +```bash +# Get available category IDs +codemie sdk categories list --json | jq -r '.[] | "\(.id) \(.name)"' + +# Create an assistant with categories +codemie sdk assistants create --data '{ + "name": "Code Reviewer", + "project": "Engineering", + "system_prompt": "You are a code review assistant.", + "categories": ["", ""] +}' + +# Update an assistant to add categories +codemie sdk assistants update --data '{ + "categories": [""] +}' +``` + +## Scripting + +```bash +# Find category ID by name +codemie sdk categories list --json | jq -r '.[] | select(.name == "DevOps") | .id' + +# List all categories with assistant counts (admin) +codemie sdk categories list --paginated --json | jq -r '.categories[] | "\(.name): \(.marketplaceAssistantCount) marketplace, \(.projectAssistantCount) project"' +``` diff --git a/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/skills.md b/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/skills.md index 8912c442..841c5a7e 100644 --- a/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/skills.md +++ b/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/skills.md @@ -22,7 +22,7 @@ codemie sdk skills list --json **List columns:** ID, Name, Project, Visibility -**JSON fields (list):** `id`, `name`, `project`, `visibility` +**JSON fields (list):** `id`, `name`, `project`, `visibility`, `description`, `created_by`, `categories`, `createdDate`, `updatedDate`, `is_attached`, `assistants_count`, `user_abilities`, `unique_likes_count`, `unique_dislikes_count` ## Get @@ -31,9 +31,142 @@ codemie sdk skills get 3d5b188f-185b-48df-b4b3-e608e4efb1ad codemie sdk skills get 3d5b188f-185b-48df-b4b3-e608e4efb1ad --json ``` -**Detail fields:** `id`, `name`, `project`, `visibility`, `description`, `created_by` (with `name`), `created_date`, `updated_date`, `content` +**Additional fields in get:** `content` (full skill markdown), `toolkits`, `mcp_servers` -> Note: `content` contains the full skill markdown text (SKILL.md body). +## Create + +```bash +# Minimal required fields +codemie sdk skills create --data '{ + "name": "my-skill", + "description": "Does something useful for the team.", + "content": "# My Skill\n\nInstructions here...", + "project": "MyProject" +}' + +# Full example from file +codemie sdk skills create --json skill.json +``` + +**Field reference:** + +| Field | Required | Type | Description | +|-------|----------|------|-------------| +| `name` | ✅ | string | Kebab-case identifier, 3–64 chars (e.g. `"my-skill"`) | +| `description` | ✅ | string | 10–1000 chars | +| `content` | ✅ | string | Markdown skill instructions, minimum 100 chars | +| `project` | ✅ | string | Project to create the skill in | +| `visibility` | — | string | `"private"` (default), `"project"`, or `"public"` | +| `categories` | — | string[] | Max 3 category values (use `list-categories` to get valid values) | +| `toolkits` | — | array | Integration toolkits | +| `mcp_servers` | — | array | MCP server connections | + +**Response:** returns the created `SkillDetail` object including its `id`. + +## Update + +```bash +codemie sdk skills update 3d5b188f-185b-48df-b4b3-e608e4efb1ad --data '{"description":"Updated description"}' +codemie sdk skills update 3d5b188f-185b-48df-b4b3-e608e4efb1ad --json updates.json +``` + +All fields are optional on update — only provided fields are changed. + +## Delete + +```bash +# Always verify before deleting +codemie sdk skills get +codemie sdk skills delete +``` + +## Import / Export + +```bash +# Import a skill from a .md file with YAML frontmatter +codemie sdk skills import ./my-skill.md --project MyProject +codemie sdk skills import ./my-skill.md --project MyProject --visibility project + +# Export a skill as markdown (pipe to file to save) +codemie sdk skills export +codemie sdk skills export > my-skill.md +``` + +The import file must include YAML frontmatter with `name` and `description`: +```markdown +--- +name: my-skill +description: What this skill does +--- + +# Instructions +... +``` + +## Attach / Detach Skills to Assistants + +```bash +# Attach a skill to an assistant +codemie sdk skills attach + +# Detach a skill from an assistant +codemie sdk skills detach + +# List all skills attached to an assistant +codemie sdk skills list-assistant-skills +codemie sdk skills list-assistant-skills --json + +# Bulk attach one skill to multiple assistants +codemie sdk skills bulk-attach --assistant-ids ,, + +# List all assistants using a skill +codemie sdk skills get-assistants +codemie sdk skills get-assistants --json +``` + +## Publish / Unpublish + +```bash +# Publish to marketplace (no categories) +codemie sdk skills publish + +# Publish with categories (max 3, use values from list-categories) +codemie sdk skills publish --categories development,testing + +# Unpublish from marketplace +codemie sdk skills unpublish +``` + +## Categories + +```bash +# List available skill categories (value + label) +codemie sdk skills list-categories +codemie sdk skills list-categories --json +``` + +Use the `value` field from `list-categories` output when setting `categories` on create/update or publish. + +## Reactions + +```bash +# Like a skill +codemie sdk skills react --reaction like + +# Dislike a skill +codemie sdk skills react --reaction dislike + +# Remove all reactions +codemie sdk skills remove-reactions +``` + +## Users + +```bash +# Get users with access to skills +codemie sdk skills get-users +codemie sdk skills get-users --json +``` ## Scripting @@ -46,4 +179,13 @@ codemie sdk skills list --scope marketplace --json | jq -r '.[] | "\(.id) \(.nam # Get skill content codemie sdk skills get --json | jq -r '.content' + +# Create then get new skill ID +codemie sdk skills create --data '{"name":"my-skill","description":"Does X","content":"# My Skill\n\nInstructions...","project":"Eng"}' +ID=$(codemie sdk skills list --json | jq -r '.[] | select(.name == "my-skill") | .id') + +# Attach a skill to all assistants in a project +codemie sdk assistants list --projects MyProject --json | jq -r '.[].id' | while read id; do + codemie sdk skills attach "$id" +done ``` diff --git a/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/users.md b/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/users.md index fd7cccb3..f21fa76a 100644 --- a/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/users.md +++ b/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/users.md @@ -7,7 +7,7 @@ codemie sdk users me codemie sdk users me --json ``` -**JSON fields:** `name`, `username`, `applications`, `picture` +**JSON fields:** `user_id`, `name`, `username`, `email`, `is_admin`, `applications`, `applications_admin`, `picture`, `knowledge_bases` ## Get current user data @@ -16,7 +16,7 @@ codemie sdk users data codemie sdk users data --json ``` -**JSON fields:** `id`, `user_id`, `date`, `update_date`, `sidebar_view`, `stt_support` +**JSON fields:** `id`, `user_id`, `date`, `update_date` ## Scripting @@ -24,9 +24,15 @@ codemie sdk users data --json # Get your username codemie sdk users me --json | jq -r '.username' -# Get your user UUID (from user data, not profile) -codemie sdk users data --json | jq -r '.user_id' +# Get your user UUID +codemie sdk users me --json | jq -r '.user_id' + +# Check if you are an admin +codemie sdk users me --json | jq -r '.is_admin' # Get list of projects you have access to codemie sdk users me --json | jq -r '.applications[]' + +# Get list of projects where you are an admin +codemie sdk users me --json | jq -r '.applications_admin[]' ``` diff --git a/src/cli/commands/sdk/categories.ts b/src/cli/commands/sdk/categories.ts new file mode 100644 index 00000000..1f9c8df9 --- /dev/null +++ b/src/cli/commands/sdk/categories.ts @@ -0,0 +1,255 @@ +import { Command } from "commander"; +import chalk from "chalk"; +import ora from "ora"; +import type { + Category, + CategoryResponse, + CategoryCreateParams, + CategoryUpdateParams, +} from "codemie-sdk"; +import { + getCategories, + listCategories, + getCategory, + createCategory, + updateCategory, + deleteCategory, +} from "./services/categories.js"; +import { + getSdkClient, + parseDataOrJsonFile, + outputJson, + handleSdkError, +} from "./utils/cli-utils.js"; +import { + printTable, + printDetail, + printEmpty, + printListHeader, + printSuccess, + optional, + type TableColumn, + type DetailRow, +} from "./utils/render.js"; + +export function createCategoriesSubcommand(): Command { + const cmd = new Command("categories").description( + "Manage CodeMie assistant categories", + ); + + cmd + .command("list") + .description( + "List all assistant categories\n" + + "Without --paginated: returns all categories (public, no admin required).\n" + + "With --paginated: returns paginated list with assistant counts (admin required).\n" + + "Examples:\n" + + " $ codemie sdk categories list\n" + + " $ codemie sdk categories list --paginated --page 0 --per-page 25\n" + + " $ codemie sdk categories list --json", + ) + .option("--json", "Output in JSON format") + .option("--paginated", "Use paginated endpoint with assistant counts (admin required)") + .option("--page ", "Page number (starts at 0)", "0") + .option("--per-page ", "Results per page", "20") + .action(async (opts) => { + const client = await getSdkClient(); + const spinner = ora("Fetching categories...").start(); + + try { + if (opts.paginated) { + const result = await listCategories( + client, + parseInt(opts.page, 10), + parseInt(opts.perPage, 10), + ); + spinner.stop(); + + if (opts.json) { + outputJson(result); + return; + } + + if (result.categories.length === 0) { + printEmpty("categories"); + return; + } + + printListHeader("Categories", result.total); + + const columns: TableColumn[] = [ + { header: "ID", width: 36, getValue: (c) => chalk.cyan(c.id) }, + { header: "Name", width: 28, getValue: (c) => c.name }, + { + header: "Marketplace", + width: 14, + getValue: (c) => String(c.marketplaceAssistantCount), + }, + { + header: "Project", + width: 10, + getValue: (c) => String(c.projectAssistantCount), + }, + ]; + printTable(result.categories, columns); + } else { + const items = await getCategories(client); + spinner.stop(); + + if (opts.json) { + outputJson(items); + return; + } + + if (items.length === 0) { + printEmpty("categories"); + return; + } + + printListHeader("Categories", items.length); + + const columns: TableColumn[] = [ + { header: "ID", width: 36, getValue: (c) => chalk.cyan(c.id) }, + { header: "Name", width: 36, getValue: (c) => c.name }, + { + header: "Description", + width: 40, + getValue: (c) => optional(c.description), + }, + ]; + printTable(items, columns); + } + } catch (error) { + spinner.stop(); + handleSdkError(error, "list categories"); + } + }); + + cmd + .command("get ") + .description( + "Get a specific category by ID (admin required)\n" + + "Examples:\n" + + " $ codemie sdk categories get \n" + + " $ codemie sdk categories get --json", + ) + .option("--json", "Output in JSON format") + .action(async (id: string, opts) => { + const client = await getSdkClient(); + const spinner = ora("Fetching category...").start(); + + try { + const item = await getCategory(client, id); + spinner.stop(); + + if (opts.json) { + outputJson(item); + return; + } + + const rows: DetailRow[] = [ + { label: "ID", value: chalk.cyan(item.id) }, + { label: "Name", value: item.name }, + { label: "Description", value: optional(item.description) }, + { + label: "Marketplace", + value: String(item.marketplaceAssistantCount), + }, + { label: "Project", value: String(item.projectAssistantCount) }, + { label: "Created", value: item.createdAt }, + ]; + + if (item.updatedAt) { + rows.push({ label: "Updated", value: item.updatedAt }); + } + + printDetail(rows); + } catch (error) { + spinner.stop(); + handleSdkError(error, "get category"); + } + }); + + cmd + .command("create") + .description( + "Create a new assistant category (admin required)\n" + + "Examples:\n" + + ' $ codemie sdk categories create --data \'{"name":"DevOps","description":"DevOps tooling and automation"}\'\n' + + " $ codemie sdk categories create --json path/to/category.json", + ) + .option("--data ", "Category data as inline JSON string") + .option("--json ", "Path to JSON file with category data") + .action(async (opts) => { + const client = await getSdkClient(); + const spinner = ora("Creating category...").start(); + + try { + const data = await parseDataOrJsonFile(opts.data, opts.json); + const result = await createCategory( + client, + data as CategoryCreateParams, + ); + spinner.stop(); + + printSuccess(`Category ${chalk.cyan(result.id)} created: '${result.name}'.`); + } catch (error) { + spinner.stop(); + handleSdkError(error, "create category"); + } + }); + + cmd + .command("update ") + .description( + "Update an existing assistant category (admin required)\n" + + "Examples:\n" + + ' $ codemie sdk categories update --data \'{"name":"Updated Name"}\'\n' + + " $ codemie sdk categories update --json path/to/update.json", + ) + .option("--data ", "Fields to update as inline JSON string") + .option("--json ", "Path to JSON file with fields to update") + .action(async (id: string, opts) => { + const client = await getSdkClient(); + const spinner = ora("Updating category...").start(); + + try { + const data = await parseDataOrJsonFile(opts.data, opts.json); + const result = await updateCategory( + client, + id, + data as CategoryUpdateParams, + ); + spinner.stop(); + + printSuccess(`Category ${chalk.cyan(result.id)} updated.`); + } catch (error) { + spinner.stop(); + handleSdkError(error, "update category"); + } + }); + + cmd + .command("delete ") + .description( + "Delete an assistant category (admin required).\n" + + "Fails with 409 if any assistants are assigned to it.\n" + + "Examples:\n" + + " $ codemie sdk categories delete ", + ) + .action(async (id: string) => { + const client = await getSdkClient(); + const spinner = ora("Deleting category...").start(); + + try { + await deleteCategory(client, id); + spinner.stop(); + printSuccess(`Category ${chalk.cyan(id)} deleted.`); + } catch (error) { + spinner.stop(); + handleSdkError(error, "delete category"); + } + }); + + return cmd; +} diff --git a/src/cli/commands/sdk/index.ts b/src/cli/commands/sdk/index.ts index 4297a8f4..57ac0219 100644 --- a/src/cli/commands/sdk/index.ts +++ b/src/cli/commands/sdk/index.ts @@ -6,12 +6,13 @@ import { createIntegrationsSubcommand } from './integrations.js'; import { createLlmModelsSubcommand } from './llm.js'; import { createSkillsSubcommand } from './skills.js'; import { createUsersSubcommand } from './users.js'; +import { createCategoriesSubcommand } from './categories.js'; export function createSdkCommand(): Command { const cmd = new Command('sdk'); cmd.description( - 'Manage CodeMie platform assets (assistants, workflows, datasources, integrations, skills, users, executions) via the SDK' + 'Manage CodeMie platform assets (assistants, workflows, datasources, integrations, skills, users, categories) via the SDK' ); cmd.addCommand(createAssistantsSubcommand()); @@ -21,6 +22,7 @@ export function createSdkCommand(): Command { cmd.addCommand(createLlmModelsSubcommand()); cmd.addCommand(createSkillsSubcommand()); cmd.addCommand(createUsersSubcommand()); + cmd.addCommand(createCategoriesSubcommand()); return cmd; } diff --git a/src/cli/commands/sdk/services/categories.ts b/src/cli/commands/sdk/services/categories.ts new file mode 100644 index 00000000..111df66c --- /dev/null +++ b/src/cli/commands/sdk/services/categories.ts @@ -0,0 +1,51 @@ +import type { + CodeMieClient, + Category, + CategoryResponse, + CategoryListResponse, + CategoryCreateParams, + CategoryUpdateParams, +} from "codemie-sdk"; + +export async function getCategories( + client: CodeMieClient, +): Promise { + return client.categories.getCategories(); +} + +export async function listCategories( + client: CodeMieClient, + page?: number, + perPage?: number, +): Promise { + return client.categories.listCategories(page, perPage); +} + +export async function getCategory( + client: CodeMieClient, + categoryId: string, +): Promise { + return client.categories.getCategory(categoryId); +} + +export async function createCategory( + client: CodeMieClient, + params: CategoryCreateParams, +): Promise { + return client.categories.createCategory(params); +} + +export async function updateCategory( + client: CodeMieClient, + categoryId: string, + params: CategoryUpdateParams, +): Promise { + return client.categories.updateCategory(categoryId, params); +} + +export async function deleteCategory( + client: CodeMieClient, + categoryId: string, +): Promise { + return client.categories.deleteCategory(categoryId); +} diff --git a/src/cli/commands/sdk/services/skills.ts b/src/cli/commands/sdk/services/skills.ts index 12d25acc..026f2ff7 100644 --- a/src/cli/commands/sdk/services/skills.ts +++ b/src/cli/commands/sdk/services/skills.ts @@ -1,4 +1,14 @@ -import type { CodeMieClient, SkillListItem, SkillDetail } from "codemie-sdk"; +import type { + CodeMieClient, + SkillListItem, + SkillDetail, + SkillCreateParams, + SkillUpdateParams, + SkillImportParams, + SkillCategoryItem, + SkillListPaginatedResponse, + AnyJson, +} from "codemie-sdk"; type SkillListParams = Parameters[0]; @@ -9,9 +19,145 @@ export async function listSkills( return client.skills.list(params); } +export async function listSkillsPaginated( + client: CodeMieClient, + params?: SkillListParams, +): Promise { + return client.skills.listPaginated(params); +} + export async function getSkill( client: CodeMieClient, skillId: string, ): Promise { return client.skills.get(skillId); } + +export async function createSkill( + client: CodeMieClient, + params: SkillCreateParams, +): Promise { + return client.skills.create(params); +} + +export async function updateSkill( + client: CodeMieClient, + skillId: string, + params: SkillUpdateParams, +): Promise { + const existing = await client.skills.get(skillId); + + const mergedParams: SkillUpdateParams = { + name: params.name ?? existing.name, + description: params.description ?? existing.description, + content: params.content ?? existing.content, + project: params.project ?? existing.project, + visibility: params.visibility ?? existing.visibility, + categories: params.categories ?? existing.categories ?? [], + toolkits: params.toolkits ?? existing.toolkits ?? [], + mcp_servers: params.mcp_servers ?? existing.mcp_servers ?? [], + }; + + return client.skills.update(skillId, mergedParams); +} + +export async function deleteSkill( + client: CodeMieClient, + skillId: string, +): Promise { + return client.skills.delete(skillId); +} + +export async function importSkill( + client: CodeMieClient, + params: SkillImportParams, +): Promise { + return client.skills.importSkill(params); +} + +export async function exportSkill( + client: CodeMieClient, + skillId: string, +): Promise { + return client.skills.export(skillId); +} + +export async function attachSkillToAssistant( + client: CodeMieClient, + assistantId: string, + skillId: string, +): Promise { + return client.skills.attachToAssistant(assistantId, skillId); +} + +export async function detachSkillFromAssistant( + client: CodeMieClient, + assistantId: string, + skillId: string, +): Promise { + return client.skills.detachFromAssistant(assistantId, skillId); +} + +export async function getAssistantSkills( + client: CodeMieClient, + assistantId: string, +): Promise { + return client.skills.getAssistantSkills(assistantId); +} + +export async function bulkAttachSkillToAssistants( + client: CodeMieClient, + skillId: string, + assistantIds: string[], +): Promise { + return client.skills.bulkAttachToAssistants(skillId, assistantIds); +} + +export async function getSkillAssistants( + client: CodeMieClient, + skillId: string, +): Promise { + return client.skills.getSkillAssistants(skillId); +} + +export async function publishSkill( + client: CodeMieClient, + skillId: string, + categories?: string[], +): Promise { + return client.skills.publish(skillId, categories); +} + +export async function unpublishSkill( + client: CodeMieClient, + skillId: string, +): Promise { + return client.skills.unpublish(skillId); +} + +export async function listSkillCategories( + client: CodeMieClient, +): Promise { + return client.skills.listCategories(); +} + +export async function getSkillUsers( + client: CodeMieClient, +): Promise { + return client.skills.getUsers(); +} + +export async function reactToSkill( + client: CodeMieClient, + skillId: string, + reaction: "like" | "dislike", +): Promise { + return client.skills.react(skillId, reaction); +} + +export async function removeSkillReactions( + client: CodeMieClient, + skillId: string, +): Promise { + return client.skills.removeReactions(skillId); +} diff --git a/src/cli/commands/sdk/skills.ts b/src/cli/commands/sdk/skills.ts index cb646512..5ddd77df 100644 --- a/src/cli/commands/sdk/skills.ts +++ b/src/cli/commands/sdk/skills.ts @@ -1,10 +1,39 @@ import { Command } from "commander"; import chalk from "chalk"; import ora from "ora"; -import type { SkillListItem, SkillDetail } from "codemie-sdk"; -import { listSkills, getSkill } from "./services/skills.js"; +import { readFile } from "node:fs/promises"; +import { basename } from "node:path"; +import type { + SkillListItem, + SkillDetail, + SkillCreateParams, + SkillUpdateParams, + SkillCategoryItem, + AnyJson, +} from "codemie-sdk"; +import { + listSkills, + getSkill, + createSkill, + updateSkill, + deleteSkill, + importSkill, + exportSkill, + attachSkillToAssistant, + detachSkillFromAssistant, + getAssistantSkills, + bulkAttachSkillToAssistants, + getSkillAssistants, + publishSkill, + unpublishSkill, + listSkillCategories, + getSkillUsers, + reactToSkill, + removeSkillReactions, +} from "./services/skills.js"; import { getSdkClient, + parseDataOrJsonFile, outputJson, handleSdkError, } from "./utils/cli-utils.js"; @@ -13,6 +42,7 @@ import { printDetail, printEmpty, printListHeader, + printSuccess, optional, type TableColumn, type DetailRow, @@ -71,11 +101,7 @@ export function createSkillsSubcommand(): Command { { header: "ID", width: 40, getValue: (s) => chalk.cyan(s.id) }, { header: "Name", width: 30, getValue: (s) => s.name }, { header: "Project", width: 20, getValue: (s) => optional(s.project) }, - { - header: "Visibility", - width: 12, - getValue: (s) => s.visibility, - }, + { header: "Visibility", width: 12, getValue: (s) => s.visibility }, ]; printTable(items, columns); } catch (error) { @@ -107,15 +133,13 @@ export function createSkillsSubcommand(): Command { { label: "Project", value: optional(item.project) }, { label: "Visibility", value: item.visibility }, { label: "Description", value: optional(item.description) }, - { - label: "Creator", - value: optional(item.created_by?.name), - }, - { label: "Created", value: item.created_date }, + { label: "Creator", value: optional(item.created_by?.name) }, + { label: "Created", value: item.createdDate }, + { label: "Assistants", value: String(item.assistants_count) }, ]; - if (item.updated_date) { - rows.push({ label: "Updated", value: item.updated_date }); + if (item.updatedDate) { + rows.push({ label: "Updated", value: item.updatedDate }); } const detailItem = item as SkillDetail; @@ -130,5 +154,522 @@ export function createSkillsSubcommand(): Command { } }); + cmd + .command("create") + .description( + "Create a new skill\n" + + "Examples:\n" + + ' $ codemie sdk skills create --data \'{"name":"my-skill","description":"Does X","content":"# Instructions\\n...","project":"MyProject"}\'\n' + + " $ codemie sdk skills create --json path/to/skill.json", + ) + .option("--data ", "Skill configuration as inline JSON string") + .option("--json ", "Path to JSON file with skill configuration") + .action(async (opts) => { + const client = await getSdkClient(); + const spinner = ora("Creating skill...").start(); + + try { + const data = await parseDataOrJsonFile(opts.data, opts.json); + const result = await createSkill(client, data as SkillCreateParams); + spinner.stop(); + + printSuccess(`Skill ${chalk.cyan(result.id)} created.`); + } catch (error) { + spinner.stop(); + handleSdkError(error, "create skill"); + } + }); + + cmd + .command("update ") + .description( + "Update an existing skill\n" + + "Examples:\n" + + ' $ codemie sdk skills update --data \'{"description":"Updated description"}\'\n' + + " $ codemie sdk skills update --json path/to/update.json", + ) + .option("--data ", "Fields to update as inline JSON string") + .option("--json ", "Path to JSON file with fields to update") + .action(async (id: string, opts) => { + const client = await getSdkClient(); + const spinner = ora("Updating skill...").start(); + + try { + const data = await parseDataOrJsonFile(opts.data, opts.json); + const result = await updateSkill(client, id, data as SkillUpdateParams); + spinner.stop(); + + printSuccess(`Skill ${chalk.cyan(result.id)} updated.`); + } catch (error) { + spinner.stop(); + handleSdkError(error, "update skill"); + } + }); + + cmd + .command("delete ") + .description("Permanently delete a skill") + .action(async (id: string) => { + const client = await getSdkClient(); + const spinner = ora("Deleting skill...").start(); + + try { + await deleteSkill(client, id); + spinner.stop(); + printSuccess(`Skill ${chalk.cyan(id)} deleted.`); + } catch (error) { + spinner.stop(); + handleSdkError(error, "delete skill"); + } + }); + + cmd + .command("import ") + .description( + "Import a skill from a markdown file\n" + + "The file must include YAML frontmatter with 'name' and 'description' fields.\n" + + "Examples:\n" + + " $ codemie sdk skills import ./my-skill.md --project MyProject\n" + + " $ codemie sdk skills import ./my-skill.md --project MyProject --visibility project", + ) + .requiredOption("--project ", "Project to import the skill into") + .option( + "--visibility ", + "Visibility: 'private', 'project', or 'public'", + ) + .option("--json", "Output imported skill as JSON") + .action(async (file: string, opts) => { + const client = await getSdkClient(); + const spinner = ora("Importing skill...").start(); + + try { + const content = await readFile(file); + const result = await importSkill(client, { + file_content: content.toString("base64"), + filename: basename(file), + project: opts.project, + ...(opts.visibility ? { visibility: opts.visibility } : {}), + }); + spinner.stop(); + + if (opts.json) { + outputJson(result); + return; + } + + printSuccess(`Skill ${chalk.cyan(result.id)} imported as '${result.name}'.`); + } catch (error) { + spinner.stop(); + handleSdkError(error, "import skill"); + } + }); + + cmd + .command("export ") + .description( + "Export a skill as markdown content\n" + + "Examples:\n" + + " $ codemie sdk skills export \n" + + " $ codemie sdk skills export > my-skill.md", + ) + .action(async (id: string) => { + const client = await getSdkClient(); + const spinner = ora("Exporting skill...").start(); + + try { + const content = await exportSkill(client, id); + spinner.stop(); + console.log(content); + } catch (error) { + spinner.stop(); + handleSdkError(error, "export skill"); + } + }); + + cmd + .command("attach ") + .description( + "Attach a skill to an assistant\n" + + "Examples:\n" + + " $ codemie sdk skills attach ", + ) + .action(async (assistantId: string, skillId: string) => { + const client = await getSdkClient(); + const spinner = ora("Attaching skill to assistant...").start(); + + try { + await attachSkillToAssistant(client, assistantId, skillId); + spinner.stop(); + printSuccess( + `Skill ${chalk.cyan(skillId)} attached to assistant ${chalk.cyan(assistantId)}.`, + ); + } catch (error) { + spinner.stop(); + handleSdkError(error, "attach skill"); + } + }); + + cmd + .command("detach ") + .description( + "Detach a skill from an assistant\n" + + "Examples:\n" + + " $ codemie sdk skills detach ", + ) + .action(async (assistantId: string, skillId: string) => { + const client = await getSdkClient(); + const spinner = ora("Detaching skill from assistant...").start(); + + try { + await detachSkillFromAssistant(client, assistantId, skillId); + spinner.stop(); + printSuccess( + `Skill ${chalk.cyan(skillId)} detached from assistant ${chalk.cyan(assistantId)}.`, + ); + } catch (error) { + spinner.stop(); + handleSdkError(error, "detach skill"); + } + }); + + cmd + .command("list-assistant-skills ") + .description( + "List all skills attached to an assistant\n" + + "Examples:\n" + + " $ codemie sdk skills list-assistant-skills \n" + + " $ codemie sdk skills list-assistant-skills --json", + ) + .option("--json", "Output in JSON format") + .action(async (assistantId: string, opts) => { + const client = await getSdkClient(); + const spinner = ora("Fetching assistant skills...").start(); + + try { + const items = await getAssistantSkills(client, assistantId); + spinner.stop(); + + if (opts.json) { + outputJson(items); + return; + } + + if (items.length === 0) { + printEmpty("skills for this assistant"); + return; + } + + printListHeader("Assistant Skills", items.length); + + const columns: TableColumn[] = [ + { header: "ID", width: 40, getValue: (s) => chalk.cyan(s.id) }, + { header: "Name", width: 30, getValue: (s) => s.name }, + { header: "Visibility", width: 12, getValue: (s) => s.visibility }, + ]; + printTable(items, columns); + } catch (error) { + spinner.stop(); + handleSdkError(error, "list assistant skills"); + } + }); + + cmd + .command("bulk-attach ") + .description( + "Attach a skill to multiple assistants at once\n" + + "Examples:\n" + + " $ codemie sdk skills bulk-attach --assistant-ids ,,", + ) + .requiredOption( + "--assistant-ids ", + "Comma-separated list of assistant IDs", + ) + .action(async (skillId: string, opts) => { + const client = await getSdkClient(); + const spinner = ora("Attaching skill to assistants...").start(); + + try { + const assistantIds = opts.assistantIds + .split(",") + .map((id: string) => id.trim()); + await bulkAttachSkillToAssistants(client, skillId, assistantIds); + spinner.stop(); + printSuccess( + `Skill ${chalk.cyan(skillId)} attached to ${assistantIds.length} assistant(s).`, + ); + } catch (error) { + spinner.stop(); + handleSdkError(error, "bulk attach skill"); + } + }); + + cmd + .command("get-assistants ") + .description( + "List all assistants using a skill\n" + + "Examples:\n" + + " $ codemie sdk skills get-assistants \n" + + " $ codemie sdk skills get-assistants --json", + ) + .option("--json", "Output in JSON format") + .action(async (skillId: string, opts) => { + const client = await getSdkClient(); + const spinner = ora("Fetching skill assistants...").start(); + + try { + const items = await getSkillAssistants(client, skillId); + spinner.stop(); + + if (opts.json) { + outputJson(items); + return; + } + + if (items.length === 0) { + printEmpty("assistants for this skill"); + return; + } + + printListHeader("Skill Assistants", items.length); + + const columns: TableColumn[] = [ + { + header: "ID", + width: 40, + getValue: (a) => + chalk.cyan( + typeof a === "object" && a !== null && "id" in a + ? String((a as Record).id) + : "", + ), + }, + { + header: "Name", + width: 30, + getValue: (a) => + typeof a === "object" && a !== null && "name" in a + ? String((a as Record).name) + : "", + }, + ]; + printTable(items, columns); + } catch (error) { + spinner.stop(); + handleSdkError(error, "get skill assistants"); + } + }); + + cmd + .command("publish ") + .description( + "Publish a skill to the marketplace\n" + + "Examples:\n" + + " $ codemie sdk skills publish \n" + + " $ codemie sdk skills publish --categories development,testing", + ) + .option( + "--categories ", + "Comma-separated list of categories (max 3)", + ) + .action(async (id: string, opts) => { + const client = await getSdkClient(); + const spinner = ora("Publishing skill...").start(); + + try { + const categories = opts.categories + ? opts.categories.split(",").map((c: string) => c.trim()) + : undefined; + await publishSkill(client, id, categories); + spinner.stop(); + printSuccess(`Skill ${chalk.cyan(id)} published to marketplace.`); + } catch (error) { + spinner.stop(); + handleSdkError(error, "publish skill"); + } + }); + + cmd + .command("unpublish ") + .description( + "Unpublish a skill from the marketplace\n" + + "Examples:\n" + + " $ codemie sdk skills unpublish ", + ) + .action(async (id: string) => { + const client = await getSdkClient(); + const spinner = ora("Unpublishing skill...").start(); + + try { + await unpublishSkill(client, id); + spinner.stop(); + printSuccess(`Skill ${chalk.cyan(id)} unpublished from marketplace.`); + } catch (error) { + spinner.stop(); + handleSdkError(error, "unpublish skill"); + } + }); + + cmd + .command("list-categories") + .description( + "List available skill categories\n" + + "Examples:\n" + + " $ codemie sdk skills list-categories\n" + + " $ codemie sdk skills list-categories --json", + ) + .option("--json", "Output in JSON format") + .action(async (opts) => { + const client = await getSdkClient(); + const spinner = ora("Fetching skill categories...").start(); + + try { + const items = await listSkillCategories(client); + spinner.stop(); + + if (opts.json) { + outputJson(items); + return; + } + + if (items.length === 0) { + printEmpty("skill categories"); + return; + } + + printListHeader("Skill Categories", items.length); + + const columns: TableColumn[] = [ + { + header: "Value", + width: 36, + getValue: (c) => chalk.cyan(c.value), + }, + { header: "Label", width: 36, getValue: (c) => c.label }, + { + header: "Description", + width: 40, + getValue: (c) => optional(c.description), + }, + ]; + printTable(items, columns); + } catch (error) { + spinner.stop(); + handleSdkError(error, "list skill categories"); + } + }); + + cmd + .command("get-users") + .description( + "Get users with access to skills\n" + + "Examples:\n" + + " $ codemie sdk skills get-users\n" + + " $ codemie sdk skills get-users --json", + ) + .option("--json", "Output in JSON format") + .action(async (opts) => { + const client = await getSdkClient(); + const spinner = ora("Fetching skill users...").start(); + + try { + const items = await getSkillUsers(client); + spinner.stop(); + + if (opts.json) { + outputJson(items); + return; + } + + if (items.length === 0) { + printEmpty("users"); + return; + } + + printListHeader("Skill Users", items.length); + + const columns: TableColumn[] = [ + { + header: "ID", + width: 40, + getValue: (u) => + chalk.cyan( + typeof u === "object" && u !== null && "id" in u + ? String((u as Record).id) + : "", + ), + }, + { + header: "Name", + width: 30, + getValue: (u) => + typeof u === "object" && u !== null && "name" in u + ? String((u as Record).name) + : "", + }, + { + header: "Username", + width: 30, + getValue: (u) => + typeof u === "object" && u !== null && "username" in u + ? String((u as Record).username) + : "", + }, + ]; + printTable(items, columns); + } catch (error) { + spinner.stop(); + handleSdkError(error, "get skill users"); + } + }); + + cmd + .command("react ") + .description( + "React to a skill with like or dislike\n" + + "Examples:\n" + + " $ codemie sdk skills react --reaction like\n" + + " $ codemie sdk skills react --reaction dislike", + ) + .requiredOption( + "--reaction ", + "Reaction type: 'like' or 'dislike'", + ) + .action(async (id: string, opts) => { + const client = await getSdkClient(); + const spinner = ora("Reacting to skill...").start(); + + try { + await reactToSkill(client, id, opts.reaction as "like" | "dislike"); + spinner.stop(); + printSuccess( + `${opts.reaction === "like" ? "Liked" : "Disliked"} skill ${chalk.cyan(id)}.`, + ); + } catch (error) { + spinner.stop(); + handleSdkError(error, "react to skill"); + } + }); + + cmd + .command("remove-reactions ") + .description( + "Remove all reactions from a skill\n" + + "Examples:\n" + + " $ codemie sdk skills remove-reactions ", + ) + .action(async (id: string) => { + const client = await getSdkClient(); + const spinner = ora("Removing reactions...").start(); + + try { + await removeSkillReactions(client, id); + spinner.stop(); + printSuccess(`Reactions removed from skill ${chalk.cyan(id)}.`); + } catch (error) { + spinner.stop(); + handleSdkError(error, "remove skill reactions"); + } + }); + return cmd; } diff --git a/src/cli/commands/sdk/users.ts b/src/cli/commands/sdk/users.ts index 45e5711c..886795cf 100644 --- a/src/cli/commands/sdk/users.ts +++ b/src/cli/commands/sdk/users.ts @@ -40,7 +40,22 @@ export function createUsersSubcommand(): Command { { label: "ID", value: chalk.cyan(user.user_id) }, { label: "Username", value: optional(user.username) }, { label: "Name", value: optional(user.name) }, + { label: "Email", value: optional(user.email) }, { label: "Admin", value: user.is_admin ? chalk.green("yes") : chalk.dim("no") }, + { + label: "Projects", + value: + user.applications?.length > 0 + ? user.applications.join(", ") + : chalk.dim("—"), + }, + { + label: "Admin Projects", + value: + user.applications_admin?.length > 0 + ? user.applications_admin.join(", ") + : chalk.dim("—"), + }, ]; printDetail(rows); From ea719308c9f5f460c31e53b62d0844103dffd9f2 Mon Sep 17 00:00:00 2001 From: Kostiantyn Pshenychnyi Date: Fri, 24 Apr 2026 11:12:36 +0300 Subject: [PATCH 05/13] feat: update workflow.md example --- .../skills/codemie-sdk/examples/workflows.md | 45 ++++++++++++++----- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/workflows.md b/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/workflows.md index dccf27b5..dbee1d4c 100644 --- a/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/workflows.md +++ b/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/workflows.md @@ -68,22 +68,47 @@ codemie sdk workflows create --json workflow-meta.json --config path/to/workflow } ``` -**`workflow.yaml` example:** +**`workflow.yaml` example (minimal working format):** ```yaml +custom_nodes: [] +tools: [] assistants: - - id: processor - assistant_id: - system_prompt: You are a data processing assistant +- id: my-node + model: gpt-4.1 + system_prompt: You are a helpful assistant. + tools: [] states: - - id: start - assistant_id: processor - next: - state_id: end +- id: my-node + assistant_id: my-node + task: Help the user with their request. + next: + state_id: end + resolve_dynamic_values_in_prompt: true ``` -Get assistant IDs to reference in YAML: +> **Important:** Pass the YAML as an **inline string** via `--config`, not as a file path — file-based config is unreliable on some systems: +> ```bash +> codemie sdk workflows create \ +> --data '{"name":"My Workflow","project":"MyProject","mode":"Sequential","shared":true}' \ +> --config 'custom_nodes: [] +> tools: [] +> assistants: +> - id: my-node +> model: gpt-4.1 +> system_prompt: You are a helpful assistant. +> tools: [] +> states: +> - id: my-node +> assistant_id: my-node +> task: Help the user with their request. +> next: +> state_id: end +> resolve_dynamic_values_in_prompt: true' +> ``` + +Get available model names: ```bash -codemie sdk assistants list --projects Engineering --json | jq -r '.[] | "\(.id) \(.name)"' +codemie sdk llm list --json | jq -r '.[] | "\(.base_name) (\(.label))"' ``` ## Update From c200bc33977307d8f45c60c2797ec54368c61c2c Mon Sep 17 00:00:00 2001 From: Kostiantyn Pshenychnyi Date: Fri, 24 Apr 2026 16:13:02 +0300 Subject: [PATCH 06/13] feat: add analytics service --- .../claude/plugin/skills/codemie-sdk/SKILL.md | 113 +-- .../skills/codemie-sdk/examples/analytics.md | 244 ++++++ src/cli/commands/sdk/analytics.ts | 717 ++++++++++++++++++ src/cli/commands/sdk/index.ts | 4 +- src/cli/commands/sdk/services/analytics.ts | 164 ++++ src/cli/commands/sdk/services/index.ts | 1 + 6 files changed, 1191 insertions(+), 52 deletions(-) create mode 100644 src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/analytics.md create mode 100644 src/cli/commands/sdk/analytics.ts create mode 100644 src/cli/commands/sdk/services/analytics.ts diff --git a/src/agents/plugins/claude/plugin/skills/codemie-sdk/SKILL.md b/src/agents/plugins/claude/plugin/skills/codemie-sdk/SKILL.md index ec39d6fa..959d9271 100644 --- a/src/agents/plugins/claude/plugin/skills/codemie-sdk/SKILL.md +++ b/src/agents/plugins/claude/plugin/skills/codemie-sdk/SKILL.md @@ -1,21 +1,24 @@ --- name: codemie-sdk description: >- - Manage CodeMie platform assets (assistants, workflows, datasources, integrations, skills, users, categories) directly from CLI + Manage CodeMie platform assets (assistants, workflows, datasources, integrations, skills, users, categories, analytics) directly from CLI using CodeMie SDK. Use when user says "create assistant", "list workflows", "update datasource", "delete assistant", "show my assistants", "get workflow details", "manage integrations", "create integration", "list integrations", "list llm models", "list embedding models", "list skills", "get skill", "create skill", "update skill", "delete skill", "publish skill", "import skill", "export skill", "attach skill", "list categories", "get category", "create category", "delete category", "who am i", "current user", "my profile", "user info", - or any request to manage CodeMie platform resources. + "analytics", "usage analytics", "summaries", "cli analytics", "spending", "users activity", + "projects activity", "assistants chats analytics", "workflows analytics", "tools usage", + "mcp servers analytics", "llms usage", "users spending", "budget limits", + or any request to manage CodeMie platform resources or view analytics. --- # CodeMie SDK Asset Management Manage CodeMie platform assets from the CLI. -**Asset Types:** `assistants`, `workflows`, `datasources`, `integrations`, `skills`, `users`, `categories` +**Asset Types:** `assistants`, `workflows`, `datasources`, `integrations`, `skills`, `users`, `categories`, `analytics` **Operations:** `list`, `get`, `create`, `update`, `delete` @@ -80,6 +83,7 @@ Once the project is known, use it in all subsequent commands: | Skills | [examples/skills.md](examples/skills.md) | | Users | [examples/users.md](examples/users.md) | | Categories | [examples/categories.md](examples/categories.md) | +| Analytics | [examples/analytics.md](examples/analytics.md) | Do **not** guess field names or skip this step — all required/optional fields, nested schemas, and asset cross-reference commands are documented there. @@ -114,14 +118,6 @@ codemie sdk assistants delete **Required on create:** `name`, `project`, `system_prompt` -**Important notes:** -- Use `context` (not `skill_ids`) to attach datasources. Get datasource IDs: `codemie sdk datasources list --json` -- Use `toolkits` to attach integrations. Get exact structure: `codemie sdk assistants get-tools --json` -- Use `base_name` from `codemie sdk llm list --json` when setting `llm_model_type` -- `skill_ids` holds built-in platform skills, not datasources - -**Responses:** `✓ Specified assistant saved` / `✓ Specified assistant updated` / `✓ Assistant deleted.` - --- ## Workflows @@ -138,20 +134,13 @@ codemie sdk workflows delete **Required on create:** `name`, `project`, `mode` (`"Sequential"`), `shared` (boolean), plus `--config` with YAML graph definition -**Important notes:** -- `--config` is required on create and optional on update -- `mode` and `shared` are required on create; both are optional on update -- Reference assistant IDs in YAML: `codemie sdk assistants list --json` - -**Responses:** `✓ Workflow created successfully` / `✓ Workflow updated successfully` / `✓ Workflow deleted.` - --- ## Datasources > See [examples/datasources.md](examples/datasources.md) for full field reference and examples. -Datasources use **type subcommands** for create/update: `confluence`, `jira`, `file`, `code`, `google`, `json`, `provider`, `summary`, `chunk-summary`, `azure-devops-wiki`, `azure-devops-work-item`, `xray`, `sharepoint`, `platform` +Type subcommands for create/update: `confluence`, `jira`, `file`, `code`, `google`, `provider`, `azure-devops-wiki`, `azure-devops-work-item`, `xray`, `sharepoint` ```bash codemie sdk datasources list [--search ] [--projects ] [--status ] [--datasource-types ] [--sort-key date|update_date] [--sort-order asc|desc] [--page ] [--per-page ] [--json] @@ -164,12 +153,6 @@ codemie sdk datasources delete **Required on create (all types):** `name` (no spaces, use hyphens), `project_name`, plus type-specific required fields -**Important notes:** -- `confluence` and `jira` require a pre-configured integration. Get integration IDs: `codemie sdk integrations list --json` -- `code` type triggers background indexing — response is `✓ Indexing of datasource has been started in the background` -- Update supports reindex control flags: `full_reindex`, `incremental_reindex`, `resume_indexing`, `skip_reindex` -- Status values: `completed`, `failed`, `fetching`, `in_progress` - --- ## Integrations @@ -185,14 +168,7 @@ codemie sdk integrations update --data '' | --json codemie sdk integrations delete [--setting-type user|project] ``` -**Required on create:** `credential_type`, `project_name`, `credential_values` - -**Important notes:** -- `credential_values` **must include** `{"key":"alias","value":""}` matching the top-level `alias` field -- `--setting-type` defaults to `user`; use `project` for team-shared integrations -- Sensitive values are masked as `**********` in all output - -**Responses:** `✓ Specified credentials saved` / `✓ Specified credentials updated` / `✓ Integration deleted.` +**Required on create:** `credential_type`, `project_name`, `credential_values` (must include `{"key":"alias","value":""}`) --- @@ -203,8 +179,6 @@ codemie sdk llm list [--json] codemie sdk llm list --embeddings [--json] ``` -Returns `LLMModel` objects. Key fields: `base_name`, `label`, `provider`, `default`, `enabled`. - Use `base_name` when setting `llm_model_type` on an assistant or `embeddings_model`/`summarization_model` on a datasource. --- @@ -236,12 +210,6 @@ codemie sdk skills remove-reactions **Required on create:** `name` (kebab-case, 3–64 chars), `description` (10–1000 chars), `content` (markdown, min 100 chars), `project` -**Key fields:** `id`, `name`, `project`, `visibility`, `description`, `content`, `created_by`, `createdDate`, `assistants_count`, `categories` - -**`--scope` values:** `marketplace`, `project`, `project_with_marketplace` - -**`visibility` values:** `private`, `project`, `public` - --- ## Users @@ -253,10 +221,6 @@ codemie sdk users me [--json] codemie sdk users data [--json] ``` -**`users me`** — current user profile. Fields: `user_id`, `name`, `username`, `email`, `is_admin`, `applications`, `applications_admin`, `picture`, `knowledge_bases` - -**`users data`** — user preferences and metadata. Fields: `id`, `user_id`, `date`, `update_date` - --- ## Categories @@ -275,10 +239,57 @@ codemie sdk categories delete **Required on create:** `name` (1–255 chars) -**Key fields:** `id`, `name`, `description`, `marketplaceAssistantCount`, `projectAssistantCount`, `createdAt` +--- + +## Analytics + +> See [examples/analytics.md](examples/analytics.md) for full examples and scripting patterns. + +Analytics endpoints generally require admin access. All commands support `--json` for raw output. + +### Filter parameters + +**`--time-period `** — preset time window. Mutually exclusive with `--start-date`/`--end-date`. Exact accepted values: +`last_hour` | `last_6_hours` | `last_24_hours` | `last_7_days` | `last_30_days` | `last_60_days` | `last_year` + +**`--start-date `** / **`--end-date `** — custom date range. Mutually exclusive with `--time-period`. Format: ISO 8601 UTC string `"YYYY-MM-DDTHH:MM:SSZ"`. Rules: +- `start_date` must be before `end_date`; `end_date` must not be in the future +- If neither time filter is provided, the backend defaults to **last 30 days** -**Important notes:** -- `list` without `--paginated` calls the public endpoint (no admin required) — returns `id`, `name`, `description` -- `list --paginated`, `get`, `create`, `update`, `delete` all require **admin access** -- `delete` fails with 409 if any assistants are still assigned to the category -- Use the category `id` in the assistant `categories` field when creating/updating assistants +**`--users `** — comma-separated user IDs. Get valid IDs via `codemie sdk analytics users --json`. Non-admin users can only filter by themselves. + +**`--projects `** — comma-separated project names (not UUIDs). Projects the caller cannot access are silently ignored. + +**`--page `** / **`--per-page `** — zero-indexed page (default `0`), items per page `1–1000` (default `20`). Tabular endpoints only. + +### Commands + +```bash +# Summaries (SummariesResponse: data.metrics[]) +codemie sdk analytics summaries [filters] [--json] +codemie sdk analytics cli-summary [filters] [--json] + +# Users list (UsersListResponse: data.users[], data.total_count) +codemie sdk analytics users [filters] [--json] + +# Tabular endpoints (TabularResponse: data.columns[], data.rows[], pagination) +codemie sdk analytics assistants-chats [filters] [--page ] [--per-page ] [--json] +codemie sdk analytics workflows [filters] [--page ] [--per-page ] [--json] +codemie sdk analytics tools-usage [filters] [--page ] [--per-page ] [--json] +codemie sdk analytics webhooks-invocation [filters] [--page ] [--per-page ] [--json] +codemie sdk analytics mcp-servers [filters] [--page ] [--per-page ] [--json] +codemie sdk analytics mcp-servers-by-users [filters] [--page ] [--per-page ] [--json] +codemie sdk analytics projects-spending [filters] [--page ] [--per-page ] [--json] +codemie sdk analytics llms-usage [filters] [--page ] [--per-page ] [--json] +codemie sdk analytics users-spending [filters] [--page ] [--per-page ] [--json] +codemie sdk analytics budget-soft-limit [filters] [--page ] [--per-page ] [--json] +codemie sdk analytics budget-hard-limit [filters] [--page ] [--per-page ] [--json] +codemie sdk analytics users-activity [filters] [--page ] [--per-page ] [--json] +codemie sdk analytics projects-activity [filters] [--page ] [--per-page ] [--json] +codemie sdk analytics agents-usage [filters] [--page ] [--per-page ] [--json] +codemie sdk analytics cli-agents [filters] [--page ] [--per-page ] [--json] +codemie sdk analytics cli-llms [filters] [--page ] [--per-page ] [--json] +codemie sdk analytics cli-users [filters] [--page ] [--per-page ] [--json] +codemie sdk analytics cli-errors [filters] [--page ] [--per-page ] [--json] +codemie sdk analytics cli-repositories [filters] [--page ] [--per-page ] [--json] +``` diff --git a/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/analytics.md b/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/analytics.md new file mode 100644 index 00000000..139ec553 --- /dev/null +++ b/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/analytics.md @@ -0,0 +1,244 @@ +# Analytics Examples + +--- + +## Filter Parameters Reference + +All analytics commands accept the same set of optional filter flags. + +### Time range (mutually exclusive — use one or the other, never both) + +**Option A — preset period:** + +| Value | Covers | +|-------|--------| +| `last_hour` | Elapsed 60 minutes from now | +| `last_6_hours` | Elapsed 6 hours from now | +| `last_24_hours` | Elapsed 24 hours from now | +| `last_7_days` | Midnight UTC (now − 7 days) → now | +| `last_30_days` | Midnight UTC (now − 30 days) → now *(backend default)* | +| `last_60_days` | Midnight UTC (now − 60 days) → now | +| `last_year` | Midnight UTC (now − 365 days) → now | + +```bash +--time-period last_30_days +``` + +**Option B — custom date range:** + +- Format: ISO 8601 UTC datetime string — `"YYYY-MM-DDTHH:MM:SSZ"` +- `--start-date` must be before `--end-date` +- `--end-date` must not be in the future +- If only `--start-date` is given, end defaults to now +- If only `--end-date` is given, start defaults to `end − 30 days` + +```bash +--start-date 2024-01-01T00:00:00Z --end-date 2024-12-31T23:59:59Z +``` + +### Entity filters + +| Flag | Format | Example | +|------|--------|---------| +| `--users` | Comma-separated **user IDs** (from `codemie sdk analytics users --json` → `.data.users[].id`). Non-admin users can only filter by themselves. **Not emails.** | `--users abc123` or `--users abc123,def456` | +| `--projects` | Comma-separated project names (not UUIDs). Whitespace around each value is stripped. Projects you cannot access are silently ignored. | `--projects my-project` or `--projects my-project,other-project` | + +> **Get valid user IDs before filtering:** +> ```bash +> codemie sdk analytics users --json | jq '.data.users[] | {id, name}' +> ``` + +### Pagination (tabular endpoints only) + +| Flag | Default | Range | Notes | +|------|---------|-------|-------| +| `--page` | `0` | `≥ 0` | Zero-indexed | +| `--per-page` | `20` | `1–1000` | | + +--- + +## Summaries + +Returns aggregated metric cards (token counts, active users, request counts, etc.). + +```bash +codemie sdk analytics summaries +codemie sdk analytics summaries --json +codemie sdk analytics summaries --time-period last_30_days +codemie sdk analytics summaries --start-date 2024-01-01T00:00:00Z --end-date 2024-12-31T23:59:59Z --json +codemie sdk analytics summaries --projects my-project --time-period last_7_days --json +``` + +**JSON fields:** `data.metrics[]` → `id`, `label`, `type`, `value`, `format`, `description` + +--- + +## CLI Summary + +Returns CLI-specific summary metrics (CLI sessions, agents used, tokens consumed via CLI). + +```bash +codemie sdk analytics cli-summary +codemie sdk analytics cli-summary --time-period last_7_days --json +codemie sdk analytics cli-summary --users --json +``` + +**JSON fields:** same as summaries — `data.metrics[]` + +--- + +## Users List + +Returns users for the last --time-period or --start-date and --end-date, visible to the caller. Non-admin users only see themselves. Use this to discover valid **user IDs** for the `--users` filter on other endpoints. + +```bash +codemie sdk analytics users +codemie sdk analytics users --json +``` + +**JSON fields:** `data.users[]` → `id`, `name`; `data.total_count` + +--- + +## Tabular Endpoints + +All tabular endpoints return the same structure: + +**JSON fields:** `data.columns[]` → `id`, `label`, `type`; `data.rows[]`; `data.totals`; `pagination` → `page`, `per_page`, `total_count`, `has_more` + +### Assistants chats + +```bash +codemie sdk analytics assistants-chats +codemie sdk analytics assistants-chats --page 0 --per-page 50 --json +codemie sdk analytics assistants-chats --projects my-project --time-period last_30_days --json +codemie sdk analytics assistants-chats --start-date 2024-06-01T00:00:00Z --end-date 2024-06-30T23:59:59Z --json +``` + +### Workflows + +```bash +codemie sdk analytics workflows +codemie sdk analytics workflows --projects my-project --json +codemie sdk analytics workflows --time-period last_7_days --json +``` + +### Tools usage + +```bash +codemie sdk analytics tools-usage +codemie sdk analytics tools-usage --time-period last_30_days --json +codemie sdk analytics tools-usage --projects my-project --users --json +``` + +### Webhooks invocation + +```bash +codemie sdk analytics webhooks-invocation +codemie sdk analytics webhooks-invocation --time-period last_7_days --json +``` + +### MCP servers + +```bash +codemie sdk analytics mcp-servers +codemie sdk analytics mcp-servers --time-period last_30_days --json + +codemie sdk analytics mcp-servers-by-users +codemie sdk analytics mcp-servers-by-users --users --json +``` + +### Spending + +```bash +# LLM spend by project +codemie sdk analytics projects-spending +codemie sdk analytics projects-spending --projects my-project --time-period last_30_days --json + +# Token usage and cost by LLM model +codemie sdk analytics llms-usage +codemie sdk analytics llms-usage --time-period last_year --json + +# Token spend per user +codemie sdk analytics users-spending +codemie sdk analytics users-spending --users alice@acme.com,bob@acme.com --json + +# Users at or near soft budget limit +codemie sdk analytics budget-soft-limit +codemie sdk analytics budget-soft-limit --projects my-project --json + +# Users at or over hard budget limit +codemie sdk analytics budget-hard-limit +codemie sdk analytics budget-hard-limit --json +``` + +### Activity + +```bash +# Activity timeline per user (sessions, messages, active days) +codemie sdk analytics users-activity +codemie sdk analytics users-activity --users alice@acme.com --time-period last_30_days --json + +# Activity timeline per project +codemie sdk analytics projects-activity +codemie sdk analytics projects-activity --projects my-project --time-period last_7_days --json +``` + +### Agents usage + +```bash +# Usage by AI agent (Claude, Gemini, OpenCode, etc.) — all channels +codemie sdk analytics agents-usage +codemie sdk analytics agents-usage --time-period last_30_days --json + +# CLI-only agent usage +codemie sdk analytics cli-agents +codemie sdk analytics cli-agents --users alice@acme.com --json +``` + +### CLI analytics + +```bash +# LLM model usage via CLI +codemie sdk analytics cli-llms +codemie sdk analytics cli-llms --time-period last_7_days --json + +# CLI usage broken down by user +codemie sdk analytics cli-users +codemie sdk analytics cli-users --projects my-project --json + +# CLI error events +codemie sdk analytics cli-errors +codemie sdk analytics cli-errors --time-period last_24_hours --json + +# CLI usage by git repository +codemie sdk analytics cli-repositories +codemie sdk analytics cli-repositories --projects my-project --json +``` + +--- + +## Scripting + +```bash +# Get total assistant chat count +codemie sdk analytics assistants-chats --json | jq '.pagination.total_count' + +# Get all metric IDs and values from summaries +codemie sdk analytics summaries --json | jq '.data.metrics[] | {id, value}' + +# Get project spending rows for a specific project +codemie sdk analytics projects-spending --projects my-project --json | jq '.data.rows[]' + +# Get user activity for last 7 days +codemie sdk analytics users-activity --time-period last_7_days --json | jq '.data.rows[]' + +# Count CLI errors in last 24 hours +codemie sdk analytics cli-errors --time-period last_24_hours --json | jq '.pagination.total_count' + +# Get column names for any tabular endpoint +codemie sdk analytics llms-usage --json | jq '[.data.columns[] | .label]' + +# Discover valid user values for --users filter +codemie sdk analytics users --json | jq '.data.users[] | .id' +``` diff --git a/src/cli/commands/sdk/analytics.ts b/src/cli/commands/sdk/analytics.ts new file mode 100644 index 00000000..79f8a2fd --- /dev/null +++ b/src/cli/commands/sdk/analytics.ts @@ -0,0 +1,717 @@ +import { Command } from "commander"; +import chalk from "chalk"; +import ora from "ora"; +import type { SummariesResponse, TabularResponse, UsersListResponse } from "codemie-sdk"; +import type { AnalyticsQueryParams, PaginatedAnalyticsQueryParams } from "codemie-sdk"; +import { + getAnalyticsSummaries, + getAnalyticsCliSummary, + getAnalyticsUsers, + getAnalyticsAssistantsChats, + getAnalyticsWorkflows, + getAnalyticsToolsUsage, + getAnalyticsWebhooksInvocation, + getAnalyticsMcpServers, + getAnalyticsMcpServersByUsers, + getAnalyticsProjectsSpending, + getAnalyticsLlmsUsage, + getAnalyticsUsersSpending, + getAnalyticsBudgetSoftLimit, + getAnalyticsBudgetHardLimit, + getAnalyticsUsersActivity, + getAnalyticsProjectsActivity, + getAnalyticsAgentsUsage, + getAnalyticsCliAgents, + getAnalyticsCliLlms, + getAnalyticsCliUsers, + getAnalyticsCliErrors, + getAnalyticsCliRepositories, +} from "./services/analytics.js"; +import { getSdkClient, outputJson, handleSdkError } from "./utils/cli-utils.js"; +import { printTable, printListHeader, printEmpty, optional, type TableColumn } from "./utils/render.js"; + +/** + * Build base analytics query params from command options + */ +function buildBaseParams(opts: { + timePeriod?: string; + startDate?: string; + endDate?: string; + users?: string; + projects?: string; +}): AnalyticsQueryParams { + return { + ...(opts.timePeriod !== undefined && { time_period: opts.timePeriod }), + ...(opts.startDate !== undefined && { start_date: opts.startDate }), + ...(opts.endDate !== undefined && { end_date: opts.endDate }), + ...(opts.users !== undefined && { users: opts.users }), + ...(opts.projects !== undefined && { projects: opts.projects }), + }; +} + +/** + * Build paginated analytics query params from command options + */ +function buildPaginatedParams(opts: { + timePeriod?: string; + startDate?: string; + endDate?: string; + users?: string; + projects?: string; + page?: number; + perPage?: number; +}): PaginatedAnalyticsQueryParams { + return { + ...buildBaseParams(opts), + ...(opts.page !== undefined && { page: opts.page }), + ...(opts.perPage !== undefined && { per_page: opts.perPage }), + }; +} + +/** + * Add common date filter options to a command + */ +function addBaseFilterOptions(cmd: Command): Command { + return cmd + .option("--time-period ", "Time period filter (e.g. last_7_days, last_30_days)") + .option("--start-date ", "Start date (ISO 8601, e.g. 2024-01-01)") + .option("--end-date ", "End date (ISO 8601, e.g. 2024-12-31)") + .option("--users ", "Filter by user(s)") + .option("--projects ", "Filter by projects (comma-separated)"); +} + +/** + * Add pagination options in addition to base filters + */ +function addPaginatedFilterOptions(cmd: Command): Command { + return addBaseFilterOptions(cmd) + .option("--page ", "Page number (0-indexed)", (v) => parseInt(v, 10)) + .option("--per-page ", "Items per page", (v) => parseInt(v, 10)); +} + +/** + * Print a SummariesResponse as a table or JSON + */ +function printSummaries(response: SummariesResponse, json: boolean): void { + if (json) { + outputJson(response); + return; + } + + const metrics = response.data?.metrics ?? []; + if (metrics.length === 0) { + printEmpty("metrics"); + return; + } + + const columns: TableColumn<(typeof metrics)[0]>[] = [ + { header: "ID", width: 30, getValue: (m) => chalk.cyan(m.id) }, + { header: "Label", width: 30, getValue: (m) => optional(m.label) }, + { header: "Type", width: 14, getValue: (m) => optional(m.type) }, + { header: "Value", width: 24, getValue: (m) => String(m.value ?? chalk.dim("—")) }, + ]; + + printListHeader("Metrics", metrics.length); + printTable(metrics, columns); +} + +/** + * Print a TabularResponse as a table or JSON + */ +function printTabular(response: TabularResponse, json: boolean): void { + if (json) { + outputJson(response); + return; + } + + const rows = response.data?.rows ?? []; + const columns = response.data?.columns ?? []; + + if (rows.length === 0) { + printEmpty("records"); + return; + } + + const tableColumns: TableColumn>[] = columns.map((col) => ({ + header: col.label, + width: Math.max(col.label.length + 4, 18), + getValue: (row) => { + const val = row[col.id]; + return val != null ? String(val) : chalk.dim("—"); + }, + })); + + printListHeader("Records", rows.length); + printTable(rows, tableColumns); + + const pagination = response.pagination; + if (pagination) { + console.log( + chalk.dim( + `\n Page ${pagination.page + 1} · ${rows.length} of ${pagination.total_count} total` + + (pagination.has_more ? " · more available" : ""), + ), + ); + } +} + +/** + * Print a UsersListResponse as a table or JSON + */ +function printUsersList(response: UsersListResponse, json: boolean): void { + if (json) { + outputJson(response); + return; + } + + const users = response.data?.users ?? []; + if (users.length === 0) { + printEmpty("users"); + return; + } + + const columns: TableColumn<(typeof users)[0]>[] = [ + { header: "ID", width: 40, getValue: (u) => chalk.cyan(u.id) }, + { header: "Name", width: 40, getValue: (u) => optional(u.name) }, + ]; + + printListHeader("Users", users.length); + printTable(users, columns); + console.log(chalk.dim(`\n Total: ${response.data.total_count}`)); +} + +export function createAnalyticsSubcommand(): Command { + const cmd = new Command("analytics").description( + "Access CodeMie platform analytics and usage data", + ); + + // summaries + addBaseFilterOptions( + cmd + .command("summaries") + .description( + "Get platform usage summaries\n" + + "Examples:\n" + + " $ codemie sdk analytics summaries\n" + + " $ codemie sdk analytics summaries --time-period last_30_days --json", + ) + .option("--json", "Output in JSON format"), + ).action(async (opts) => { + const client = await getSdkClient(); + const spinner = ora("Fetching summaries...").start(); + try { + const result = await getAnalyticsSummaries(client, buildBaseParams(opts)); + spinner.stop(); + printSummaries(result, opts.json); + } catch (error) { + spinner.stop(); + handleSdkError(error, "get summaries"); + } + }); + + // cli-summary + addBaseFilterOptions( + cmd + .command("cli-summary") + .description( + "Get CLI usage summary\n" + + "Examples:\n" + + " $ codemie sdk analytics cli-summary\n" + + " $ codemie sdk analytics cli-summary --time-period last_7_days --json", + ) + .option("--json", "Output in JSON format"), + ).action(async (opts) => { + const client = await getSdkClient(); + const spinner = ora("Fetching CLI summary...").start(); + try { + const result = await getAnalyticsCliSummary(client, buildBaseParams(opts)); + spinner.stop(); + printSummaries(result, opts.json); + } catch (error) { + spinner.stop(); + handleSdkError(error, "get CLI summary"); + } + }); + + // users + addBaseFilterOptions( + cmd + .command("users") + .description( + "List users with analytics access\n" + + "Examples:\n" + + " $ codemie sdk analytics users\n" + + " $ codemie sdk analytics users --json", + ) + .option("--json", "Output in JSON format"), + ).action(async (opts) => { + const client = await getSdkClient(); + const spinner = ora("Fetching users...").start(); + try { + const result = await getAnalyticsUsers(client, buildBaseParams(opts)); + spinner.stop(); + printUsersList(result, opts.json); + } catch (error) { + spinner.stop(); + handleSdkError(error, "get analytics users"); + } + }); + + // assistants-chats + addPaginatedFilterOptions( + cmd + .command("assistants-chats") + .description( + "Get assistant chat analytics\n" + + "Examples:\n" + + " $ codemie sdk analytics assistants-chats\n" + + " $ codemie sdk analytics assistants-chats --page 0 --per-page 50 --json", + ) + .option("--json", "Output in JSON format"), + ).action(async (opts) => { + const client = await getSdkClient(); + const spinner = ora("Fetching assistant chats analytics...").start(); + try { + const result = await getAnalyticsAssistantsChats(client, buildPaginatedParams(opts)); + spinner.stop(); + printTabular(result, opts.json); + } catch (error) { + spinner.stop(); + handleSdkError(error, "get assistants chats analytics"); + } + }); + + // workflows + addPaginatedFilterOptions( + cmd + .command("workflows") + .description( + "Get workflow analytics\n" + + "Examples:\n" + + " $ codemie sdk analytics workflows\n" + + " $ codemie sdk analytics workflows --projects myproject --json", + ) + .option("--json", "Output in JSON format"), + ).action(async (opts) => { + const client = await getSdkClient(); + const spinner = ora("Fetching workflows analytics...").start(); + try { + const result = await getAnalyticsWorkflows(client, buildPaginatedParams(opts)); + spinner.stop(); + printTabular(result, opts.json); + } catch (error) { + spinner.stop(); + handleSdkError(error, "get workflows analytics"); + } + }); + + // tools-usage + addPaginatedFilterOptions( + cmd + .command("tools-usage") + .description( + "Get tools usage analytics\n" + + "Examples:\n" + + " $ codemie sdk analytics tools-usage\n" + + " $ codemie sdk analytics tools-usage --json", + ) + .option("--json", "Output in JSON format"), + ).action(async (opts) => { + const client = await getSdkClient(); + const spinner = ora("Fetching tools usage analytics...").start(); + try { + const result = await getAnalyticsToolsUsage(client, buildPaginatedParams(opts)); + spinner.stop(); + printTabular(result, opts.json); + } catch (error) { + spinner.stop(); + handleSdkError(error, "get tools usage analytics"); + } + }); + + // webhooks-invocation + addPaginatedFilterOptions( + cmd + .command("webhooks-invocation") + .description( + "Get webhooks invocation analytics\n" + + "Examples:\n" + + " $ codemie sdk analytics webhooks-invocation\n" + + " $ codemie sdk analytics webhooks-invocation --json", + ) + .option("--json", "Output in JSON format"), + ).action(async (opts) => { + const client = await getSdkClient(); + const spinner = ora("Fetching webhooks invocation analytics...").start(); + try { + const result = await getAnalyticsWebhooksInvocation(client, buildPaginatedParams(opts)); + spinner.stop(); + printTabular(result, opts.json); + } catch (error) { + spinner.stop(); + handleSdkError(error, "get webhooks invocation analytics"); + } + }); + + // mcp-servers + addPaginatedFilterOptions( + cmd + .command("mcp-servers") + .description( + "Get MCP servers analytics\n" + + "Examples:\n" + + " $ codemie sdk analytics mcp-servers\n" + + " $ codemie sdk analytics mcp-servers --json", + ) + .option("--json", "Output in JSON format"), + ).action(async (opts) => { + const client = await getSdkClient(); + const spinner = ora("Fetching MCP servers analytics...").start(); + try { + const result = await getAnalyticsMcpServers(client, buildPaginatedParams(opts)); + spinner.stop(); + printTabular(result, opts.json); + } catch (error) { + spinner.stop(); + handleSdkError(error, "get MCP servers analytics"); + } + }); + + // mcp-servers-by-users + addPaginatedFilterOptions( + cmd + .command("mcp-servers-by-users") + .description( + "Get MCP servers by users analytics\n" + + "Examples:\n" + + " $ codemie sdk analytics mcp-servers-by-users\n" + + " $ codemie sdk analytics mcp-servers-by-users --json", + ) + .option("--json", "Output in JSON format"), + ).action(async (opts) => { + const client = await getSdkClient(); + const spinner = ora("Fetching MCP servers by users analytics...").start(); + try { + const result = await getAnalyticsMcpServersByUsers(client, buildPaginatedParams(opts)); + spinner.stop(); + printTabular(result, opts.json); + } catch (error) { + spinner.stop(); + handleSdkError(error, "get MCP servers by users analytics"); + } + }); + + // projects-spending + addPaginatedFilterOptions( + cmd + .command("projects-spending") + .description( + "Get projects spending analytics\n" + + "Examples:\n" + + " $ codemie sdk analytics projects-spending\n" + + " $ codemie sdk analytics projects-spending --projects myproject --json", + ) + .option("--json", "Output in JSON format"), + ).action(async (opts) => { + const client = await getSdkClient(); + const spinner = ora("Fetching projects spending analytics...").start(); + try { + const result = await getAnalyticsProjectsSpending(client, buildPaginatedParams(opts)); + spinner.stop(); + printTabular(result, opts.json); + } catch (error) { + spinner.stop(); + handleSdkError(error, "get projects spending analytics"); + } + }); + + // llms-usage + addPaginatedFilterOptions( + cmd + .command("llms-usage") + .description( + "Get LLMs usage analytics\n" + + "Examples:\n" + + " $ codemie sdk analytics llms-usage\n" + + " $ codemie sdk analytics llms-usage --json", + ) + .option("--json", "Output in JSON format"), + ).action(async (opts) => { + const client = await getSdkClient(); + const spinner = ora("Fetching LLMs usage analytics...").start(); + try { + const result = await getAnalyticsLlmsUsage(client, buildPaginatedParams(opts)); + spinner.stop(); + printTabular(result, opts.json); + } catch (error) { + spinner.stop(); + handleSdkError(error, "get LLMs usage analytics"); + } + }); + + // users-spending + addPaginatedFilterOptions( + cmd + .command("users-spending") + .description( + "Get users spending analytics\n" + + "Examples:\n" + + " $ codemie sdk analytics users-spending\n" + + " $ codemie sdk analytics users-spending --users alice --json", + ) + .option("--json", "Output in JSON format"), + ).action(async (opts) => { + const client = await getSdkClient(); + const spinner = ora("Fetching users spending analytics...").start(); + try { + const result = await getAnalyticsUsersSpending(client, buildPaginatedParams(opts)); + spinner.stop(); + printTabular(result, opts.json); + } catch (error) { + spinner.stop(); + handleSdkError(error, "get users spending analytics"); + } + }); + + // budget-soft-limit + addPaginatedFilterOptions( + cmd + .command("budget-soft-limit") + .description( + "Get budget soft limit analytics\n" + + "Examples:\n" + + " $ codemie sdk analytics budget-soft-limit\n" + + " $ codemie sdk analytics budget-soft-limit --json", + ) + .option("--json", "Output in JSON format"), + ).action(async (opts) => { + const client = await getSdkClient(); + const spinner = ora("Fetching budget soft limit analytics...").start(); + try { + const result = await getAnalyticsBudgetSoftLimit(client, buildPaginatedParams(opts)); + spinner.stop(); + printTabular(result, opts.json); + } catch (error) { + spinner.stop(); + handleSdkError(error, "get budget soft limit analytics"); + } + }); + + // budget-hard-limit + addPaginatedFilterOptions( + cmd + .command("budget-hard-limit") + .description( + "Get budget hard limit analytics\n" + + "Examples:\n" + + " $ codemie sdk analytics budget-hard-limit\n" + + " $ codemie sdk analytics budget-hard-limit --json", + ) + .option("--json", "Output in JSON format"), + ).action(async (opts) => { + const client = await getSdkClient(); + const spinner = ora("Fetching budget hard limit analytics...").start(); + try { + const result = await getAnalyticsBudgetHardLimit(client, buildPaginatedParams(opts)); + spinner.stop(); + printTabular(result, opts.json); + } catch (error) { + spinner.stop(); + handleSdkError(error, "get budget hard limit analytics"); + } + }); + + // users-activity + addPaginatedFilterOptions( + cmd + .command("users-activity") + .description( + "Get users activity analytics\n" + + "Examples:\n" + + " $ codemie sdk analytics users-activity\n" + + " $ codemie sdk analytics users-activity --users alice --json", + ) + .option("--json", "Output in JSON format"), + ).action(async (opts) => { + const client = await getSdkClient(); + const spinner = ora("Fetching users activity analytics...").start(); + try { + const result = await getAnalyticsUsersActivity(client, buildPaginatedParams(opts)); + spinner.stop(); + printTabular(result, opts.json); + } catch (error) { + spinner.stop(); + handleSdkError(error, "get users activity analytics"); + } + }); + + // projects-activity + addPaginatedFilterOptions( + cmd + .command("projects-activity") + .description( + "Get projects activity analytics\n" + + "Examples:\n" + + " $ codemie sdk analytics projects-activity\n" + + " $ codemie sdk analytics projects-activity --projects myproject --json", + ) + .option("--json", "Output in JSON format"), + ).action(async (opts) => { + const client = await getSdkClient(); + const spinner = ora("Fetching projects activity analytics...").start(); + try { + const result = await getAnalyticsProjectsActivity(client, buildPaginatedParams(opts)); + spinner.stop(); + printTabular(result, opts.json); + } catch (error) { + spinner.stop(); + handleSdkError(error, "get projects activity analytics"); + } + }); + + // agents-usage + addPaginatedFilterOptions( + cmd + .command("agents-usage") + .description( + "Get agents usage analytics\n" + + "Examples:\n" + + " $ codemie sdk analytics agents-usage\n" + + " $ codemie sdk analytics agents-usage --json", + ) + .option("--json", "Output in JSON format"), + ).action(async (opts) => { + const client = await getSdkClient(); + const spinner = ora("Fetching agents usage analytics...").start(); + try { + const result = await getAnalyticsAgentsUsage(client, buildPaginatedParams(opts)); + spinner.stop(); + printTabular(result, opts.json); + } catch (error) { + spinner.stop(); + handleSdkError(error, "get agents usage analytics"); + } + }); + + // cli-agents + addPaginatedFilterOptions( + cmd + .command("cli-agents") + .description( + "Get CLI agents analytics\n" + + "Examples:\n" + + " $ codemie sdk analytics cli-agents\n" + + " $ codemie sdk analytics cli-agents --json", + ) + .option("--json", "Output in JSON format"), + ).action(async (opts) => { + const client = await getSdkClient(); + const spinner = ora("Fetching CLI agents analytics...").start(); + try { + const result = await getAnalyticsCliAgents(client, buildPaginatedParams(opts)); + spinner.stop(); + printTabular(result, opts.json); + } catch (error) { + spinner.stop(); + handleSdkError(error, "get CLI agents analytics"); + } + }); + + // cli-llms + addPaginatedFilterOptions( + cmd + .command("cli-llms") + .description( + "Get CLI LLMs analytics\n" + + "Examples:\n" + + " $ codemie sdk analytics cli-llms\n" + + " $ codemie sdk analytics cli-llms --json", + ) + .option("--json", "Output in JSON format"), + ).action(async (opts) => { + const client = await getSdkClient(); + const spinner = ora("Fetching CLI LLMs analytics...").start(); + try { + const result = await getAnalyticsCliLlms(client, buildPaginatedParams(opts)); + spinner.stop(); + printTabular(result, opts.json); + } catch (error) { + spinner.stop(); + handleSdkError(error, "get CLI LLMs analytics"); + } + }); + + // cli-users + addPaginatedFilterOptions( + cmd + .command("cli-users") + .description( + "Get CLI users analytics\n" + + "Examples:\n" + + " $ codemie sdk analytics cli-users\n" + + " $ codemie sdk analytics cli-users --json", + ) + .option("--json", "Output in JSON format"), + ).action(async (opts) => { + const client = await getSdkClient(); + const spinner = ora("Fetching CLI users analytics...").start(); + try { + const result = await getAnalyticsCliUsers(client, buildPaginatedParams(opts)); + spinner.stop(); + printTabular(result, opts.json); + } catch (error) { + spinner.stop(); + handleSdkError(error, "get CLI users analytics"); + } + }); + + // cli-errors + addPaginatedFilterOptions( + cmd + .command("cli-errors") + .description( + "Get CLI errors analytics\n" + + "Examples:\n" + + " $ codemie sdk analytics cli-errors\n" + + " $ codemie sdk analytics cli-errors --json", + ) + .option("--json", "Output in JSON format"), + ).action(async (opts) => { + const client = await getSdkClient(); + const spinner = ora("Fetching CLI errors analytics...").start(); + try { + const result = await getAnalyticsCliErrors(client, buildPaginatedParams(opts)); + spinner.stop(); + printTabular(result, opts.json); + } catch (error) { + spinner.stop(); + handleSdkError(error, "get CLI errors analytics"); + } + }); + + // cli-repositories + addPaginatedFilterOptions( + cmd + .command("cli-repositories") + .description( + "Get CLI repositories analytics\n" + + "Examples:\n" + + " $ codemie sdk analytics cli-repositories\n" + + " $ codemie sdk analytics cli-repositories --json", + ) + .option("--json", "Output in JSON format"), + ).action(async (opts) => { + const client = await getSdkClient(); + const spinner = ora("Fetching CLI repositories analytics...").start(); + try { + const result = await getAnalyticsCliRepositories(client, buildPaginatedParams(opts)); + spinner.stop(); + printTabular(result, opts.json); + } catch (error) { + spinner.stop(); + handleSdkError(error, "get CLI repositories analytics"); + } + }); + + return cmd; +} diff --git a/src/cli/commands/sdk/index.ts b/src/cli/commands/sdk/index.ts index 57ac0219..b846b339 100644 --- a/src/cli/commands/sdk/index.ts +++ b/src/cli/commands/sdk/index.ts @@ -7,12 +7,13 @@ import { createLlmModelsSubcommand } from './llm.js'; import { createSkillsSubcommand } from './skills.js'; import { createUsersSubcommand } from './users.js'; import { createCategoriesSubcommand } from './categories.js'; +import { createAnalyticsSubcommand } from './analytics.js'; export function createSdkCommand(): Command { const cmd = new Command('sdk'); cmd.description( - 'Manage CodeMie platform assets (assistants, workflows, datasources, integrations, skills, users, categories) via the SDK' + 'Manage CodeMie platform assets (assistants, workflows, datasources, integrations, skills, users, categories, analytics) via the SDK' ); cmd.addCommand(createAssistantsSubcommand()); @@ -23,6 +24,7 @@ export function createSdkCommand(): Command { cmd.addCommand(createSkillsSubcommand()); cmd.addCommand(createUsersSubcommand()); cmd.addCommand(createCategoriesSubcommand()); + cmd.addCommand(createAnalyticsSubcommand()); return cmd; } diff --git a/src/cli/commands/sdk/services/analytics.ts b/src/cli/commands/sdk/services/analytics.ts new file mode 100644 index 00000000..42413214 --- /dev/null +++ b/src/cli/commands/sdk/services/analytics.ts @@ -0,0 +1,164 @@ +import type { + CodeMieClient, + SummariesResponse, + TabularResponse, + UsersListResponse, +} from "codemie-sdk"; +import type { + AnalyticsQueryParams, + PaginatedAnalyticsQueryParams, +} from "codemie-sdk"; + +export async function getAnalyticsSummaries( + client: CodeMieClient, + params: AnalyticsQueryParams = {}, +): Promise { + return client.analytics.getSummaries(params); +} + +export async function getAnalyticsCliSummary( + client: CodeMieClient, + params: AnalyticsQueryParams = {}, +): Promise { + return client.analytics.getCliSummary(params); +} + +export async function getAnalyticsUsers( + client: CodeMieClient, + params: AnalyticsQueryParams = {}, +): Promise { + return client.analytics.getUsers(params); +} + +export async function getAnalyticsAssistantsChats( + client: CodeMieClient, + params: PaginatedAnalyticsQueryParams = {}, +): Promise { + return client.analytics.getAssistantsChats(params); +} + +export async function getAnalyticsWorkflows( + client: CodeMieClient, + params: PaginatedAnalyticsQueryParams = {}, +): Promise { + return client.analytics.getWorkflows(params); +} + +export async function getAnalyticsToolsUsage( + client: CodeMieClient, + params: PaginatedAnalyticsQueryParams = {}, +): Promise { + return client.analytics.getToolsUsage(params); +} + +export async function getAnalyticsWebhooksInvocation( + client: CodeMieClient, + params: PaginatedAnalyticsQueryParams = {}, +): Promise { + return client.analytics.getWebhooksInvocation(params); +} + +export async function getAnalyticsMcpServers( + client: CodeMieClient, + params: PaginatedAnalyticsQueryParams = {}, +): Promise { + return client.analytics.getMcpServers(params); +} + +export async function getAnalyticsMcpServersByUsers( + client: CodeMieClient, + params: PaginatedAnalyticsQueryParams = {}, +): Promise { + return client.analytics.getMcpServersByUsers(params); +} + +export async function getAnalyticsProjectsSpending( + client: CodeMieClient, + params: PaginatedAnalyticsQueryParams = {}, +): Promise { + return client.analytics.getProjectsSpending(params); +} + +export async function getAnalyticsLlmsUsage( + client: CodeMieClient, + params: PaginatedAnalyticsQueryParams = {}, +): Promise { + return client.analytics.getLlmsUsage(params); +} + +export async function getAnalyticsUsersSpending( + client: CodeMieClient, + params: PaginatedAnalyticsQueryParams = {}, +): Promise { + return client.analytics.getUsersSpending(params); +} + +export async function getAnalyticsBudgetSoftLimit( + client: CodeMieClient, + params: PaginatedAnalyticsQueryParams = {}, +): Promise { + return client.analytics.getBudgetSoftLimit(params); +} + +export async function getAnalyticsBudgetHardLimit( + client: CodeMieClient, + params: PaginatedAnalyticsQueryParams = {}, +): Promise { + return client.analytics.getBudgetHardLimit(params); +} + +export async function getAnalyticsUsersActivity( + client: CodeMieClient, + params: PaginatedAnalyticsQueryParams = {}, +): Promise { + return client.analytics.getUsersActivity(params); +} + +export async function getAnalyticsProjectsActivity( + client: CodeMieClient, + params: PaginatedAnalyticsQueryParams = {}, +): Promise { + return client.analytics.getProjectsActivity(params); +} + +export async function getAnalyticsAgentsUsage( + client: CodeMieClient, + params: PaginatedAnalyticsQueryParams = {}, +): Promise { + return client.analytics.getAgentsUsage(params); +} + +export async function getAnalyticsCliAgents( + client: CodeMieClient, + params: PaginatedAnalyticsQueryParams = {}, +): Promise { + return client.analytics.getCliAgents(params); +} + +export async function getAnalyticsCliLlms( + client: CodeMieClient, + params: PaginatedAnalyticsQueryParams = {}, +): Promise { + return client.analytics.getCliLlms(params); +} + +export async function getAnalyticsCliUsers( + client: CodeMieClient, + params: PaginatedAnalyticsQueryParams = {}, +): Promise { + return client.analytics.getCliUsers(params); +} + +export async function getAnalyticsCliErrors( + client: CodeMieClient, + params: PaginatedAnalyticsQueryParams = {}, +): Promise { + return client.analytics.getCliErrors(params); +} + +export async function getAnalyticsCliRepositories( + client: CodeMieClient, + params: PaginatedAnalyticsQueryParams = {}, +): Promise { + return client.analytics.getCliRepositories(params); +} diff --git a/src/cli/commands/sdk/services/index.ts b/src/cli/commands/sdk/services/index.ts index bd74dc89..8cfa4d51 100644 --- a/src/cli/commands/sdk/services/index.ts +++ b/src/cli/commands/sdk/services/index.ts @@ -3,3 +3,4 @@ export * from "./workflows.js"; export * from "./datasources.js"; export * from "./integrations.js"; export * from "./llm.js"; +export * from "./analytics.js"; From 37f9d8eef99fbab9b5bd3b4be1375a40bcc8505b Mon Sep 17 00:00:00 2001 From: Kostiantyn Pshenychnyi Date: Mon, 27 Apr 2026 10:42:26 +0300 Subject: [PATCH 07/13] refactor: update cli descriptions --- .../codemie-sdk/examples/datasources.md | 43 --- src/cli/commands/sdk/analytics.ts | 317 ++++++++---------- src/cli/commands/sdk/assistants.ts | 29 +- src/cli/commands/sdk/datasources.ts | 45 +-- src/cli/commands/sdk/skills.ts | 89 ++--- src/cli/commands/sdk/users.ts | 25 +- src/cli/commands/sdk/utils/cli-utils.ts | 13 +- .../commands/sdk/utils/datasource-types.ts | 44 +-- src/cli/commands/sdk/utils/render.ts | 3 + src/cli/commands/sdk/workflows.ts | 16 +- 10 files changed, 203 insertions(+), 421 deletions(-) diff --git a/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/datasources.md b/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/datasources.md index 2057986e..9693ceca 100644 --- a/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/datasources.md +++ b/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/datasources.md @@ -390,49 +390,6 @@ codemie sdk datasources create provider --data '{ }' ``` -### Summary - -```bash -codemie sdk datasources create summary --data '{ - "name": "my-summary", - "project_name": "Team", - "description": "Summary datasource", - "shared_with_project": true -}' -``` - -### Chunk Summary - -```bash -codemie sdk datasources create chunk-summary --data '{ - "name": "my-chunk-summary", - "project_name": "Team", - "description": "Chunk summary datasource", - "shared_with_project": true -}' -``` - -### Platform - -```bash -codemie sdk datasources create platform --data '{ - "name": "platform-assistant", - "project_name": "Team", - "description": "Platform marketplace assistant", - "shared_with_project": true -}' -``` - -**Fields for json, provider, summary, chunk-summary, platform (base fields only):** - -| Field | Required | Description | -|-------|----------|-------------| -| `name` | ✅ | Must match `^[a-zA-Z0-9][\w-]*$` — no spaces, use hyphens | -| `project_name` | ✅ | Project to create the datasource in | -| `description` | — | Human-readable description | -| `shared_with_project` | — | `true` = visible to all project members | -| `setting_id` | — | Integration ID for authentication | - ## Scripting ```bash diff --git a/src/cli/commands/sdk/analytics.ts b/src/cli/commands/sdk/analytics.ts index 79f8a2fd..e7ecc1b4 100644 --- a/src/cli/commands/sdk/analytics.ts +++ b/src/cli/commands/sdk/analytics.ts @@ -1,8 +1,15 @@ import { Command } from "commander"; import chalk from "chalk"; import ora from "ora"; -import type { SummariesResponse, TabularResponse, UsersListResponse } from "codemie-sdk"; -import type { AnalyticsQueryParams, PaginatedAnalyticsQueryParams } from "codemie-sdk"; +import type { + SummariesResponse, + TabularResponse, + UsersListResponse, +} from "codemie-sdk"; +import type { + AnalyticsQueryParams, + PaginatedAnalyticsQueryParams, +} from "codemie-sdk"; import { getAnalyticsSummaries, getAnalyticsCliSummary, @@ -28,7 +35,13 @@ import { getAnalyticsCliRepositories, } from "./services/analytics.js"; import { getSdkClient, outputJson, handleSdkError } from "./utils/cli-utils.js"; -import { printTable, printListHeader, printEmpty, optional, type TableColumn } from "./utils/render.js"; +import { + printTable, + printListHeader, + printEmpty, + optional, + type TableColumn, +} from "./utils/render.js"; /** * Build base analytics query params from command options @@ -73,7 +86,10 @@ function buildPaginatedParams(opts: { */ function addBaseFilterOptions(cmd: Command): Command { return cmd - .option("--time-period ", "Time period filter (e.g. last_7_days, last_30_days)") + .option( + "--time-period ", + "Time period filter (e.g. last_7_days, last_30_days)", + ) .option("--start-date ", "Start date (ISO 8601, e.g. 2024-01-01)") .option("--end-date ", "End date (ISO 8601, e.g. 2024-12-31)") .option("--users ", "Filter by user(s)") @@ -108,7 +124,11 @@ function printSummaries(response: SummariesResponse, json: boolean): void { { header: "ID", width: 30, getValue: (m) => chalk.cyan(m.id) }, { header: "Label", width: 30, getValue: (m) => optional(m.label) }, { header: "Type", width: 14, getValue: (m) => optional(m.type) }, - { header: "Value", width: 24, getValue: (m) => String(m.value ?? chalk.dim("—")) }, + { + header: "Value", + width: 24, + getValue: (m) => String(m.value ?? chalk.dim("—")), + }, ]; printListHeader("Metrics", metrics.length); @@ -132,14 +152,16 @@ function printTabular(response: TabularResponse, json: boolean): void { return; } - const tableColumns: TableColumn>[] = columns.map((col) => ({ - header: col.label, - width: Math.max(col.label.length + 4, 18), - getValue: (row) => { - const val = row[col.id]; - return val != null ? String(val) : chalk.dim("—"); - }, - })); + const tableColumns: TableColumn>[] = columns.map( + (col) => ({ + header: col.label, + width: Math.max(col.label.length + 4, 18), + getValue: (row) => { + const val = row[col.id]; + return val != null ? String(val) : chalk.dim("—"); + }, + }), + ); printListHeader("Records", rows.length); printTable(rows, tableColumns); @@ -185,7 +207,6 @@ export function createAnalyticsSubcommand(): Command { "Access CodeMie platform analytics and usage data", ); - // summaries addBaseFilterOptions( cmd .command("summaries") @@ -209,22 +230,19 @@ export function createAnalyticsSubcommand(): Command { } }); - // cli-summary addBaseFilterOptions( cmd .command("cli-summary") - .description( - "Get CLI usage summary\n" + - "Examples:\n" + - " $ codemie sdk analytics cli-summary\n" + - " $ codemie sdk analytics cli-summary --time-period last_7_days --json", - ) + .description("Get CLI usage summary") .option("--json", "Output in JSON format"), ).action(async (opts) => { const client = await getSdkClient(); const spinner = ora("Fetching CLI summary...").start(); try { - const result = await getAnalyticsCliSummary(client, buildBaseParams(opts)); + const result = await getAnalyticsCliSummary( + client, + buildBaseParams(opts), + ); spinner.stop(); printSummaries(result, opts.json); } catch (error) { @@ -233,16 +251,10 @@ export function createAnalyticsSubcommand(): Command { } }); - // users addBaseFilterOptions( cmd .command("users") - .description( - "List users with analytics access\n" + - "Examples:\n" + - " $ codemie sdk analytics users\n" + - " $ codemie sdk analytics users --json", - ) + .description("List users") .option("--json", "Output in JSON format"), ).action(async (opts) => { const client = await getSdkClient(); @@ -257,22 +269,19 @@ export function createAnalyticsSubcommand(): Command { } }); - // assistants-chats addPaginatedFilterOptions( cmd .command("assistants-chats") - .description( - "Get assistant chat analytics\n" + - "Examples:\n" + - " $ codemie sdk analytics assistants-chats\n" + - " $ codemie sdk analytics assistants-chats --page 0 --per-page 50 --json", - ) + .description("Get assistant chat analytics") .option("--json", "Output in JSON format"), ).action(async (opts) => { const client = await getSdkClient(); const spinner = ora("Fetching assistant chats analytics...").start(); try { - const result = await getAnalyticsAssistantsChats(client, buildPaginatedParams(opts)); + const result = await getAnalyticsAssistantsChats( + client, + buildPaginatedParams(opts), + ); spinner.stop(); printTabular(result, opts.json); } catch (error) { @@ -281,22 +290,19 @@ export function createAnalyticsSubcommand(): Command { } }); - // workflows addPaginatedFilterOptions( cmd .command("workflows") - .description( - "Get workflow analytics\n" + - "Examples:\n" + - " $ codemie sdk analytics workflows\n" + - " $ codemie sdk analytics workflows --projects myproject --json", - ) + .description("Get workflow analytics") .option("--json", "Output in JSON format"), ).action(async (opts) => { const client = await getSdkClient(); const spinner = ora("Fetching workflows analytics...").start(); try { - const result = await getAnalyticsWorkflows(client, buildPaginatedParams(opts)); + const result = await getAnalyticsWorkflows( + client, + buildPaginatedParams(opts), + ); spinner.stop(); printTabular(result, opts.json); } catch (error) { @@ -305,22 +311,19 @@ export function createAnalyticsSubcommand(): Command { } }); - // tools-usage addPaginatedFilterOptions( cmd .command("tools-usage") - .description( - "Get tools usage analytics\n" + - "Examples:\n" + - " $ codemie sdk analytics tools-usage\n" + - " $ codemie sdk analytics tools-usage --json", - ) + .description("Get tools usage analytics") .option("--json", "Output in JSON format"), ).action(async (opts) => { const client = await getSdkClient(); const spinner = ora("Fetching tools usage analytics...").start(); try { - const result = await getAnalyticsToolsUsage(client, buildPaginatedParams(opts)); + const result = await getAnalyticsToolsUsage( + client, + buildPaginatedParams(opts), + ); spinner.stop(); printTabular(result, opts.json); } catch (error) { @@ -329,22 +332,19 @@ export function createAnalyticsSubcommand(): Command { } }); - // webhooks-invocation addPaginatedFilterOptions( cmd .command("webhooks-invocation") - .description( - "Get webhooks invocation analytics\n" + - "Examples:\n" + - " $ codemie sdk analytics webhooks-invocation\n" + - " $ codemie sdk analytics webhooks-invocation --json", - ) + .description("Get webhooks invocation analytics") .option("--json", "Output in JSON format"), ).action(async (opts) => { const client = await getSdkClient(); const spinner = ora("Fetching webhooks invocation analytics...").start(); try { - const result = await getAnalyticsWebhooksInvocation(client, buildPaginatedParams(opts)); + const result = await getAnalyticsWebhooksInvocation( + client, + buildPaginatedParams(opts), + ); spinner.stop(); printTabular(result, opts.json); } catch (error) { @@ -353,22 +353,19 @@ export function createAnalyticsSubcommand(): Command { } }); - // mcp-servers addPaginatedFilterOptions( cmd .command("mcp-servers") - .description( - "Get MCP servers analytics\n" + - "Examples:\n" + - " $ codemie sdk analytics mcp-servers\n" + - " $ codemie sdk analytics mcp-servers --json", - ) + .description("Get MCP servers analytics") .option("--json", "Output in JSON format"), ).action(async (opts) => { const client = await getSdkClient(); const spinner = ora("Fetching MCP servers analytics...").start(); try { - const result = await getAnalyticsMcpServers(client, buildPaginatedParams(opts)); + const result = await getAnalyticsMcpServers( + client, + buildPaginatedParams(opts), + ); spinner.stop(); printTabular(result, opts.json); } catch (error) { @@ -377,22 +374,19 @@ export function createAnalyticsSubcommand(): Command { } }); - // mcp-servers-by-users addPaginatedFilterOptions( cmd .command("mcp-servers-by-users") - .description( - "Get MCP servers by users analytics\n" + - "Examples:\n" + - " $ codemie sdk analytics mcp-servers-by-users\n" + - " $ codemie sdk analytics mcp-servers-by-users --json", - ) + .description("Get MCP servers by users analytics") .option("--json", "Output in JSON format"), ).action(async (opts) => { const client = await getSdkClient(); const spinner = ora("Fetching MCP servers by users analytics...").start(); try { - const result = await getAnalyticsMcpServersByUsers(client, buildPaginatedParams(opts)); + const result = await getAnalyticsMcpServersByUsers( + client, + buildPaginatedParams(opts), + ); spinner.stop(); printTabular(result, opts.json); } catch (error) { @@ -401,22 +395,19 @@ export function createAnalyticsSubcommand(): Command { } }); - // projects-spending addPaginatedFilterOptions( cmd .command("projects-spending") - .description( - "Get projects spending analytics\n" + - "Examples:\n" + - " $ codemie sdk analytics projects-spending\n" + - " $ codemie sdk analytics projects-spending --projects myproject --json", - ) + .description("Get projects spending analytics") .option("--json", "Output in JSON format"), ).action(async (opts) => { const client = await getSdkClient(); const spinner = ora("Fetching projects spending analytics...").start(); try { - const result = await getAnalyticsProjectsSpending(client, buildPaginatedParams(opts)); + const result = await getAnalyticsProjectsSpending( + client, + buildPaginatedParams(opts), + ); spinner.stop(); printTabular(result, opts.json); } catch (error) { @@ -425,22 +416,19 @@ export function createAnalyticsSubcommand(): Command { } }); - // llms-usage addPaginatedFilterOptions( cmd .command("llms-usage") - .description( - "Get LLMs usage analytics\n" + - "Examples:\n" + - " $ codemie sdk analytics llms-usage\n" + - " $ codemie sdk analytics llms-usage --json", - ) + .description("Get LLMs usage analytics") .option("--json", "Output in JSON format"), ).action(async (opts) => { const client = await getSdkClient(); const spinner = ora("Fetching LLMs usage analytics...").start(); try { - const result = await getAnalyticsLlmsUsage(client, buildPaginatedParams(opts)); + const result = await getAnalyticsLlmsUsage( + client, + buildPaginatedParams(opts), + ); spinner.stop(); printTabular(result, opts.json); } catch (error) { @@ -449,22 +437,19 @@ export function createAnalyticsSubcommand(): Command { } }); - // users-spending addPaginatedFilterOptions( cmd .command("users-spending") - .description( - "Get users spending analytics\n" + - "Examples:\n" + - " $ codemie sdk analytics users-spending\n" + - " $ codemie sdk analytics users-spending --users alice --json", - ) + .description("Get users spending analytics") .option("--json", "Output in JSON format"), ).action(async (opts) => { const client = await getSdkClient(); const spinner = ora("Fetching users spending analytics...").start(); try { - const result = await getAnalyticsUsersSpending(client, buildPaginatedParams(opts)); + const result = await getAnalyticsUsersSpending( + client, + buildPaginatedParams(opts), + ); spinner.stop(); printTabular(result, opts.json); } catch (error) { @@ -473,22 +458,19 @@ export function createAnalyticsSubcommand(): Command { } }); - // budget-soft-limit addPaginatedFilterOptions( cmd .command("budget-soft-limit") - .description( - "Get budget soft limit analytics\n" + - "Examples:\n" + - " $ codemie sdk analytics budget-soft-limit\n" + - " $ codemie sdk analytics budget-soft-limit --json", - ) + .description("Get budget soft limit analytics") .option("--json", "Output in JSON format"), ).action(async (opts) => { const client = await getSdkClient(); const spinner = ora("Fetching budget soft limit analytics...").start(); try { - const result = await getAnalyticsBudgetSoftLimit(client, buildPaginatedParams(opts)); + const result = await getAnalyticsBudgetSoftLimit( + client, + buildPaginatedParams(opts), + ); spinner.stop(); printTabular(result, opts.json); } catch (error) { @@ -497,22 +479,19 @@ export function createAnalyticsSubcommand(): Command { } }); - // budget-hard-limit addPaginatedFilterOptions( cmd .command("budget-hard-limit") - .description( - "Get budget hard limit analytics\n" + - "Examples:\n" + - " $ codemie sdk analytics budget-hard-limit\n" + - " $ codemie sdk analytics budget-hard-limit --json", - ) + .description("Get budget hard limit analytics") .option("--json", "Output in JSON format"), ).action(async (opts) => { const client = await getSdkClient(); const spinner = ora("Fetching budget hard limit analytics...").start(); try { - const result = await getAnalyticsBudgetHardLimit(client, buildPaginatedParams(opts)); + const result = await getAnalyticsBudgetHardLimit( + client, + buildPaginatedParams(opts), + ); spinner.stop(); printTabular(result, opts.json); } catch (error) { @@ -521,22 +500,19 @@ export function createAnalyticsSubcommand(): Command { } }); - // users-activity addPaginatedFilterOptions( cmd .command("users-activity") - .description( - "Get users activity analytics\n" + - "Examples:\n" + - " $ codemie sdk analytics users-activity\n" + - " $ codemie sdk analytics users-activity --users alice --json", - ) + .description("Get users activity analytics") .option("--json", "Output in JSON format"), ).action(async (opts) => { const client = await getSdkClient(); const spinner = ora("Fetching users activity analytics...").start(); try { - const result = await getAnalyticsUsersActivity(client, buildPaginatedParams(opts)); + const result = await getAnalyticsUsersActivity( + client, + buildPaginatedParams(opts), + ); spinner.stop(); printTabular(result, opts.json); } catch (error) { @@ -545,22 +521,19 @@ export function createAnalyticsSubcommand(): Command { } }); - // projects-activity addPaginatedFilterOptions( cmd .command("projects-activity") - .description( - "Get projects activity analytics\n" + - "Examples:\n" + - " $ codemie sdk analytics projects-activity\n" + - " $ codemie sdk analytics projects-activity --projects myproject --json", - ) + .description("Get projects activity analytics") .option("--json", "Output in JSON format"), ).action(async (opts) => { const client = await getSdkClient(); const spinner = ora("Fetching projects activity analytics...").start(); try { - const result = await getAnalyticsProjectsActivity(client, buildPaginatedParams(opts)); + const result = await getAnalyticsProjectsActivity( + client, + buildPaginatedParams(opts), + ); spinner.stop(); printTabular(result, opts.json); } catch (error) { @@ -569,22 +542,19 @@ export function createAnalyticsSubcommand(): Command { } }); - // agents-usage addPaginatedFilterOptions( cmd .command("agents-usage") - .description( - "Get agents usage analytics\n" + - "Examples:\n" + - " $ codemie sdk analytics agents-usage\n" + - " $ codemie sdk analytics agents-usage --json", - ) + .description("Get agents usage analytics") .option("--json", "Output in JSON format"), ).action(async (opts) => { const client = await getSdkClient(); const spinner = ora("Fetching agents usage analytics...").start(); try { - const result = await getAnalyticsAgentsUsage(client, buildPaginatedParams(opts)); + const result = await getAnalyticsAgentsUsage( + client, + buildPaginatedParams(opts), + ); spinner.stop(); printTabular(result, opts.json); } catch (error) { @@ -593,22 +563,19 @@ export function createAnalyticsSubcommand(): Command { } }); - // cli-agents addPaginatedFilterOptions( cmd .command("cli-agents") - .description( - "Get CLI agents analytics\n" + - "Examples:\n" + - " $ codemie sdk analytics cli-agents\n" + - " $ codemie sdk analytics cli-agents --json", - ) + .description("Get CLI agents analytics") .option("--json", "Output in JSON format"), ).action(async (opts) => { const client = await getSdkClient(); const spinner = ora("Fetching CLI agents analytics...").start(); try { - const result = await getAnalyticsCliAgents(client, buildPaginatedParams(opts)); + const result = await getAnalyticsCliAgents( + client, + buildPaginatedParams(opts), + ); spinner.stop(); printTabular(result, opts.json); } catch (error) { @@ -617,22 +584,19 @@ export function createAnalyticsSubcommand(): Command { } }); - // cli-llms addPaginatedFilterOptions( cmd .command("cli-llms") - .description( - "Get CLI LLMs analytics\n" + - "Examples:\n" + - " $ codemie sdk analytics cli-llms\n" + - " $ codemie sdk analytics cli-llms --json", - ) + .description("Get CLI LLMs analytics") .option("--json", "Output in JSON format"), ).action(async (opts) => { const client = await getSdkClient(); const spinner = ora("Fetching CLI LLMs analytics...").start(); try { - const result = await getAnalyticsCliLlms(client, buildPaginatedParams(opts)); + const result = await getAnalyticsCliLlms( + client, + buildPaginatedParams(opts), + ); spinner.stop(); printTabular(result, opts.json); } catch (error) { @@ -641,22 +605,19 @@ export function createAnalyticsSubcommand(): Command { } }); - // cli-users addPaginatedFilterOptions( cmd .command("cli-users") - .description( - "Get CLI users analytics\n" + - "Examples:\n" + - " $ codemie sdk analytics cli-users\n" + - " $ codemie sdk analytics cli-users --json", - ) + .description("Get CLI users analytics") .option("--json", "Output in JSON format"), ).action(async (opts) => { const client = await getSdkClient(); const spinner = ora("Fetching CLI users analytics...").start(); try { - const result = await getAnalyticsCliUsers(client, buildPaginatedParams(opts)); + const result = await getAnalyticsCliUsers( + client, + buildPaginatedParams(opts), + ); spinner.stop(); printTabular(result, opts.json); } catch (error) { @@ -665,22 +626,19 @@ export function createAnalyticsSubcommand(): Command { } }); - // cli-errors addPaginatedFilterOptions( cmd .command("cli-errors") - .description( - "Get CLI errors analytics\n" + - "Examples:\n" + - " $ codemie sdk analytics cli-errors\n" + - " $ codemie sdk analytics cli-errors --json", - ) + .description("Get CLI errors analytics") .option("--json", "Output in JSON format"), ).action(async (opts) => { const client = await getSdkClient(); const spinner = ora("Fetching CLI errors analytics...").start(); try { - const result = await getAnalyticsCliErrors(client, buildPaginatedParams(opts)); + const result = await getAnalyticsCliErrors( + client, + buildPaginatedParams(opts), + ); spinner.stop(); printTabular(result, opts.json); } catch (error) { @@ -689,22 +647,19 @@ export function createAnalyticsSubcommand(): Command { } }); - // cli-repositories addPaginatedFilterOptions( cmd .command("cli-repositories") - .description( - "Get CLI repositories analytics\n" + - "Examples:\n" + - " $ codemie sdk analytics cli-repositories\n" + - " $ codemie sdk analytics cli-repositories --json", - ) + .description("Get CLI repositories analytics") .option("--json", "Output in JSON format"), ).action(async (opts) => { const client = await getSdkClient(); const spinner = ora("Fetching CLI repositories analytics...").start(); try { - const result = await getAnalyticsCliRepositories(client, buildPaginatedParams(opts)); + const result = await getAnalyticsCliRepositories( + client, + buildPaginatedParams(opts), + ); spinner.stop(); printTabular(result, opts.json); } catch (error) { diff --git a/src/cli/commands/sdk/assistants.ts b/src/cli/commands/sdk/assistants.ts index 3144897c..1d3a2e7f 100644 --- a/src/cli/commands/sdk/assistants.ts +++ b/src/cli/commands/sdk/assistants.ts @@ -158,16 +158,10 @@ export function createAssistantsSubcommand(): Command { "Create a new assistant with the specified configuration\n" + "Examples:\n" + ' $ codemie assistants create --data \'{"name":"My Assistant","description":"Helpful bot"}\'\n' + - ' $ codemie assistants create --json path/to/assistant.json\n', - ) - .option( - "--data ", - "Assistant configuration as inline JSON string", - ) - .option( - "--json ", - "Path to JSON file with assistant configuration", + " $ codemie assistants create --json path/to/assistant.json\n", ) + .option("--data ", "Assistant configuration as inline JSON string") + .option("--json ", "Path to JSON file with assistant configuration") .action(async (opts) => { const client = await getSdkClient(); const spinner = ora("Creating assistant...").start(); @@ -196,16 +190,10 @@ export function createAssistantsSubcommand(): Command { "Update an existing assistant's configuration\n" + "Examples:\n" + ' $ codemie assistants update ast_abc123 --data \'{"name":"Updated Name"}\'\n' + - ' $ codemie assistants update ast_abc123 --json path/to/update.json\n', - ) - .option( - "--data ", - "Fields to update as inline JSON string", - ) - .option( - "--json ", - "Path to JSON file with fields to update", + " $ codemie assistants update ast_abc123 --json path/to/update.json\n", ) + .option("--data ", "Fields to update as inline JSON string") + .option("--json ", "Path to JSON file with fields to update") .action(async (id: string, opts) => { const client = await getSdkClient(); const spinner = ora("Updating assistant...").start(); @@ -245,10 +233,7 @@ export function createAssistantsSubcommand(): Command { .command("get-tools") .description( "List available toolkits that can be assigned to an assistant\n" + - "Use toolkit names from this list in create/update --data toolkits field.\n" + - "Examples:\n" + - " $ codemie sdk assistants get-tools\n" + - " $ codemie sdk assistants get-tools --json", + "Use toolkit names from this list in create/update --data toolkits field.", ) .option("--json", "Output in JSON format") .action(async (opts) => { diff --git a/src/cli/commands/sdk/datasources.ts b/src/cli/commands/sdk/datasources.ts index 5bd2a6f3..68515521 100644 --- a/src/cli/commands/sdk/datasources.ts +++ b/src/cli/commands/sdk/datasources.ts @@ -206,10 +206,7 @@ export function createDatasourcesSubcommand(): Command { "--data ", "Datasource metadata as inline JSON string (name, project_name, description, etc.)", ) - .option( - "--json ", - "Path to JSON file with datasource metadata", - ) + .option("--json ", "Path to JSON file with datasource metadata") .action(async (opts) => { const client = await getSdkClient(); const spinner = ora("Creating file datasource...").start(); @@ -246,10 +243,12 @@ export function createDatasourcesSubcommand(): Command { createCmd .command(typeConfig.command) .description( - `Create ${typeConfig.description}\n` + - `Examples:\n` + - ` $ codemie sdk datasources create ${typeConfig.command} --data '${typeConfig.example}'\n` + - ` $ codemie sdk datasources create ${typeConfig.command} --json path/to/config.json`, + `Create ${typeConfig.description}` + + (typeConfig.example + ? `\nExamples:\n` + + ` $ codemie sdk datasources create ${typeConfig.command} --data '${typeConfig.example}'\n` + + ` $ codemie sdk datasources create ${typeConfig.command} --json path/to/config.json` + : ""), ) .option( "--data ", @@ -302,14 +301,8 @@ export function createDatasourcesSubcommand(): Command { ` $ codemie sdk datasources update file ds_123 --data '{"description":"Updated docs"}'\n` + ` $ codemie sdk datasources update file ds_123 --json path/to/update.json`, ) - .option( - "--data ", - "Fields to update as inline JSON string", - ) - .option( - "--json ", - "Path to JSON file with fields to update", - ) + .option("--data ", "Fields to update as inline JSON string") + .option("--json ", "Path to JSON file with fields to update") .action(async (id: string, opts) => { const client = await getSdkClient(); const spinner = ora("Updating file datasource...").start(); @@ -333,19 +326,15 @@ export function createDatasourcesSubcommand(): Command { updateCmd .command(`${typeConfig.command} `) .description( - `Update ${typeConfig.description}\n` + - `Examples:\n` + - ` $ codemie sdk datasources update ${typeConfig.command} ds_123 --data '${typeConfig.example}'\n` + - ` $ codemie sdk datasources update ${typeConfig.command} ds_123 --json path/to/update.json`, - ) - .option( - "--data ", - "Fields to update as inline JSON string", - ) - .option( - "--json ", - "Path to JSON file with fields to update", + `Update ${typeConfig.description}` + + (typeConfig.example + ? `\nExamples:\n` + + ` $ codemie sdk datasources update ${typeConfig.command} ds_123 --data '${typeConfig.example}'\n` + + ` $ codemie sdk datasources update ${typeConfig.command} ds_123 --json path/to/update.json` + : ""), ) + .option("--data ", "Fields to update as inline JSON string") + .option("--json ", "Path to JSON file with fields to update") .action(async (id: string, opts) => { const client = await getSdkClient(); const spinner = ora( diff --git a/src/cli/commands/sdk/skills.ts b/src/cli/commands/sdk/skills.ts index 5ddd77df..669356fe 100644 --- a/src/cli/commands/sdk/skills.ts +++ b/src/cli/commands/sdk/skills.ts @@ -100,7 +100,11 @@ export function createSkillsSubcommand(): Command { const columns: TableColumn[] = [ { header: "ID", width: 40, getValue: (s) => chalk.cyan(s.id) }, { header: "Name", width: 30, getValue: (s) => s.name }, - { header: "Project", width: 20, getValue: (s) => optional(s.project) }, + { + header: "Project", + width: 20, + getValue: (s) => optional(s.project), + }, { header: "Visibility", width: 12, getValue: (s) => s.visibility }, ]; printTable(items, columns); @@ -257,7 +261,9 @@ export function createSkillsSubcommand(): Command { return; } - printSuccess(`Skill ${chalk.cyan(result.id)} imported as '${result.name}'.`); + printSuccess( + `Skill ${chalk.cyan(result.id)} imported as '${result.name}'.`, + ); } catch (error) { spinner.stop(); handleSdkError(error, "import skill"); @@ -266,12 +272,7 @@ export function createSkillsSubcommand(): Command { cmd .command("export ") - .description( - "Export a skill as markdown content\n" + - "Examples:\n" + - " $ codemie sdk skills export \n" + - " $ codemie sdk skills export > my-skill.md", - ) + .description("Export a skill as markdown content") .action(async (id: string) => { const client = await getSdkClient(); const spinner = ora("Exporting skill...").start(); @@ -288,11 +289,7 @@ export function createSkillsSubcommand(): Command { cmd .command("attach ") - .description( - "Attach a skill to an assistant\n" + - "Examples:\n" + - " $ codemie sdk skills attach ", - ) + .description("Attach a skill to an assistant") .action(async (assistantId: string, skillId: string) => { const client = await getSdkClient(); const spinner = ora("Attaching skill to assistant...").start(); @@ -311,11 +308,7 @@ export function createSkillsSubcommand(): Command { cmd .command("detach ") - .description( - "Detach a skill from an assistant\n" + - "Examples:\n" + - " $ codemie sdk skills detach ", - ) + .description("Detach a skill from an assistant") .action(async (assistantId: string, skillId: string) => { const client = await getSdkClient(); const spinner = ora("Detaching skill from assistant...").start(); @@ -334,12 +327,7 @@ export function createSkillsSubcommand(): Command { cmd .command("list-assistant-skills ") - .description( - "List all skills attached to an assistant\n" + - "Examples:\n" + - " $ codemie sdk skills list-assistant-skills \n" + - " $ codemie sdk skills list-assistant-skills --json", - ) + .description("List all skills attached to an assistant") .option("--json", "Output in JSON format") .action(async (assistantId: string, opts) => { const client = await getSdkClient(); @@ -375,11 +363,7 @@ export function createSkillsSubcommand(): Command { cmd .command("bulk-attach ") - .description( - "Attach a skill to multiple assistants at once\n" + - "Examples:\n" + - " $ codemie sdk skills bulk-attach --assistant-ids ,,", - ) + .description("Attach a skill to multiple assistants at once") .requiredOption( "--assistant-ids ", "Comma-separated list of assistant IDs", @@ -405,12 +389,7 @@ export function createSkillsSubcommand(): Command { cmd .command("get-assistants ") - .description( - "List all assistants using a skill\n" + - "Examples:\n" + - " $ codemie sdk skills get-assistants \n" + - " $ codemie sdk skills get-assistants --json", - ) + .description("List all assistants using a skill") .option("--json", "Output in JSON format") .action(async (skillId: string, opts) => { const client = await getSdkClient(); @@ -461,12 +440,7 @@ export function createSkillsSubcommand(): Command { cmd .command("publish ") - .description( - "Publish a skill to the marketplace\n" + - "Examples:\n" + - " $ codemie sdk skills publish \n" + - " $ codemie sdk skills publish --categories development,testing", - ) + .description("Publish a skill to the marketplace") .option( "--categories ", "Comma-separated list of categories (max 3)", @@ -490,11 +464,7 @@ export function createSkillsSubcommand(): Command { cmd .command("unpublish ") - .description( - "Unpublish a skill from the marketplace\n" + - "Examples:\n" + - " $ codemie sdk skills unpublish ", - ) + .description("Unpublish a skill from the marketplace") .action(async (id: string) => { const client = await getSdkClient(); const spinner = ora("Unpublishing skill...").start(); @@ -511,12 +481,7 @@ export function createSkillsSubcommand(): Command { cmd .command("list-categories") - .description( - "List available skill categories\n" + - "Examples:\n" + - " $ codemie sdk skills list-categories\n" + - " $ codemie sdk skills list-categories --json", - ) + .description("List available skill categories") .option("--json", "Output in JSON format") .action(async (opts) => { const client = await getSdkClient(); @@ -560,12 +525,7 @@ export function createSkillsSubcommand(): Command { cmd .command("get-users") - .description( - "Get users with access to skills\n" + - "Examples:\n" + - " $ codemie sdk skills get-users\n" + - " $ codemie sdk skills get-users --json", - ) + .description("Get users with access to skills") .option("--json", "Output in JSON format") .action(async (opts) => { const client = await getSdkClient(); @@ -624,12 +584,7 @@ export function createSkillsSubcommand(): Command { cmd .command("react ") - .description( - "React to a skill with like or dislike\n" + - "Examples:\n" + - " $ codemie sdk skills react --reaction like\n" + - " $ codemie sdk skills react --reaction dislike", - ) + .description("React to a skill with like or dislike") .requiredOption( "--reaction ", "Reaction type: 'like' or 'dislike'", @@ -652,11 +607,7 @@ export function createSkillsSubcommand(): Command { cmd .command("remove-reactions ") - .description( - "Remove all reactions from a skill\n" + - "Examples:\n" + - " $ codemie sdk skills remove-reactions ", - ) + .description("Remove all reactions from a skill") .action(async (id: string) => { const client = await getSdkClient(); const spinner = ora("Removing reactions...").start(); diff --git a/src/cli/commands/sdk/users.ts b/src/cli/commands/sdk/users.ts index 886795cf..12310888 100644 --- a/src/cli/commands/sdk/users.ts +++ b/src/cli/commands/sdk/users.ts @@ -2,11 +2,7 @@ import { Command } from "commander"; import chalk from "chalk"; import ora from "ora"; import { getUserProfile, getUserData } from "./services/users.js"; -import { - getSdkClient, - outputJson, - handleSdkError, -} from "./utils/cli-utils.js"; +import { getSdkClient, outputJson, handleSdkError } from "./utils/cli-utils.js"; import { printDetail, optional, type DetailRow } from "./utils/render.js"; export function createUsersSubcommand(): Command { @@ -16,12 +12,7 @@ export function createUsersSubcommand(): Command { cmd .command("me") - .description( - "Get current user profile\n" + - "Examples:\n" + - " $ codemie sdk users me\n" + - " $ codemie sdk users me --json", - ) + .description("Get current user profile") .option("--json", "Output in JSON format") .action(async (opts) => { const client = await getSdkClient(); @@ -41,7 +32,10 @@ export function createUsersSubcommand(): Command { { label: "Username", value: optional(user.username) }, { label: "Name", value: optional(user.name) }, { label: "Email", value: optional(user.email) }, - { label: "Admin", value: user.is_admin ? chalk.green("yes") : chalk.dim("no") }, + { + label: "Admin", + value: user.is_admin ? chalk.green("yes") : chalk.dim("no"), + }, { label: "Projects", value: @@ -67,12 +61,7 @@ export function createUsersSubcommand(): Command { cmd .command("data") - .description( - "Get current user data and preferences\n" + - "Examples:\n" + - " $ codemie sdk users data\n" + - " $ codemie sdk users data --json", - ) + .description("Get current user data and preferences") .option("--json", "Output in JSON format") .action(async (opts) => { const client = await getSdkClient(); diff --git a/src/cli/commands/sdk/utils/cli-utils.ts b/src/cli/commands/sdk/utils/cli-utils.ts index 567bcf21..554605f2 100644 --- a/src/cli/commands/sdk/utils/cli-utils.ts +++ b/src/cli/commands/sdk/utils/cli-utils.ts @@ -120,22 +120,17 @@ export function getResponseMessage(response: unknown): string { } /** - * Parse --config flag value: YAML string or @file path + * Parse --config flag value: path to a YAML file */ export async function parseConfigInput( configFlag: string | undefined, ): Promise { if (!configFlag) { throw new Error( - "No config provided. Use --config 'yaml string' or --config @file.yaml", + "No config provided. Use --config path/to/file.yaml", ); } - if (configFlag.startsWith("@")) { - const filePath = configFlag.slice(1); - const content = await readFile(filePath, "utf-8"); - return content; - } - - return configFlag; + const content = await readFile(configFlag, "utf-8"); + return content; } diff --git a/src/cli/commands/sdk/utils/datasource-types.ts b/src/cli/commands/sdk/utils/datasource-types.ts index 24a33d1f..abe1e5a1 100644 --- a/src/cli/commands/sdk/utils/datasource-types.ts +++ b/src/cli/commands/sdk/utils/datasource-types.ts @@ -3,7 +3,7 @@ export interface DatasourceTypeConfig { serviceKey?: string; // Override for service function name when command contains invalid identifier chars type: string; // SDK type value description: string; - example: string; + example?: string; } export const DATASOURCE_TYPES: DatasourceTypeConfig[] = [ @@ -18,94 +18,52 @@ export const DATASOURCE_TYPES: DatasourceTypeConfig[] = [ command: "jira", type: "knowledge_base_jira", description: "Jira datasource", - example: - '{"name":"Tickets","project_name":"Support","jql":"project=SUP","description":"Support tickets","shared_with_project":true}', }, { command: "file", type: "knowledge_base_file", description: "File datasource (use --file flags for local files)", - example: - '{"name":"Docs","project_name":"Team","description":"Team documents","shared_with_project":true}', }, { command: "code", type: "code", description: "Code repository datasource", - example: - '{"name":"Repo","project_name":"Eng","link":"https://github.com/org/repo","branch":"main","index_type":"code","description":"Main codebase"}', }, { command: "google", type: "llm_routing_google", description: "Google Docs datasource", - example: - '{"name":"Docs","project_name":"Team","google_doc":"doc-id-or-url","description":"Team docs","shared_with_project":true}', - }, - { - command: "json", - type: "knowledge_base_json", - description: "JSON knowledge base datasource", - example: - '{"name":"json-data","project_name":"Team","description":"JSON knowledge base","shared_with_project":true}', }, { command: "provider", type: "provider", description: "Provider datasource", - example: - '{"name":"my-provider","project_name":"Team","description":"Provider datasource","shared_with_project":true}', - }, - { - command: "summary", - type: "summary", - description: "Summary datasource", - example: - '{"name":"my-summary","project_name":"Team","description":"Summary datasource","shared_with_project":true}', - }, - { - command: "chunk-summary", - serviceKey: "chunkSummary", - type: "chunk-summary", - description: "Chunk summary datasource", - example: - '{"name":"my-chunk-summary","project_name":"Team","description":"Chunk summary datasource","shared_with_project":true}', }, { command: "azure-devops-wiki", serviceKey: "azureDevOpsWiki", type: "knowledge_base_azure_devops_wiki", description: "Azure DevOps Wiki datasource", - example: - '{"name":"ado-wiki","project_name":"Engineering","organization":"my-org","project":"my-project","wiki_name":"my-wiki","description":"Team wiki","shared_with_project":true}', }, { command: "azure-devops-work-item", serviceKey: "azureDevOpsWorkItem", type: "knowledge_base_azure_devops_work_item", description: "Azure DevOps Work Item datasource", - example: - '{"name":"ado-work-items","project_name":"Engineering","organization":"my-org","project":"my-project","wiql_query":"SELECT [Id],[Title] FROM WorkItems WHERE [System.TeamProject]=@project","description":"ADO work items","shared_with_project":true}', }, { command: "xray", type: "knowledge_base_xray", description: "Xray test management datasource", - example: - '{"name":"xray-tests","project_name":"QA","jql":"project=QA AND issuetype in testExecutions()","description":"Xray test data","shared_with_project":true}', }, { command: "sharepoint", type: "knowledge_base_sharepoint", description: "SharePoint datasource", - example: - '{"name":"sharepoint-docs","project_name":"Engineering","site_url":"https://company.sharepoint.com/sites/team","include_pages":true,"include_documents":true,"description":"SharePoint team site","shared_with_project":true}', }, { command: "platform", type: "platform_marketplace_assistant", description: "Platform marketplace assistant datasource", - example: - '{"name":"platform-assistant","project_name":"Team","description":"Platform marketplace assistant","shared_with_project":true}', }, ]; diff --git a/src/cli/commands/sdk/utils/render.ts b/src/cli/commands/sdk/utils/render.ts index 7b924733..3e7abd87 100644 --- a/src/cli/commands/sdk/utils/render.ts +++ b/src/cli/commands/sdk/utils/render.ts @@ -21,6 +21,9 @@ export interface TableColumn { */ export function printTable(items: T[], columns: TableColumn[]): void { const table = new Table({ + style: { + head: columns.map(() => "white"), + }, head: columns.map((col) => chalk.bold(col.header)), colWidths: columns.map((col) => col.width), wordWrap: true, diff --git a/src/cli/commands/sdk/workflows.ts b/src/cli/commands/sdk/workflows.ts index b4421102..30379d87 100644 --- a/src/cli/commands/sdk/workflows.ts +++ b/src/cli/commands/sdk/workflows.ts @@ -147,8 +147,8 @@ export function createWorkflowsSubcommand(): Command { .description( "Create a new workflow with the specified configuration\n" + "Examples:\n" + - ' $ codemie workflows create --data \'{"name":"My Workflow","description":"Custom workflow"}\' --config @workflow.yaml\n' + - ' $ codemie workflows create --json path/to/workflow.json --config @workflow.yaml\n', + ' $ codemie workflows create --data \'{"name":"My Workflow","description":"Custom workflow"}\' --config workflow.yaml\n' + + ' $ codemie workflows create --json path/to/workflow.json --config workflow.yaml\n', ) .option( "--data ", @@ -159,8 +159,8 @@ export function createWorkflowsSubcommand(): Command { "Path to JSON file with workflow configuration", ) .option( - "--config ", - "Workflow YAML config as string or @file.yaml path", + "--config ", + "Path to workflow YAML config file", ) .action(async (opts) => { const client = await getSdkClient(); @@ -190,8 +190,8 @@ export function createWorkflowsSubcommand(): Command { .description( "Update an existing workflow's configuration\n" + "Examples:\n" + - ' $ codemie workflows update wfl_abc123 --data \'{"name":"Updated Name"}\' --config @workflow.yaml\n' + - ' $ codemie workflows update wfl_abc123 --json path/to/update.json --config @workflow.yaml\n', + ' $ codemie workflows update wfl_abc123 --data \'{"name":"Updated Name"}\' --config workflow.yaml\n' + + ' $ codemie workflows update wfl_abc123 --json path/to/update.json --config workflow.yaml\n', ) .option( "--data ", @@ -202,8 +202,8 @@ export function createWorkflowsSubcommand(): Command { "Path to JSON file with fields to update", ) .option( - "--config ", - "Workflow YAML config as string or @file.yaml path", + "--config ", + "Path to workflow YAML config file", ) .action(async (id: string, opts) => { const client = await getSdkClient(); From 519ff0d0e2ef343ccaa3a73ad01737e9d47314fa Mon Sep 17 00:00:00 2001 From: Kostiantyn Pshenychnyi Date: Tue, 28 Apr 2026 15:44:35 +0300 Subject: [PATCH 08/13] feat: add analytics skill --- .../plugin/skills/codemie-analytics/SKILL.md | 486 +++ .../leaderboard-dashboard-report.md | 225 ++ .../people-spending-dashboard-report.md | 746 ++++ .../people-spending-dashboard-template.html | 3270 +++++++++++++++++ .../scripts/analytics-cli.js | 793 ++++ .../claude/plugin/skills/codemie-sdk/SKILL.md | 67 +- .../skills/codemie-sdk/examples/analytics.md | 244 -- src/cli/commands/sdk/analytics.ts | 672 ---- src/cli/commands/sdk/index.ts | 4 +- src/cli/commands/sdk/services/analytics.ts | 164 - src/cli/commands/sdk/services/index.ts | 1 - 11 files changed, 5527 insertions(+), 1145 deletions(-) create mode 100644 src/agents/plugins/claude/plugin/skills/codemie-analytics/SKILL.md create mode 100644 src/agents/plugins/claude/plugin/skills/codemie-analytics/references/leaderboard-dashboard-report.md create mode 100644 src/agents/plugins/claude/plugin/skills/codemie-analytics/references/people-spending-dashboard-report.md create mode 100644 src/agents/plugins/claude/plugin/skills/codemie-analytics/references/people-spending-dashboard-template.html create mode 100644 src/agents/plugins/claude/plugin/skills/codemie-analytics/scripts/analytics-cli.js delete mode 100644 src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/analytics.md delete mode 100644 src/cli/commands/sdk/analytics.ts delete mode 100644 src/cli/commands/sdk/services/analytics.ts diff --git a/src/agents/plugins/claude/plugin/skills/codemie-analytics/SKILL.md b/src/agents/plugins/claude/plugin/skills/codemie-analytics/SKILL.md new file mode 100644 index 00000000..1c487c3e --- /dev/null +++ b/src/agents/plugins/claude/plugin/skills/codemie-analytics/SKILL.md @@ -0,0 +1,486 @@ +--- +name: codemie-analytics +description: > + CodeMie Analytics expert — use this skill whenever the user asks about CodeMie usage data, + AI adoption metrics, user leaderboards, CLI insights, spending, LiteLLM costs, token usage, + or wants to build a dashboard/report from CodeMie or LiteLLM APIs. + Also triggers for: "who uses CodeMie most", "show me AI analytics", "get spending data", + "generate a report", "leaderboard", "cost analysis", "LiteLLM customer info", + "enrich CSV with costs", "top performers", "AI champions", "tier distribution", + or any custom analytics query against the platform. + Always use this skill when CodeMie analytics, reporting, or cost data is involved. +--- + +# CodeMie Analytics Skill + +You are an analytics expert for the CodeMie (EPAM AI/Run) platform. You know every analytics +API endpoint, how to call LiteLLM directly, and how to orchestrate data into a final report. + +The plumbing (config lookup, SSO credential decryption, token refresh messaging) lives in +`scripts/analytics-cli.js`. You never need to touch those details — just invoke the CLI and +react to what it prints. + +--- + +## Step 1 — Understand what the user wants + +Identify the analytics scenario. The CLI supports these command families: + +### Leaderboard (AI Champions) + +The leaderboard ranks users across **6 scoring dimensions**: +- D1: Core Platform Usage (20%) — conversations, assistant interactions +- D2: Core Platform Creation (20%) — assistants, datasources created +- D3: Workflow Usage (10%) — workflow executions +- D4: Workflow Creation (10%) — workflows authored +- D5: CLI & Agentic Engineering (30%) — coding agent sessions, tokens, repos +- D6: Impact & Knowledge (10%) — marketplace publishing, knowledge sharing + +**Tiers**: pioneer (80+), expert (65+), advanced (45+), practitioner (25+), newcomer (<25) + +| Scenario | Command | Output | +|----------|---------|--------| +| Full leaderboard (paginated, filterable) | `leaderboard` | Ranked entries with score, tier, dimensions | +| Leaderboard KPI summary | `leaderboard-summary` | Total users, tier counts, top score | +| Single user champion profile | `leaderboard-user ` | Full dimension breakdown for one user | +| Tier distribution | `leaderboard-tiers` | Tier name, user count, % | +| Average dimension scores | `leaderboard-dimensions` | D1–D6 averages across all users | +| Top N performers | `leaderboard-top [limit]` | Top users by total score (default 10) | +| Score histogram | `leaderboard-scores` | Score distribution in 10-point bins | +| Framework metadata | `leaderboard-framework` | Dimension descriptions, tier defs, scoring rules | +| Computation snapshots | `leaderboard-snapshots` | List of snapshot runs | +| Available seasons | `leaderboard-seasons --view monthly\|quarterly` | Seasonal periods for selectors | + +Leaderboard filters: `--view` (current/monthly/quarterly), `--season-key` (2026-03, 2026-Q1), +`--tier`, `--intent` (cli_focused/platform_focused/hybrid/sdlc_unicorn), `--search`, `--sort-by`, `--sort-order`. + +### CLI Insights + +| Scenario | Command | Output | +|----------|---------|--------| +| Full CLI overview (agents, repos, tools, errors) | `cli-insights` | Multi-section JSON | +| User classification & top spenders | `cli-insights-users` | Classification + spend tables | +| Detailed single-user CLI profile | `cli-insights-user ` | Key metrics, tools, models, repos, categories | +| Project classification & top by cost | `cli-insights-projects` | Project-level breakdown | +| Usage patterns (weekday, hourly, session depth) | `cli-insights-patterns` | Temporal pattern data | + +### General Analytics + +| Scenario | Command | Output | +|----------|---------|--------| +| Overall usage summary (tokens, cost, users) | `summaries` | KPI totals | +| User list + activity trends | `users` | Users + time-series | +| Per-project spending | `projects-spending` | Table | +| LLM model breakdown | `llms-usage` | Table | +| Tool usage | `tools-usage` | Table | +| Workflow execution analytics | `workflows` | Table | +| Budget alerts (soft + hard limits) | `budget` | Warning tables | +| Personal spending & budget | `spending` | Current user's spend + budget usage | +| Per-user spending (platform + cli split) | `spending-by-users` | Breakdown tables | +| Weekly engagement histogram | `engagement` | 3h-interval heatmap data | + +### LiteLLM & CSV Enrichment + +| Scenario | Command | Output | +|----------|---------|--------| +| LiteLLM customer lookup | `litellm-customer [user_id]` | JSON | +| LiteLLM spend logs | `litellm-spend` | Spend entries | +| LiteLLM virtual keys | `litellm-keys` | Key info | +| Enrich CSV/Excel with LiteLLM costs | `enrich-csv ` | Enriched table | +| Any custom endpoint | `custom /v1/analytics/` | Raw JSON | + +--- + +## Security + +**Never include raw API keys, bearer tokens, cookie values, LiteLLM keys, or session +credentials in your responses to the user.** If CLI output contains sensitive fields +(e.g. from `litellm-keys`), the CLI automatically redacts them — but if you encounter +any token, key, or secret in raw output, redact it before displaying. Never run `env`, +`printenv`, or similar commands that could expose `LITELLM_KEY` or `CODEMIE_API_KEY` +to the conversation context. + +--- + +## Step 2 — Run the analytics CLI + +The CLI script lives at `scripts/analytics-cli.js` next to this skill. It handles +authentication internally. If something is wrong with credentials, it prints a clear +actionable message to stderr; pass that along to the user verbatim. + +```bash +node ~/.claude/skills/codemie-analytics/scripts/analytics-cli.js [options] +``` + +LiteLLM commands (`litellm-*`, `enrich-csv`) require `LITELLM_URL` + `LITELLM_KEY` env vars. + +### Common filter flags + +| Flag | Example | Notes | +|------|---------|-------| +| `--time-period` | `last_30_days` | Predefined period | +| `--start-date` | `2024-01-01T00:00:00` | Custom range start | +| `--end-date` | `2024-03-31T23:59:59` | Custom range end | +| `--users` | `john.doe,jane.smith` | Comma-separated usernames | +| `--projects` | `my-project` | Comma-separated project names | +| `--page` | `1` | Pagination | +| `--per-page` | `100` | Results per page (default 50) | +| `--output` | `json` | `json` \| `table` \| `csv` | +| `--pretty` | (flag) | Pretty-print JSON | + +### Leaderboard-specific flags + +| Flag | Example | Notes | +|------|---------|-------| +| `--view` | `monthly` | `current` \| `monthly` \| `quarterly` | +| `--season-key` | `2026-Q1` | Specific season to query | +| `--tier` | `pioneer` | Filter by tier | +| `--intent` | `cli_focused` | Filter by user intent profile | +| `--search` | `john` | Partial name/email search | +| `--sort-by` | `total_score` | `rank` \| `total_score` \| `user_name` \| `tier_level` | +| `--sort-order` | `desc` | `asc` \| `desc` | +| `--limit` | `20` | Max entries for `leaderboard-top` (max 50) | + +### Example invocations + +```bash +CLI=~/.claude/skills/codemie-analytics/scripts/analytics-cli.js + +# Full leaderboard — top 50 pioneers sorted by score +node $CLI leaderboard --tier pioneer --sort-by total_score --sort-order desc --per-page 50 --pretty + +# Single user champion profile +node $CLI leaderboard-user john.doe@epam.com --pretty + +# Leaderboard KPI summary for Q1 2026 +node $CLI leaderboard-summary --view quarterly --season-key 2026-Q1 --pretty + +# Dimension averages (D1–D6) for current snapshot +node $CLI leaderboard-dimensions --pretty + +# Tier distribution +node $CLI leaderboard-tiers --pretty + +# Top 10 performers +node $CLI leaderboard-top 10 --pretty + +# 30-day platform summary +node $CLI summaries --time-period last_30_days --pretty + +# Full CLI insights +node $CLI cli-insights --time-period last_30_days --pretty + +# Detailed CLI profile for a specific user +node $CLI cli-insights-user John_Doe --time-period last_30_days --pretty + +# Usage patterns (weekday + hourly + session depth) +node $CLI cli-insights-patterns --time-period last_30_days --pretty + +# Per-user spending breakdown +node $CLI spending-by-users --time-period last_30_days --pretty + +# Custom endpoint +node $CLI custom /v1/analytics/mcp-servers --time-period last_30_days --pretty +``` + +--- + +## Step 3 — Build the HTML report + +Once you have the JSON data, delegate the presentation layer to the **`codemie-html-report`** +skill. That skill knows the CodeMie design system, Chart.js palette, and component library. +Do **not** hand-write HTML/CSS in this skill. + +### Output location + +**Always save reports to `reports/` in the user's current working directory.** Create the +folder if it doesn't exist. Use descriptive filenames: + +``` +reports/leaderboard-2026-Q1.html +reports/cli-insights-last-30-days.html +reports/spending-by-users-2026-04.html +``` + +### What to pass to the report skill + +When invoking `codemie-html-report`, include: + +1. **The raw JSON** collected from the CLI (one object per command/endpoint). +2. **The user's intent** — e.g. "leaderboard dashboard with tier distribution and dimension + breakdown", "CLI insights with usage patterns". +3. **Timestamp context** — most endpoints return `metadata.data_as_of`; pass it through for + the report subtitle. +4. **Output path** — tell the report skill where to save, e.g. `reports/leaderboard.html`. +5. **Pagination hints** if the data was truncated. + +--- + +## Full API Reference + +### Leaderboard endpoints (`GET /v1/analytics/leaderboard/...`) + +Admin-only. All accept `snapshot_id`, `view`, `season_key` query params. + +| Endpoint | Additional Params | Returns | +|----------|------------------|---------| +| `/leaderboard/summary` | — | Total users, tier counts, top score | +| `/leaderboard/entries` | `tier`, `search`, `intent`, `sort_by`, `sort_order`, `page`, `per_page` | Paginated ranked entries | +| `/leaderboard/user/{user_id}` | path: user ID or email | Full user profile with D1–D6 breakdown | +| `/leaderboard/tiers` | — | Tier name, count, percentage | +| `/leaderboard/scores` | — | Score histogram (10-point bins) | +| `/leaderboard/dimensions` | — | Average D1–D6 scores | +| `/leaderboard/top-performers` | `limit` (default 3, max 50) | Top N by total score | +| `/leaderboard/snapshots` | `view`, `status`, `is_final`, `page`, `per_page` | Computation snapshots | +| `/leaderboard/seasons` | `view` (required: monthly/quarterly), `page`, `per_page` | Available seasonal periods | +| `/leaderboard/framework` | — | Static metadata: dimensions, tiers, intents, scoring | +| `/leaderboard/compute` (POST) | `period_days`, `view`, `season_key` | Triggers manual computation | + +### CLI Insights endpoints (`GET /v1/analytics/cli-insights-...`) + +| Endpoint | Params | Returns | +|----------|--------|---------| +| `/cli-insights-weekday-pattern` | time filters | Weekday usage patterns | +| `/cli-insights-hourly-usage` | time filters | Hourly usage patterns | +| `/cli-insights-session-depth` | time filters | Session depth distribution | +| `/cli-insights-user-classification` | time filters | User classification breakdown | +| `/cli-insights-top-users-by-cost` | time filters | Top users ranked by cost | +| `/cli-insights-top-spenders` | time filters | Top spenders | +| `/cli-insights-users` | time filters | CLI user list | +| `/cli-insights-user-detail` | `user_name` (required), `user_id` | Full user detail | +| `/cli-insights-user-key-metrics` | `user_name` (required), `user_id` | User KPIs | +| `/cli-insights-user-tools` | `user_name` (required), `user_id` | User tool usage | +| `/cli-insights-user-models` | `user_name` (required), `user_id` | User model usage | +| `/cli-insights-user-workflow-intent` | `user_name` (required), `user_id` | User workflow intent | +| `/cli-insights-user-classification-detail` | `user_name` (required), `user_id` | User classification detail | +| `/cli-insights-user-category-breakdown` | `user_name` (required), `user_id` | User category breakdown | +| `/cli-insights-user-repositories` | `user_name` (required), `user_id` | User repositories | +| `/cli-insights-project-classification` | time filters | Project classification | +| `/cli-insights-top-projects-by-cost` | time filters | Top projects by cost | + +### Standard CLI analytics (`GET /v1/analytics/cli-...`) + +| Endpoint | Returns | +|----------|---------| +| `/cli-summary` | CLI totals (tokens, cost, sessions) | +| `/cli-agents` | Agent breakdown | +| `/cli-llms` | Model breakdown | +| `/cli-users` | CLI user activity | +| `/cli-errors` | Error logs | +| `/cli-repositories` | Repo activity | +| `/cli-top-performers` | Top by lines added | +| `/cli-top-versions` | CLI version distribution | +| `/cli-top-proxy-endpoints` | LiteLLM endpoint usage | +| `/cli-tools` | Tool usage | + +### Dashboard analytics (`GET /v1/analytics/...`) + +All accept time filters + `users` + `projects` + `page` + `per_page`. + +| Endpoint | Returns | +|----------|---------| +| `/summaries` | Platform totals (tokens, cost, unique users) | +| `/users-spending` | Per-user cost + tokens | +| `/users-activity` | Activity time-series | +| `/users-unique-daily` | Unique users/day | +| `/users` | User list | +| `/projects-spending` | Per-project spending | +| `/projects-activity` | Project activity time-series | +| `/projects-unique-daily` | Unique projects/day | +| `/llms-usage` | LLM model usage | +| `/tools-usage` | Tool usage | +| `/workflows` | Workflow runs | +| `/agents-usage` | Agent executions | +| `/embeddings-usage` | Embedding model usage | +| `/assistants-chats` | Chat assistant conversations | +| `/webhooks-invocation` | Webhook usage | +| `/mcp-servers` | MCP server usage | +| `/mcp-servers-by-users` | MCP by user | +| `/power-users` | Power user analytics | +| `/knowledge-sharing` | Knowledge sharing metrics | +| `/top-agents-usage` | Top agents | +| `/top-workflow-usage` | Top workflows | +| `/published-to-marketplace` | Marketplace publishing | + +### Spending & Budget endpoints + +| Endpoint | Method | Returns | +|----------|--------|---------| +| `/spending` | GET | Current user: spend, budget_limit, hard_budget_limit, reset time | +| `/budget_usage` | GET | Per-key budget rows with % used | +| `/budget-soft-limit` | GET | Soft limit warnings | +| `/budget-hard-limit` | GET | Hard limit hits | +| `/spending/by-users/platform` | GET | Per-user platform spending | +| `/spending/by-users/cli` | GET | Per-user CLI spending | + +### Engagement + +| Endpoint | Method | Returns | +|----------|--------|---------| +| `/engagement/weekly-histogram` | GET | 3h intervals, last 7 days, by feature type | + +### LiteLLM endpoints (via `LITELLM_URL` + `LITELLM_KEY`) + +| Endpoint | Method | Params | Description | +|----------|--------|--------|-------------| +| `/customer/info` | GET | `user_id` | Customer spend + budget + allowed models | +| `/spend/logs` | GET | `start_date`, `end_date`, `user_id` | Spend log entries | +| `/key/info` | GET | `key` | Virtual key details + spend | +| `/model/info` | GET | — | Available models | +| `/health` | GET | — | Proxy health | + +### Response envelope + +Most CodeMie endpoints return: +```json +{ + "data": { ... }, + "metadata": { + "timestamp": "2024-03-15T12:00:00Z", + "data_as_of": "2024-03-15T12:00:00Z", + "filters_applied": {}, + "execution_time_ms": 45.2 + } +} +``` + +Always extract `response.data` for the actual payload. + +--- + +## Custom analytics requests + +For endpoints not covered by preset commands: + +```bash +node analytics-cli.js custom /v1/analytics/mcp-servers --time-period last_30_days + +# POST endpoints +node analytics-cli.js custom /v1/analytics/ai-adoption-overview --method POST \ + --time-period last_30_days +``` + +--- + +## Offline CLI analytics (no API key needed) + +The `codemie analytics` CLI command reads **local session files** from `~/.codemie/sessions/` +with no API calls: + +```bash +codemie analytics --last 7d --output json +codemie analytics --agent claude --last 30d --export csv +``` + +--- + +## Report References + +Reference files in `references/` describe canonical report layouts. **Always check the +relevant reference before building a new HTML report** — it defines the exact components, +charts, data structure, and modal design to use, ensuring consistency across users. + +| Report type | Reference file | When to use | +|-------------|---------------|-------------| +| Leaderboard dashboard | [`references/leaderboard-dashboard-report.md`](references/leaderboard-dashboard-report.md) | Any request for leaderboard rankings, AI champions, top performers, tier distribution | +| People spending dashboard | [`references/people-spending-dashboard-report.md`](references/people-spending-dashboard-report.md) | Any request to track LiteLLM costs for a specific list of users (cohort, team, bootcamp, project) | + +--- + +## Use Cases + +### Use Case: People Spending Dashboard (cohort / team / bootcamp) + +**Trigger phrases**: "build a spending dashboard for people from X", "track LiteLLM costs +for a list of users", "how much did this team spend", "bootcamp spending report", +"costs for people in this CSV/Excel". + +**Also applies when**: the user asks to enrich analytics with EPAM employee data, map +platform users to EPAM people, or look up user assignments/org details. + +**⚠️ EPAM People & Assignments Finder — required for this use case only** + +Before proceeding, verify the assistant is accessible: + +```bash +node ~/.claude/skills/codemie-analytics/scripts/analytics-cli.js \ + custom /v1/assistants/5ca384d0-d042-480c-a0a9-d28150e2352f 2>&1 | head -5 +``` + +If the command returns an auth error, HTTP 401/403/404, or "No CodeMie credentials" — +**stop. Do not run anything else.** Notify the user: + +> ⛔ **EPAM People & Assignments Finder** assistant is not configured on your account. +> +> **Assistant:** EPAM People & Assignments Finder +> **ID:** `5ca384d0-d042-480c-a0a9-d28150e2352f` +> +> Add it with: +> ```bash +> codemie assistants add 5ca384d0-d042-480c-a0a9-d28150e2352f +> ``` +> Or open in browser and click **Add to my assistants**: +> https://codemie.lab.epam.com/#/assistants/5ca384d0-d042-480c-a0a9-d28150e2352f +> +> Once added, resume this session with: +> ```bash +> claude -f +> ``` + +**Full workflow** (see `references/people-spending-dashboard-report.md` for all details): + +1. **Parse the list** from Excel/CSV using `openpyxl`. Skip header and TOTAL rows. +2. **Fetch 3 LiteLLM accounts per user** using Python `asyncio` + `aiohttp` (semaphore 25, + `ssl=False`). Account patterns: + - Web: `email` (plain) + - CLI: `email_codemie_cli` + - Premium: `email_codemie_premium_models` + Use `end_user_id` param (not `user_id`) on `GET /customer/info`. +3. **Save raw results** to `/tmp/` to avoid re-fetching on HTML rebuild. +4. **Build users array** — sum three spend values, extract budget fields per account. +5. **Fetch leaderboard** — paginate ALL pages (`--per-page 500`), ~25 pages for 12k users. + Expect ~60% of a typical cohort to appear. +6. **Fetch CLI insights** — `cli-insights-users --per-page 500 topBySpend` for top CLI users. + Expect ~3–5% coverage for a general cohort. +7. **Compute KPIs** — grand total, per-type totals, active user count, avg spend. + Budget projection: `avg_spend_per_active × total_users × 1.20`. +8. **Generate HTML** — use `str.replace()` with `__TOKEN__` markers (never f-strings, which + conflict with JS `${...}` template literals). +9. **Wire table clicks** — use `data-email` attribute + event delegation (never `onclick=""` + attributes, which break under Python quote escaping). +10. **Save** to `reports/.html`. + +**Key commands:** +```bash +CLI=~/.claude/skills/codemie-analytics/scripts/analytics-cli.js + +# Leaderboard (run in a loop for all pages) +node $CLI leaderboard --per-page 500 --page --output json + +# CLI top spenders +node $CLI cli-insights-users --time-period last_30_days --per-page 500 --output json +``` + +**LiteLLM fetch** requires Python (not the analytics CLI) because it needs +`LITELLM_URL` + `LITELLM_KEY` env vars and concurrent calls for 1,000+ accounts. + +--- + +## Tips + +- **Always run the CLI first**, capture JSON, then hand it to the report skill — don't + hardcode example data. +- If a command returns paginated data, loop through all pages or set `--per-page 500`. +- For time-series charts, use `/users-unique-daily` or `/projects-unique-daily` endpoints. +- Budget warnings: flag rows where `spend / max_budget > 0.8` (warn) and `> 1.0` (error). +- For the **leaderboard dashboard**, combine `leaderboard` + `leaderboard-summary` + + `leaderboard-tiers` + `leaderboard-dimensions` to build a comprehensive view. Then follow + `references/leaderboard-dashboard-report.md` for the exact HTML structure. +- For a **people spending dashboard**, fetch LiteLLM directly with Python async (3 accounts + per user), then enrich with leaderboard + CLI insights. Follow + `references/people-spending-dashboard-report.md` for the exact HTML structure. +- For a **single user deep-dive**, combine `leaderboard-user ` with + `cli-insights-user ` for the full picture (champion score + CLI activity). +- If the CLI prints an auth error, forward its message verbatim — it already tells the user + what to do next. +- Always save HTML reports to `reports/` in the user's working directory. \ No newline at end of file diff --git a/src/agents/plugins/claude/plugin/skills/codemie-analytics/references/leaderboard-dashboard-report.md b/src/agents/plugins/claude/plugin/skills/codemie-analytics/references/leaderboard-dashboard-report.md new file mode 100644 index 00000000..2fbc05fb --- /dev/null +++ b/src/agents/plugins/claude/plugin/skills/codemie-analytics/references/leaderboard-dashboard-report.md @@ -0,0 +1,225 @@ +# Leaderboard Dashboard HTML Report — Reference + +> **Purpose**: This document describes the canonical leaderboard HTML report built for +> CodeMie AI/Run analytics. Use it as a template spec when generating new leaderboard +> dashboards so all reports are consistent in structure, components, and UX. + +--- + +## Overview + +The leaderboard report is a **self-contained single-file HTML dashboard** that combines +leaderboard ranking data (from the analytics API) with EPAM profile data (from OneHub). +It is dark-themed by default, uses the CodeMie design system (inlined CSS), and Chart.js +for all visualisations. + +**Key properties:** +- No external CSS dependencies (all 8 CodeMie CSS files inlined in ` + + +
+ + + + +
+
+ 💰 Grand Total + __GRAND_TOTAL__ + All 3 spend types combined +
+
+ 🌐 Web / Platform + __WEB_TOTAL__ + email@domain (UI spend) +
+
+ 💻 CLI + __CLI_TOTAL__ + email_codemie_cli +
+
+ ✨ Premium Models + __PREMIUM_TOTAL__ + email_codemie_premium_models +
+
+ + +
+
+ Participants + __TOTAL_USERS__ + From bootcamp.xlsx +
+
+ In LiteLLM + __IN_LITELLM__ + Registered as customers +
+
+ Active Spenders + __ACTIVE_USERS__ + Any spend > $0 +
+
+ Avg Spend (active) + __AVG_SPEND__ + Mean — active users only +
+ +
+ + +
+
+
+
+ 💳 +

Estimated Monthly Program Budget

+ Projection +
+

+ Projected cost for the full cohort based on the + current average spend per active user (__AVG_SPEND__) + scaled to all 656 participants, + with a +20% overhead buffer for spikes, onboarding, and premium model usage. +

+
+
+
Base estimate
+
__BASE_ESTIMATE__  (__AVG_SPEND__ × __TOTAL_USERS__)
+
+
+
+20% buffer
+
__BUFFER_AMOUNT__
+
+
+
+
+
RECOMMENDED BUDGET
+
__BUDGET_PROJECTION__
+
per month / full cohort
+
avg __AVG_SPEND__ × __TOTAL_USERS__ × 1.20
+
+
+
+ + +
+
+
Spend Distribution (Total per User)
+
+
+
+
Top 10 by Total Spend
+
+
+
+
+
Spend Breakdown by Type
+
+
+ + +
+
+
All Participants
+ +
+
+
+ + + + + + + + + + + + + +
#EmailWeb ($)CLI ($)Premium ($)Total ($)Status
+
+ +
+
+
+ + + + + + + \ No newline at end of file diff --git a/src/agents/plugins/claude/plugin/skills/codemie-analytics/scripts/analytics-cli.js b/src/agents/plugins/claude/plugin/skills/codemie-analytics/scripts/analytics-cli.js new file mode 100644 index 00000000..058a231a --- /dev/null +++ b/src/agents/plugins/claude/plugin/skills/codemie-analytics/scripts/analytics-cli.js @@ -0,0 +1,793 @@ +#!/usr/bin/env node +/** + * CodeMie Analytics CLI + * Generic, flexible Node.js script for querying CodeMie and LiteLLM APIs. + * + * Auth mirrors the `codemie assistants chat` flow exactly: + * 1. Load config from ~/.codemie/codemie-cli.config.json (active profile) + * 2. Extract codeMieUrl (or baseUrl) from profile + * 3. Normalize URL to protocol://host + * 4. Look up per-URL SSO credentials from encrypted file + * 5. Fall back to global SSO credentials (verify apiUrl matches) + * 6. Check credential expiry + * 7. Send cookies as Cookie header on every API request + * + * Usage: + * node analytics-cli.js [options] + * + * Commands: + * summaries Overall token/cost/user summary + * leaderboard AI champions leaderboard (entries, with filters) + * leaderboard-summary Leaderboard KPI summary (totals, tier counts) + * leaderboard-user Single user leaderboard profile with dimension breakdown + * leaderboard-tiers Tier distribution (name, count, %) + * leaderboard-dimensions Average scores per dimension (D1–D6) + * leaderboard-top Top N performers by total score (default 10) + * leaderboard-scores Score histogram in 10-point bins + * leaderboard-framework Static metadata: dimensions, tiers, intents, scoring + * leaderboard-snapshots List computation snapshots + * leaderboard-seasons Available seasonal periods (monthly/quarterly) + * cli-insights CLI usage: agents, repos, tools, errors, top-performers + * cli-insights-users CLI user classification & top spenders + * cli-insights-user Detailed CLI profile for a single user + * cli-insights-projects CLI project classification & top projects by cost + * cli-insights-patterns Weekday + hourly + session-depth usage patterns + * users List users + activity + * projects-spending Per-project spending + * llms-usage LLM model usage breakdown + * tools-usage Tool usage analytics + * workflows Workflow execution analytics + * budget Budget limits (soft + hard) + * spending Current user spending & budget (personal) + * spending-by-users Per-user spending breakdown (platform + cli) + * engagement Weekly engagement histogram + * litellm-customer [user_id] LiteLLM customer/info (needs LITELLM_URL + LITELLM_KEY) + * litellm-spend LiteLLM /spend/logs (needs LITELLM_URL + LITELLM_KEY) + * litellm-keys LiteLLM /key/info for all virtual keys + * custom Call any analytics endpoint: e.g. custom /v1/analytics/mcp-servers + * enrich-csv Read CSV/Excel, lookup each user in LiteLLM, output enriched data + * + * Filters (most commands): + * --time-period last_hour | last_6_hours | last_24_hours | last_7_days | last_30_days | last_60_days | last_year + * --start-date ISO8601 e.g. 2024-01-01T00:00:00 + * --end-date ISO8601 + * --users comma-separated usernames + * --projects comma-separated project names + * --page page number (default 1) + * --per-page results per page (default 50) + * --output json | table | csv (default json) + * --pretty pretty-print JSON (flag) + * + * Leaderboard-specific filters: + * --view current | monthly | quarterly (default current) + * --season-key e.g. 2026-03 or 2026-Q1 + * --snapshot-id explicit snapshot ID + * --tier pioneer | expert | advanced | practitioner | newcomer + * --intent cli_focused | platform_focused | hybrid | sdlc_unicorn + * --search partial name/email search + * --sort-by rank | total_score | user_name | tier_level (default rank) + * --sort-order asc | desc (default asc) + * --limit max entries for top-performers (default 10, max 50) + */ + +import { createDecipheriv, createHash } from 'crypto'; +import { readFileSync, existsSync } from 'fs'; +import { homedir, hostname, platform, arch } from 'os'; +import { join, resolve } from 'path'; + +// ─── Argument Parsing ──────────────────────────────────────────────────────── + +const args = process.argv.slice(2); +const command = args[0]; + +function parseArgs(argv) { + const opts = { _: [] }; + for (let i = 0; i < argv.length; i++) { + if (argv[i].startsWith('--')) { + const key = argv[i].slice(2).replace(/-([a-z])/g, (_, c) => c.toUpperCase()); + const val = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[++i] : true; + opts[key] = val; + } else { + opts._.push(argv[i]); + } + } + return opts; +} + +const opts = parseArgs(args.slice(1)); + +// ─── Constants ─────────────────────────────────────────────────────────────── + +const CODEMIE_HOME = process.env.CODEMIE_HOME || join(homedir(), '.codemie'); +const CREDENTIALS_DIR = join(CODEMIE_HOME, 'credentials'); +const GLOBAL_SSO_FILE = join(CODEMIE_HOME, 'sso-credentials.enc'); +const CONFIG_FILE = join(CODEMIE_HOME, 'codemie-cli.config.json'); + +// ─── Encryption (matches CredentialStore in codemie-code exactly) ──────────── + +function getAESKey() { + const machineId = hostname() + platform() + arch(); + const encryptionKeyHex = createHash('sha256').update(machineId).digest('hex'); + return createHash('sha256').update(encryptionKeyHex).digest(); +} + +function decrypt(text) { + const colonIndex = text.indexOf(':'); + if (colonIndex === -1) return null; + const ivHex = text.slice(0, colonIndex); + const encryptedHex = text.slice(colonIndex + 1); + const iv = Buffer.from(ivHex, 'hex'); + const key = getAESKey(); + const decipher = createDecipheriv('aes-256-cbc', key, iv); + let decrypted = decipher.update(encryptedHex, 'hex', 'utf8'); + decrypted += decipher.final('utf8'); + return decrypted; +} + +function readEncryptedFile(filePath) { + if (!existsSync(filePath)) return null; + try { + const text = readFileSync(filePath, 'utf8'); + const json = decrypt(text); + return json ? JSON.parse(json) : null; + } catch { + return null; + } +} + +// ─── URL Normalization ────────────────────────────────────────────────────── + +function normalizeToBase(url) { + try { + const parsed = new URL(url); + return `${parsed.protocol}//${parsed.host}`; + } catch { + return url; + } +} + +function getUrlStorageKey(baseUrl) { + const normalized = baseUrl.replace(/\/$/, '').toLowerCase(); + return `sso-${createHash('sha256').update(normalized).digest('hex')}`; +} + +// ─── Security helpers ─────────────────────────────────────────────────────── + +const SENSITIVE_PATTERNS = /\b(Bearer\s+\S+|sk-[a-zA-Z0-9_-]{20,}|token[=:]\S+|cookie[=:]\S+|password[=:]\S+|api[_-]?key[=:]\S+)\b/gi; + +function sanitizeErrorText(text) { + if (!text) return ''; + return text.replace(SENSITIVE_PATTERNS, '[REDACTED]'); +} + +const REDACT_KEYS = new Set(['token', 'key', 'api_key', 'secret', 'master_key', 'hashed_token']); + +function redactSensitiveFields(data) { + if (data === null || data === undefined) return data; + if (Array.isArray(data)) return data.map(redactSensitiveFields); + if (typeof data === 'object') { + const out = {}; + for (const [k, v] of Object.entries(data)) { + if (REDACT_KEYS.has(k) && typeof v === 'string' && v.length > 0) { + out[k] = v.slice(0, 4) + '...' + v.slice(-4); + } else { + out[k] = redactSensitiveFields(v); + } + } + return out; + } + return data; +} + +// ─── Config Loading ────────────────────────────────────────────────────────── + +function loadConfig() { + const localConfig = join(process.cwd(), '.codemie', 'codemie-cli.config.json'); + const configs = []; + if (existsSync(CONFIG_FILE)) { + try { configs.push(JSON.parse(readFileSync(CONFIG_FILE, 'utf8'))); } catch {} + } + if (existsSync(localConfig)) { + try { configs.push(JSON.parse(readFileSync(localConfig, 'utf8'))); } catch {} + } + if (configs.length === 0) return null; + return configs[configs.length - 1] || configs[0]; +} + +function getActiveProfile(config) { + if (!config) return null; + if (config.version === 2 && config.profiles) { + const name = config.activeProfile || Object.keys(config.profiles)[0]; + return config.profiles[name] || null; + } + return config; +} + +// ─── Credential Resolution ────────────────────────────────────────────────── + +function resolveAuth() { + const SENTINEL_KEYS = new Set(['sso-provided', 'proxy-handled']); + if (process.env.CODEMIE_API_KEY && !SENTINEL_KEYS.has(process.env.CODEMIE_API_KEY) && process.env.CODEMIE_URL) { + const baseUrl = process.env.CODEMIE_URL.replace(/\/$/, ''); + const apiUrl = baseUrl.includes('/code-assistant-api') ? baseUrl : `${baseUrl}/code-assistant-api`; + return { type: 'bearer', token: process.env.CODEMIE_API_KEY, baseUrl: apiUrl }; + } + + const config = loadConfig(); + const profile = getActiveProfile(config); + const codeMieUrl = process.env.CODEMIE_URL || profile?.codeMieUrl || profile?.baseUrl; + if (!codeMieUrl) return null; + + const normalizedBase = normalizeToBase(codeMieUrl); + const storageKey = getUrlStorageKey(normalizedBase); + const perUrlFile = join(CREDENTIALS_DIR, `${storageKey}.enc`); + let credentials = readEncryptedFile(perUrlFile); + + if (!credentials) { + credentials = readEncryptedFile(GLOBAL_SSO_FILE); + if (credentials && credentials.apiUrl) { + const credentialBase = normalizeToBase(credentials.apiUrl); + if (credentialBase !== normalizedBase) credentials = null; + } + } + + if (!credentials || !credentials.cookies || !credentials.apiUrl) { + if (profile?.apiKey) { + const apiUrl = codeMieUrl.includes('/code-assistant-api') + ? codeMieUrl + : `${codeMieUrl}/code-assistant-api`; + return { type: 'bearer', token: profile.apiKey, baseUrl: apiUrl }; + } + return null; + } + + if (credentials.expiresAt && Date.now() > credentials.expiresAt) { + process.stderr.write('[analytics-cli] SSO credentials expired. Run `codemie setup` to re-authenticate.\n'); + return null; + } + + const cookieStr = Object.entries(credentials.cookies) + .map(([k, v]) => `${k}=${v}`) + .join('; '); + + return { type: 'cookie', cookie: cookieStr, baseUrl: credentials.apiUrl }; +} + +// ─── HTTP Client ───────────────────────────────────────────────────────────── + +async function apiFetch(url, { method = 'GET', body, auth, extraHeaders = {} } = {}) { + const headers = { + 'Content-Type': 'application/json', + 'X-CodeMie-Client': 'codemie-cli', + ...extraHeaders, + }; + + if (auth?.type === 'cookie') { + headers['Cookie'] = auth.cookie; + } else if (auth?.type === 'bearer') { + headers['Authorization'] = `Bearer ${auth.token}`; + } + + const fetchOpts = { method, headers }; + if (body) fetchOpts.body = JSON.stringify(body); + + const res = await fetch(url, fetchOpts); + + if (!res.ok) { + const text = await res.text().catch(() => ''); + // Strip the URL to avoid leaking query params that may contain tokens + const safeUrl = url.split('?')[0]; + throw new Error(`HTTP ${res.status} ${res.statusText} for ${safeUrl}\n${sanitizeErrorText(text)}`); + } + + const ct = res.headers.get('content-type') || ''; + if (ct.includes('application/json')) return res.json(); + return res.text(); +} + +// ─── CodeMie API helpers ───────────────────────────────────────────────────── + +function buildQuery(opts) { + const params = new URLSearchParams(); + if (opts.timePeriod) params.set('time_period', opts.timePeriod); + if (opts.startDate) params.set('start_date', opts.startDate); + if (opts.endDate) params.set('end_date', opts.endDate); + if (opts.users) params.set('users', opts.users); + if (opts.projects) params.set('projects', opts.projects); + if (opts.page) params.set('page', opts.page); + if (opts.perPage) params.set('per_page', opts.perPage); + return params.toString() ? `?${params}` : ''; +} + +function buildLeaderboardQuery(opts) { + const params = new URLSearchParams(); + if (opts.snapshotId) params.set('snapshot_id', opts.snapshotId); + if (opts.view) params.set('view', opts.view); + if (opts.seasonKey) params.set('season_key', opts.seasonKey); + if (opts.tier) params.set('tier', opts.tier); + if (opts.intent) params.set('intent', opts.intent); + if (opts.search) params.set('search', opts.search); + if (opts.sortBy) params.set('sort_by', opts.sortBy); + if (opts.sortOrder) params.set('sort_order', opts.sortOrder); + if (opts.page) params.set('page', opts.page); + if (opts.perPage) params.set('per_page', opts.perPage); + if (opts.limit) params.set('limit', opts.limit); + return params.toString() ? `?${params}` : ''; +} + +async function analyticsGet(auth, path, opts) { + const qs = buildQuery(opts); + const url = `${auth.baseUrl}${path}${qs}`; + return apiFetch(url, { auth }); +} + +async function analyticsLeaderboardGet(auth, path, opts) { + const qs = buildLeaderboardQuery(opts); + const url = `${auth.baseUrl}${path}${qs}`; + return apiFetch(url, { auth }); +} + +async function analyticsPost(auth, path, bodyExtra = {}, opts) { + const body = {}; + if (opts.timePeriod) body.time_period = opts.timePeriod; + if (opts.startDate) body.start_date = opts.startDate; + if (opts.endDate) body.end_date = opts.endDate; + if (opts.users) body.users = opts.users.split(','); + if (opts.projects) body.projects = opts.projects.split(','); + if (opts.page) body.page = parseInt(opts.page); + if (opts.perPage) body.per_page = parseInt(opts.perPage); + Object.assign(body, bodyExtra); + const url = `${auth.baseUrl}${path}`; + return apiFetch(url, { method: 'POST', body, auth }); +} + +// ─── LiteLLM helpers ───────────────────────────────────────────────────────── + +function getLiteLLMAuth() { + const url = process.env.LITELLM_URL; + const key = process.env.LITELLM_KEY; + if (!url || !key) { + throw new Error('LITELLM_URL and LITELLM_KEY env vars are required for LiteLLM commands'); + } + return { url: url.replace(/\/$/, ''), key }; +} + +async function litellmFetch(llm, path, { method = 'GET', body, params } = {}) { + let url = `${llm.url}${path}`; + if (params) { + const qs = new URLSearchParams(params).toString(); + if (qs) url += `?${qs}`; + } + return apiFetch(url, { + method, + body, + auth: { type: 'bearer', token: llm.key }, + }); +} + +// ─── CSV/Excel parsing ─────────────────────────────────────────────────────── + +async function parseInputFile(filePath) { + const resolvedPath = resolve(filePath); + if (!existsSync(resolvedPath)) { + throw new Error(`File not found: ${resolvedPath}`); + } + + const ext = resolvedPath.split('.').pop().toLowerCase(); + + if (ext === 'csv') { + const text = readFileSync(resolvedPath, 'utf8'); + const lines = text.trim().split(/\r?\n/); + const headers = lines[0].split(',').map(h => h.trim().replace(/^"|"$/g, '')); + return lines.slice(1).map(line => { + const vals = line.split(',').map(v => v.trim().replace(/^"|"$/g, '')); + return Object.fromEntries(headers.map((h, i) => [h, vals[i] ?? ''])); + }); + } + + if (ext === 'xlsx' || ext === 'xls') { + try { + const XLSX = await import('xlsx').catch(() => null); + if (!XLSX) throw new Error('xlsx package not installed. Run: npm install -g xlsx'); + const workbook = XLSX.default.readFile(resolvedPath); + const sheet = workbook.Sheets[workbook.SheetNames[0]]; + return XLSX.default.utils.sheet_to_json(sheet); + } catch (e) { + throw new Error(`Cannot parse Excel file: ${e.message}`); + } + } + + throw new Error(`Unsupported file type: .${ext}. Use .csv, .xlsx, or .xls`); +} + +// ─── Output helpers ────────────────────────────────────────────────────────── + +function output(data) { + const fmt = opts.output || 'json'; + if (fmt === 'json' || !fmt) { + if (opts.pretty) { + console.log(JSON.stringify(data, null, 2)); + } else { + console.log(JSON.stringify(data)); + } + } else if (fmt === 'table') { + printTable(data); + } else if (fmt === 'csv') { + printCSV(data); + } +} + +function printTable(data) { + const rows = Array.isArray(data) ? data : (data?.data || data?.items || [data]); + if (!rows.length) { console.log('(no data)'); return; } + const keys = Object.keys(rows[0]); + const widths = keys.map(k => Math.max(k.length, ...rows.map(r => String(r[k] ?? '').slice(0, 40).length))); + const header = keys.map((k, i) => k.padEnd(widths[i])).join(' | '); + console.log(header); + console.log(widths.map(w => '-'.repeat(w)).join('-+-')); + rows.forEach(row => { + console.log(keys.map((k, i) => String(row[k] ?? '').slice(0, 40).padEnd(widths[i])).join(' | ')); + }); +} + +function printCSV(data) { + const rows = Array.isArray(data) ? data : (data?.data || data?.items || [data]); + if (!rows.length) return; + const keys = Object.keys(rows[0]); + console.log(keys.join(',')); + rows.forEach(row => console.log(keys.map(k => JSON.stringify(row[k] ?? '')).join(','))); +} + +// ─── Commands ──────────────────────────────────────────────────────────────── + +// --- Summaries --- + +async function cmdSummaries(auth) { + const data = await analyticsGet(auth, '/v1/analytics/summaries', opts); + output(data); +} + +// --- Leaderboard family (uses /v1/analytics/leaderboard/*) --- + +async function cmdLeaderboard(auth) { + const data = await analyticsLeaderboardGet(auth, '/v1/analytics/leaderboard/entries', opts); + output(data); +} + +async function cmdLeaderboardSummary(auth) { + const data = await analyticsLeaderboardGet(auth, '/v1/analytics/leaderboard/summary', opts); + output(data); +} + +async function cmdLeaderboardUser(auth) { + const userId = opts._[0]; + if (!userId) throw new Error('Usage: leaderboard-user '); + const qs = buildLeaderboardQuery(opts); + const url = `${auth.baseUrl}/v1/analytics/leaderboard/user/${encodeURIComponent(userId)}${qs}`; + const data = await apiFetch(url, { auth }); + output(data); +} + +async function cmdLeaderboardTiers(auth) { + const data = await analyticsLeaderboardGet(auth, '/v1/analytics/leaderboard/tiers', opts); + output(data); +} + +async function cmdLeaderboardDimensions(auth) { + const data = await analyticsLeaderboardGet(auth, '/v1/analytics/leaderboard/dimensions', opts); + output(data); +} + +async function cmdLeaderboardTop(auth) { + if (!opts.limit) opts.limit = opts._[0] || '10'; + const data = await analyticsLeaderboardGet(auth, '/v1/analytics/leaderboard/top-performers', opts); + output(data); +} + +async function cmdLeaderboardScores(auth) { + const data = await analyticsLeaderboardGet(auth, '/v1/analytics/leaderboard/scores', opts); + output(data); +} + +async function cmdLeaderboardFramework(auth) { + const url = `${auth.baseUrl}/v1/analytics/leaderboard/framework`; + const data = await apiFetch(url, { auth }); + output(data); +} + +async function cmdLeaderboardSnapshots(auth) { + const data = await analyticsLeaderboardGet(auth, '/v1/analytics/leaderboard/snapshots', opts); + output(data); +} + +async function cmdLeaderboardSeasons(auth) { + if (!opts.view) throw new Error('Usage: leaderboard-seasons --view monthly|quarterly'); + const data = await analyticsLeaderboardGet(auth, '/v1/analytics/leaderboard/seasons', opts); + output(data); +} + +// --- CLI Insights family --- + +async function cmdCliInsights(auth) { + const [summary, agents, llms, users, errors, repos, tools, topVersions, topEndpoints] = await Promise.all([ + analyticsGet(auth, '/v1/analytics/cli-summary', opts), + analyticsGet(auth, '/v1/analytics/cli-agents', opts), + analyticsGet(auth, '/v1/analytics/cli-llms', opts), + analyticsGet(auth, '/v1/analytics/cli-users', opts), + analyticsGet(auth, '/v1/analytics/cli-errors', opts), + analyticsGet(auth, '/v1/analytics/cli-repositories', opts), + analyticsGet(auth, '/v1/analytics/cli-tools', opts), + analyticsGet(auth, '/v1/analytics/cli-top-versions', opts).catch(() => null), + analyticsGet(auth, '/v1/analytics/cli-top-proxy-endpoints', opts).catch(() => null), + ]); + output({ summary, agents, llms, users, errors, repos, tools, topVersions, topEndpoints }); +} + +async function cmdCliInsightsUsers(auth) { + const [classification, topBySpend, topSpenders, userList] = await Promise.all([ + analyticsGet(auth, '/v1/analytics/cli-insights-user-classification', opts).catch(() => null), + analyticsGet(auth, '/v1/analytics/cli-insights-top-users-by-cost', opts).catch(() => null), + analyticsGet(auth, '/v1/analytics/cli-insights-top-spenders', opts).catch(() => null), + analyticsGet(auth, '/v1/analytics/cli-insights-users', opts).catch(() => null), + ]); + output({ classification, topBySpend, topSpenders, userList }); +} + +async function cmdCliInsightsUser(auth) { + const userName = opts._[0] || opts.userName; + if (!userName) throw new Error('Usage: cli-insights-user [--user-id ]'); + const userQs = new URLSearchParams(); + userQs.set('user_name', userName); + if (opts.userId) userQs.set('user_id', opts.userId); + // Add time filters + if (opts.timePeriod) userQs.set('time_period', opts.timePeriod); + if (opts.startDate) userQs.set('start_date', opts.startDate); + if (opts.endDate) userQs.set('end_date', opts.endDate); + const qs = userQs.toString() ? `?${userQs}` : ''; + + const base = auth.baseUrl; + const [detail, keyMetrics, tools, models, workflowIntent, classDetail, categoryBreakdown, repos] = await Promise.all([ + apiFetch(`${base}/v1/analytics/cli-insights-user-detail${qs}`, { auth }), + apiFetch(`${base}/v1/analytics/cli-insights-user-key-metrics${qs}`, { auth }).catch(() => null), + apiFetch(`${base}/v1/analytics/cli-insights-user-tools${qs}`, { auth }).catch(() => null), + apiFetch(`${base}/v1/analytics/cli-insights-user-models${qs}`, { auth }).catch(() => null), + apiFetch(`${base}/v1/analytics/cli-insights-user-workflow-intent${qs}`, { auth }).catch(() => null), + apiFetch(`${base}/v1/analytics/cli-insights-user-classification-detail${qs}`, { auth }).catch(() => null), + apiFetch(`${base}/v1/analytics/cli-insights-user-category-breakdown${qs}`, { auth }).catch(() => null), + apiFetch(`${base}/v1/analytics/cli-insights-user-repositories${qs}`, { auth }).catch(() => null), + ]); + output({ detail, keyMetrics, tools, models, workflowIntent, classDetail, categoryBreakdown, repos }); +} + +async function cmdCliInsightsProjects(auth) { + const [classification, topBySpend] = await Promise.all([ + analyticsGet(auth, '/v1/analytics/cli-insights-project-classification', opts).catch(() => null), + analyticsGet(auth, '/v1/analytics/cli-insights-top-projects-by-cost', opts).catch(() => null), + ]); + output({ classification, topBySpend }); +} + +async function cmdCliInsightsPatterns(auth) { + const [weekday, hourly, sessionDepth] = await Promise.all([ + analyticsGet(auth, '/v1/analytics/cli-insights-weekday-pattern', opts).catch(() => null), + analyticsGet(auth, '/v1/analytics/cli-insights-hourly-usage', opts).catch(() => null), + analyticsGet(auth, '/v1/analytics/cli-insights-session-depth', opts).catch(() => null), + ]); + output({ weekday, hourly, sessionDepth }); +} + +// --- Standard analytics --- + +async function cmdUsers(auth) { + const [users, activity, uniqueDaily] = await Promise.all([ + analyticsGet(auth, '/v1/analytics/users', opts), + analyticsGet(auth, '/v1/analytics/users-activity', opts), + analyticsGet(auth, '/v1/analytics/users-unique-daily', opts), + ]); + output({ users, activity, uniqueDaily }); +} + +async function cmdProjectsSpending(auth) { + const data = await analyticsGet(auth, '/v1/analytics/projects-spending', opts); + output(data); +} + +async function cmdLlmsUsage(auth) { + const data = await analyticsGet(auth, '/v1/analytics/llms-usage', opts); + output(data); +} + +async function cmdToolsUsage(auth) { + const data = await analyticsGet(auth, '/v1/analytics/tools-usage', opts); + output(data); +} + +async function cmdWorkflows(auth) { + const data = await analyticsGet(auth, '/v1/analytics/workflows', opts); + output(data); +} + +async function cmdBudget(auth) { + const [soft, hard] = await Promise.all([ + analyticsGet(auth, '/v1/analytics/budget-soft-limit', opts), + analyticsGet(auth, '/v1/analytics/budget-hard-limit', opts), + ]); + output({ soft, hard }); +} + +async function cmdSpending(auth) { + const [spending, budgetUsage] = await Promise.all([ + analyticsGet(auth, '/v1/analytics/spending', opts), + analyticsGet(auth, '/v1/analytics/budget_usage', opts).catch(() => null), + ]); + output({ spending, budgetUsage }); +} + +async function cmdSpendingByUsers(auth) { + const [platform, cli] = await Promise.all([ + analyticsGet(auth, '/v1/analytics/spending/by-users/platform', opts), + analyticsGet(auth, '/v1/analytics/spending/by-users/cli', opts), + ]); + output({ platform, cli }); +} + +async function cmdEngagement(auth) { + const data = await analyticsGet(auth, '/v1/analytics/engagement/weekly-histogram', opts); + output(data); +} + +// --- Custom --- + +async function cmdCustom(auth) { + const path = opts._[0]; + if (!path) throw new Error('Usage: custom e.g. custom /v1/analytics/mcp-servers'); + const method = (opts.method || 'GET').toUpperCase(); + let data; + if (method === 'POST') { + data = await analyticsPost(auth, path, {}, opts); + } else { + data = await analyticsGet(auth, path, opts); + } + output(data); +} + +// --- LiteLLM --- + +async function cmdLitellmCustomer() { + const llm = getLiteLLMAuth(); + const userId = opts._[0] || opts.user; + const params = userId ? { user_id: userId } : undefined; + const data = await litellmFetch(llm, '/customer/info', { params }); + output(data); +} + +async function cmdLitellmSpend() { + const llm = getLiteLLMAuth(); + const params = {}; + if (opts.startDate) params.start_date = opts.startDate; + if (opts.endDate) params.end_date = opts.endDate; + if (opts.users) params.user_id = opts.users; + const data = await litellmFetch(llm, '/spend/logs', { params }); + output(data); +} + +async function cmdLitellmKeys() { + const llm = getLiteLLMAuth(); + const data = await litellmFetch(llm, '/key/info'); + output(redactSensitiveFields(data)); +} + +async function cmdEnrichCSV() { + const filePath = opts._[0]; + if (!filePath) throw new Error('Usage: enrich-csv '); + + const llm = getLiteLLMAuth(); + const rows = await parseInputFile(filePath); + + const userCol = ['user', 'user_id', 'email', 'username', 'User', 'Email'].find(c => rows[0]?.[c] !== undefined); + if (!userCol) { + throw new Error(`Cannot find user column. Available columns: ${Object.keys(rows[0] || {}).join(', ')}`); + } + + const enriched = []; + for (const row of rows) { + const userId = row[userCol]; + let litellmInfo = null; + try { + litellmInfo = await litellmFetch(llm, '/customer/info', { params: { user_id: userId } }); + } catch { + litellmInfo = { error: 'not_found' }; + } + enriched.push({ + ...row, + litellm_spend: litellmInfo?.spend ?? litellmInfo?.total_spend ?? null, + litellm_max_budget: litellmInfo?.max_budget ?? null, + litellm_models: Array.isArray(litellmInfo?.allowed_model_region) + ? litellmInfo.allowed_model_region.join(';') + : null, + litellm_raw: JSON.stringify(litellmInfo), + }); + } + + output(enriched); +} + +// ─── Main ──────────────────────────────────────────────────────────────────── + +const LITELLM_COMMANDS = ['litellm-customer', 'litellm-spend', 'litellm-keys']; + +async function main() { + if (!command || command === 'help') { + const src = readFileSync(new URL(import.meta.url).pathname, 'utf8'); + const docBlock = src.match(/\/\*\*([\s\S]*?)\*\//)?.[0] ?? ''; + console.log(docBlock); + process.exit(0); + } + + // LiteLLM-only commands + if (command === 'enrich-csv') return cmdEnrichCSV(); + + if (LITELLM_COMMANDS.includes(command)) { + switch (command) { + case 'litellm-customer': return cmdLitellmCustomer(); + case 'litellm-spend': return cmdLitellmSpend(); + case 'litellm-keys': return cmdLitellmKeys(); + } + } + + // CodeMie API commands + const auth = resolveAuth(); + if (!auth) { + throw new Error( + 'No CodeMie credentials found. Either:\n' + + ' 1. Run `codemie setup` with SSO provider to store credentials\n' + + ' 2. Set CODEMIE_API_KEY + CODEMIE_URL env vars' + ); + } + + switch (command) { + // Summaries + case 'summaries': return cmdSummaries(auth); + + // Leaderboard + case 'leaderboard': return cmdLeaderboard(auth); + case 'leaderboard-summary': return cmdLeaderboardSummary(auth); + case 'leaderboard-user': return cmdLeaderboardUser(auth); + case 'leaderboard-tiers': return cmdLeaderboardTiers(auth); + case 'leaderboard-dimensions': return cmdLeaderboardDimensions(auth); + case 'leaderboard-top': return cmdLeaderboardTop(auth); + case 'leaderboard-scores': return cmdLeaderboardScores(auth); + case 'leaderboard-framework': return cmdLeaderboardFramework(auth); + case 'leaderboard-snapshots': return cmdLeaderboardSnapshots(auth); + case 'leaderboard-seasons': return cmdLeaderboardSeasons(auth); + + // CLI Insights + case 'cli-insights': return cmdCliInsights(auth); + case 'cli-insights-users': return cmdCliInsightsUsers(auth); + case 'cli-insights-user': return cmdCliInsightsUser(auth); + case 'cli-insights-projects': return cmdCliInsightsProjects(auth); + case 'cli-insights-patterns': return cmdCliInsightsPatterns(auth); + + // Standard analytics + case 'users': return cmdUsers(auth); + case 'projects-spending': return cmdProjectsSpending(auth); + case 'llms-usage': return cmdLlmsUsage(auth); + case 'tools-usage': return cmdToolsUsage(auth); + case 'workflows': return cmdWorkflows(auth); + case 'budget': return cmdBudget(auth); + case 'spending': return cmdSpending(auth); + case 'spending-by-users': return cmdSpendingByUsers(auth); + case 'engagement': return cmdEngagement(auth); + + // Custom + case 'custom': return cmdCustom(auth); + + default: + throw new Error(`Unknown command: ${command}\nRun with 'help' for usage.`); + } +} + +main().catch(err => { + console.error('[analytics-cli] Error:', err.message); + process.exit(1); +}); \ No newline at end of file diff --git a/src/agents/plugins/claude/plugin/skills/codemie-sdk/SKILL.md b/src/agents/plugins/claude/plugin/skills/codemie-sdk/SKILL.md index 959d9271..60e5d272 100644 --- a/src/agents/plugins/claude/plugin/skills/codemie-sdk/SKILL.md +++ b/src/agents/plugins/claude/plugin/skills/codemie-sdk/SKILL.md @@ -1,24 +1,24 @@ --- name: codemie-sdk description: >- - Manage CodeMie platform assets (assistants, workflows, datasources, integrations, skills, users, categories, analytics) directly from CLI + Manage CodeMie platform assets (assistants, workflows, datasources, integrations, skills, users, categories) directly from CLI using CodeMie SDK. Use when user says "create assistant", "list workflows", "update datasource", "delete assistant", "show my assistants", "get workflow details", "manage integrations", "create integration", "list integrations", "list llm models", "list embedding models", "list skills", "get skill", "create skill", "update skill", "delete skill", "publish skill", "import skill", "export skill", "attach skill", "list categories", "get category", "create category", "delete category", "who am i", "current user", "my profile", "user info", - "analytics", "usage analytics", "summaries", "cli analytics", "spending", "users activity", - "projects activity", "assistants chats analytics", "workflows analytics", "tools usage", - "mcp servers analytics", "llms usage", "users spending", "budget limits", - or any request to manage CodeMie platform resources or view analytics. + or any request to manage CodeMie platform resources. + NOTE: For analytics requests (usage analytics, summaries, spending, users activity, leaderboards, etc.) use the codemie-analytics skill instead. --- # CodeMie SDK Asset Management Manage CodeMie platform assets from the CLI. -**Asset Types:** `assistants`, `workflows`, `datasources`, `integrations`, `skills`, `users`, `categories`, `analytics` +> **Analytics requests** (usage data, summaries, spending, activity, etc.) are handled by the **codemie-analytics** skill — use that instead. + +**Asset Types:** `assistants`, `workflows`, `datasources`, `integrations`, `skills`, `users`, `categories` **Operations:** `list`, `get`, `create`, `update`, `delete` @@ -83,7 +83,6 @@ Once the project is known, use it in all subsequent commands: | Skills | [examples/skills.md](examples/skills.md) | | Users | [examples/users.md](examples/users.md) | | Categories | [examples/categories.md](examples/categories.md) | -| Analytics | [examples/analytics.md](examples/analytics.md) | Do **not** guess field names or skip this step — all required/optional fields, nested schemas, and asset cross-reference commands are documented there. @@ -239,57 +238,3 @@ codemie sdk categories delete **Required on create:** `name` (1–255 chars) ---- - -## Analytics - -> See [examples/analytics.md](examples/analytics.md) for full examples and scripting patterns. - -Analytics endpoints generally require admin access. All commands support `--json` for raw output. - -### Filter parameters - -**`--time-period `** — preset time window. Mutually exclusive with `--start-date`/`--end-date`. Exact accepted values: -`last_hour` | `last_6_hours` | `last_24_hours` | `last_7_days` | `last_30_days` | `last_60_days` | `last_year` - -**`--start-date `** / **`--end-date `** — custom date range. Mutually exclusive with `--time-period`. Format: ISO 8601 UTC string `"YYYY-MM-DDTHH:MM:SSZ"`. Rules: -- `start_date` must be before `end_date`; `end_date` must not be in the future -- If neither time filter is provided, the backend defaults to **last 30 days** - -**`--users `** — comma-separated user IDs. Get valid IDs via `codemie sdk analytics users --json`. Non-admin users can only filter by themselves. - -**`--projects `** — comma-separated project names (not UUIDs). Projects the caller cannot access are silently ignored. - -**`--page `** / **`--per-page `** — zero-indexed page (default `0`), items per page `1–1000` (default `20`). Tabular endpoints only. - -### Commands - -```bash -# Summaries (SummariesResponse: data.metrics[]) -codemie sdk analytics summaries [filters] [--json] -codemie sdk analytics cli-summary [filters] [--json] - -# Users list (UsersListResponse: data.users[], data.total_count) -codemie sdk analytics users [filters] [--json] - -# Tabular endpoints (TabularResponse: data.columns[], data.rows[], pagination) -codemie sdk analytics assistants-chats [filters] [--page ] [--per-page ] [--json] -codemie sdk analytics workflows [filters] [--page ] [--per-page ] [--json] -codemie sdk analytics tools-usage [filters] [--page ] [--per-page ] [--json] -codemie sdk analytics webhooks-invocation [filters] [--page ] [--per-page ] [--json] -codemie sdk analytics mcp-servers [filters] [--page ] [--per-page ] [--json] -codemie sdk analytics mcp-servers-by-users [filters] [--page ] [--per-page ] [--json] -codemie sdk analytics projects-spending [filters] [--page ] [--per-page ] [--json] -codemie sdk analytics llms-usage [filters] [--page ] [--per-page ] [--json] -codemie sdk analytics users-spending [filters] [--page ] [--per-page ] [--json] -codemie sdk analytics budget-soft-limit [filters] [--page ] [--per-page ] [--json] -codemie sdk analytics budget-hard-limit [filters] [--page ] [--per-page ] [--json] -codemie sdk analytics users-activity [filters] [--page ] [--per-page ] [--json] -codemie sdk analytics projects-activity [filters] [--page ] [--per-page ] [--json] -codemie sdk analytics agents-usage [filters] [--page ] [--per-page ] [--json] -codemie sdk analytics cli-agents [filters] [--page ] [--per-page ] [--json] -codemie sdk analytics cli-llms [filters] [--page ] [--per-page ] [--json] -codemie sdk analytics cli-users [filters] [--page ] [--per-page ] [--json] -codemie sdk analytics cli-errors [filters] [--page ] [--per-page ] [--json] -codemie sdk analytics cli-repositories [filters] [--page ] [--per-page ] [--json] -``` diff --git a/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/analytics.md b/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/analytics.md deleted file mode 100644 index 139ec553..00000000 --- a/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/analytics.md +++ /dev/null @@ -1,244 +0,0 @@ -# Analytics Examples - ---- - -## Filter Parameters Reference - -All analytics commands accept the same set of optional filter flags. - -### Time range (mutually exclusive — use one or the other, never both) - -**Option A — preset period:** - -| Value | Covers | -|-------|--------| -| `last_hour` | Elapsed 60 minutes from now | -| `last_6_hours` | Elapsed 6 hours from now | -| `last_24_hours` | Elapsed 24 hours from now | -| `last_7_days` | Midnight UTC (now − 7 days) → now | -| `last_30_days` | Midnight UTC (now − 30 days) → now *(backend default)* | -| `last_60_days` | Midnight UTC (now − 60 days) → now | -| `last_year` | Midnight UTC (now − 365 days) → now | - -```bash ---time-period last_30_days -``` - -**Option B — custom date range:** - -- Format: ISO 8601 UTC datetime string — `"YYYY-MM-DDTHH:MM:SSZ"` -- `--start-date` must be before `--end-date` -- `--end-date` must not be in the future -- If only `--start-date` is given, end defaults to now -- If only `--end-date` is given, start defaults to `end − 30 days` - -```bash ---start-date 2024-01-01T00:00:00Z --end-date 2024-12-31T23:59:59Z -``` - -### Entity filters - -| Flag | Format | Example | -|------|--------|---------| -| `--users` | Comma-separated **user IDs** (from `codemie sdk analytics users --json` → `.data.users[].id`). Non-admin users can only filter by themselves. **Not emails.** | `--users abc123` or `--users abc123,def456` | -| `--projects` | Comma-separated project names (not UUIDs). Whitespace around each value is stripped. Projects you cannot access are silently ignored. | `--projects my-project` or `--projects my-project,other-project` | - -> **Get valid user IDs before filtering:** -> ```bash -> codemie sdk analytics users --json | jq '.data.users[] | {id, name}' -> ``` - -### Pagination (tabular endpoints only) - -| Flag | Default | Range | Notes | -|------|---------|-------|-------| -| `--page` | `0` | `≥ 0` | Zero-indexed | -| `--per-page` | `20` | `1–1000` | | - ---- - -## Summaries - -Returns aggregated metric cards (token counts, active users, request counts, etc.). - -```bash -codemie sdk analytics summaries -codemie sdk analytics summaries --json -codemie sdk analytics summaries --time-period last_30_days -codemie sdk analytics summaries --start-date 2024-01-01T00:00:00Z --end-date 2024-12-31T23:59:59Z --json -codemie sdk analytics summaries --projects my-project --time-period last_7_days --json -``` - -**JSON fields:** `data.metrics[]` → `id`, `label`, `type`, `value`, `format`, `description` - ---- - -## CLI Summary - -Returns CLI-specific summary metrics (CLI sessions, agents used, tokens consumed via CLI). - -```bash -codemie sdk analytics cli-summary -codemie sdk analytics cli-summary --time-period last_7_days --json -codemie sdk analytics cli-summary --users --json -``` - -**JSON fields:** same as summaries — `data.metrics[]` - ---- - -## Users List - -Returns users for the last --time-period or --start-date and --end-date, visible to the caller. Non-admin users only see themselves. Use this to discover valid **user IDs** for the `--users` filter on other endpoints. - -```bash -codemie sdk analytics users -codemie sdk analytics users --json -``` - -**JSON fields:** `data.users[]` → `id`, `name`; `data.total_count` - ---- - -## Tabular Endpoints - -All tabular endpoints return the same structure: - -**JSON fields:** `data.columns[]` → `id`, `label`, `type`; `data.rows[]`; `data.totals`; `pagination` → `page`, `per_page`, `total_count`, `has_more` - -### Assistants chats - -```bash -codemie sdk analytics assistants-chats -codemie sdk analytics assistants-chats --page 0 --per-page 50 --json -codemie sdk analytics assistants-chats --projects my-project --time-period last_30_days --json -codemie sdk analytics assistants-chats --start-date 2024-06-01T00:00:00Z --end-date 2024-06-30T23:59:59Z --json -``` - -### Workflows - -```bash -codemie sdk analytics workflows -codemie sdk analytics workflows --projects my-project --json -codemie sdk analytics workflows --time-period last_7_days --json -``` - -### Tools usage - -```bash -codemie sdk analytics tools-usage -codemie sdk analytics tools-usage --time-period last_30_days --json -codemie sdk analytics tools-usage --projects my-project --users --json -``` - -### Webhooks invocation - -```bash -codemie sdk analytics webhooks-invocation -codemie sdk analytics webhooks-invocation --time-period last_7_days --json -``` - -### MCP servers - -```bash -codemie sdk analytics mcp-servers -codemie sdk analytics mcp-servers --time-period last_30_days --json - -codemie sdk analytics mcp-servers-by-users -codemie sdk analytics mcp-servers-by-users --users --json -``` - -### Spending - -```bash -# LLM spend by project -codemie sdk analytics projects-spending -codemie sdk analytics projects-spending --projects my-project --time-period last_30_days --json - -# Token usage and cost by LLM model -codemie sdk analytics llms-usage -codemie sdk analytics llms-usage --time-period last_year --json - -# Token spend per user -codemie sdk analytics users-spending -codemie sdk analytics users-spending --users alice@acme.com,bob@acme.com --json - -# Users at or near soft budget limit -codemie sdk analytics budget-soft-limit -codemie sdk analytics budget-soft-limit --projects my-project --json - -# Users at or over hard budget limit -codemie sdk analytics budget-hard-limit -codemie sdk analytics budget-hard-limit --json -``` - -### Activity - -```bash -# Activity timeline per user (sessions, messages, active days) -codemie sdk analytics users-activity -codemie sdk analytics users-activity --users alice@acme.com --time-period last_30_days --json - -# Activity timeline per project -codemie sdk analytics projects-activity -codemie sdk analytics projects-activity --projects my-project --time-period last_7_days --json -``` - -### Agents usage - -```bash -# Usage by AI agent (Claude, Gemini, OpenCode, etc.) — all channels -codemie sdk analytics agents-usage -codemie sdk analytics agents-usage --time-period last_30_days --json - -# CLI-only agent usage -codemie sdk analytics cli-agents -codemie sdk analytics cli-agents --users alice@acme.com --json -``` - -### CLI analytics - -```bash -# LLM model usage via CLI -codemie sdk analytics cli-llms -codemie sdk analytics cli-llms --time-period last_7_days --json - -# CLI usage broken down by user -codemie sdk analytics cli-users -codemie sdk analytics cli-users --projects my-project --json - -# CLI error events -codemie sdk analytics cli-errors -codemie sdk analytics cli-errors --time-period last_24_hours --json - -# CLI usage by git repository -codemie sdk analytics cli-repositories -codemie sdk analytics cli-repositories --projects my-project --json -``` - ---- - -## Scripting - -```bash -# Get total assistant chat count -codemie sdk analytics assistants-chats --json | jq '.pagination.total_count' - -# Get all metric IDs and values from summaries -codemie sdk analytics summaries --json | jq '.data.metrics[] | {id, value}' - -# Get project spending rows for a specific project -codemie sdk analytics projects-spending --projects my-project --json | jq '.data.rows[]' - -# Get user activity for last 7 days -codemie sdk analytics users-activity --time-period last_7_days --json | jq '.data.rows[]' - -# Count CLI errors in last 24 hours -codemie sdk analytics cli-errors --time-period last_24_hours --json | jq '.pagination.total_count' - -# Get column names for any tabular endpoint -codemie sdk analytics llms-usage --json | jq '[.data.columns[] | .label]' - -# Discover valid user values for --users filter -codemie sdk analytics users --json | jq '.data.users[] | .id' -``` diff --git a/src/cli/commands/sdk/analytics.ts b/src/cli/commands/sdk/analytics.ts deleted file mode 100644 index e7ecc1b4..00000000 --- a/src/cli/commands/sdk/analytics.ts +++ /dev/null @@ -1,672 +0,0 @@ -import { Command } from "commander"; -import chalk from "chalk"; -import ora from "ora"; -import type { - SummariesResponse, - TabularResponse, - UsersListResponse, -} from "codemie-sdk"; -import type { - AnalyticsQueryParams, - PaginatedAnalyticsQueryParams, -} from "codemie-sdk"; -import { - getAnalyticsSummaries, - getAnalyticsCliSummary, - getAnalyticsUsers, - getAnalyticsAssistantsChats, - getAnalyticsWorkflows, - getAnalyticsToolsUsage, - getAnalyticsWebhooksInvocation, - getAnalyticsMcpServers, - getAnalyticsMcpServersByUsers, - getAnalyticsProjectsSpending, - getAnalyticsLlmsUsage, - getAnalyticsUsersSpending, - getAnalyticsBudgetSoftLimit, - getAnalyticsBudgetHardLimit, - getAnalyticsUsersActivity, - getAnalyticsProjectsActivity, - getAnalyticsAgentsUsage, - getAnalyticsCliAgents, - getAnalyticsCliLlms, - getAnalyticsCliUsers, - getAnalyticsCliErrors, - getAnalyticsCliRepositories, -} from "./services/analytics.js"; -import { getSdkClient, outputJson, handleSdkError } from "./utils/cli-utils.js"; -import { - printTable, - printListHeader, - printEmpty, - optional, - type TableColumn, -} from "./utils/render.js"; - -/** - * Build base analytics query params from command options - */ -function buildBaseParams(opts: { - timePeriod?: string; - startDate?: string; - endDate?: string; - users?: string; - projects?: string; -}): AnalyticsQueryParams { - return { - ...(opts.timePeriod !== undefined && { time_period: opts.timePeriod }), - ...(opts.startDate !== undefined && { start_date: opts.startDate }), - ...(opts.endDate !== undefined && { end_date: opts.endDate }), - ...(opts.users !== undefined && { users: opts.users }), - ...(opts.projects !== undefined && { projects: opts.projects }), - }; -} - -/** - * Build paginated analytics query params from command options - */ -function buildPaginatedParams(opts: { - timePeriod?: string; - startDate?: string; - endDate?: string; - users?: string; - projects?: string; - page?: number; - perPage?: number; -}): PaginatedAnalyticsQueryParams { - return { - ...buildBaseParams(opts), - ...(opts.page !== undefined && { page: opts.page }), - ...(opts.perPage !== undefined && { per_page: opts.perPage }), - }; -} - -/** - * Add common date filter options to a command - */ -function addBaseFilterOptions(cmd: Command): Command { - return cmd - .option( - "--time-period ", - "Time period filter (e.g. last_7_days, last_30_days)", - ) - .option("--start-date ", "Start date (ISO 8601, e.g. 2024-01-01)") - .option("--end-date ", "End date (ISO 8601, e.g. 2024-12-31)") - .option("--users ", "Filter by user(s)") - .option("--projects ", "Filter by projects (comma-separated)"); -} - -/** - * Add pagination options in addition to base filters - */ -function addPaginatedFilterOptions(cmd: Command): Command { - return addBaseFilterOptions(cmd) - .option("--page ", "Page number (0-indexed)", (v) => parseInt(v, 10)) - .option("--per-page ", "Items per page", (v) => parseInt(v, 10)); -} - -/** - * Print a SummariesResponse as a table or JSON - */ -function printSummaries(response: SummariesResponse, json: boolean): void { - if (json) { - outputJson(response); - return; - } - - const metrics = response.data?.metrics ?? []; - if (metrics.length === 0) { - printEmpty("metrics"); - return; - } - - const columns: TableColumn<(typeof metrics)[0]>[] = [ - { header: "ID", width: 30, getValue: (m) => chalk.cyan(m.id) }, - { header: "Label", width: 30, getValue: (m) => optional(m.label) }, - { header: "Type", width: 14, getValue: (m) => optional(m.type) }, - { - header: "Value", - width: 24, - getValue: (m) => String(m.value ?? chalk.dim("—")), - }, - ]; - - printListHeader("Metrics", metrics.length); - printTable(metrics, columns); -} - -/** - * Print a TabularResponse as a table or JSON - */ -function printTabular(response: TabularResponse, json: boolean): void { - if (json) { - outputJson(response); - return; - } - - const rows = response.data?.rows ?? []; - const columns = response.data?.columns ?? []; - - if (rows.length === 0) { - printEmpty("records"); - return; - } - - const tableColumns: TableColumn>[] = columns.map( - (col) => ({ - header: col.label, - width: Math.max(col.label.length + 4, 18), - getValue: (row) => { - const val = row[col.id]; - return val != null ? String(val) : chalk.dim("—"); - }, - }), - ); - - printListHeader("Records", rows.length); - printTable(rows, tableColumns); - - const pagination = response.pagination; - if (pagination) { - console.log( - chalk.dim( - `\n Page ${pagination.page + 1} · ${rows.length} of ${pagination.total_count} total` + - (pagination.has_more ? " · more available" : ""), - ), - ); - } -} - -/** - * Print a UsersListResponse as a table or JSON - */ -function printUsersList(response: UsersListResponse, json: boolean): void { - if (json) { - outputJson(response); - return; - } - - const users = response.data?.users ?? []; - if (users.length === 0) { - printEmpty("users"); - return; - } - - const columns: TableColumn<(typeof users)[0]>[] = [ - { header: "ID", width: 40, getValue: (u) => chalk.cyan(u.id) }, - { header: "Name", width: 40, getValue: (u) => optional(u.name) }, - ]; - - printListHeader("Users", users.length); - printTable(users, columns); - console.log(chalk.dim(`\n Total: ${response.data.total_count}`)); -} - -export function createAnalyticsSubcommand(): Command { - const cmd = new Command("analytics").description( - "Access CodeMie platform analytics and usage data", - ); - - addBaseFilterOptions( - cmd - .command("summaries") - .description( - "Get platform usage summaries\n" + - "Examples:\n" + - " $ codemie sdk analytics summaries\n" + - " $ codemie sdk analytics summaries --time-period last_30_days --json", - ) - .option("--json", "Output in JSON format"), - ).action(async (opts) => { - const client = await getSdkClient(); - const spinner = ora("Fetching summaries...").start(); - try { - const result = await getAnalyticsSummaries(client, buildBaseParams(opts)); - spinner.stop(); - printSummaries(result, opts.json); - } catch (error) { - spinner.stop(); - handleSdkError(error, "get summaries"); - } - }); - - addBaseFilterOptions( - cmd - .command("cli-summary") - .description("Get CLI usage summary") - .option("--json", "Output in JSON format"), - ).action(async (opts) => { - const client = await getSdkClient(); - const spinner = ora("Fetching CLI summary...").start(); - try { - const result = await getAnalyticsCliSummary( - client, - buildBaseParams(opts), - ); - spinner.stop(); - printSummaries(result, opts.json); - } catch (error) { - spinner.stop(); - handleSdkError(error, "get CLI summary"); - } - }); - - addBaseFilterOptions( - cmd - .command("users") - .description("List users") - .option("--json", "Output in JSON format"), - ).action(async (opts) => { - const client = await getSdkClient(); - const spinner = ora("Fetching users...").start(); - try { - const result = await getAnalyticsUsers(client, buildBaseParams(opts)); - spinner.stop(); - printUsersList(result, opts.json); - } catch (error) { - spinner.stop(); - handleSdkError(error, "get analytics users"); - } - }); - - addPaginatedFilterOptions( - cmd - .command("assistants-chats") - .description("Get assistant chat analytics") - .option("--json", "Output in JSON format"), - ).action(async (opts) => { - const client = await getSdkClient(); - const spinner = ora("Fetching assistant chats analytics...").start(); - try { - const result = await getAnalyticsAssistantsChats( - client, - buildPaginatedParams(opts), - ); - spinner.stop(); - printTabular(result, opts.json); - } catch (error) { - spinner.stop(); - handleSdkError(error, "get assistants chats analytics"); - } - }); - - addPaginatedFilterOptions( - cmd - .command("workflows") - .description("Get workflow analytics") - .option("--json", "Output in JSON format"), - ).action(async (opts) => { - const client = await getSdkClient(); - const spinner = ora("Fetching workflows analytics...").start(); - try { - const result = await getAnalyticsWorkflows( - client, - buildPaginatedParams(opts), - ); - spinner.stop(); - printTabular(result, opts.json); - } catch (error) { - spinner.stop(); - handleSdkError(error, "get workflows analytics"); - } - }); - - addPaginatedFilterOptions( - cmd - .command("tools-usage") - .description("Get tools usage analytics") - .option("--json", "Output in JSON format"), - ).action(async (opts) => { - const client = await getSdkClient(); - const spinner = ora("Fetching tools usage analytics...").start(); - try { - const result = await getAnalyticsToolsUsage( - client, - buildPaginatedParams(opts), - ); - spinner.stop(); - printTabular(result, opts.json); - } catch (error) { - spinner.stop(); - handleSdkError(error, "get tools usage analytics"); - } - }); - - addPaginatedFilterOptions( - cmd - .command("webhooks-invocation") - .description("Get webhooks invocation analytics") - .option("--json", "Output in JSON format"), - ).action(async (opts) => { - const client = await getSdkClient(); - const spinner = ora("Fetching webhooks invocation analytics...").start(); - try { - const result = await getAnalyticsWebhooksInvocation( - client, - buildPaginatedParams(opts), - ); - spinner.stop(); - printTabular(result, opts.json); - } catch (error) { - spinner.stop(); - handleSdkError(error, "get webhooks invocation analytics"); - } - }); - - addPaginatedFilterOptions( - cmd - .command("mcp-servers") - .description("Get MCP servers analytics") - .option("--json", "Output in JSON format"), - ).action(async (opts) => { - const client = await getSdkClient(); - const spinner = ora("Fetching MCP servers analytics...").start(); - try { - const result = await getAnalyticsMcpServers( - client, - buildPaginatedParams(opts), - ); - spinner.stop(); - printTabular(result, opts.json); - } catch (error) { - spinner.stop(); - handleSdkError(error, "get MCP servers analytics"); - } - }); - - addPaginatedFilterOptions( - cmd - .command("mcp-servers-by-users") - .description("Get MCP servers by users analytics") - .option("--json", "Output in JSON format"), - ).action(async (opts) => { - const client = await getSdkClient(); - const spinner = ora("Fetching MCP servers by users analytics...").start(); - try { - const result = await getAnalyticsMcpServersByUsers( - client, - buildPaginatedParams(opts), - ); - spinner.stop(); - printTabular(result, opts.json); - } catch (error) { - spinner.stop(); - handleSdkError(error, "get MCP servers by users analytics"); - } - }); - - addPaginatedFilterOptions( - cmd - .command("projects-spending") - .description("Get projects spending analytics") - .option("--json", "Output in JSON format"), - ).action(async (opts) => { - const client = await getSdkClient(); - const spinner = ora("Fetching projects spending analytics...").start(); - try { - const result = await getAnalyticsProjectsSpending( - client, - buildPaginatedParams(opts), - ); - spinner.stop(); - printTabular(result, opts.json); - } catch (error) { - spinner.stop(); - handleSdkError(error, "get projects spending analytics"); - } - }); - - addPaginatedFilterOptions( - cmd - .command("llms-usage") - .description("Get LLMs usage analytics") - .option("--json", "Output in JSON format"), - ).action(async (opts) => { - const client = await getSdkClient(); - const spinner = ora("Fetching LLMs usage analytics...").start(); - try { - const result = await getAnalyticsLlmsUsage( - client, - buildPaginatedParams(opts), - ); - spinner.stop(); - printTabular(result, opts.json); - } catch (error) { - spinner.stop(); - handleSdkError(error, "get LLMs usage analytics"); - } - }); - - addPaginatedFilterOptions( - cmd - .command("users-spending") - .description("Get users spending analytics") - .option("--json", "Output in JSON format"), - ).action(async (opts) => { - const client = await getSdkClient(); - const spinner = ora("Fetching users spending analytics...").start(); - try { - const result = await getAnalyticsUsersSpending( - client, - buildPaginatedParams(opts), - ); - spinner.stop(); - printTabular(result, opts.json); - } catch (error) { - spinner.stop(); - handleSdkError(error, "get users spending analytics"); - } - }); - - addPaginatedFilterOptions( - cmd - .command("budget-soft-limit") - .description("Get budget soft limit analytics") - .option("--json", "Output in JSON format"), - ).action(async (opts) => { - const client = await getSdkClient(); - const spinner = ora("Fetching budget soft limit analytics...").start(); - try { - const result = await getAnalyticsBudgetSoftLimit( - client, - buildPaginatedParams(opts), - ); - spinner.stop(); - printTabular(result, opts.json); - } catch (error) { - spinner.stop(); - handleSdkError(error, "get budget soft limit analytics"); - } - }); - - addPaginatedFilterOptions( - cmd - .command("budget-hard-limit") - .description("Get budget hard limit analytics") - .option("--json", "Output in JSON format"), - ).action(async (opts) => { - const client = await getSdkClient(); - const spinner = ora("Fetching budget hard limit analytics...").start(); - try { - const result = await getAnalyticsBudgetHardLimit( - client, - buildPaginatedParams(opts), - ); - spinner.stop(); - printTabular(result, opts.json); - } catch (error) { - spinner.stop(); - handleSdkError(error, "get budget hard limit analytics"); - } - }); - - addPaginatedFilterOptions( - cmd - .command("users-activity") - .description("Get users activity analytics") - .option("--json", "Output in JSON format"), - ).action(async (opts) => { - const client = await getSdkClient(); - const spinner = ora("Fetching users activity analytics...").start(); - try { - const result = await getAnalyticsUsersActivity( - client, - buildPaginatedParams(opts), - ); - spinner.stop(); - printTabular(result, opts.json); - } catch (error) { - spinner.stop(); - handleSdkError(error, "get users activity analytics"); - } - }); - - addPaginatedFilterOptions( - cmd - .command("projects-activity") - .description("Get projects activity analytics") - .option("--json", "Output in JSON format"), - ).action(async (opts) => { - const client = await getSdkClient(); - const spinner = ora("Fetching projects activity analytics...").start(); - try { - const result = await getAnalyticsProjectsActivity( - client, - buildPaginatedParams(opts), - ); - spinner.stop(); - printTabular(result, opts.json); - } catch (error) { - spinner.stop(); - handleSdkError(error, "get projects activity analytics"); - } - }); - - addPaginatedFilterOptions( - cmd - .command("agents-usage") - .description("Get agents usage analytics") - .option("--json", "Output in JSON format"), - ).action(async (opts) => { - const client = await getSdkClient(); - const spinner = ora("Fetching agents usage analytics...").start(); - try { - const result = await getAnalyticsAgentsUsage( - client, - buildPaginatedParams(opts), - ); - spinner.stop(); - printTabular(result, opts.json); - } catch (error) { - spinner.stop(); - handleSdkError(error, "get agents usage analytics"); - } - }); - - addPaginatedFilterOptions( - cmd - .command("cli-agents") - .description("Get CLI agents analytics") - .option("--json", "Output in JSON format"), - ).action(async (opts) => { - const client = await getSdkClient(); - const spinner = ora("Fetching CLI agents analytics...").start(); - try { - const result = await getAnalyticsCliAgents( - client, - buildPaginatedParams(opts), - ); - spinner.stop(); - printTabular(result, opts.json); - } catch (error) { - spinner.stop(); - handleSdkError(error, "get CLI agents analytics"); - } - }); - - addPaginatedFilterOptions( - cmd - .command("cli-llms") - .description("Get CLI LLMs analytics") - .option("--json", "Output in JSON format"), - ).action(async (opts) => { - const client = await getSdkClient(); - const spinner = ora("Fetching CLI LLMs analytics...").start(); - try { - const result = await getAnalyticsCliLlms( - client, - buildPaginatedParams(opts), - ); - spinner.stop(); - printTabular(result, opts.json); - } catch (error) { - spinner.stop(); - handleSdkError(error, "get CLI LLMs analytics"); - } - }); - - addPaginatedFilterOptions( - cmd - .command("cli-users") - .description("Get CLI users analytics") - .option("--json", "Output in JSON format"), - ).action(async (opts) => { - const client = await getSdkClient(); - const spinner = ora("Fetching CLI users analytics...").start(); - try { - const result = await getAnalyticsCliUsers( - client, - buildPaginatedParams(opts), - ); - spinner.stop(); - printTabular(result, opts.json); - } catch (error) { - spinner.stop(); - handleSdkError(error, "get CLI users analytics"); - } - }); - - addPaginatedFilterOptions( - cmd - .command("cli-errors") - .description("Get CLI errors analytics") - .option("--json", "Output in JSON format"), - ).action(async (opts) => { - const client = await getSdkClient(); - const spinner = ora("Fetching CLI errors analytics...").start(); - try { - const result = await getAnalyticsCliErrors( - client, - buildPaginatedParams(opts), - ); - spinner.stop(); - printTabular(result, opts.json); - } catch (error) { - spinner.stop(); - handleSdkError(error, "get CLI errors analytics"); - } - }); - - addPaginatedFilterOptions( - cmd - .command("cli-repositories") - .description("Get CLI repositories analytics") - .option("--json", "Output in JSON format"), - ).action(async (opts) => { - const client = await getSdkClient(); - const spinner = ora("Fetching CLI repositories analytics...").start(); - try { - const result = await getAnalyticsCliRepositories( - client, - buildPaginatedParams(opts), - ); - spinner.stop(); - printTabular(result, opts.json); - } catch (error) { - spinner.stop(); - handleSdkError(error, "get CLI repositories analytics"); - } - }); - - return cmd; -} diff --git a/src/cli/commands/sdk/index.ts b/src/cli/commands/sdk/index.ts index b846b339..57ac0219 100644 --- a/src/cli/commands/sdk/index.ts +++ b/src/cli/commands/sdk/index.ts @@ -7,13 +7,12 @@ import { createLlmModelsSubcommand } from './llm.js'; import { createSkillsSubcommand } from './skills.js'; import { createUsersSubcommand } from './users.js'; import { createCategoriesSubcommand } from './categories.js'; -import { createAnalyticsSubcommand } from './analytics.js'; export function createSdkCommand(): Command { const cmd = new Command('sdk'); cmd.description( - 'Manage CodeMie platform assets (assistants, workflows, datasources, integrations, skills, users, categories, analytics) via the SDK' + 'Manage CodeMie platform assets (assistants, workflows, datasources, integrations, skills, users, categories) via the SDK' ); cmd.addCommand(createAssistantsSubcommand()); @@ -24,7 +23,6 @@ export function createSdkCommand(): Command { cmd.addCommand(createSkillsSubcommand()); cmd.addCommand(createUsersSubcommand()); cmd.addCommand(createCategoriesSubcommand()); - cmd.addCommand(createAnalyticsSubcommand()); return cmd; } diff --git a/src/cli/commands/sdk/services/analytics.ts b/src/cli/commands/sdk/services/analytics.ts deleted file mode 100644 index 42413214..00000000 --- a/src/cli/commands/sdk/services/analytics.ts +++ /dev/null @@ -1,164 +0,0 @@ -import type { - CodeMieClient, - SummariesResponse, - TabularResponse, - UsersListResponse, -} from "codemie-sdk"; -import type { - AnalyticsQueryParams, - PaginatedAnalyticsQueryParams, -} from "codemie-sdk"; - -export async function getAnalyticsSummaries( - client: CodeMieClient, - params: AnalyticsQueryParams = {}, -): Promise { - return client.analytics.getSummaries(params); -} - -export async function getAnalyticsCliSummary( - client: CodeMieClient, - params: AnalyticsQueryParams = {}, -): Promise { - return client.analytics.getCliSummary(params); -} - -export async function getAnalyticsUsers( - client: CodeMieClient, - params: AnalyticsQueryParams = {}, -): Promise { - return client.analytics.getUsers(params); -} - -export async function getAnalyticsAssistantsChats( - client: CodeMieClient, - params: PaginatedAnalyticsQueryParams = {}, -): Promise { - return client.analytics.getAssistantsChats(params); -} - -export async function getAnalyticsWorkflows( - client: CodeMieClient, - params: PaginatedAnalyticsQueryParams = {}, -): Promise { - return client.analytics.getWorkflows(params); -} - -export async function getAnalyticsToolsUsage( - client: CodeMieClient, - params: PaginatedAnalyticsQueryParams = {}, -): Promise { - return client.analytics.getToolsUsage(params); -} - -export async function getAnalyticsWebhooksInvocation( - client: CodeMieClient, - params: PaginatedAnalyticsQueryParams = {}, -): Promise { - return client.analytics.getWebhooksInvocation(params); -} - -export async function getAnalyticsMcpServers( - client: CodeMieClient, - params: PaginatedAnalyticsQueryParams = {}, -): Promise { - return client.analytics.getMcpServers(params); -} - -export async function getAnalyticsMcpServersByUsers( - client: CodeMieClient, - params: PaginatedAnalyticsQueryParams = {}, -): Promise { - return client.analytics.getMcpServersByUsers(params); -} - -export async function getAnalyticsProjectsSpending( - client: CodeMieClient, - params: PaginatedAnalyticsQueryParams = {}, -): Promise { - return client.analytics.getProjectsSpending(params); -} - -export async function getAnalyticsLlmsUsage( - client: CodeMieClient, - params: PaginatedAnalyticsQueryParams = {}, -): Promise { - return client.analytics.getLlmsUsage(params); -} - -export async function getAnalyticsUsersSpending( - client: CodeMieClient, - params: PaginatedAnalyticsQueryParams = {}, -): Promise { - return client.analytics.getUsersSpending(params); -} - -export async function getAnalyticsBudgetSoftLimit( - client: CodeMieClient, - params: PaginatedAnalyticsQueryParams = {}, -): Promise { - return client.analytics.getBudgetSoftLimit(params); -} - -export async function getAnalyticsBudgetHardLimit( - client: CodeMieClient, - params: PaginatedAnalyticsQueryParams = {}, -): Promise { - return client.analytics.getBudgetHardLimit(params); -} - -export async function getAnalyticsUsersActivity( - client: CodeMieClient, - params: PaginatedAnalyticsQueryParams = {}, -): Promise { - return client.analytics.getUsersActivity(params); -} - -export async function getAnalyticsProjectsActivity( - client: CodeMieClient, - params: PaginatedAnalyticsQueryParams = {}, -): Promise { - return client.analytics.getProjectsActivity(params); -} - -export async function getAnalyticsAgentsUsage( - client: CodeMieClient, - params: PaginatedAnalyticsQueryParams = {}, -): Promise { - return client.analytics.getAgentsUsage(params); -} - -export async function getAnalyticsCliAgents( - client: CodeMieClient, - params: PaginatedAnalyticsQueryParams = {}, -): Promise { - return client.analytics.getCliAgents(params); -} - -export async function getAnalyticsCliLlms( - client: CodeMieClient, - params: PaginatedAnalyticsQueryParams = {}, -): Promise { - return client.analytics.getCliLlms(params); -} - -export async function getAnalyticsCliUsers( - client: CodeMieClient, - params: PaginatedAnalyticsQueryParams = {}, -): Promise { - return client.analytics.getCliUsers(params); -} - -export async function getAnalyticsCliErrors( - client: CodeMieClient, - params: PaginatedAnalyticsQueryParams = {}, -): Promise { - return client.analytics.getCliErrors(params); -} - -export async function getAnalyticsCliRepositories( - client: CodeMieClient, - params: PaginatedAnalyticsQueryParams = {}, -): Promise { - return client.analytics.getCliRepositories(params); -} diff --git a/src/cli/commands/sdk/services/index.ts b/src/cli/commands/sdk/services/index.ts index 8cfa4d51..bd74dc89 100644 --- a/src/cli/commands/sdk/services/index.ts +++ b/src/cli/commands/sdk/services/index.ts @@ -3,4 +3,3 @@ export * from "./workflows.js"; export * from "./datasources.js"; export * from "./integrations.js"; export * from "./llm.js"; -export * from "./analytics.js"; From dc710403324fb335054a1681b10c855ed2a58633 Mon Sep 17 00:00:00 2001 From: Kostiantyn Pshenychnyi Date: Tue, 28 Apr 2026 16:06:08 +0300 Subject: [PATCH 09/13] fix(cli): apply code review fixes to sdk commands and analytics script - Use fileURLToPath for cross-platform path resolution in analytics-cli.js - Replace generic Error with ConfigurationError in cli-utils - Add logger.error + sanitizeLogArgs to handleSdkError for debug log visibility - Fix updateConfluenceDatasource data loss bug (add ?? fallbacks for optional fields) - Remove dead guard in createFileDatasource - Type code/google datasource functions with proper SDK types instead of any - Remove invalid id field from updateCodeDatasource/updateGoogleDatasource params - Add empty-array guard before llmModels[0] access in assistants service - Use import type for CodeMieClient/LLMModel in llm.ts - Use node: prefix for built-in imports and Promise.all in file-utils.ts Generated with AI Co-Authored-By: codemie-ai --- .../scripts/analytics-cli.js | 3 ++- src/cli/commands/sdk/services/assistants.ts | 7 ++++++ src/cli/commands/sdk/services/datasources.ts | 23 ++++++++----------- src/cli/commands/sdk/services/llm.ts | 2 +- src/cli/commands/sdk/utils/cli-utils.ts | 19 +++++++++------ src/cli/commands/sdk/utils/file-utils.ts | 23 ++++++++----------- 6 files changed, 42 insertions(+), 35 deletions(-) diff --git a/src/agents/plugins/claude/plugin/skills/codemie-analytics/scripts/analytics-cli.js b/src/agents/plugins/claude/plugin/skills/codemie-analytics/scripts/analytics-cli.js index 058a231a..11857733 100644 --- a/src/agents/plugins/claude/plugin/skills/codemie-analytics/scripts/analytics-cli.js +++ b/src/agents/plugins/claude/plugin/skills/codemie-analytics/scripts/analytics-cli.js @@ -74,6 +74,7 @@ import { createDecipheriv, createHash } from 'crypto'; import { readFileSync, existsSync } from 'fs'; import { homedir, hostname, platform, arch } from 'os'; import { join, resolve } from 'path'; +import { fileURLToPath } from 'url'; // ─── Argument Parsing ──────────────────────────────────────────────────────── @@ -718,7 +719,7 @@ const LITELLM_COMMANDS = ['litellm-customer', 'litellm-spend', 'litellm-keys']; async function main() { if (!command || command === 'help') { - const src = readFileSync(new URL(import.meta.url).pathname, 'utf8'); + const src = readFileSync(fileURLToPath(import.meta.url), 'utf8'); const docBlock = src.match(/\/\*\*([\s\S]*?)\*\//)?.[0] ?? ''; console.log(docBlock); process.exit(0); diff --git a/src/cli/commands/sdk/services/assistants.ts b/src/cli/commands/sdk/services/assistants.ts index 7f26e723..2f8ccd1f 100644 --- a/src/cli/commands/sdk/services/assistants.ts +++ b/src/cli/commands/sdk/services/assistants.ts @@ -7,6 +7,7 @@ import type { AssistantListParams, ToolKitDetails, } from "codemie-sdk"; +import { ConfigurationError } from "@/utils/errors.js"; import { listLlmModels } from "./llm.js"; type Writeable = { -readonly [P in keyof T]: T[P] }; @@ -36,6 +37,9 @@ export async function createAssistant( params: Partial, ): Promise<{ message: string; assistant_id?: string }> { const llmModels = await listLlmModels(client); + if (llmModels.length === 0) { + throw new ConfigurationError("No LLM models are available. Contact your administrator."); + } const defaultLlmModel = llmModels.find((m) => m.default)?.base_name ?? llmModels[0].base_name; @@ -61,6 +65,9 @@ export async function updateAssistant( client.assistants.get(assistantId), listLlmModels(client), ]); + if (llmModels.length === 0) { + throw new ConfigurationError("No LLM models are available. Contact your administrator."); + } const defaultLlmModel = llmModels.find((m) => m.default)?.base_name ?? llmModels[0].base_name; diff --git a/src/cli/commands/sdk/services/datasources.ts b/src/cli/commands/sdk/services/datasources.ts index 8ce7373e..61f8903e 100644 --- a/src/cli/commands/sdk/services/datasources.ts +++ b/src/cli/commands/sdk/services/datasources.ts @@ -4,6 +4,8 @@ import type { AzureDevOpsWikiDataSourceUpdateParams, AzureDevOpsWorkItemDataSourceCreateParams, AzureDevOpsWorkItemDataSourceUpdateParams, + CodeDataSourceCreateParams, + CodeDataSourceUpdateParams, ConfluenceDataSourceCreateParams, ConfluenceDataSourceUpdateParams, DataSource, @@ -11,6 +13,7 @@ import type { FileDataSourceCreateParams, FileDataSourceUpdateDto, GoogleDataSourceCreateParams, + GoogleDataSourceUpdateParams, JiraDataSourceCreateParams, JiraDataSourceUpdateParams, OtherDataSourceCreateParams, @@ -63,12 +66,12 @@ export async function updateConfluenceDatasource( const params: ConfluenceDataSourceUpdateParams = { type: "knowledge_base_confluence", - cql: data.cql, - description: data.description, + cql: data.cql ?? existing.confluence?.cql, + description: data.description ?? existing.description, name: existing.name, project_name: data.project_name ?? existing.project_name, - setting_id: data.setting_id, - shared_with_project: data.shared_with_project, + setting_id: data.setting_id ?? existing.setting_id, + shared_with_project: data.shared_with_project ?? existing.shared_with_project, }; return client.datasources.update(params); @@ -120,10 +123,6 @@ export async function createFileDatasource( data: FileDataSourceCreateParams, filePaths: string[], ): Promise { - if (!filePaths || !Array.isArray(filePaths)) { - throw new Error("files array is required for file datasources"); - } - const files = await readFilesFromPaths(filePaths); return client.datasources.create({ @@ -153,7 +152,7 @@ export async function updateFileDatasource( // CODE export async function createCodeDatasource( client: CodeMieClient, - data: any, + data: Omit, ): Promise { return client.datasources.create({ ...data, @@ -164,11 +163,10 @@ export async function createCodeDatasource( export async function updateCodeDatasource( client: CodeMieClient, id: string, - data: any, + data: Partial>, ): Promise { const existing = await client.datasources.get(id); return client.datasources.update({ - id, type: "code", name: existing.name, project_name: existing.project_name, @@ -190,11 +188,10 @@ export async function createGoogleDatasource( export async function updateGoogleDatasource( client: CodeMieClient, id: string, - data: any, + data: Partial>, ): Promise { const existing = await client.datasources.get(id); return client.datasources.update({ - id, type: "llm_routing_google", name: existing.name, project_name: existing.project_name, diff --git a/src/cli/commands/sdk/services/llm.ts b/src/cli/commands/sdk/services/llm.ts index 88438bc5..a020e35e 100644 --- a/src/cli/commands/sdk/services/llm.ts +++ b/src/cli/commands/sdk/services/llm.ts @@ -1,4 +1,4 @@ -import { CodeMieClient, LLMModel } from "codemie-sdk"; +import type { CodeMieClient, LLMModel } from "codemie-sdk"; export async function listLlmModels( client: CodeMieClient, diff --git a/src/cli/commands/sdk/utils/cli-utils.ts b/src/cli/commands/sdk/utils/cli-utils.ts index 554605f2..c7145704 100644 --- a/src/cli/commands/sdk/utils/cli-utils.ts +++ b/src/cli/commands/sdk/utils/cli-utils.ts @@ -4,6 +4,9 @@ import type { CodeMieClient } from "codemie-sdk"; import { ApiError } from "codemie-sdk"; import { ConfigLoader } from "@/utils/config.js"; import { getAuthenticatedClient } from "@/utils/auth.js"; +import { ConfigurationError } from "@/utils/errors.js"; +import { logger } from "@/utils/logger.js"; +import { sanitizeLogArgs } from "@/utils/security.js"; import z, { ZodError } from "zod"; /** @@ -21,7 +24,7 @@ export async function parseDataInput( dataFlag: string | undefined, ): Promise { if (!dataFlag) { - throw new Error('No data provided. Use --data \'{"key":"value"}\''); + throw new ConfigurationError('No data provided. Use --data \'{"key":"value"}\''); } return JSON.parse(dataFlag); @@ -34,7 +37,7 @@ export async function parseJsonFileInput( jsonFlag: string | undefined, ): Promise { if (!jsonFlag) { - throw new Error("No JSON file provided. Use --json path/to/file.json"); + throw new ConfigurationError("No JSON file provided. Use --json path/to/file.json"); } const content = await readFile(jsonFlag, "utf-8"); @@ -50,13 +53,13 @@ export async function parseDataOrJsonFile( jsonFlag: string | undefined, ): Promise { if (dataFlag && jsonFlag) { - throw new Error( + throw new ConfigurationError( "Cannot use both --data and --json. Use --data for inline JSON string or --json for JSON file path.", ); } if (!dataFlag && !jsonFlag) { - throw new Error( + throw new ConfigurationError( 'Either --data or --json is required. Use --data \'{"key":"value"}\' or --json path/to/file.json', ); } @@ -79,6 +82,9 @@ export function outputJson(data: unknown): void { * Handle API errors with user-friendly messages and exit */ export function handleSdkError(error: unknown, operation: string): never { + const msg = error instanceof Error ? error.message : String(error); + logger.error("SDK operation failed", ...sanitizeLogArgs({ operation, error: msg })); + if (error instanceof ApiError) { const status = (error as ApiError & { status?: number }).status; if (status === 401 || status === 403) { @@ -97,13 +103,12 @@ export function handleSdkError(error: unknown, operation: string): never { chalk.red(`❌ Not found: The requested resource does not exist.`), ); } else { - console.error(chalk.red(`❌ API error: ${error.message}`)); + console.error(chalk.red(`❌ API error: ${msg}`)); } } else if (error instanceof ZodError) { console.error(chalk.red(`❌ Operation failed:`)); console.error(chalk.red(z.prettifyError(error))); } else { - const msg = error instanceof Error ? error.message : String(error); console.error(chalk.red(`❌ ${msg}`)); } process.exit(1); @@ -126,7 +131,7 @@ export async function parseConfigInput( configFlag: string | undefined, ): Promise { if (!configFlag) { - throw new Error( + throw new ConfigurationError( "No config provided. Use --config path/to/file.yaml", ); } diff --git a/src/cli/commands/sdk/utils/file-utils.ts b/src/cli/commands/sdk/utils/file-utils.ts index 3923eb20..9c9b63a1 100644 --- a/src/cli/commands/sdk/utils/file-utils.ts +++ b/src/cli/commands/sdk/utils/file-utils.ts @@ -1,5 +1,5 @@ -import { readFile } from "fs/promises"; -import { basename } from "path"; +import { readFile } from "node:fs/promises"; +import { basename } from "node:path"; import { lookup } from "mime-types"; import type { File } from "codemie-sdk"; @@ -11,15 +11,12 @@ import type { File } from "codemie-sdk"; export async function readFilesFromPaths( filePaths: string[], ): Promise { - const files: File[] = []; - - for (const filePath of filePaths) { - const content = await readFile(filePath); - const name = basename(filePath); - const mime_type = lookup(filePath) || "application/octet-stream"; - - files.push({ name, content, mime_type }); - } - - return files; + return Promise.all( + filePaths.map(async (filePath) => { + const content = await readFile(filePath); + const name = basename(filePath); + const mime_type = lookup(filePath) || "application/octet-stream"; + return { name, content, mime_type }; + }), + ); } From e852f613fa78f84579819b291621c9e7fef49b88 Mon Sep 17 00:00:00 2001 From: Kostiantyn Pshenychnyi Date: Wed, 29 Apr 2026 10:15:15 +0300 Subject: [PATCH 10/13] feat: rename categories to assistant-categories --- .../claude/plugin/skills/codemie-sdk/SKILL.md | 24 +++++++------- .../skills/codemie-sdk/examples/assistants.md | 2 +- .../skills/codemie-sdk/examples/categories.md | 32 +++++++++---------- src/cli/commands/sdk/categories.ts | 22 ++++++------- src/cli/commands/sdk/index.ts | 2 +- 5 files changed, 41 insertions(+), 41 deletions(-) diff --git a/src/agents/plugins/claude/plugin/skills/codemie-sdk/SKILL.md b/src/agents/plugins/claude/plugin/skills/codemie-sdk/SKILL.md index 60e5d272..444650c3 100644 --- a/src/agents/plugins/claude/plugin/skills/codemie-sdk/SKILL.md +++ b/src/agents/plugins/claude/plugin/skills/codemie-sdk/SKILL.md @@ -1,13 +1,13 @@ --- name: codemie-sdk description: >- - Manage CodeMie platform assets (assistants, workflows, datasources, integrations, skills, users, categories) directly from CLI + Manage CodeMie platform assets (assistants, workflows, datasources, integrations, skills, users, assistant-categories) directly from CLI using CodeMie SDK. Use when user says "create assistant", "list workflows", "update datasource", "delete assistant", "show my assistants", "get workflow details", "manage integrations", "create integration", "list integrations", "list llm models", "list embedding models", "list skills", "get skill", "create skill", "update skill", "delete skill", "publish skill", - "import skill", "export skill", "attach skill", "list categories", "get category", - "create category", "delete category", "who am i", "current user", "my profile", "user info", + "import skill", "export skill", "attach skill", "list assistant categories", "get assistant category", + "create assistant category", "delete assistant category", "who am i", "current user", "my profile", "user info", or any request to manage CodeMie platform resources. NOTE: For analytics requests (usage analytics, summaries, spending, users activity, leaderboards, etc.) use the codemie-analytics skill instead. --- @@ -18,7 +18,7 @@ Manage CodeMie platform assets from the CLI. > **Analytics requests** (usage data, summaries, spending, activity, etc.) are handled by the **codemie-analytics** skill — use that instead. -**Asset Types:** `assistants`, `workflows`, `datasources`, `integrations`, `skills`, `users`, `categories` +**Asset Types:** `assistants`, `workflows`, `datasources`, `integrations`, `skills`, `users`, `assistant-categories` **Operations:** `list`, `get`, `create`, `update`, `delete` @@ -65,7 +65,7 @@ Only select a project automatically if the user has explicitly named it, or used ### Step 4 — Proceed Once the project is known, use it in all subsequent commands: -- Assistants, skills, categories: `"project": ""` +- Assistants, skills, assistant-categories: `"project": ""` - Workflows, datasources, integrations: `"project_name": ""` --- @@ -82,7 +82,7 @@ Once the project is known, use it in all subsequent commands: | Integrations | [examples/integrations.md](examples/integrations.md) | | Skills | [examples/skills.md](examples/skills.md) | | Users | [examples/users.md](examples/users.md) | -| Categories | [examples/categories.md](examples/categories.md) | +| Assistant Categories | [examples/categories.md](examples/categories.md) | Do **not** guess field names or skip this step — all required/optional fields, nested schemas, and asset cross-reference commands are documented there. @@ -222,18 +222,18 @@ codemie sdk users data [--json] --- -## Categories +## Assistant Categories > See [examples/categories.md](examples/categories.md) for full field reference and examples. **Note:** Categories can only be used for assistants (set via the `categories` field on create/update). ```bash -codemie sdk categories list [--paginated] [--page ] [--per-page ] [--json] -codemie sdk categories get [--json] -codemie sdk categories create --data '' | --json -codemie sdk categories update --data '' | --json -codemie sdk categories delete +codemie sdk assistant-categories list [--paginated] [--page ] [--per-page ] [--json] +codemie sdk assistant-categories get [--json] +codemie sdk assistant-categories create --data '' | --json +codemie sdk assistant-categories update --data '' | --json +codemie sdk assistant-categories delete ``` **Required on create:** `name` (1–255 chars) diff --git a/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/assistants.md b/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/assistants.md index 0ca86bc5..6160f364 100644 --- a/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/assistants.md +++ b/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/assistants.md @@ -65,7 +65,7 @@ codemie sdk assistants create --json assistant.json | `mcp_servers` | — | array | MCP server connections for additional tools — see schema below | | `assistant_ids` | — | string[] | Sub-assistant IDs for orchestration (multi-agent workflows) | | `prompt_variables` | — | array | Dynamic `{{variable}}` placeholders in the system prompt — see schema below | -| `categories` | — | string[] | Label IDs for marketplace classification (e.g. `["devops", "code-review"]`). You can manage assistants categories via codemie sdk categories command (get, create, update and so on). Only categories listed via this command can be used in this field | +| `categories` | — | string[] | Label IDs for marketplace classification (e.g. `["devops", "code-review"]`). You can manage assistants categories via codemie sdk assistant-categories command (get, create, update and so on). Only categories listed via this command can be used in this field | | `skill_ids` | — | string[] | Built-in platform skill IDs — **not** datasource IDs | | `skip_integration_validation` | — | boolean | Skip credential validation when attaching toolkits (useful with test credentials) | diff --git a/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/categories.md b/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/categories.md index 757c9c5c..ae98fe87 100644 --- a/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/categories.md +++ b/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/categories.md @@ -6,12 +6,12 @@ ```bash # List all categories (public, no admin required) -codemie sdk categories list -codemie sdk categories list --json +codemie sdk assistant-categories list +codemie sdk assistant-categories list --json # Paginated list with assistant counts (admin required) -codemie sdk categories list --paginated -codemie sdk categories list --paginated --page 0 --per-page 25 --json +codemie sdk assistant-categories list --paginated +codemie sdk assistant-categories list --paginated --page 0 --per-page 25 --json ``` **Non-paginated JSON fields:** `id`, `name`, `description` @@ -21,8 +21,8 @@ codemie sdk categories list --paginated --page 0 --per-page 25 --json ## Get ```bash -codemie sdk categories get -codemie sdk categories get --json +codemie sdk assistant-categories get +codemie sdk assistant-categories get --json ``` Admin access required. Returns `id`, `name`, `description`, `marketplaceAssistantCount`, `projectAssistantCount`, `createdAt`, `updatedAt`. @@ -31,13 +31,13 @@ Admin access required. Returns `id`, `name`, `description`, `marketplaceAssistan ```bash # Minimal (name only) -codemie sdk categories create --data '{"name":"DevOps"}' +codemie sdk assistant-categories create --data '{"name":"DevOps"}' # With description -codemie sdk categories create --data '{"name":"Code Review","description":"Skills for reviewing code quality and security"}' +codemie sdk assistant-categories create --data '{"name":"Code Review","description":"Skills for reviewing code quality and security"}' # From file -codemie sdk categories create --json category.json +codemie sdk assistant-categories create --json category.json ``` **Field reference:** @@ -52,8 +52,8 @@ Admin access required. ## Update ```bash -codemie sdk categories update --data '{"name":"Updated Name"}' -codemie sdk categories update --data '{"name":"DevOps","description":"Updated description"}' +codemie sdk assistant-categories update --data '{"name":"Updated Name"}' +codemie sdk assistant-categories update --data '{"name":"DevOps","description":"Updated description"}' ``` Admin access required. @@ -62,8 +62,8 @@ Admin access required. ```bash # Verify before deleting -codemie sdk categories get -codemie sdk categories delete +codemie sdk assistant-categories get +codemie sdk assistant-categories delete ``` Admin access required. Fails with **409** if any assistants are still assigned to this category — reassign or remove those assistants first. @@ -74,7 +74,7 @@ Categories are referenced by their `id` in the assistant `categories` field. ```bash # Get available category IDs -codemie sdk categories list --json | jq -r '.[] | "\(.id) \(.name)"' +codemie sdk assistant-categories list --json | jq -r '.[] | "\(.id) \(.name)"' # Create an assistant with categories codemie sdk assistants create --data '{ @@ -94,8 +94,8 @@ codemie sdk assistants update --data '{ ```bash # Find category ID by name -codemie sdk categories list --json | jq -r '.[] | select(.name == "DevOps") | .id' +codemie sdk assistant-categories list --json | jq -r '.[] | select(.name == "DevOps") | .id' # List all categories with assistant counts (admin) -codemie sdk categories list --paginated --json | jq -r '.categories[] | "\(.name): \(.marketplaceAssistantCount) marketplace, \(.projectAssistantCount) project"' +codemie sdk assistant-categories list --paginated --json | jq -r '.categories[] | "\(.name): \(.marketplaceAssistantCount) marketplace, \(.projectAssistantCount) project"' ``` diff --git a/src/cli/commands/sdk/categories.ts b/src/cli/commands/sdk/categories.ts index 1f9c8df9..23afe717 100644 --- a/src/cli/commands/sdk/categories.ts +++ b/src/cli/commands/sdk/categories.ts @@ -33,7 +33,7 @@ import { } from "./utils/render.js"; export function createCategoriesSubcommand(): Command { - const cmd = new Command("categories").description( + const cmd = new Command("assistant-categories").description( "Manage CodeMie assistant categories", ); @@ -44,9 +44,9 @@ export function createCategoriesSubcommand(): Command { "Without --paginated: returns all categories (public, no admin required).\n" + "With --paginated: returns paginated list with assistant counts (admin required).\n" + "Examples:\n" + - " $ codemie sdk categories list\n" + - " $ codemie sdk categories list --paginated --page 0 --per-page 25\n" + - " $ codemie sdk categories list --json", + " $ codemie sdk assistant-categories list\n" + + " $ codemie sdk assistant-categories list --paginated --page 0 --per-page 25\n" + + " $ codemie sdk assistant-categories list --json", ) .option("--json", "Output in JSON format") .option("--paginated", "Use paginated endpoint with assistant counts (admin required)") @@ -130,8 +130,8 @@ export function createCategoriesSubcommand(): Command { .description( "Get a specific category by ID (admin required)\n" + "Examples:\n" + - " $ codemie sdk categories get \n" + - " $ codemie sdk categories get --json", + " $ codemie sdk assistant-categories get \n" + + " $ codemie sdk assistant-categories get --json", ) .option("--json", "Output in JSON format") .action(async (id: string, opts) => { @@ -175,8 +175,8 @@ export function createCategoriesSubcommand(): Command { .description( "Create a new assistant category (admin required)\n" + "Examples:\n" + - ' $ codemie sdk categories create --data \'{"name":"DevOps","description":"DevOps tooling and automation"}\'\n' + - " $ codemie sdk categories create --json path/to/category.json", + ' $ codemie sdk assistant-categories create --data \'{"name":"DevOps","description":"DevOps tooling and automation"}\'\n' + + " $ codemie sdk assistant-categories create --json path/to/category.json", ) .option("--data ", "Category data as inline JSON string") .option("--json ", "Path to JSON file with category data") @@ -204,8 +204,8 @@ export function createCategoriesSubcommand(): Command { .description( "Update an existing assistant category (admin required)\n" + "Examples:\n" + - ' $ codemie sdk categories update --data \'{"name":"Updated Name"}\'\n' + - " $ codemie sdk categories update --json path/to/update.json", + ' $ codemie sdk assistant-categories update --data \'{"name":"Updated Name"}\'\n' + + " $ codemie sdk assistant-categories update --json path/to/update.json", ) .option("--data ", "Fields to update as inline JSON string") .option("--json ", "Path to JSON file with fields to update") @@ -235,7 +235,7 @@ export function createCategoriesSubcommand(): Command { "Delete an assistant category (admin required).\n" + "Fails with 409 if any assistants are assigned to it.\n" + "Examples:\n" + - " $ codemie sdk categories delete ", + " $ codemie sdk assistant-categories delete ", ) .action(async (id: string) => { const client = await getSdkClient(); diff --git a/src/cli/commands/sdk/index.ts b/src/cli/commands/sdk/index.ts index 57ac0219..eee4392a 100644 --- a/src/cli/commands/sdk/index.ts +++ b/src/cli/commands/sdk/index.ts @@ -12,7 +12,7 @@ export function createSdkCommand(): Command { const cmd = new Command('sdk'); cmd.description( - 'Manage CodeMie platform assets (assistants, workflows, datasources, integrations, skills, users, categories) via the SDK' + 'Manage CodeMie platform assets (assistants, workflows, datasources, integrations, skills, users, assistant-categories) via the SDK' ); cmd.addCommand(createAssistantsSubcommand()); From 73126193de8ed7630270c6503c6233953a5bebe1 Mon Sep 17 00:00:00 2001 From: Kostiantyn Pshenychnyi Date: Wed, 29 Apr 2026 15:34:28 +0300 Subject: [PATCH 11/13] fix: add plugin dir placeholders to analytics skill --- .../claude/plugin/skills/codemie-analytics/SKILL.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/agents/plugins/claude/plugin/skills/codemie-analytics/SKILL.md b/src/agents/plugins/claude/plugin/skills/codemie-analytics/SKILL.md index 1c487c3e..b6d6827d 100644 --- a/src/agents/plugins/claude/plugin/skills/codemie-analytics/SKILL.md +++ b/src/agents/plugins/claude/plugin/skills/codemie-analytics/SKILL.md @@ -109,7 +109,7 @@ authentication internally. If something is wrong with credentials, it prints a c actionable message to stderr; pass that along to the user verbatim. ```bash -node ~/.claude/skills/codemie-analytics/scripts/analytics-cli.js [options] +node ${CLAUDE_PLUGIN_ROOT}/skills/codemie-analytics/scripts/analytics-cli.js [options] ``` LiteLLM commands (`litellm-*`, `enrich-csv`) require `LITELLM_URL` + `LITELLM_KEY` env vars. @@ -144,7 +144,7 @@ LiteLLM commands (`litellm-*`, `enrich-csv`) require `LITELLM_URL` + `LITELLM_KE ### Example invocations ```bash -CLI=~/.claude/skills/codemie-analytics/scripts/analytics-cli.js +CLI=${CLAUDE_PLUGIN_ROOT}/skills/codemie-analytics/scripts/analytics-cli.js # Full leaderboard — top 50 pioneers sorted by score node $CLI leaderboard --tier pioneer --sort-by total_score --sort-order desc --per-page 50 --pretty @@ -403,7 +403,7 @@ platform users to EPAM people, or look up user assignments/org details. Before proceeding, verify the assistant is accessible: ```bash -node ~/.claude/skills/codemie-analytics/scripts/analytics-cli.js \ +node ${CLAUDE_PLUGIN_ROOT}/skills/codemie-analytics/scripts/analytics-cli.js \ custom /v1/assistants/5ca384d0-d042-480c-a0a9-d28150e2352f 2>&1 | head -5 ``` @@ -452,7 +452,7 @@ If the command returns an auth error, HTTP 401/403/404, or "No CodeMie credentia **Key commands:** ```bash -CLI=~/.claude/skills/codemie-analytics/scripts/analytics-cli.js +CLI=${CLAUDE_PLUGIN_ROOT}/skills/codemie-analytics/scripts/analytics-cli.js # Leaderboard (run in a loop for all pages) node $CLI leaderboard --per-page 500 --page --output json From 131a92a4ca11595f4de18a7d3d900d292aee4d76 Mon Sep 17 00:00:00 2001 From: Kostiantyn Pshenychnyi Date: Thu, 7 May 2026 14:05:14 +0300 Subject: [PATCH 12/13] perf: optimize analytics report generation --- .../plugin/skills/codemie-analytics/SKILL.md | 263 ++++++++++++++---- .../scripts/analytics-cli.js | 12 +- .../scripts/inspect-schema.js | 211 ++++++++++++++ .../skills/codemie-html-report/README.md | 39 +++ .../skills/codemie-html-report/SKILL.md | 143 ++++++++-- .../codemie-html-report/scripts/inject-css.js | 40 +++ .../scripts/inject-data.js | 68 +++++ .../style-guide/css/bundle.css | 1 + 8 files changed, 689 insertions(+), 88 deletions(-) create mode 100644 src/agents/plugins/claude/plugin/skills/codemie-analytics/scripts/inspect-schema.js create mode 100644 src/agents/plugins/claude/plugin/skills/codemie-html-report/README.md create mode 100644 src/agents/plugins/claude/plugin/skills/codemie-html-report/scripts/inject-css.js create mode 100644 src/agents/plugins/claude/plugin/skills/codemie-html-report/scripts/inject-data.js create mode 100644 src/agents/plugins/claude/plugin/skills/codemie-html-report/style-guide/css/bundle.css diff --git a/src/agents/plugins/claude/plugin/skills/codemie-analytics/SKILL.md b/src/agents/plugins/claude/plugin/skills/codemie-analytics/SKILL.md index b6d6827d..fa30096e 100644 --- a/src/agents/plugins/claude/plugin/skills/codemie-analytics/SKILL.md +++ b/src/agents/plugins/claude/plugin/skills/codemie-analytics/SKILL.md @@ -38,46 +38,46 @@ The leaderboard ranks users across **6 scoring dimensions**: **Tiers**: pioneer (80+), expert (65+), advanced (45+), practitioner (25+), newcomer (<25) -| Scenario | Command | Output | -|----------|---------|--------| -| Full leaderboard (paginated, filterable) | `leaderboard` | Ranked entries with score, tier, dimensions | -| Leaderboard KPI summary | `leaderboard-summary` | Total users, tier counts, top score | -| Single user champion profile | `leaderboard-user ` | Full dimension breakdown for one user | -| Tier distribution | `leaderboard-tiers` | Tier name, user count, % | -| Average dimension scores | `leaderboard-dimensions` | D1–D6 averages across all users | -| Top N performers | `leaderboard-top [limit]` | Top users by total score (default 10) | -| Score histogram | `leaderboard-scores` | Score distribution in 10-point bins | -| Framework metadata | `leaderboard-framework` | Dimension descriptions, tier defs, scoring rules | -| Computation snapshots | `leaderboard-snapshots` | List of snapshot runs | -| Available seasons | `leaderboard-seasons --view monthly\|quarterly` | Seasonal periods for selectors | +| Scenario | Command | What it retrieves | +|----------|---------|-------------------| +| Full leaderboard (paginated, filterable) | `leaderboard` | `data.rows[]` — rank, user_name, total_score, tier_name, score_delta, `dimensions[]` (id/score/weight per D1–D6), `summary_metrics{}` (cli_sessions, active_days, total_lines_added, total_spend, web_conversations, …) | +| Leaderboard KPI summary | `leaderboard-summary` | `data` — total_users, avg_score, top_score, `tier_counts{}` (pioneer/expert/advanced/practitioner/newcomer counts and percentages) | +| Single user champion profile | `leaderboard-user ` | `data` — same shape as a leaderboard row but for one user; includes full dimension breakdown and all summary_metrics | +| Tier distribution | `leaderboard-tiers` | `data.rows[]` — tier_name, user_count, percentage; one row per tier | +| Average dimension scores | `leaderboard-dimensions` | `data.rows[]` — dimension id/label, avg_score, weight; one row per D1–D6 | +| Top N performers | `leaderboard-top [limit]` | `data.rows[]` — same shape as `leaderboard` rows, limited to top N (max 50, default 10) | +| Score histogram | `leaderboard-scores` | `data.rows[]` — score_range (e.g. "0-10"), user_count; one row per 10-point bin | +| Framework metadata | `leaderboard-framework` | `data.framework{}` — title, principles, calculation_steps; `data.tiers[]` — name, label, min_score; `data.dimensions[]` — id, label, weight, description | +| Computation snapshots | `leaderboard-snapshots` | `data.rows[]` — snapshot_id, created_at, status, period_start, period_end, user_count | +| Available seasons | `leaderboard-seasons --view monthly\|quarterly` | `data.rows[]` — season_key (e.g. "2026-03"), label, start_date, end_date | Leaderboard filters: `--view` (current/monthly/quarterly), `--season-key` (2026-03, 2026-Q1), `--tier`, `--intent` (cli_focused/platform_focused/hybrid/sdlc_unicorn), `--search`, `--sort-by`, `--sort-order`. ### CLI Insights -| Scenario | Command | Output | -|----------|---------|--------| -| Full CLI overview (agents, repos, tools, errors) | `cli-insights` | Multi-section JSON | -| User classification & top spenders | `cli-insights-users` | Classification + spend tables | -| Detailed single-user CLI profile | `cli-insights-user ` | Key metrics, tools, models, repos, categories | -| Project classification & top by cost | `cli-insights-projects` | Project-level breakdown | -| Usage patterns (weekday, hourly, session depth) | `cli-insights-patterns` | Temporal pattern data | +| Scenario | Command | What it retrieves | +|----------|---------|-------------------| +| Full CLI overview (agents, repos, tools, errors) | `cli-insights` | Composite object with sub-keys: `summary` (total sessions, cost, tokens, repos, users), `agents[]` (agent name + session count), `top_users[]`, `top_repos[]`, `errors[]`, `llms[]` | +| User classification & top spenders | `cli-insights-users` | `data.rows[]` — user_name, classification (cli_focused/platform_focused/hybrid/sdlc_unicorn), total_cost, session_count, token_count | +| Detailed single-user CLI profile | `cli-insights-user ` | Composite: key_metrics (sessions, cost, tokens, repos, tools), tools[], models[], repositories[], workflow_intent, category_breakdown[] | +| Project classification & top by cost | `cli-insights-projects` | `data.rows[]` — project_name, classification, total_cost, session_count, user_count | +| Usage patterns (weekday, hourly, session depth) | `cli-insights-patterns` | Composite with sub-keys: `weekday.data.rows[]` (weekday_name, session_count), `hourly.data.rows[]` (hour_utc, session_count), `session_depth.data.rows[]` (depth_bucket, count) | ### General Analytics -| Scenario | Command | Output | -|----------|---------|--------| -| Overall usage summary (tokens, cost, users) | `summaries` | KPI totals | -| User list + activity trends | `users` | Users + time-series | -| Per-project spending | `projects-spending` | Table | -| LLM model breakdown | `llms-usage` | Table | -| Tool usage | `tools-usage` | Table | -| Workflow execution analytics | `workflows` | Table | -| Budget alerts (soft + hard limits) | `budget` | Warning tables | -| Personal spending & budget | `spending` | Current user's spend + budget usage | -| Per-user spending (platform + cli split) | `spending-by-users` | Breakdown tables | -| Weekly engagement histogram | `engagement` | 3h-interval heatmap data | +| Scenario | Command | What it retrieves | +|----------|---------|-------------------| +| Overall usage summary (tokens, cost, users) | `summaries` | `data` — total_cost, total_tokens, total_requests, unique_users (MAU), unique_users_daily (DAU), cli_invocations, assistants_count, workflows_count, skills_count, mcp_servers_count | +| User list + activity trends | `users` | `data.rows[]` — user_name, email, total_cost, total_tokens, last_active; plus `activity[]` time-series | +| Per-project spending | `projects-spending` | `data.rows[]` — project_name, total_cost, total_tokens, user_count, request_count | +| LLM model breakdown | `llms-usage` | `data.rows[]` — model_name, request_count, total_tokens, input_tokens, output_tokens, total_cost | +| Tool usage | `tools-usage` | `data.rows[]` — tool_name, invocation_count, success_count, error_count, total_tokens | +| Workflow execution analytics | `workflows` | `data.rows[]` — workflow_name, run_count, success_count, failure_count, avg_duration_ms, total_cost | +| Budget alerts (soft + hard limits) | `budget` | Composite with `soft.data.rows[]` and `hard.data.rows[]` — user_email, max_spent (users approaching or over limit) | +| Personal spending & budget | `spending` | `data` — current_spend, budget_limit, hard_budget_limit, budget_reset_at, percentage_used | +| Per-user spending (platform + cli split) | `spending-by-users` | Composite with `platform.data.rows[]` and `cli.data.rows[]` — user_name/email, total_cost, token_count | +| Weekly engagement histogram | `engagement` | `data.rows[]` — day_label, hour_start, feature_type, session_count, cost; covers last 7 days in 3-hour intervals | ### LiteLLM & CSV Enrichment @@ -114,6 +114,62 @@ node ${CLAUDE_PLUGIN_ROOT}/skills/codemie-analytics/scripts/analytics-cli.js ` on each. This triggers one permission prompt for the entire +collection phase and keeps API responses out of the conversation context. + +### Directory layout + +Every report lives in its own folder under `reports/`. Derive the folder name from the +current date and a short kebab-case description of the report: + +``` +reports/ + 2026-05-07-cli-usage/ ← report folder (date + name) + cli-usage.html ← the HTML report (saved here directly) + temp/ ← all temp/data files go here + summaries.json + summaries.schema.json + cli-insights.json + ... +``` + +**Never overwrite an existing report folder.** Always resolve a free name before creating +anything. Use a suffix loop — this is mandatory, not optional: + +```bash +BASE=reports/$(date +%Y-%m-%d)- +REPORT_DIR=$BASE +n=2 +while [ -d "$REPORT_DIR" ]; do REPORT_DIR="${BASE}-${n}"; n=$((n+1)); done +OUT="$REPORT_DIR/temp" +``` + +Then run only the commands the report needs: + +```bash +CLI=${CLAUDE_PLUGIN_ROOT}/skills/codemie-analytics/scripts/analytics-cli.js +mkdir -p "$OUT" && \ +node $CLI summaries --save "$OUT/summaries.json" && \ +node $CLI cli-insights --save "$OUT/cli-insights.json" && \ +# ... only endpoints needed for this report ... +echo "✓ All data saved → $OUT" +``` + +Each command prints: `✓ Saved → `. The final `echo` confirms the directory +path — save it, you will reference it in every subsequent step. + +**Do not `cat`, `Read`, or print the saved JSON files into the conversation.** +Raw API responses can be hundreds of KB. Use Step 2.5 to inspect structure instead. + +Temp files are not cleaned up automatically. + ### Common filter flags | Flag | Example | Notes | @@ -121,7 +177,7 @@ LiteLLM commands (`litellm-*`, `enrich-csv`) require `LITELLM_URL` + `LITELLM_KE | `--time-period` | `last_30_days` | Predefined period | | `--start-date` | `2024-01-01T00:00:00` | Custom range start | | `--end-date` | `2024-03-31T23:59:59` | Custom range end | -| `--users` | `john.doe,jane.smith` | Comma-separated usernames | +| `--users` | `alice,bob` | Comma-separated usernames | | `--projects` | `my-project` | Comma-separated project names | | `--page` | `1` | Pagination | | `--per-page` | `100` | Results per page (default 50) | @@ -150,7 +206,7 @@ CLI=${CLAUDE_PLUGIN_ROOT}/skills/codemie-analytics/scripts/analytics-cli.js node $CLI leaderboard --tier pioneer --sort-by total_score --sort-order desc --per-page 50 --pretty # Single user champion profile -node $CLI leaderboard-user john.doe@epam.com --pretty +node $CLI leaderboard-user user@example.com --pretty # Leaderboard KPI summary for Q1 2026 node $CLI leaderboard-summary --view quarterly --season-key 2026-Q1 --pretty @@ -171,7 +227,7 @@ node $CLI summaries --time-period last_30_days --pretty node $CLI cli-insights --time-period last_30_days --pretty # Detailed CLI profile for a specific user -node $CLI cli-insights-user John_Doe --time-period last_30_days --pretty +node $CLI cli-insights-user alice@example.com --time-period last_30_days --pretty # Usage patterns (weekday + hourly + session depth) node $CLI cli-insights-patterns --time-period last_30_days --pretty @@ -185,34 +241,111 @@ node $CLI custom /v1/analytics/mcp-servers --time-period last_30_days --pretty --- -## Step 3 — Build the HTML report +## Step 2.5 — Inspect data structure -Once you have the JSON data, delegate the presentation layer to the **`codemie-html-report`** -skill. That skill knows the CodeMie design system, Chart.js palette, and component library. -Do **not** hand-write HTML/CSS in this skill. +After collection, run `inspect-schema.js` to generate a compact `.schema.json` file +alongside each saved JSON — one permission prompt, no data in context: -### Output location +```bash +node ${CLAUDE_PLUGIN_ROOT}/skills/codemie-analytics/scripts/inspect-schema.js "$OUT" +``` -**Always save reports to `reports/` in the user's current working directory.** Create the -folder if it doesn't exist. Use descriptive filenames: +Output is a short listing of generated schema files with sizes, for example: +``` +Schemas written to: reports/2026-05-07-cli-usage/temp/ + ✓ leaderboard-top.schema.json (1.3 KB) + ✓ summaries.schema.json (0.4 KB) + ... +``` +Then **read only the `.schema.json` files you actually need** for the report using the +`Read` tool. Each schema shows field names, types, array lengths, and string samples — +enough to write extraction code without touching the raw responses: + +> **CRITICAL — schema is the source of truth for field names.** +> Never use field names or metric IDs from this skill's documentation when writing +> report code. Documentation can be stale. The `.schema.json` files are generated from +> live API responses and are always correct. Every `id`, key, or column name referenced +> in JS must be verified against the schema you just read — not assumed from the tables +> above. If the schema contradicts the docs, trust the schema. + +```json +{ + "_envelope": "[envelope] data_as_of: '2026-05-07T02:00:10', total_count: 50", + "data": { + "rows": { + "_type": "array", + "_count": 50, + "_item": { + "rank": "number", + "user_name": "string ~ 'Jane Smith'", + "total_score": "number", + "tier_name": "string ~ 'expert'", + "score_delta": "number | null", + "dimensions": { "_type": "array", "_count": 6, "_item": { "id": "string ~ 'd1'", "score": "number", "weight": "number" } }, + "summary_metrics": { "cli_sessions": "number", "active_days": "number", "total_lines_added": "number", "total_spend": "number" } + } + } + } +} ``` -reports/leaderboard-2026-Q1.html -reports/cli-insights-last-30-days.html -reports/spending-by-users-2026-04.html + +--- + +## Step 3 — Build the HTML report + +**Save the HTML report directly inside `$REPORT_DIR`** (not in `temp/`). +Use a kebab-case filename matching the folder name: + +``` +reports/2026-05-07-cli-usage/cli-usage.html +reports/2026-05-07-leaderboard/leaderboard.html +reports/2026-05-07-spending/spending.html +``` + +### Step 3a — Write the HTML + +Invoke the **`codemie-html-report`** skill. Pass: + +1. **The schemas** inspected in Step 2.5 — field names and structure. +2. **The user's intent** — e.g. "leaderboard dashboard with tier distribution". +3. **Timestamp context** — `_envelope` lines in schemas include `data_as_of`. +4. **Output path** — e.g. `$REPORT_DIR/leaderboard.html`. + +The HTML skill writes the file using `/*__CSS__*/` for styles and +**`/*__DATA:name__*/` placeholders for all embedded JS data arrays**: + +```html + ``` -### What to pass to the report skill +Each placeholder name maps to a saved file in `$OUT` (without the `.json` extension). -When invoking `codemie-html-report`, include: +### Step 3b — Inject data -1. **The raw JSON** collected from the CLI (one object per command/endpoint). -2. **The user's intent** — e.g. "leaderboard dashboard with tier distribution and dimension - breakdown", "CLI insights with usage patterns". -3. **Timestamp context** — most endpoints return `metadata.data_as_of`; pass it through for - the report subtitle. -4. **Output path** — tell the report skill where to save, e.g. `reports/leaderboard.html`. -5. **Pagination hints** if the data was truncated. +**After the HTML file exists**, run the shared `inject-data.js` from the `codemie-html-report` +skill. It matches each JSON file in `$OUT` to a `/*__DATA:name__*/` placeholder by filename +(without `.json`) and replaces it in-place. + +**Do not run inject-data.js before the HTML file is written.** + +```bash +node ${CLAUDE_PLUGIN_ROOT}/skills/codemie-html-report/scripts/inject-data.js \ + "$REPORT_DIR/.html" "$OUT" +``` + +Placeholder names must exactly match the JSON filenames — `/*__DATA:leaderboard-top__*/` +is only replaced when `leaderboard-top.json` exists in `$OUT`. + +Expected output: +``` + ✓ injected leaderboard-top + ✓ injected summaries +✓ 2 data block(s) injected into reports/2026-05-07-/.html +``` --- @@ -382,8 +515,8 @@ charts, data structure, and modal design to use, ensuring consistency across use | Report type | Reference file | When to use | |-------------|---------------|-------------| -| Leaderboard dashboard | [`references/leaderboard-dashboard-report.md`](references/leaderboard-dashboard-report.md) | Any request for leaderboard rankings, AI champions, top performers, tier distribution | -| People spending dashboard | [`references/people-spending-dashboard-report.md`](references/people-spending-dashboard-report.md) | Any request to track LiteLLM costs for a specific list of users (cohort, team, bootcamp, project) | +| Leaderboard dashboard | [`${CLAUDE_PLUGIN_ROOT}/references/leaderboard-dashboard-report.md`](${CLAUDE_PLUGIN_ROOT}/references/leaderboard-dashboard-report.md) | Any request for leaderboard rankings, AI champions, top performers, tier distribution | +| People spending dashboard | [`${CLAUDE_PLUGIN_ROOT}/references/people-spending-dashboard-report.md`](${CLAUDE_PLUGIN_ROOT}/references/people-spending-dashboard-report.md) | Any request to track LiteLLM costs for a specific list of users (cohort, team, bootcamp, project) | --- @@ -427,7 +560,7 @@ If the command returns an auth error, HTTP 401/403/404, or "No CodeMie credentia > claude -f > ``` -**Full workflow** (see `references/people-spending-dashboard-report.md` for all details): +**Full workflow** (see `${CLAUDE_PLUGIN_ROOT}/references/people-spending-dashboard-report.md` for all details): 1. **Parse the list** from Excel/CSV using `openpyxl`. Skip header and TOTAL rows. 2. **Fetch 3 LiteLLM accounts per user** using Python `asyncio` + `aiohttp` (semaphore 25, @@ -448,7 +581,7 @@ If the command returns an auth error, HTTP 401/403/404, or "No CodeMie credentia conflict with JS `${...}` template literals). 9. **Wire table clicks** — use `data-email` attribute + event delegation (never `onclick=""` attributes, which break under Python quote escaping). -10. **Save** to `reports/.html`. +10. **Save** to `reports/-/.html` (temp/data files in `reports/-/temp/`). **Key commands:** ```bash @@ -475,12 +608,22 @@ node $CLI cli-insights-users --time-period last_30_days --per-page 500 --output - Budget warnings: flag rows where `spend / max_budget > 0.8` (warn) and `> 1.0` (error). - For the **leaderboard dashboard**, combine `leaderboard` + `leaderboard-summary` + `leaderboard-tiers` + `leaderboard-dimensions` to build a comprehensive view. Then follow - `references/leaderboard-dashboard-report.md` for the exact HTML structure. + `${CLAUDE_PLUGIN_ROOT}/references/leaderboard-dashboard-report.md` for the exact HTML structure. - For a **people spending dashboard**, fetch LiteLLM directly with Python async (3 accounts per user), then enrich with leaderboard + CLI insights. Follow - `references/people-spending-dashboard-report.md` for the exact HTML structure. + `${CLAUDE_PLUGIN_ROOT}/references/people-spending-dashboard-report.md` for the exact HTML structure. - For a **single user deep-dive**, combine `leaderboard-user ` with `cli-insights-user ` for the full picture (champion score + CLI activity). - If the CLI prints an auth error, forward its message verbatim — it already tells the user what to do next. -- Always save HTML reports to `reports/` in the user's working directory. \ No newline at end of file +- Always save HTML reports to `reports/-/.html`; temp/data files go in `reports/-/temp/`. + +## Type-Aware Rendering for `metrics[]` Arrays + +Analytics `metrics[]` arrays are **heterogeneous** — each item carries a `type` field +(`"number"`, `"string"`, `"date"`, …) and a `format` field. The `value` is numeric for most +items but may be an ISO date string or plain string for others. + +**Always inspect `m.type` (and `m.format`) per item before formatting.** Never apply a single +numeric or percent formatter to the whole array — doing so silently produces `NaN` for +string/date items. \ No newline at end of file diff --git a/src/agents/plugins/claude/plugin/skills/codemie-analytics/scripts/analytics-cli.js b/src/agents/plugins/claude/plugin/skills/codemie-analytics/scripts/analytics-cli.js index 11857733..fd81e83f 100644 --- a/src/agents/plugins/claude/plugin/skills/codemie-analytics/scripts/analytics-cli.js +++ b/src/agents/plugins/claude/plugin/skills/codemie-analytics/scripts/analytics-cli.js @@ -71,9 +71,9 @@ */ import { createDecipheriv, createHash } from 'crypto'; -import { readFileSync, existsSync } from 'fs'; +import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'fs'; import { homedir, hostname, platform, arch } from 'os'; -import { join, resolve } from 'path'; +import { join, resolve, dirname } from 'path'; import { fileURLToPath } from 'url'; // ─── Argument Parsing ──────────────────────────────────────────────────────── @@ -404,6 +404,14 @@ async function parseInputFile(filePath) { // ─── Output helpers ────────────────────────────────────────────────────────── function output(data) { + if (opts.save) { + const filePath = resolve(opts.save); + mkdirSync(dirname(filePath), { recursive: true }); + writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8'); + console.log(`✓ Saved → ${filePath}`); + return; + } + const fmt = opts.output || 'json'; if (fmt === 'json' || !fmt) { if (opts.pretty) { diff --git a/src/agents/plugins/claude/plugin/skills/codemie-analytics/scripts/inspect-schema.js b/src/agents/plugins/claude/plugin/skills/codemie-analytics/scripts/inspect-schema.js new file mode 100644 index 00000000..fb8905b9 --- /dev/null +++ b/src/agents/plugins/claude/plugin/skills/codemie-analytics/scripts/inspect-schema.js @@ -0,0 +1,211 @@ +#!/usr/bin/env node +/** + * inspect-schema.js + * + * Reads all .json files in and prints a compact schema to stdout. + * Designed to give Claude enough structural information to write data extraction + * code without reading raw API responses into the conversation context. + * + * Output format (per file): + * - primitives: "number" | "boolean" | "string ~ 'sample'" + * - arrays: { _type: "array", _count: N, _item: } + * - objects: { key: , ... } + * - null fields: "null" or " | null" when nullable across samples + * + * Usage: + * node inspect-schema.js /tmp/codemie-analytics-20260507/ + */ + +import { readFileSync, writeFileSync, readdirSync } from 'fs'; +import { join } from 'path'; + +const MAX_STRING_PREVIEW = 50; +const NULL_CHECK_SAMPLES = 5; +// For type-diversity detection: scan all items in small arrays, first N in large ones. +const TYPE_CHECK_SAMPLES = 20; + +// Fields whose complete vocabulary is always emitted regardless of array context. +const ALWAYS_ENUMERATE = new Set([ + 'type', 'format', 'classification', 'tier_name', + 'weekday', 'range', 'client_name', 'dimension_id', +]); + +// Values containing these chars are entity/path identifiers, not vocabulary — skip them. +const ENTITY_VALUE_RE = /[@/]/; + +// UUID-shaped strings are entity IDs, not vocabulary. +const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-/i; + +/** + * Returns the set of fields in itemSchema that should be fully enumerated. + * + * Rules: + * - ALWAYS_ENUMERATE fields: always included. + * - 'id': only when the parent item has a 'label' key (metrics or columns context), + * ensuring we capture metric/column identifiers without enumerating entity IDs. + */ +function getEnumerableFields(itemSchema) { + const keys = new Set(); + for (const key of Object.keys(itemSchema)) { + if (ALWAYS_ENUMERATE.has(key)) keys.add(key); + } + if ('id' in itemSchema && 'label' in itemSchema) { + keys.add('id'); + } + return keys; +} + +/** + * Returns the bare type category of an already-inferred schema string. + * e.g. "string ~ 'foo'" → "string", "number" → "number", "null" → "null" + */ +function typeCategory(schema) { + if (schema === 'null') return 'null'; + if (schema === 'number' || schema === 'boolean') return schema; + if (typeof schema === 'string' && schema.startsWith('string')) return 'string'; + if (schema && typeof schema === 'object' && schema._type === 'array') return 'array'; + if (schema && typeof schema === 'object') return 'object'; + return String(schema); +} + +function infer(value) { + if (value === null || value === undefined) return 'null'; + + const t = typeof value; + + if (t === 'boolean') return 'boolean'; + if (t === 'number') return 'number'; + + if (t === 'string') { + const preview = value.length > MAX_STRING_PREVIEW + ? value.slice(0, MAX_STRING_PREVIEW) + '...' + : value; + return `string ~ '${preview}'`; + } + + if (Array.isArray(value)) { + if (value.length === 0) return { _type: 'array', _count: 0, _item: 'unknown' }; + + const itemSchema = infer(value[0]); + + // Scan multiple items to detect nullable fields AND type diversity. + // For small arrays scan everything; for large ones sample the first N. + if ( + itemSchema !== null && + typeof itemSchema === 'object' && + !Array.isArray(itemSchema) && + value.length > 1 + ) { + const sampleSize = Math.min( + Math.max(NULL_CHECK_SAMPLES, TYPE_CHECK_SAMPLES), + value.length + ); + const samples = value.slice(0, sampleSize); + + for (const key of Object.keys(itemSchema)) { + if (typeof itemSchema[key] !== 'string') continue; // skip nested objects/arrays + if (itemSchema[key] === 'null') continue; + + const baseCategory = typeCategory(itemSchema[key]); + const seenCategories = new Set([baseCategory]); + + for (const sample of samples.slice(1)) { + const v = sample[key]; + if (v === null || v === undefined) { + seenCategories.add('null'); + } else { + seenCategories.add(typeCategory(infer(v))); + } + } + + const nonNullTypes = [...seenCategories].filter(c => c !== 'null'); + const hasNull = seenCategories.has('null') || itemSchema[key].includes('| null'); + + if (nonNullTypes.length > 1) { + // Multiple distinct types observed — drop string preview, show union + itemSchema[key] = nonNullTypes.join(' | ') + (hasNull ? ' | null' : ''); + } else if (hasNull && !itemSchema[key].includes('| null')) { + itemSchema[key] += ' | null'; + } + } + + // Pass 2: enumerate vocabulary fields across ALL items + const enumerableFields = getEnumerableFields(itemSchema); + for (const key of enumerableFields) { + if (typeof itemSchema[key] !== 'string') continue; + if (!itemSchema[key].startsWith('string')) continue; + + const uniqueVals = new Set(); + for (const item of value) { + const v = item[key]; + if (typeof v === 'string' && !UUID_RE.test(v) && !ENTITY_VALUE_RE.test(v)) { + uniqueVals.add(v); + } + } + + // Always-enumerate fields emit even a single observed value; id requires ≥ 2. + const minUnique = ALWAYS_ENUMERATE.has(key) ? 1 : 2; + if (uniqueVals.size >= minUnique && uniqueVals.size <= 50) { + const sorted = [...uniqueVals].sort(); + const preview = sorted.map(v => `'${v}'`).join(' | '); + const nullable = itemSchema[key].includes('| null'); + itemSchema[key] = `string (enum) ~ ${preview}${nullable ? ' | null' : ''}`; + } + } + } + + return { _type: 'array', _count: value.length, _item: itemSchema }; + } + + if (t === 'object') { + const schema = {}; + for (const [k, v] of Object.entries(value)) { + schema[k] = infer(v); + } + return schema; + } + + return t; +} + +function processFile(filePath) { + let raw; + try { + raw = JSON.parse(readFileSync(filePath, 'utf8')); + } catch (e) { + return { _error: `Failed to parse: ${e.message}` }; + } + return infer(raw); +} + +const dataDir = process.argv[2]; + +if (!dataDir) { + console.error('Usage: node inspect-schema.js '); + process.exit(1); +} + +let files; +try { + files = readdirSync(dataDir).filter(f => f.endsWith('.json') && !f.endsWith('.schema.json')).sort(); +} catch (e) { + console.error(`Cannot read directory: ${dataDir}\n${e.message}`); + process.exit(1); +} + +if (files.length === 0) { + console.error(`No .json files found in: ${dataDir}`); + process.exit(1); +} + +const written = []; +for (const file of files) { + const schema = processFile(join(dataDir, file)); + const schemaFile = file.replace(/\.json$/, '.schema.json'); + const schemaPath = join(dataDir, schemaFile); + writeFileSync(schemaPath, JSON.stringify(schema, null, 2)); + written.push(` ✓ ${schemaFile}`); +} + +console.log(`Schemas written to: ${dataDir}`); +console.log(written.join('\n')); diff --git a/src/agents/plugins/claude/plugin/skills/codemie-html-report/README.md b/src/agents/plugins/claude/plugin/skills/codemie-html-report/README.md new file mode 100644 index 00000000..d3851f97 --- /dev/null +++ b/src/agents/plugins/claude/plugin/skills/codemie-html-report/README.md @@ -0,0 +1,39 @@ +# CodeMie HTML Report — Style Guide + +## CSS Bundle + +`css/bundle.css` is a pre-built, minified concatenation of all 8 design-system CSS files. +It is committed to the repo so report generation requires no build step at runtime. + +**Do not edit `bundle.css` directly.** Edit the individual source files instead, then rebuild. + +### Source files (order matters) + +| File | What it covers | +|------|---------------| +| `css/tokens.css` | CSS custom properties — colors, spacing, radii, shadows, gradients | +| `css/base.css` | Reset, body, scrollbar, code blocks, links, focus ring | +| `css/typography.css` | Headings h1–h6, text size/weight/color utilities | +| `css/buttons.css` | All button variants and sizes | +| `css/forms.css` | input, textarea, select, checkbox, radio, switch | +| `css/components.css` | card, badge, alert, avatar, stat-card, chip, empty-state, etc. | +| `css/layout.css` | table, tabs, pagination, modal, nav-sidebar, app-shell | +| `css/utilities.css` | flex, grid, gap, padding, margin, width, overflow, border | + +### Rebuilding bundle.css + +Run this command from the repo root whenever any source CSS file changes: + +```bash +npx clean-css-cli -o src/agents/plugins/claude/plugin/skills/codemie-html-report/style-guide/css/bundle.css \ + src/agents/plugins/claude/plugin/skills/codemie-html-report/style-guide/css/tokens.css \ + src/agents/plugins/claude/plugin/skills/codemie-html-report/style-guide/css/base.css \ + src/agents/plugins/claude/plugin/skills/codemie-html-report/style-guide/css/typography.css \ + src/agents/plugins/claude/plugin/skills/codemie-html-report/style-guide/css/buttons.css \ + src/agents/plugins/claude/plugin/skills/codemie-html-report/style-guide/css/forms.css \ + src/agents/plugins/claude/plugin/skills/codemie-html-report/style-guide/css/components.css \ + src/agents/plugins/claude/plugin/skills/codemie-html-report/style-guide/css/layout.css \ + src/agents/plugins/claude/plugin/skills/codemie-html-report/style-guide/css/utilities.css +``` + +Commit the updated `bundle.css` alongside the source CSS change. diff --git a/src/agents/plugins/claude/plugin/skills/codemie-html-report/SKILL.md b/src/agents/plugins/claude/plugin/skills/codemie-html-report/SKILL.md index 4cd5a34b..f68e8b2f 100644 --- a/src/agents/plugins/claude/plugin/skills/codemie-html-report/SKILL.md +++ b/src/agents/plugins/claude/plugin/skills/codemie-html-report/SKILL.md @@ -17,32 +17,49 @@ description: > You are building a standalone HTML page that visually matches the CodeMie (EPAM AI/Run) product UI. The design system is a dark-first, professional theme with Inter font, subtle borders, and semantic color tokens. Every page you produce should feel like a native screen of the CodeMie platform. -## Step 1 — Read the CSS files +## Step 1 — CSS placeholder -Read **all 8 CSS files** from `${CLAUDE_PLUGIN_ROOT}/skills/codemie-html-report/style-guide/css/` — you will inline them all: +**Do NOT read any CSS files. Do NOT inline any CSS yourself.** -| File | What it covers | -|------|---------------| -| `tokens.css` | All CSS custom properties (colors, spacing, radii, shadows, gradients) | -| `base.css` | Reset, body, scrollbar, code blocks, links, focus ring | -| `typography.css` | Headings h1-h6, text size/weight/color utilities | -| `buttons.css` | btn-primary, btn-secondary, btn-base, btn-delete, btn-tertiary, btn-magical, sizes | -| `forms.css` | input, textarea, select, checkbox, radio, switch | -| `components.css` | card, badge, tag, alert, avatar, spinner, progress, tooltip, stat-card, chip, empty-state | -| `layout.css` | table, tabs, pagination, modal, nav-sidebar, app-shell | -| `utilities.css` | flex, grid, gap, padding, margin, width, overflow, position, border, shadow | +In the ` @@ -271,3 +283,82 @@ body.p-6 > .container ``` This pattern matches the analytics dashboard layout in the live CodeMie product and works for most reporting use cases. + +## Final Step — Inject CSS + +After writing the HTML file, run this command to replace the placeholder with the full +design system bundle and make the report self-contained: + +```bash +node ${CLAUDE_PLUGIN_ROOT}/skills/codemie-html-report/scripts/inject-css.js +``` + +For example: +```bash +node ${CLAUDE_PLUGIN_ROOT}/skills/codemie-html-report/scripts/inject-css.js reports/leaderboard-2026-Q1.html +``` + +Expected output: `✓ CSS injected into reports/leaderboard-2026-Q1.html` + +## Final Step — Inject Data (analytics pipeline only — skip for standalone use) + +> This step applies **only** when invoked from the **codemie-analytics** skill. Standalone +> HTML generation embeds data inline and must skip this step. + +After the HTML file is written, run `inject-data.js` to replace every `/*__DATA:name__*/` +placeholder with the matching JSON file from the temp directory: + +```bash +node ${CLAUDE_PLUGIN_ROOT}/skills/codemie-html-report/scripts/inject-data.js \ + +``` + +For example: +```bash +node ${CLAUDE_PLUGIN_ROOT}/skills/codemie-html-report/scripts/inject-data.js \ + reports/2026-05-07-leaderboard/leaderboard.html \ + reports/2026-05-07-leaderboard/temp/ +``` + +**Placeholder names must exactly match the JSON filenames** (without `.json`): +- `/*__DATA:leaderboard-top__*/` is replaced from `leaderboard-top.json` +- `/*__DATA:summaries__*/` is replaced from `summaries.json` + +A wrong name means the placeholder is silently skipped. If no placeholders are matched at +all, the script exits with an error. + +Expected output: +``` + ✓ injected leaderboard-top + ✓ injected summaries +✓ 2 data block(s) injected into reports/2026-05-07-leaderboard/leaderboard.html +``` + +**Do not run inject-data.js before the HTML file exists.** +Run inject-css.js after inject-data.js. + +## Final Step — Temp file cleanup (analytics pipeline only — skip for standalone use) + +> **Backwards compatibility:** This step applies **only** when this skill is invoked +> from the **codemie-analytics** skill. Standalone HTML generation has no temp +> directory and must skip this step. + +After the CSS is injected and the report is complete, **always ask the user**: + +> The report is ready at ``. The `temp/` directory (``) contains the raw +> API response files used to build it (~N files). Would you like to delete it? + +If the user says **yes**, delete the temp directory: + +```bash +rm -rf "" +# e.g. rm -rf "reports/2026-05-07-executive-spending/temp" +``` + +Confirm deletion: +``` +✓ Temp files deleted → reports/2026-05-07-executive-spending/temp +``` + +If the user says **no** (or does not respond), leave the directory intact and note its +location so they can inspect or re-use the raw data later. diff --git a/src/agents/plugins/claude/plugin/skills/codemie-html-report/scripts/inject-css.js b/src/agents/plugins/claude/plugin/skills/codemie-html-report/scripts/inject-css.js new file mode 100644 index 00000000..0acc18cb --- /dev/null +++ b/src/agents/plugins/claude/plugin/skills/codemie-html-report/scripts/inject-css.js @@ -0,0 +1,40 @@ +#!/usr/bin/env node +import { readFileSync, writeFileSync, existsSync } from 'fs'; +import { join, dirname, resolve } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +const htmlPath = process.argv[2]; + +if (!htmlPath) { + console.error('Usage: node inject-css.js '); + console.error('Example: node inject-css.js reports/leaderboard-2026-Q1.html'); + process.exit(1); +} + +const resolvedHtml = resolve(htmlPath); +const bundlePath = join(__dirname, '..', 'style-guide', 'css', 'bundle.css'); + +if (!existsSync(resolvedHtml)) { + console.error(`Error: HTML file not found: ${resolvedHtml}`); + process.exit(1); +} + +if (!existsSync(bundlePath)) { + console.error(`Error: bundle.css not found at ${bundlePath}`); + console.error('Rebuild it — see style-guide/README.md for the command.'); + process.exit(1); +} + +const html = readFileSync(resolvedHtml, 'utf8'); + +if (!html.includes('/* __CODEMIE_CSS__ */')) { + console.error(`Error: placeholder "/* __CODEMIE_CSS__ */" not found in ${resolvedHtml}`); + console.error('The HTML file must contain: '); + process.exit(1); +} + +const css = readFileSync(bundlePath, 'utf8'); +writeFileSync(resolvedHtml, html.replace('/* __CODEMIE_CSS__ */', css), 'utf8'); +console.log(`✓ CSS injected into ${resolvedHtml}`); diff --git a/src/agents/plugins/claude/plugin/skills/codemie-html-report/scripts/inject-data.js b/src/agents/plugins/claude/plugin/skills/codemie-html-report/scripts/inject-data.js new file mode 100644 index 00000000..2e01dbb8 --- /dev/null +++ b/src/agents/plugins/claude/plugin/skills/codemie-html-report/scripts/inject-data.js @@ -0,0 +1,68 @@ +#!/usr/bin/env node +import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from 'fs'; +import { join, resolve, basename, extname } from 'path'; + +const [htmlPath, ...sources] = process.argv.slice(2); + +if (!htmlPath || sources.length === 0) { + console.error('Usage: node inject-data.js [...]'); + console.error('Example: node inject-data.js reports/report.html reports/temp/'); + process.exit(1); +} + +const resolvedHtml = resolve(htmlPath); + +if (!existsSync(resolvedHtml)) { + console.error(`Error: HTML file not found: ${resolvedHtml}`); + process.exit(1); +} + +// Collect all JSON files from files and/or directories +const jsonFiles = []; +for (const src of sources) { + const resolved = resolve(src); + if (!existsSync(resolved)) { + console.error(`Error: source not found: ${resolved}`); + process.exit(1); + } + if (statSync(resolved).isDirectory()) { + for (const entry of readdirSync(resolved)) { + if (extname(entry) === '.json' && !entry.endsWith('.schema.json')) jsonFiles.push(join(resolved, entry)); + } + } else { + if (extname(resolved) !== '.json') { + console.error(`Error: not a JSON file: ${resolved}`); + process.exit(1); + } + jsonFiles.push(resolved); + } +} + +if (jsonFiles.length === 0) { + console.error('Error: no JSON files found in the provided sources'); + process.exit(1); +} + +let html = readFileSync(resolvedHtml, 'utf8'); + +let injected = 0; +for (const jsonFile of jsonFiles) { + const name = basename(jsonFile, '.json'); + const placeholder = `/*__DATA:${name}__*/`; + if (!html.includes(placeholder)) { + console.warn(` warn: no placeholder for "${name}" — skipping`); + continue; + } + const data = readFileSync(jsonFile, 'utf8').trim(); + html = html.replaceAll(placeholder, data); + console.log(` ✓ injected ${name}`); + injected++; +} + +if (injected === 0) { + console.error('Error: no placeholders were matched — HTML unchanged'); + process.exit(1); +} + +writeFileSync(resolvedHtml, html, 'utf8'); +console.log(`✓ ${injected} data block(s) injected into ${resolvedHtml}`); diff --git a/src/agents/plugins/claude/plugin/skills/codemie-html-report/style-guide/css/bundle.css b/src/agents/plugins/claude/plugin/skills/codemie-html-report/style-guide/css/bundle.css new file mode 100644 index 00000000..ebce9bcd --- /dev/null +++ b/src/agents/plugins/claude/plugin/skills/codemie-html-report/style-guide/css/bundle.css @@ -0,0 +1 @@ +@import url(https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,400;0,500;0,600;0,700;1,400&family=JetBrains+Mono:wght@400;500&display=swap);:root{--font-sans:'Inter','Geist',-apple-system,BlinkMacSystemFont,'Segoe UI',Arial,sans-serif;--font-mono:'JetBrains Mono','GeistMono','Fira Code','Courier New',monospace;--text-xs-1:0.625rem;--text-xs:0.75rem;--text-sm-1:0.813rem;--text-sm:0.875rem;--text-base:1rem;--text-h1:2rem;--text-h2:1.5rem;--text-h3:1rem;--text-h4:0.875rem;--text-h5:0.75rem;--lh-h1:2rem;--lh-h2:1.5rem;--lh-h3:1.3125rem;--lh-h4:1.125rem;--lh-h5:1rem;--radius-sm:4px;--radius-md:6px;--radius-lg:8px;--radius-xl:12px;--radius-2xl:16px;--radius-full:9999px;--space-0-5:0.125rem;--space-1:0.25rem;--space-1-5:0.375rem;--space-2:0.5rem;--space-2-5:0.625rem;--space-3:0.75rem;--space-4:1rem;--space-5:1.25rem;--space-6:1.5rem;--space-8:2rem;--shadow-sm:0 1px 2px rgba(0, 0, 0, 0.4);--shadow-md:0 4px 6px -1px rgba(0, 0, 0, 0.5),0 2px 4px -2px rgba(0, 0, 0, 0.3);--shadow-lg:0 10px 15px -3px rgba(0, 0, 0, 0.6),0 4px 6px -4px rgba(0, 0, 0, 0.4);--shadow-sidebar:-1px 0 0 0 rgba(255, 255, 255, 0.1);--transition-fast:120ms ease;--transition-base:200ms ease;--transition-slow:300ms ease;--navbar-width:72px;--navbar-width-expanded:196px;--sidebar-width:308px;--layout-header-height:56px;--card-height:158px;--z-dropdown:30;--z-sticky:40;--z-modal:50;--z-toast:60}:root,:root.dark{--color-bg-page:#1A1A1A;--color-bg-sidebar:#151515;--color-bg-card:#151515;--color-bg-nav:#000000;--color-bg-elevated:#2E2E2E;--color-bg-secondary:#212224;--color-bg-tertiary:#2E3033;--color-bg-quaternary:#333436;--color-bg-hover:#212224;--color-bg-hover-strong:#333436;--color-bg-input:#1A1A1A;--color-bg-input-prefix:#333436;--color-bg-btn-primary:#20222E;--color-bg-btn-primary-h:#262941;--color-bg-pagination-active:#212224;--color-text-primary:#FFFFFF;--color-text-secondary:#F2F0EF;--color-text-tertiary:#CCCCCC;--color-text-muted:#BBBBBB;--color-text-placeholder:#CCCCCC;--color-text-link:#F2F0EF;--color-text-link-hover:#F2F0EF;--color-text-inverse:#FFFFFF;--color-text-heading:#BBBBBB;--color-text-nav:#CCCCCC;--color-border-primary:#333436;--color-border-secondary:#47484A;--color-border-structural:#333436;--color-border-subtle:#4C4C4C;--color-border-accent:#FFFFFF;--color-border-focus:#FFFFFF;--color-border-error:#F9303C;--color-border-panel:#333436;--color-border-btn-secondary:#333436;--color-border-btn-secondary-h:transparent;--color-icon-primary:#FFFFFF;--color-icon-secondary:#CCCCCC;--color-icon-tertiary:#999999;--color-icon-error:#F9303C;--color-success:#259F4C;--color-success-bg:#1B271F;--color-success-border:#259F4C;--color-success-text:#259F4C;--color-error:#F9303C;--color-error-bg:#262121;--color-error-border:#FE3B4C;--color-error-text:#FE3B4C;--color-warning:#F5A534;--color-warning-bg:#492B00;--color-warning-border:#663B00;--color-warning-text:#F5A534;--color-info:#2297F6;--color-info-bg:#002442;--color-info-border:#003A69;--color-info-text:#2297F6;--color-purple:#C084FC;--color-purple-bg:#2D1B3D;--color-purple-border:#2D1B3D;--color-purple-text:#F3E8FF;--color-cyan:#06B6D4;--color-cyan-bg:#003942;--color-cyan-border:#005866;--status-not-started-text:#A0A0A0;--status-not-started-bg:#333333;--status-not-started-border:#4C4C4C;--status-in-progress-text:#2297F6;--status-in-progress-bg:#002442;--status-in-progress-border:#003A69;--status-pending-text:#06B6D4;--status-pending-bg:#003942;--status-pending-border:#005866;--status-success-text:#259F4C;--status-success-bg:#1B271F;--status-success-border:#259F4C;--status-error-text:#FE3B4C;--status-error-bg:#262121;--status-error-border:#FE3B4C;--status-warning-text:#F5A534;--status-warning-bg:#492B00;--status-warning-border:#663B00;--status-advanced-text:#C084FC;--status-advanced-bg:#2D1B3D;--status-advanced-border:#2D1B3D;--gradient-primary-btn:linear-gradient(90deg, #672D92, #547CCC);--gradient-brand:linear-gradient(152deg, #0078C2, #0047FF, #8453D2);--gradient-magical:linear-gradient(90deg, #672D92, #5677C8);--gradient-purple-radial:radial-gradient(271.77% 163.1% at 50% -10.71%, #200E32 0%, #9E00FF 75.14%, #EC56FF 100%);--gradient-switch-off:linear-gradient(to right, #BBB, #666);--gradient-switch-on:linear-gradient(to right, #672C92, #547CCC);--blue-25:#F1F8FF;--blue-50:#D5E7FC;--blue-100:#B2D7FF;--blue-300:#2297F6;--blue-400:#007AFF;--blue-500:#4E32FF;--blue-550:#0C4DAF;--blue-600:#003A69;--blue-800:#002442}.light{--color-bg-page:#F9F9F9;--color-bg-sidebar:#FBFBFB;--color-bg-card:#FFFFFF;--color-bg-nav:#FBFBFB;--color-bg-elevated:#FFFFFF;--color-bg-secondary:#FFFFFF;--color-bg-tertiary:#FBFBFB;--color-bg-quaternary:#B2D7FF;--color-bg-hover:#D5E7FC;--color-bg-hover-strong:#D5E7FC;--color-bg-input:#FFFFFF;--color-bg-input-prefix:#EEEEEE;--color-bg-btn-primary:#D5E7FC;--color-bg-btn-primary-h:#B2D7FF;--color-bg-pagination-active:#D5E7FC;--color-text-primary:#333333;--color-text-secondary:#333333;--color-text-tertiary:#333333;--color-text-muted:#666666;--color-text-placeholder:#999999;--color-text-link:#007AFF;--color-text-link-hover:#0C4DAF;--color-text-inverse:#FFFFFF;--color-text-heading:#007AFF;--color-text-nav:#FFFFFF;--color-border-primary:#CCCCCC;--color-border-secondary:#BBBBBB;--color-border-structural:#E5E5E5;--color-border-subtle:#999999;--color-border-accent:#007AFF;--color-border-focus:#000000;--color-border-error:#F9303C;--color-border-panel:#E5E5E5;--color-border-btn-secondary:transparent;--color-border-btn-secondary-h:#007AFF;--color-icon-primary:#666666;--color-icon-secondary:#333333;--color-icon-tertiary:#707070;--color-icon-error:#F9303C;--color-success:#259F4C;--color-success-bg:#E6F7E6;--color-success-border:#259F4C;--color-success-text:#259F4C;--color-error:#F9303C;--color-error-bg:#F0E2E3;--color-error-border:#FE3B4C;--color-error-text:#FE3B4C;--color-warning:#F5A534;--color-warning-bg:#FAF2E7;--color-warning-border:#F5A534;--color-warning-text:#F5A534;--color-info:#2297F6;--color-info-bg:#D5E7FC;--color-info-border:#2297F6;--color-info-text:#2297F6;--color-purple:#8B5CF6;--color-purple-bg:#F3E8FF;--color-purple-border:#F3E8FF;--color-purple-text:#C084FC;--color-cyan:#06B6D4;--color-cyan-bg:#DFFAFF;--color-cyan-border:#005866;--status-not-started-text:#A0A0A0;--status-not-started-bg:#EEEEEE;--status-not-started-border:#999999;--status-in-progress-text:#2297F6;--status-in-progress-bg:#D5E7FC;--status-in-progress-border:#2297F6;--status-pending-text:#06B6D4;--status-pending-bg:#DFFAFF;--status-pending-border:#06B6D4;--status-success-text:#259F4C;--status-success-bg:#E6F7E6;--status-success-border:#259F4C;--status-error-text:#FE3B4C;--status-error-bg:#F0E2E3;--status-error-border:#FE3B4C;--status-warning-text:#F5A534;--status-warning-bg:#FAF2E7;--status-warning-border:#F5A534;--status-advanced-text:#8B5CF6;--status-advanced-bg:#F3E8FF;--status-advanced-border:#C084FC;--gradient-primary-btn:linear-gradient(90deg, #3676f7, #cc22f2);--gradient-magical:linear-gradient(90deg, #3676f7, #cc22f2);--gradient-switch-off:linear-gradient(to right, #fff, #fff);--gradient-switch-on:linear-gradient(to right, #007AFF, #007AFF);--shadow-sm:0 1px 2px rgba(0, 0, 0, 0.06);--shadow-md:0 4px 6px -1px rgba(0, 0, 0, 0.08),0 2px 4px -2px rgba(0, 0, 0, 0.06);--shadow-lg:0 10px 15px -3px rgba(0, 0, 0, 0.1),0 4px 6px -4px rgba(0, 0, 0, 0.08)}*,::after,::before{box-sizing:border-box;margin:0;padding:0}html{font-size:16px;-webkit-text-size-adjust:100%;scroll-behavior:smooth}body{font-family:var(--font-sans);font-size:var(--text-sm);line-height:1.5;color:var(--color-text-primary);background-color:var(--color-bg-page);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}a{color:var(--color-text-link);text-decoration:none;transition:color var(--transition-fast)}a:hover{color:var(--color-text-link-hover);text-decoration:underline}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:0 0}::-webkit-scrollbar-thumb{background:var(--color-border-subtle);border-radius:var(--radius-full)}::-webkit-scrollbar-thumb:hover{background:var(--color-border-secondary)}::selection{background:rgba(34,151,246,.3);color:var(--color-text-primary)}:focus-visible{outline:2px solid var(--color-border-focus);outline-offset:2px;border-radius:var(--radius-sm)}img,svg,video{display:block;max-width:100%}ol,ul{list-style:none}code,kbd,pre,samp{font-family:var(--font-mono)}code{font-size:.875em;background:var(--color-bg-quaternary);color:var(--color-text-secondary);padding:.1em .4em;border-radius:var(--radius-sm);border:1px solid var(--color-border-primary)}pre{background:var(--color-bg-quaternary);border:1px solid var(--color-border-primary);border-radius:var(--radius-lg);padding:var(--space-4);overflow-x:auto;font-size:var(--text-xs);line-height:1.6;color:var(--color-text-secondary)}pre code{background:0 0;border:none;padding:0;font-size:inherit}.divider,hr{border:none;border-top:1px solid var(--color-border-structural);margin:var(--space-4) 0}.container{width:100%;max-width:1200px;margin:0 auto;padding:0 var(--space-4)}.section{padding:var(--space-8) 0}.icon-xs{width:12px;height:12px;flex-shrink:0}.icon-sm{width:16px;height:16px;flex-shrink:0}.icon-md{width:18px;height:18px;flex-shrink:0}.icon-lg{width:20px;height:20px;flex-shrink:0}.icon-xl{width:24px;height:24px;flex-shrink:0}.icon-2xl{width:32px;height:32px;flex-shrink:0}.h1,h1{font-size:var(--text-h1);line-height:var(--lh-h1);font-weight:700;color:var(--color-text-primary);letter-spacing:-.02em}.h2,h2{font-size:var(--text-h2);line-height:var(--lh-h2);font-weight:600;color:var(--color-text-primary)}.h3,h3{font-size:var(--text-h3);line-height:var(--lh-h3);font-weight:600;color:var(--color-text-primary)}.h4,h4{font-size:var(--text-h4);line-height:var(--lh-h4);font-weight:600;color:var(--color-text-primary)}.h5,h5{font-size:var(--text-h5);line-height:var(--lh-h5);font-weight:600;color:var(--color-text-primary)}.h6,h6{font-size:var(--text-xs);line-height:1.4;font-weight:600;color:var(--color-text-muted);text-transform:uppercase;letter-spacing:.05em}.section-label{font-size:var(--text-xs);font-weight:600;color:var(--color-text-heading);text-transform:uppercase;letter-spacing:.05em;margin-bottom:var(--space-2)}.text-2xl{font-size:1.5rem}.text-xl{font-size:1.25rem}.text-lg{font-size:1.125rem}.text-base{font-size:var(--text-base)}.text-sm{font-size:var(--text-sm)}.text-sm-1{font-size:var(--text-sm-1)}.text-xs{font-size:var(--text-xs)}.text-xs-1{font-size:var(--text-xs-1)}.font-normal{font-weight:400}.font-medium{font-weight:500}.font-semibold{font-weight:600}.font-bold{font-weight:700}.text-primary{color:var(--color-text-primary)}.text-secondary{color:var(--color-text-secondary)}.text-tertiary{color:var(--color-text-tertiary)}.text-muted{color:var(--color-text-muted)}.text-link{color:var(--color-text-link)}.text-inverse{color:var(--color-text-inverse)}.text-success{color:var(--color-success-text)}.text-error{color:var(--color-error-text)}.text-warning{color:var(--color-warning-text)}.text-info{color:var(--color-info-text)}.uppercase{text-transform:uppercase}.capitalize{text-transform:capitalize}.lowercase{text-transform:lowercase}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.line-clamp-2{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.line-clamp-3{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}.nowrap{white-space:nowrap}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}p{color:var(--color-text-secondary);font-size:var(--text-sm);line-height:1.6}p+p{margin-top:var(--space-3)}.caption{font-size:var(--text-xs);color:var(--color-text-muted);line-height:1.4}.mono{font-family:var(--font-mono);font-size:.9em}.btn{display:inline-flex;align-items:center;justify-content:center;gap:var(--space-1-5);font-family:var(--font-sans);font-weight:600;white-space:nowrap;border-radius:var(--radius-lg);border:1px solid transparent;cursor:pointer;transition:background-color var(--transition-base),border-color var(--transition-base),color var(--transition-base),opacity var(--transition-base);text-decoration:none;user-select:none;outline:0;position:relative;overflow:hidden;-webkit-font-smoothing:antialiased}.btn:focus-visible{outline:2px solid var(--color-border-focus);outline-offset:2px}.btn:disabled,.btn[aria-disabled=true]{cursor:not-allowed;opacity:.5;pointer-events:none}.btn-sm{height:24px;padding:0 var(--space-1-5);gap:var(--space-1);font-size:var(--text-xs);line-height:20px}.btn-md{height:28px;padding:0 var(--space-2);gap:var(--space-1-5);font-size:var(--text-xs);line-height:24px}.btn-lg{height:44px;padding:0 var(--space-4);gap:var(--space-2-5);font-size:var(--text-sm);line-height:28px}.btn-primary{background-color:var(--color-bg-btn-primary);color:var(--color-text-secondary);border:1px solid transparent;background-image:linear-gradient(var(--color-bg-btn-primary),var(--color-bg-btn-primary)),var(--gradient-primary-btn);background-origin:border-box;background-clip:padding-box,border-box}.btn-primary:hover:not(:disabled){background-image:linear-gradient(var(--color-bg-btn-primary-h),var(--color-bg-btn-primary-h)),var(--gradient-primary-btn)}.btn-secondary{background-color:var(--color-bg-secondary);color:var(--color-text-secondary);border-color:var(--color-border-btn-secondary)}.btn-secondary:hover:not(:disabled){background-color:var(--color-bg-hover-strong);border-color:var(--color-border-btn-secondary-h)}.btn-base{background-color:var(--color-bg-secondary);color:var(--color-text-primary);border-color:var(--color-border-structural)}.btn-base:hover:not(:disabled){background-color:var(--color-border-structural)}.btn-action{background-color:var(--color-bg-btn-primary);color:var(--color-text-secondary);border-color:var(--color-bg-input-prefix)}.btn-action:hover:not(:disabled){border-color:var(--color-border-btn-secondary-h);background-color:var(--color-bg-btn-primary-h)}.btn-delete{background-color:rgba(254,59,76,.1);color:var(--color-error-text);border-color:var(--color-error-border)}.btn-delete:hover:not(:disabled){background-color:rgba(254,59,76,.15);border-color:var(--color-error)}.btn-ghost,.btn-tertiary{background-color:transparent;color:var(--color-text-primary);border-color:transparent}.btn-ghost:hover:not(:disabled),.btn-tertiary:hover:not(:disabled){background-color:var(--color-border-structural)}.btn-magical{background:var(--gradient-magical);color:var(--color-text-inverse);border-color:var(--color-border-structural)}.btn-magical:hover:not(:disabled){filter:brightness(1.1)}.btn-link{background:0 0;border-color:transparent;color:var(--color-text-link);padding-left:0;padding-right:0;font-weight:500;height:auto}.btn-link:hover:not(:disabled){text-decoration:underline}.btn-icon{padding:0;aspect-ratio:1}.btn-icon.btn-sm{width:24px}.btn-icon.btn-md{width:28px}.btn-icon.btn-lg{width:44px}.btn-full{width:100%}.btn-loading{cursor:wait;pointer-events:none}.btn-loading::after{content:'';position:absolute;inset:0;background:linear-gradient(90deg,transparent 0,rgba(255,255,255,.12) 50%,transparent 100%);background-size:200% 100%;animation:btn-shimmer 1.5s infinite linear}@keyframes btn-shimmer{from{background-position:-200% 0}to{background-position:200% 0}}.btn-group{display:inline-flex;gap:0}.btn-group .btn{border-radius:0}.btn-group .btn:first-child{border-radius:var(--radius-lg) 0 0 var(--radius-lg)}.btn-group .btn:last-child{border-radius:0 var(--radius-lg) var(--radius-lg) 0}.btn-group .btn:not(:first-child){margin-left:-1px}.form-group{display:flex;flex-direction:column;gap:var(--space-1)}.form-label{display:flex;align-items:center;gap:var(--space-0-5);font-size:var(--text-xs);font-weight:500;color:var(--color-text-muted)}.form-label .required{color:var(--color-error);margin-left:1px}.input-wrapper{display:flex;align-items:stretch;min-height:32px;max-height:32px;border:1px solid var(--color-border-primary);border-radius:var(--radius-lg);background-color:var(--color-bg-input);transition:border-color var(--transition-base),box-shadow var(--transition-base);overflow:hidden}.input-wrapper:hover:not(.input-disabled){border-color:var(--color-border-secondary)}.input-wrapper:focus-within:not(.input-disabled){border-color:var(--color-border-secondary)}.input-wrapper.input-error{border-color:var(--color-border-error)}.input-wrapper.input-disabled{opacity:.6;cursor:not-allowed}.form-input{flex:1;background:0 0;border:none;outline:0;font-family:var(--font-sans);font-size:var(--text-sm);color:var(--color-text-primary);padding:0 var(--space-2);min-width:0}.form-input::placeholder{color:var(--color-text-placeholder)}.form-input:disabled{cursor:not-allowed}.input,input.input{display:block;width:100%;height:32px;padding:0 var(--space-2);background-color:var(--color-bg-input);border:1px solid var(--color-border-primary);border-radius:var(--radius-lg);font-family:var(--font-sans);font-size:var(--text-sm);color:var(--color-text-primary);outline:0;transition:border-color var(--transition-base)}.input::placeholder,input.input::placeholder{color:var(--color-text-placeholder)}.input:hover,input.input:hover{border-color:var(--color-border-secondary)}.input:focus,input.input:focus{border-color:var(--color-border-secondary)}.input.input-error,input.input.input-error{border-color:var(--color-border-error)}.input:disabled,input.input:disabled{opacity:.6;cursor:not-allowed}.input-lg{height:40px;font-size:var(--text-base);padding:0 var(--space-3)}.input-prefix,.input-suffix{display:flex;align-items:center;padding:0 var(--space-2);font-size:var(--text-xs);color:var(--color-text-tertiary);background-color:var(--color-bg-input-prefix);white-space:nowrap;flex-shrink:0;border-right:1px solid var(--color-border-subtle)}.input-suffix{border-right:none;border-left:1px solid var(--color-border-subtle)}.input-adornment{display:flex;align-items:center;padding:0 var(--space-2);color:var(--color-icon-tertiary);flex-shrink:0}.textarea,textarea.textarea{display:block;width:100%;min-height:80px;max-height:384px;padding:var(--space-2-5) var(--space-3);background-color:var(--color-bg-input);border:1px solid var(--color-border-primary);border-radius:var(--radius-lg);font-family:var(--font-sans);font-size:var(--text-sm);color:var(--color-text-primary);outline:0;resize:vertical;line-height:1.5;transition:border-color var(--transition-base)}.textarea::placeholder,textarea.textarea::placeholder{color:var(--color-text-placeholder)}.textarea:hover,textarea.textarea:hover{border-color:var(--color-border-secondary)}.textarea:focus,textarea.textarea:focus{border-color:var(--color-border-secondary)}.textarea.textarea-error,textarea.textarea.textarea-error{border-color:var(--color-border-error)}.textarea:disabled,textarea.textarea:disabled{background-color:var(--color-bg-secondary);color:var(--color-text-tertiary);cursor:not-allowed;opacity:.7}.select-trigger{display:flex;align-items:center;justify-content:space-between;height:32px;padding:0 var(--space-2);background-color:var(--color-bg-input);border:1px solid var(--color-border-primary);border-radius:var(--radius-lg);font-family:var(--font-sans);font-size:var(--text-sm);color:var(--color-text-primary);cursor:pointer;user-select:none;transition:border-color var(--transition-base);gap:var(--space-2)}.select-trigger:hover{border-color:var(--color-border-secondary)}.select-trigger.select-open{border-color:var(--color-border-secondary)}.select-trigger .select-arrow{color:var(--color-icon-tertiary);flex-shrink:0;transition:transform var(--transition-base)}.select-trigger.select-open .select-arrow{transform:rotate(180deg)}select.select{display:block;width:100%;height:32px;padding:0 var(--space-2);background-color:var(--color-bg-input);border:1px solid var(--color-border-primary);border-radius:var(--radius-lg);font-family:var(--font-sans);font-size:var(--text-sm);color:var(--color-text-primary);outline:0;cursor:pointer;appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23999' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 8px center;padding-right:28px}select.select:hover{border-color:var(--color-border-secondary)}select.select:focus{border-color:var(--color-border-secondary)}.select-panel{position:absolute;z-index:var(--z-dropdown);min-width:160px;max-width:256px;margin-top:var(--space-2);padding:var(--space-1-5);background-color:var(--color-bg-elevated);border:1px solid var(--color-border-panel);border-radius:var(--radius-lg);box-shadow:var(--shadow-md);max-height:240px;overflow-y:auto}.select-option{display:block;padding:var(--space-1-5) var(--space-2-5);font-size:var(--text-sm);color:var(--color-text-primary);border-radius:var(--radius-md);cursor:pointer;transition:background-color var(--transition-fast);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.select-option:hover{background-color:var(--color-bg-hover-strong)}.select-option.selected{background-color:var(--color-bg-hover);font-weight:500}.checkbox-wrapper{display:inline-flex;align-items:center;gap:var(--space-2);cursor:pointer;user-select:none}.checkbox-wrapper input[type=checkbox]{position:absolute;opacity:0;width:0;height:0}.checkbox-box{display:flex;align-items:center;justify-content:center;width:20px;height:20px;flex-shrink:0;border-radius:var(--radius-md);border:2px solid var(--color-border-primary);background-color:var(--color-bg-elevated);transition:border-color var(--transition-fast),background-color var(--transition-fast)}.checkbox-wrapper:hover .checkbox-box{border-color:var(--color-border-secondary)}.checkbox-wrapper input[type=checkbox]:checked+.checkbox-box{border-color:var(--color-text-primary);background-color:var(--color-text-primary)}.checkbox-check{display:none;width:12px;height:12px;color:var(--color-bg-page)}.checkbox-wrapper input[type=checkbox]:checked+.checkbox-box .checkbox-check{display:block}.checkbox-label{font-size:var(--text-sm);color:var(--color-text-primary)}.checkbox-wrapper.disabled{opacity:.6;cursor:not-allowed;pointer-events:none}.radio-wrapper{display:inline-flex;align-items:center;gap:var(--space-2);cursor:pointer;user-select:none}.radio-wrapper input[type=radio]{position:absolute;opacity:0;width:0;height:0}.radio-dot{display:flex;align-items:center;justify-content:center;width:18px;height:18px;flex-shrink:0;border-radius:var(--radius-full);border:2px solid var(--color-border-primary);background-color:var(--color-bg-elevated);transition:border-color var(--transition-fast),background-color var(--transition-fast)}.radio-dot::after{content:'';width:8px;height:8px;border-radius:var(--radius-full);background-color:var(--color-bg-page);opacity:0;transition:opacity var(--transition-fast)}.radio-wrapper:hover .radio-dot{border-color:var(--color-border-secondary)}.radio-wrapper input[type=radio]:checked+.radio-dot{border-color:var(--color-text-primary);background-color:var(--color-text-primary)}.radio-wrapper input[type=radio]:checked+.radio-dot::after{opacity:1}.radio-label{font-size:var(--text-sm);color:var(--color-text-primary)}.switch-wrapper{display:inline-flex;align-items:center;gap:var(--space-2);cursor:pointer;user-select:none}.switch-wrapper input[type=checkbox]{position:absolute;opacity:0;width:0;height:0}.switch-track{position:relative;width:32px;height:16px;border-radius:var(--radius-full);background:var(--gradient-switch-off);border:1px solid var(--color-border-primary);transition:background var(--transition-base),border-color var(--transition-base);flex-shrink:0}.switch-wrapper input[type=checkbox]:checked+.switch-track{background:var(--gradient-switch-on);border-color:var(--color-border-accent)}.switch-knob{position:absolute;top:1px;left:2px;width:12px;height:12px;border-radius:var(--radius-full);background-color:#ccc;transition:transform var(--transition-base),background-color var(--transition-base);pointer-events:none}.switch-wrapper input[type=checkbox]:checked~.switch-track .switch-knob{transform:translateX(16px);background-color:#fff}.switch-track.switch-on .switch-knob{transform:translateX(16px);background-color:#fff}.switch-label{font-size:var(--text-xs);color:var(--color-text-muted);transition:color var(--transition-fast)}.switch-wrapper:hover .switch-label{color:var(--color-border-accent)}.form-error{font-size:var(--text-sm);color:var(--color-error-text);display:flex;align-items:center;gap:var(--space-1);margin-top:var(--space-0-5)}.form-helper{font-size:var(--text-xs);color:var(--color-text-muted);margin-top:var(--space-0-5)}.search-input-wrapper{position:relative;display:flex;align-items:center}.search-input-wrapper .search-icon{position:absolute;left:var(--space-2);color:var(--color-icon-tertiary);pointer-events:none}.search-input-wrapper input{padding-left:28px}.card{display:flex;flex-direction:column;background-color:var(--color-bg-card);border:1px solid var(--color-border-structural);border-radius:var(--radius-xl);overflow:hidden;transition:border-color var(--transition-base),box-shadow var(--transition-base)}.card:hover{border-color:var(--color-border-secondary)}.card-fixed{height:var(--card-height)}.card-header{display:flex;align-items:center;justify-content:space-between;padding:var(--space-4);border-bottom:1px solid var(--color-border-structural)}.card-body{flex:1;padding:var(--space-4);overflow:hidden}.card-footer{padding:var(--space-3) var(--space-4);border-top:1px solid var(--color-border-structural);display:flex;align-items:center;gap:var(--space-2)}.card-title{font-size:var(--text-base);font-weight:600;color:var(--color-text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.card-subtitle{font-size:var(--text-xs);color:var(--color-text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.card-description{font-size:var(--text-xs);color:var(--color-text-tertiary);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.panel{background-color:var(--color-bg-secondary);border:1px solid var(--color-border-panel);border-radius:var(--radius-lg);overflow:hidden}.panel-header{display:flex;align-items:center;justify-content:space-between;padding:var(--space-3) var(--space-4);border-bottom:1px solid var(--color-border-structural);background-color:var(--color-bg-tertiary)}.panel-body{padding:var(--space-4)}.badge{display:inline-flex;align-items:center;gap:var(--space-1-5);padding:0 var(--space-2);height:17px;line-height:17px;border-radius:var(--radius-full);border:1px solid;font-size:var(--text-xs-1);font-weight:700;text-transform:uppercase;letter-spacing:.04em;white-space:nowrap;user-select:none;flex-shrink:0}.badge-dot{width:7px;height:7px;border-radius:var(--radius-full);flex-shrink:0;display:inline-block}.badge-not-started{background-color:var(--status-not-started-bg);color:var(--status-not-started-text);border-color:var(--status-not-started-border)}.badge-not-started .badge-dot{background-color:var(--status-not-started-text)}.badge-in-progress{background-color:var(--status-in-progress-bg);color:var(--status-in-progress-text);border-color:var(--status-in-progress-border)}.badge-in-progress .badge-dot{background-color:var(--status-in-progress-text);animation:badge-pulse 2s cubic-bezier(.4,0,.6,1) infinite}.badge-pending{background-color:var(--status-pending-bg);color:var(--status-pending-text);border-color:var(--status-pending-border)}.badge-pending .badge-dot{background-color:var(--status-pending-text)}.badge-success{background-color:var(--status-success-bg);color:var(--status-success-text);border-color:var(--status-success-border)}.badge-success .badge-dot{background-color:var(--status-success-text)}.badge-error{background-color:var(--status-error-bg);color:var(--status-error-text);border-color:var(--status-error-border)}.badge-error .badge-dot{background-color:var(--status-error-text)}.badge-warning{background-color:var(--status-warning-bg);color:var(--status-warning-text);border-color:var(--status-warning-border)}.badge-warning .badge-dot{background-color:var(--status-warning-text)}.badge-advanced{background-color:var(--status-advanced-bg);color:var(--status-advanced-text);border-color:var(--status-advanced-border)}.badge-advanced .badge-dot{background-color:var(--status-advanced-text)}@keyframes badge-pulse{0%,100%{opacity:1}50%{opacity:.4}}.tag{display:inline-flex;align-items:center;gap:var(--space-1);padding:var(--space-1) var(--space-2);border-radius:var(--radius-lg);border:1px solid var(--color-border-secondary);background-color:var(--color-bg-secondary);font-size:var(--text-xs);font-weight:600;color:var(--color-text-tertiary);white-space:nowrap;user-select:none}.tag-sm{padding:1px var(--space-1-5);font-size:var(--text-xs-1);border-radius:var(--radius-md)}.tag-blue{background-color:var(--color-info-bg);border-color:var(--color-info-border);color:var(--color-info-text)}.tag-green{background-color:var(--color-success-bg);border-color:var(--color-success-border);color:var(--color-success-text)}.tag-red{background-color:var(--color-error-bg);border-color:var(--color-error-border);color:var(--color-error-text)}.tag-yellow{background-color:var(--color-warning-bg);border-color:var(--color-warning-border);color:var(--color-warning-text)}.tag-purple{background-color:var(--color-purple-bg);border-color:var(--color-purple-border);color:var(--color-purple-text)}.alert{display:flex;align-items:flex-start;gap:var(--space-2);padding:var(--space-2) var(--space-3);border-radius:var(--radius-md);border:1px solid;font-size:var(--text-xs);line-height:1.5}.alert-icon{flex-shrink:0;margin-top:1px}.alert-info{background-color:var(--color-info-bg);border-color:var(--color-info-border);color:var(--color-info-text)}.alert-success{background-color:var(--color-success-bg);border-color:var(--color-success-border);color:var(--color-success-text)}.alert-warning{background-color:var(--color-warning-bg);border-color:var(--color-warning-border);color:var(--color-warning-text)}.alert-error{background-color:var(--color-error-bg);border-color:var(--color-error-border);color:var(--color-error-text)}.avatar{display:flex;align-items:center;justify-content:center;border-radius:var(--radius-full);border:1px solid var(--color-border-secondary);background-color:var(--color-bg-elevated);overflow:hidden;flex-shrink:0;user-select:none;font-weight:600;color:#fff;font-size:var(--text-xs)}.avatar img{width:100%;height:100%;object-fit:cover;border-radius:var(--radius-full)}.avatar-xs{width:20px;height:20px;font-size:8px;border-width:1px}.avatar-sm{width:24px;height:24px;font-size:9px;border-width:1px}.avatar-md{width:32px;height:32px;font-size:var(--text-xs)}.avatar-chat{width:40px;height:40px;font-size:var(--text-sm)}.avatar-lg{width:72px;height:72px;font-size:var(--text-h3);border-width:2px}.avatar-xl{width:96px;height:96px;font-size:var(--text-h2);border-width:2px}.avatar-modal{width:176px;height:176px;font-size:var(--text-h1);border-width:2px}.avatar-color-0{background-color:#aa47bc}.avatar-color-1{background-color:#7a1fa2}.avatar-color-2{background-color:#6b8592}.avatar-color-3{background-color:#465a65}.avatar-color-4{background-color:#ec407a}.avatar-color-5{background-color:#c2175b}.avatar-color-6{background-color:#5c6bc0}.avatar-color-7{background-color:#0288d1}.avatar-color-8{background-color:#00579c}.avatar-color-9{background-color:#0098a6}.avatar-color-10{background-color:#00887a}.avatar-color-11{background-color:#004c3f}.avatar-color-12{background-color:#689f39}.avatar-color-13{background-color:#33691e}.avatar-color-14{background-color:#8c6e63}.avatar-color-15{background-color:#5d4038}.avatar-color-16{background-color:#7e57c2}.avatar-color-17{background-color:#512da7}.avatar-color-18{background-color:#ef6c00}.avatar-color-19{background-color:#f5511e}.avatar-color-20{background-color:#aa3410}.avatar-group{display:flex;align-items:center}.avatar-group .avatar{margin-left:-8px}.avatar-group .avatar:first-child{margin-left:0}.spinner{display:inline-block;border-radius:var(--radius-full);border-style:solid;border-color:var(--color-border-secondary);border-top-color:var(--color-text-primary);animation:spinner-spin .8s linear infinite;flex-shrink:0}.spinner-xs{width:12px;height:12px;border-width:1.5px}.spinner-sm{width:16px;height:16px;border-width:2px}.spinner-md{width:24px;height:24px;border-width:2px}.spinner-lg{width:32px;height:32px;border-width:3px}.spinner-xl{width:48px;height:48px;border-width:3px}.spinner-page{display:flex;justify-content:center;align-items:center;min-height:200px}@keyframes spinner-spin{to{transform:rotate(360deg)}}.progress-track{position:relative;overflow:hidden;background-color:var(--color-bg-tertiary);border-radius:68px;border:1px solid var(--color-border-secondary);width:85px;height:14px}.progress-fill{height:100%;background:var(--gradient-primary-btn);border-radius:68px;transition:width .2s ease-out}.progress-label{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);font-size:9px;line-height:10px;font-weight:600;color:var(--color-text-primary);white-space:nowrap}.progress-bar{width:100%;height:4px;background-color:var(--color-bg-tertiary);border-radius:var(--radius-full);overflow:hidden}.progress-bar .progress-fill{height:4px;border-radius:var(--radius-full)}.tooltip-wrapper{position:relative;display:inline-flex}.tooltip{position:absolute;z-index:var(--z-toast);bottom:calc(100% + 8px);left:50%;transform:translateX(-50%);background-color:var(--color-bg-elevated);color:var(--color-text-primary);padding:var(--space-2) var(--space-3);border-radius:var(--radius-md);font-size:var(--text-xs);line-height:1;white-space:nowrap;box-shadow:var(--shadow-md);pointer-events:none;opacity:0;transition:opacity var(--transition-fast)}.tooltip-wrapper:hover .tooltip{opacity:1}.tooltip-right{bottom:auto;top:50%;left:calc(100% + 8px);transform:translateY(-50%)}.tooltip-bottom{bottom:auto;top:calc(100% + 8px)}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:var(--space-8) var(--space-4);text-align:center;gap:var(--space-3)}.empty-state-icon{color:var(--color-icon-tertiary);margin-bottom:var(--space-1)}.empty-state-title{font-size:var(--text-h4);font-weight:600;color:var(--color-text-primary)}.empty-state-description{font-size:var(--text-sm);color:var(--color-text-muted);max-width:320px}.chip{display:inline-flex;align-items:center;gap:var(--space-1);padding:2px var(--space-1-5);border-radius:var(--radius-full);background-color:var(--color-bg-quaternary);border:1px solid var(--color-border-primary);font-size:var(--text-xs);color:var(--color-text-secondary)}.chip-close{display:flex;align-items:center;justify-content:center;width:14px;height:14px;border-radius:var(--radius-full);background:0 0;border:none;color:var(--color-icon-tertiary);cursor:pointer;padding:0;transition:color var(--transition-fast),background-color var(--transition-fast)}.chip-close:hover{color:var(--color-text-primary);background-color:var(--color-bg-hover)}.stat-card{display:flex;flex-direction:column;gap:var(--space-1);padding:var(--space-3) var(--space-4);background-color:var(--color-bg-card);border:1px solid var(--color-border-structural);border-radius:var(--radius-xl);min-width:0}.stat-card-label{font-size:var(--text-xs-1);font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--color-text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.stat-card-value{font-size:var(--text-h2);font-weight:700;line-height:1.2;color:var(--color-text-primary)}.stat-card-desc{font-size:var(--text-xs);color:var(--color-text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.stat-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:var(--space-3)}.dl-grid{display:grid;grid-template-columns:max-content 1fr;gap:var(--space-2) var(--space-4);font-size:var(--text-sm)}.dl-grid dt{color:var(--color-text-muted);font-weight:500;white-space:nowrap}.dl-grid dd{color:var(--color-text-primary)}.table-wrapper{width:100%;overflow-x:auto}.table,table.table{width:100%;border-collapse:separate;border-spacing:0;font-size:var(--text-xs);line-height:1.3}.table thead tr th{text-align:left;padding:var(--space-2-5) var(--space-4);font-weight:600;color:var(--color-text-primary);background-color:var(--color-bg-quaternary);border-top:1px solid var(--color-border-structural);border-bottom:1px solid var(--color-border-structural);white-space:nowrap;position:sticky;top:0;z-index:1}.table thead tr th:first-child{border-left:1px solid var(--color-border-structural);border-radius:var(--radius-lg) 0 0 0}.table thead tr th:last-child{border-right:1px solid var(--color-border-structural);border-radius:0 var(--radius-lg) 0 0}.table tbody tr td{padding:var(--space-3) var(--space-4);color:var(--color-text-secondary);border-bottom:1px solid var(--color-border-structural);vertical-align:middle}.table tbody tr:hover td{background-color:var(--color-bg-hover)}.table tbody tr:last-child td{border-bottom:none}.th-sortable{cursor:pointer;user-select:none;gap:var(--space-1)}.th-sortable:hover{color:var(--color-text-link)}.sort-icon{display:inline-flex;flex-direction:column;gap:1px;color:var(--color-icon-tertiary);flex-shrink:0}.th-sortable.sort-asc .sort-icon,.th-sortable.sort-desc .sort-icon{color:var(--color-text-primary)}.td-number{text-align:right;font-variant-numeric:tabular-nums}.tabs{display:flex;flex-direction:column}.tabs-list{display:flex;align-items:stretch;border-bottom:1px solid var(--color-border-panel);flex-shrink:0}.tab-item{display:inline-flex;align-items:center;justify-content:center;gap:var(--space-2);padding:var(--space-2) var(--space-2);padding-bottom:var(--space-4);font-size:var(--text-sm);color:var(--color-text-primary);cursor:pointer;border-bottom:2px solid transparent;transition:border-color var(--transition-base),color var(--transition-base);text-decoration:none;white-space:nowrap;user-select:none;background:0 0;border-top:none;border-left:none;border-right:none;font-family:var(--font-sans)}.tab-item:hover{border-bottom-color:var(--color-text-tertiary)}.tab-item.active,.tab-item[aria-selected=true]{border-bottom-color:var(--color-text-primary);font-weight:600;cursor:default}.tabs-sm .tab-item{font-size:var(--text-xs);padding:var(--space-2-5);padding-bottom:var(--space-2-5)}.tabs-panel{flex:1;padding-top:var(--space-4)}.pagination{display:flex;align-items:center;gap:var(--space-2);font-size:var(--text-sm);padding:var(--space-3) 0;border-top:1px solid var(--color-border-structural)}.pagination-info{color:var(--color-text-muted);font-size:var(--text-h5);margin-right:auto}.page-btn{display:flex;align-items:center;justify-content:center;height:32px;min-width:32px;padding:0 var(--space-2);background-color:var(--color-bg-secondary);border:1px solid var(--color-border-structural);border-radius:var(--radius-lg);color:var(--color-text-link);font-size:var(--text-h5);cursor:pointer;transition:border-color var(--transition-fast),background-color var(--transition-fast);user-select:none;text-decoration:none;font-family:var(--font-sans);font-weight:400}.page-btn:hover:not(.disabled){border-color:var(--color-border-accent)}.page-btn.active{border-color:var(--color-border-accent);background-color:var(--color-bg-pagination-active);font-weight:600}.page-btn.disabled{opacity:.4;cursor:not-allowed;pointer-events:none}.modal-overlay{position:fixed;inset:0;background-color:rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center;z-index:var(--z-modal);padding:var(--space-4)}.modal{display:flex;flex-direction:column;background-color:var(--color-bg-elevated);border:1px solid var(--color-border-panel);border-radius:var(--radius-lg);box-shadow:var(--shadow-lg);max-height:95vh;width:100%;overflow:hidden}.modal-sm{max-width:400px}.modal-md{max-width:512px}.modal-lg{max-width:720px}.modal-xl{max-width:1024px}.modal-full{max-width:90vw}.modal-header{display:flex;align-items:center;justify-content:space-between;gap:var(--space-4);padding:var(--space-3) var(--space-4);border-bottom:1px solid var(--color-border-structural);flex-shrink:0}.modal-title{font-size:var(--text-base);font-weight:600;color:var(--color-text-primary)}.modal-close{display:flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:var(--radius-lg);border:none;background:0 0;color:var(--color-icon-tertiary);cursor:pointer;transition:color var(--transition-fast),background-color var(--transition-fast);flex-shrink:0}.modal-close:hover{color:var(--color-text-primary);background-color:var(--color-bg-quaternary)}.modal-body{flex:1;padding:var(--space-4);overflow-y:auto}.modal-footer{display:flex;align-items:center;justify-content:flex-end;gap:var(--space-3);padding:var(--space-4);border-top:1px solid var(--color-border-structural);flex-shrink:0;background-color:var(--color-bg-elevated);border-radius:0 0 var(--radius-lg) var(--radius-lg)}.nav-sidebar{display:flex;flex-direction:column;gap:var(--space-0-5);padding:var(--space-2)}.nav-item{display:flex;align-items:center;gap:var(--space-2);padding:var(--space-2) var(--space-3);border-radius:var(--radius-lg);font-size:var(--text-sm);color:var(--color-text-nav);cursor:pointer;transition:background-color var(--transition-fast),color var(--transition-fast);text-decoration:none;user-select:none;white-space:nowrap;overflow:hidden}.nav-item:hover{background-color:var(--color-bg-hover);color:var(--color-text-primary)}.nav-item.active{background-color:var(--color-bg-secondary);color:var(--color-text-primary);font-weight:500}.nav-item .nav-icon{flex-shrink:0;color:var(--color-icon-tertiary);transition:color var(--transition-fast)}.nav-item.active .nav-icon,.nav-item:hover .nav-icon{color:var(--color-icon-primary)}.nav-group-label{padding:var(--space-2) var(--space-3) var(--space-1);font-size:var(--text-xs);font-weight:600;color:var(--color-text-muted);text-transform:uppercase;letter-spacing:.05em}.app-shell{display:flex;min-height:100vh}.app-navbar{width:var(--navbar-width);min-height:100vh;background-color:var(--color-bg-nav);display:flex;flex-direction:column;align-items:center;padding:var(--space-3) 0;flex-shrink:0;border-right:1px solid var(--color-border-structural)}.app-sidebar{width:var(--sidebar-width);min-height:100vh;background-color:var(--color-bg-sidebar);display:flex;flex-direction:column;border-right:1px solid var(--color-border-structural);flex-shrink:0}.app-content{flex:1;display:flex;flex-direction:column;min-width:0;overflow:hidden}.app-header{height:var(--layout-header-height);display:flex;align-items:center;padding:0 var(--space-4);border-bottom:1px solid var(--color-border-structural);background-color:var(--color-bg-page);flex-shrink:0;gap:var(--space-4)}.app-main{flex:1;padding:var(--space-4);overflow-y:auto}.hidden{display:none!important}.block{display:block}.inline{display:inline}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.flex-col{flex-direction:column}.flex-row{flex-direction:row}.flex-wrap{flex-wrap:wrap}.flex-nowrap{flex-wrap:nowrap}.flex-1{flex:1}.flex-auto{flex:auto}.flex-none{flex:none}.flex-shrink-0{flex-shrink:0}.grow{flex-grow:1}.items-start{align-items:flex-start}.items-center{align-items:center}.items-end{align-items:flex-end}.items-stretch{align-items:stretch}.justify-start{justify-content:flex-start}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.justify-between{justify-content:space-between}.grid-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-4{grid-template-columns:repeat(4,minmax(0,1fr))}.grid-auto-fill-sm{grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}.grid-auto-fill-md{grid-template-columns:repeat(auto-fill,minmax(280px,1fr))}.grid-auto-fill-lg{grid-template-columns:repeat(auto-fill,minmax(320px,1fr))}.gap-1{gap:var(--space-1)}.gap-2{gap:var(--space-2)}.gap-3{gap:var(--space-3)}.gap-4{gap:var(--space-4)}.gap-5{gap:var(--space-5)}.gap-6{gap:var(--space-6)}.gap-8{gap:var(--space-8)}.gap-x-2{column-gap:var(--space-2)}.gap-x-3{column-gap:var(--space-3)}.gap-x-4{column-gap:var(--space-4)}.gap-y-2{row-gap:var(--space-2)}.gap-y-3{row-gap:var(--space-3)}.gap-y-4{row-gap:var(--space-4)}.p-1{padding:var(--space-1)}.p-2{padding:var(--space-2)}.p-3{padding:var(--space-3)}.p-4{padding:var(--space-4)}.p-6{padding:var(--space-6)}.p-8{padding:var(--space-8)}.px-2{padding-left:var(--space-2);padding-right:var(--space-2)}.px-3{padding-left:var(--space-3);padding-right:var(--space-3)}.px-4{padding-left:var(--space-4);padding-right:var(--space-4)}.py-1{padding-top:var(--space-1);padding-bottom:var(--space-1)}.py-2{padding-top:var(--space-2);padding-bottom:var(--space-2)}.py-3{padding-top:var(--space-3);padding-bottom:var(--space-3)}.py-4{padding-top:var(--space-4);padding-bottom:var(--space-4)}.pt-2{padding-top:var(--space-2)}.pt-4{padding-top:var(--space-4)}.pt-8{padding-top:var(--space-8)}.pb-2{padding-bottom:var(--space-2)}.pb-4{padding-bottom:var(--space-4)}.m-auto{margin:auto}.mx-auto{margin-left:auto;margin-right:auto}.ml-auto{margin-left:auto}.mr-auto{margin-right:auto}.mt-1{margin-top:var(--space-1)}.mt-2{margin-top:var(--space-2)}.mt-3{margin-top:var(--space-3)}.mt-4{margin-top:var(--space-4)}.mt-6{margin-top:var(--space-6)}.mt-8{margin-top:var(--space-8)}.mb-1{margin-bottom:var(--space-1)}.mb-2{margin-bottom:var(--space-2)}.mb-3{margin-bottom:var(--space-3)}.mb-4{margin-bottom:var(--space-4)}.mb-6{margin-bottom:var(--space-6)}.w-full{width:100%}.w-auto{width:auto}.h-full{height:100%}.min-h-screen{min-height:100vh}.min-w-0{min-width:0}.max-w-sm{max-width:384px}.max-w-md{max-width:512px}.max-w-lg{max-width:720px}.max-w-xl{max-width:960px}.max-w-full{max-width:100%}.overflow-hidden{overflow:hidden}.overflow-auto{overflow:auto}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-visible{overflow:visible}.relative{position:relative}.absolute{position:absolute}.fixed{position:fixed}.sticky{position:sticky}.inset-0{inset:0}.top-0{top:0}.bottom-0{bottom:0}.left-0{left:0}.right-0{right:0}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:var(--z-dropdown)}.z-50{z-index:var(--z-modal)}.z-60{z-index:var(--z-toast)}.rounded-sm{border-radius:var(--radius-sm)}.rounded-md{border-radius:var(--radius-md)}.rounded-lg{border-radius:var(--radius-lg)}.rounded-xl{border-radius:var(--radius-xl)}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-full{border-radius:var(--radius-full)}.bg-page{background-color:var(--color-bg-page)}.bg-card{background-color:var(--color-bg-card)}.bg-elevated{background-color:var(--color-bg-elevated)}.bg-secondary{background-color:var(--color-bg-secondary)}.bg-tertiary{background-color:var(--color-bg-tertiary)}.bg-success{background-color:var(--color-success-bg)}.bg-error{background-color:var(--color-error-bg)}.bg-warning{background-color:var(--color-warning-bg)}.bg-info{background-color:var(--color-info-bg)}.bg-brand{background:var(--gradient-brand)}.bg-magical{background:var(--gradient-magical)}.bg-purple-radial{background:var(--gradient-purple-radial)}.border{border:1px solid var(--color-border-structural)}.border-top{border-top:1px solid var(--color-border-structural)}.border-bottom{border-bottom:1px solid var(--color-border-structural)}.border-left{border-left:1px solid var(--color-border-structural)}.border-right{border-right:1px solid var(--color-border-structural)}.border-none{border:none}.cursor-pointer{cursor:pointer}.cursor-default{cursor:default}.cursor-not-allowed{cursor:not-allowed}.cursor-wait{cursor:wait}.pointer-none{pointer-events:none}.opacity-0{opacity:0}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-100{opacity:1}.select-none{user-select:none}.select-text{user-select:text}.shadow-sm{box-shadow:var(--shadow-sm)}.shadow-md{box-shadow:var(--shadow-md)}.shadow-lg{box-shadow:var(--shadow-lg)}.transition{transition:all var(--transition-base)}.transition-colors{transition:color var(--transition-base),background-color var(--transition-base),border-color var(--transition-base)} \ No newline at end of file From f87276b8c20d950c5781287d977fa3f69b41966b Mon Sep 17 00:00:00 2001 From: Kostiantyn Pshenychnyi Date: Thu, 7 May 2026 15:12:42 +0300 Subject: [PATCH 13/13] feat: add new analytics-cli commands --- .../plugin/skills/codemie-analytics/SKILL.md | 14 ++- .../scripts/analytics-cli.js | 97 ++++++++++++++++++- 2 files changed, 107 insertions(+), 4 deletions(-) diff --git a/src/agents/plugins/claude/plugin/skills/codemie-analytics/SKILL.md b/src/agents/plugins/claude/plugin/skills/codemie-analytics/SKILL.md index fa30096e..47c196ad 100644 --- a/src/agents/plugins/claude/plugin/skills/codemie-analytics/SKILL.md +++ b/src/agents/plugins/claude/plugin/skills/codemie-analytics/SKILL.md @@ -71,9 +71,21 @@ Leaderboard filters: `--view` (current/monthly/quarterly), `--season-key` (2026- | Overall usage summary (tokens, cost, users) | `summaries` | `data` — total_cost, total_tokens, total_requests, unique_users (MAU), unique_users_daily (DAU), cli_invocations, assistants_count, workflows_count, skills_count, mcp_servers_count | | User list + activity trends | `users` | `data.rows[]` — user_name, email, total_cost, total_tokens, last_active; plus `activity[]` time-series | | Per-project spending | `projects-spending` | `data.rows[]` — project_name, total_cost, total_tokens, user_count, request_count | +| Per-project activity time-series | `projects-activity` | Composite with `activity.data.rows[]` and `uniqueDaily.data.rows[]` | | LLM model breakdown | `llms-usage` | `data.rows[]` — model_name, request_count, total_tokens, input_tokens, output_tokens, total_cost | | Tool usage | `tools-usage` | `data.rows[]` — tool_name, invocation_count, success_count, error_count, total_tokens | | Workflow execution analytics | `workflows` | `data.rows[]` — workflow_name, run_count, success_count, failure_count, avg_duration_ms, total_cost | +| Agent execution analytics | `agents-usage` | `data.rows[]` — assistant_name, execution_count, total_cost, total_tokens | +| Embedding model usage | `embeddings-usage` | `data.rows[]` — model_name, request_count, total_tokens, total_cost | +| Chat assistant conversations | `assistants-chats` | `data.rows[]` — assistant, conversation_count, user_count, total_cost | +| Webhook invocation analytics | `webhooks-usage` | `data.rows[]` — user_id, invocation_count, total_cost | +| MCP server usage | `mcp-servers` | `data.rows[]` — mcp_name, request_count, user_count, total_cost | +| MCP server usage by user | `mcp-servers-by-users` | `data.rows[]` — user_name, mcp_name, request_count | +| Power user analytics | `power-users` | `data.rows[]` — user_email, session_count, total_cost, features_used | +| Knowledge sharing metrics | `knowledge-sharing` | `data.rows[]` — user_email, shared_count, viewed_count | +| Top agents by usage | `top-agents` | `data.rows[]` — assistant_name, execution_count, total_cost | +| Top workflows by usage | `top-workflows` | `data.rows[]` — workflow_name, run_count, total_cost | +| Assets published to marketplace | `marketplace` | `data.rows[]` — user_email, asset_name, published_at | | Budget alerts (soft + hard limits) | `budget` | Composite with `soft.data.rows[]` and `hard.data.rows[]` — user_email, max_spent (users approaching or over limit) | | Personal spending & budget | `spending` | `data` — current_spend, budget_limit, hard_budget_limit, budget_reset_at, percentage_used | | Per-user spending (platform + cli split) | `spending-by-users` | Composite with `platform.data.rows[]` and `cli.data.rows[]` — user_name/email, total_cost, token_count | @@ -87,7 +99,7 @@ Leaderboard filters: `--view` (current/monthly/quarterly), `--season-key` (2026- | LiteLLM spend logs | `litellm-spend` | Spend entries | | LiteLLM virtual keys | `litellm-keys` | Key info | | Enrich CSV/Excel with LiteLLM costs | `enrich-csv ` | Enriched table | -| Any custom endpoint | `custom /v1/analytics/` | Raw JSON | +| Unlisted / experimental endpoint | `custom /v1/analytics/` | Raw JSON — use a named command if one exists | --- diff --git a/src/agents/plugins/claude/plugin/skills/codemie-analytics/scripts/analytics-cli.js b/src/agents/plugins/claude/plugin/skills/codemie-analytics/scripts/analytics-cli.js index fd81e83f..8a60bcc9 100644 --- a/src/agents/plugins/claude/plugin/skills/codemie-analytics/scripts/analytics-cli.js +++ b/src/agents/plugins/claude/plugin/skills/codemie-analytics/scripts/analytics-cli.js @@ -34,9 +34,21 @@ * cli-insights-patterns Weekday + hourly + session-depth usage patterns * users List users + activity * projects-spending Per-project spending + * projects-activity Per-project activity time-series * llms-usage LLM model usage breakdown * tools-usage Tool usage analytics * workflows Workflow execution analytics + * agents-usage Agent execution analytics + * embeddings-usage Embedding model usage + * assistants-chats Chat assistant conversations + * webhooks-usage Webhook invocation analytics + * mcp-servers MCP server usage + * mcp-servers-by-users MCP server usage broken down by user + * power-users Power user analytics + * knowledge-sharing Knowledge sharing metrics + * top-agents Top agents by usage + * top-workflows Top workflows by usage + * marketplace Assets published to marketplace * budget Budget limits (soft + hard) * spending Current user spending & budget (personal) * spending-by-users Per-user spending breakdown (platform + cli) @@ -44,7 +56,7 @@ * litellm-customer [user_id] LiteLLM customer/info (needs LITELLM_URL + LITELLM_KEY) * litellm-spend LiteLLM /spend/logs (needs LITELLM_URL + LITELLM_KEY) * litellm-keys LiteLLM /key/info for all virtual keys - * custom Call any analytics endpoint: e.g. custom /v1/analytics/mcp-servers + * custom Fallback for unlisted endpoints — prefer a named command if one exists * enrich-csv Read CSV/Excel, lookup each user in LiteLLM, output enriched data * * Filters (most commands): @@ -645,11 +657,78 @@ async function cmdEngagement(auth) { output(data); } +async function cmdProjectsActivity(auth) { + const [activity, uniqueDaily] = await Promise.all([ + analyticsGet(auth, '/v1/analytics/projects-activity', opts), + analyticsGet(auth, '/v1/analytics/projects-unique-daily', opts).catch(() => null), + ]); + output({ activity, uniqueDaily }); +} + +async function cmdAgentsUsage(auth) { + const data = await analyticsGet(auth, '/v1/analytics/agents-usage', opts); + output(data); +} + +async function cmdEmbeddingsUsage(auth) { + const data = await analyticsGet(auth, '/v1/analytics/embeddings-usage', opts); + output(data); +} + +async function cmdAssistantsChats(auth) { + const data = await analyticsGet(auth, '/v1/analytics/assistants-chats', opts); + output(data); +} + +async function cmdWebhooksUsage(auth) { + const data = await analyticsGet(auth, '/v1/analytics/webhooks-invocation', opts); + output(data); +} + +async function cmdMcpServers(auth) { + const data = await analyticsGet(auth, '/v1/analytics/mcp-servers', opts); + output(data); +} + +async function cmdMcpServersByUsers(auth) { + const data = await analyticsGet(auth, '/v1/analytics/mcp-servers-by-users', opts); + output(data); +} + +async function cmdPowerUsers(auth) { + const data = await analyticsGet(auth, '/v1/analytics/power-users', opts); + output(data); +} + +async function cmdKnowledgeSharing(auth) { + const data = await analyticsGet(auth, '/v1/analytics/knowledge-sharing', opts); + output(data); +} + +async function cmdTopAgents(auth) { + const data = await analyticsGet(auth, '/v1/analytics/top-agents-usage', opts); + output(data); +} + +async function cmdTopWorkflows(auth) { + const data = await analyticsGet(auth, '/v1/analytics/top-workflow-usage', opts); + output(data); +} + +async function cmdMarketplace(auth) { + const data = await analyticsGet(auth, '/v1/analytics/published-to-marketplace', opts); + output(data); +} + // --- Custom --- async function cmdCustom(auth) { const path = opts._[0]; - if (!path) throw new Error('Usage: custom e.g. custom /v1/analytics/mcp-servers'); + if (!path) throw new Error( + 'Usage: custom \n' + + 'NOTE: prefer a named command over custom when one exists (run with "help" to list them).\n' + + 'Example: custom /v1/analytics/some-new-endpoint' + ); const method = (opts.method || 'GET').toUpperCase(); let data; if (method === 'POST') { @@ -780,15 +859,27 @@ async function main() { // Standard analytics case 'users': return cmdUsers(auth); case 'projects-spending': return cmdProjectsSpending(auth); + case 'projects-activity': return cmdProjectsActivity(auth); case 'llms-usage': return cmdLlmsUsage(auth); case 'tools-usage': return cmdToolsUsage(auth); case 'workflows': return cmdWorkflows(auth); + case 'agents-usage': return cmdAgentsUsage(auth); + case 'embeddings-usage': return cmdEmbeddingsUsage(auth); + case 'assistants-chats': return cmdAssistantsChats(auth); + case 'webhooks-usage': return cmdWebhooksUsage(auth); + case 'mcp-servers': return cmdMcpServers(auth); + case 'mcp-servers-by-users': return cmdMcpServersByUsers(auth); + case 'power-users': return cmdPowerUsers(auth); + case 'knowledge-sharing': return cmdKnowledgeSharing(auth); + case 'top-agents': return cmdTopAgents(auth); + case 'top-workflows': return cmdTopWorkflows(auth); + case 'marketplace': return cmdMarketplace(auth); case 'budget': return cmdBudget(auth); case 'spending': return cmdSpending(auth); case 'spending-by-users': return cmdSpendingByUsers(auth); case 'engagement': return cmdEngagement(auth); - // Custom + // Custom — use only for endpoints without a dedicated command case 'custom': return cmdCustom(auth); default: