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..47c196ad --- /dev/null +++ b/src/agents/plugins/claude/plugin/skills/codemie-analytics/SKILL.md @@ -0,0 +1,641 @@ +--- +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 | 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 | 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 | 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 | +| 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 | +| 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 + +| 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 | +| Unlisted / experimental endpoint | `custom /v1/analytics/` | Raw JSON — use a named command if one exists | + +--- + +## 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_PLUGIN_ROOT}/skills/codemie-analytics/scripts/analytics-cli.js [options] +``` + +LiteLLM commands (`litellm-*`, `enrich-csv`) require `LITELLM_URL` + `LITELLM_KEY` env vars. + +### Collecting data for reports (batch pattern) + +**Only fetch the data the report actually needs.** Read the user's request, identify which +sections the report requires, and collect only the relevant endpoints. Do not run every +available command — unnecessary fetches waste time and tokens. + +When building an HTML report, run all needed CLI commands in a single Bash call using +`--save ` 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 | +|------|---------|-------| +| `--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` | `alice,bob` | 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_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 + +# Single user champion profile +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 + +# 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 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 + +# 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 2.5 — Inspect data structure + +After collection, run `inspect-schema.js` to generate a compact `.schema.json` file +alongside each saved JSON — one permission prompt, no data in context: + +```bash +node ${CLAUDE_PLUGIN_ROOT}/skills/codemie-analytics/scripts/inspect-schema.js "$OUT" +``` + +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" } + } + } + } +} +``` + +--- + +## 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 + +``` + +Each placeholder name maps to a saved file in `$OUT` (without the `.json` extension). + +### Step 3b — Inject data + +**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 +``` + +--- + +## 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 | [`${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) | + +--- + +## 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_PLUGIN_ROOT}/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 `${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, + `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` (temp/data files in `reports/-/temp/`). + +**Key commands:** +```bash +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 + +# 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 + `${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 + `${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/-/.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/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..8a60bcc9 --- /dev/null +++ b/src/agents/plugins/claude/plugin/skills/codemie-analytics/scripts/analytics-cli.js @@ -0,0 +1,893 @@ +#!/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 + * 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) + * 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 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): + * --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, writeFileSync, mkdirSync } from 'fs'; +import { homedir, hostname, platform, arch } from 'os'; +import { join, resolve, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +// ─── 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) { + 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) { + 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); +} + +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 \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') { + 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(fileURLToPath(import.meta.url), '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 '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 — use only for endpoints without a dedicated command + 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-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 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..444650c3 --- /dev/null +++ b/src/agents/plugins/claude/plugin/skills/codemie-sdk/SKILL.md @@ -0,0 +1,240 @@ +--- +name: codemie-sdk +description: >- + 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 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. +--- + +# CodeMie SDK Asset Management + +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`, `assistant-categories` + +**Operations:** `list`, `get`, `create`, `update`, `delete` + +--- + +## 🚨 Project Clarification (MANDATORY) + +**Before proceeding with any work, you must determine which project to use by following these steps:** + +### Step 1 — Fetch the User Profile + +```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 + +Once the project is known, use it in all subsequent commands: +- Assistants, skills, assistant-categories: `"project": ""` +- Workflows, datasources, integrations: `"project_name": ""` + +--- + +## 📖 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) | +| Skills | [examples/skills.md](examples/skills.md) | +| Users | [examples/users.md](examples/users.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. + +--- + +## 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` + +--- + +## 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 + +--- + +## Datasources + +> See [examples/datasources.md](examples/datasources.md) for full field reference and examples. + +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] +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 + +--- + +## 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` (must include `{"key":"alias","value":""}`) + +--- + +## LLM Models + +```bash +codemie sdk llm list [--json] +codemie sdk llm list --embeddings [--json] +``` + +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] +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 +``` + +**Required on create:** `name` (kebab-case, 3–64 chars), `description` (10–1000 chars), `content` (markdown, min 100 chars), `project` + +--- + +## 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] +``` + +--- + +## 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 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 new file mode 100644 index 00000000..6160f364 --- /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[] | 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) | + +**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/categories.md b/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/categories.md new file mode 100644 index 00000000..ae98fe87 --- /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 assistant-categories list +codemie sdk assistant-categories list --json + +# Paginated list with assistant counts (admin required) +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` + +**Paginated JSON fields:** `categories[]` (with `id`, `name`, `description`, `marketplaceAssistantCount`, `projectAssistantCount`, `createdAt`, `updatedAt`), `page`, `per_page`, `total`, `pages` + +## Get + +```bash +codemie sdk assistant-categories get +codemie sdk assistant-categories get --json +``` + +Admin access required. Returns `id`, `name`, `description`, `marketplaceAssistantCount`, `projectAssistantCount`, `createdAt`, `updatedAt`. + +## Create + +```bash +# Minimal (name only) +codemie sdk assistant-categories create --data '{"name":"DevOps"}' + +# With description +codemie sdk assistant-categories create --data '{"name":"Code Review","description":"Skills for reviewing code quality and security"}' + +# From file +codemie sdk assistant-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 assistant-categories update --data '{"name":"Updated Name"}' +codemie sdk assistant-categories update --data '{"name":"DevOps","description":"Updated description"}' +``` + +Admin access required. + +## Delete + +```bash +# Verify before deleting +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. + +## Using Categories with Assistants + +Categories are referenced by their `id` in the assistant `categories` field. + +```bash +# Get available category IDs +codemie sdk assistant-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 assistant-categories list --json | jq -r '.[] | select(.name == "DevOps") | .id' + +# List all categories with assistant counts (admin) +codemie sdk assistant-categories list --paginated --json | jq -r '.categories[] | "\(.name): \(.marketplaceAssistantCount) marketplace, \(.projectAssistantCount) project"' +``` 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..9693ceca --- /dev/null +++ b/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/datasources.md @@ -0,0 +1,401 @@ +# Datasources Examples + +> **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`) + +## 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 +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): + +| 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, Google, Azure DevOps Wiki, Azure DevOps Work Item, Xray, SharePoint, and JSON 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. + +### 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 +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 +}' +``` + +## 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..68c56bad --- /dev/null +++ b/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/integrations.md @@ -0,0 +1,242 @@ +# 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 | + +**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. + +### 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"}, + {"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"} + ] +} +``` + +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/skills.md b/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/skills.md new file mode 100644 index 00000000..841c5a7e --- /dev/null +++ b/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/skills.md @@ -0,0 +1,191 @@ +# 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`, `description`, `created_by`, `categories`, `createdDate`, `updatedDate`, `is_attached`, `assistants_count`, `user_abilities`, `unique_likes_count`, `unique_dislikes_count` + +## Get + +```bash +codemie sdk skills get 3d5b188f-185b-48df-b4b3-e608e4efb1ad +codemie sdk skills get 3d5b188f-185b-48df-b4b3-e608e4efb1ad --json +``` + +**Additional fields in get:** `content` (full skill markdown), `toolkits`, `mcp_servers` + +## 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 + +```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' + +# 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 new file mode 100644 index 00000000..f21fa76a --- /dev/null +++ b/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/users.md @@ -0,0 +1,38 @@ +# Users Examples + +## Get current user profile + +```bash +codemie sdk users me +codemie sdk users me --json +``` + +**JSON fields:** `user_id`, `name`, `username`, `email`, `is_admin`, `applications`, `applications_admin`, `picture`, `knowledge_bases` + +## Get current user data + +```bash +codemie sdk users data +codemie sdk users data --json +``` + +**JSON fields:** `id`, `user_id`, `date`, `update_date` + +## Scripting + +```bash +# Get your username +codemie sdk users me --json | jq -r '.username' + +# 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/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..dbee1d4c --- /dev/null +++ b/src/agents/plugins/claude/plugin/skills/codemie-sdk/examples/workflows.md @@ -0,0 +1,151 @@ +# 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 (minimal working format):** +```yaml +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 +``` + +> **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 llm list --json | jq -r '.[] | "\(.base_name) (\(.label))"' +``` + +## 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..1d3a2e7f --- /dev/null +++ b/src/cli/commands/sdk/assistants.ts @@ -0,0 +1,285 @@ +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.", + ) + .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/categories.ts b/src/cli/commands/sdk/categories.ts new file mode 100644 index 00000000..23afe717 --- /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("assistant-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 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)") + .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 assistant-categories get \n" + + " $ codemie sdk assistant-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 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") + .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 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") + .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 assistant-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/datasources.ts b/src/cli/commands/sdk/datasources.ts new file mode 100644 index 00000000..68515521 --- /dev/null +++ b/src/cli/commands/sdk/datasources.ts @@ -0,0 +1,381 @@ +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}` + + (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 ", + "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}` + + (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( + `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..eee4392a --- /dev/null +++ b/src/cli/commands/sdk/index.ts @@ -0,0 +1,28 @@ +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'; +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, assistant-categories) via the SDK' + ); + + cmd.addCommand(createAssistantsSubcommand()); + cmd.addCommand(createWorkflowsSubcommand()); + cmd.addCommand(createDatasourcesSubcommand()); + cmd.addCommand(createIntegrationsSubcommand()); + cmd.addCommand(createLlmModelsSubcommand()); + cmd.addCommand(createSkillsSubcommand()); + cmd.addCommand(createUsersSubcommand()); + cmd.addCommand(createCategoriesSubcommand()); + + 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..2f8ccd1f --- /dev/null +++ b/src/cli/commands/sdk/services/assistants.ts @@ -0,0 +1,107 @@ +import type { + CodeMieClient, + Assistant, + AssistantBase, + AssistantCreateParams, + AssistantUpdateParams, + 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] }; + +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); + 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; + + 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), + ]); + 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; + + 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/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/datasources.ts b/src/cli/commands/sdk/services/datasources.ts new file mode 100644 index 00000000..61f8903e --- /dev/null +++ b/src/cli/commands/sdk/services/datasources.ts @@ -0,0 +1,441 @@ +import type { + CodeMieClient, + AzureDevOpsWikiDataSourceCreateParams, + AzureDevOpsWikiDataSourceUpdateParams, + AzureDevOpsWorkItemDataSourceCreateParams, + AzureDevOpsWorkItemDataSourceUpdateParams, + CodeDataSourceCreateParams, + CodeDataSourceUpdateParams, + ConfluenceDataSourceCreateParams, + ConfluenceDataSourceUpdateParams, + DataSource, + DataSourceListParams, + FileDataSourceCreateParams, + FileDataSourceUpdateDto, + GoogleDataSourceCreateParams, + GoogleDataSourceUpdateParams, + JiraDataSourceCreateParams, + JiraDataSourceUpdateParams, + OtherDataSourceCreateParams, + OtherDataSourceUpdateParams, + SharePointDataSourceCreateParams, + SharePointDataSourceUpdateParams, + XrayDataSourceCreateParams, + XrayDataSourceUpdateParams, +} 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 ?? existing.confluence?.cql, + description: data.description ?? existing.description, + name: existing.name, + project_name: data.project_name ?? existing.project_name, + setting_id: data.setting_id ?? existing.setting_id, + shared_with_project: data.shared_with_project ?? existing.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 { + 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: Omit, +): Promise { + return client.datasources.create({ + ...data, + type: "code", + }); +} + +export async function updateCodeDatasource( + client: CodeMieClient, + id: string, + data: Partial>, +): Promise { + const existing = await client.datasources.get(id); + return client.datasources.update({ + 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: Partial>, +): Promise { + const existing = await client.datasources.get(id); + return client.datasources.update({ + 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); +} + +// 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, +): 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..a020e35e --- /dev/null +++ b/src/cli/commands/sdk/services/llm.ts @@ -0,0 +1,13 @@ +import type { 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/skills.ts b/src/cli/commands/sdk/services/skills.ts new file mode 100644 index 00000000..026f2ff7 --- /dev/null +++ b/src/cli/commands/sdk/services/skills.ts @@ -0,0 +1,163 @@ +import type { + CodeMieClient, + SkillListItem, + SkillDetail, + SkillCreateParams, + SkillUpdateParams, + SkillImportParams, + SkillCategoryItem, + SkillListPaginatedResponse, + AnyJson, +} from "codemie-sdk"; + +type SkillListParams = Parameters[0]; + +export async function listSkills( + client: CodeMieClient, + params?: SkillListParams, +): Promise { + 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/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/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/skills.ts b/src/cli/commands/sdk/skills.ts new file mode 100644 index 00000000..669356fe --- /dev/null +++ b/src/cli/commands/sdk/skills.ts @@ -0,0 +1,626 @@ +import { Command } from "commander"; +import chalk from "chalk"; +import ora from "ora"; +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"; +import { + printTable, + printDetail, + printEmpty, + printListHeader, + printSuccess, + 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.createdDate }, + { label: "Assistants", value: String(item.assistants_count) }, + ]; + + if (item.updatedDate) { + rows.push({ label: "Updated", value: item.updatedDate }); + } + + 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"); + } + }); + + 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") + .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") + .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") + .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") + .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") + .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") + .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") + .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") + .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") + .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") + .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") + .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") + .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 new file mode 100644 index 00000000..12310888 --- /dev/null +++ b/src/cli/commands/sdk/users.ts @@ -0,0 +1,94 @@ +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") + .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: "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); + } catch (error) { + spinner.stop(); + handleSdkError(error, "get user profile"); + } + }); + + cmd + .command("data") + .description("Get current user data and preferences") + .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; +} 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..c7145704 --- /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 { ConfigurationError } from "@/utils/errors.js"; +import { logger } from "@/utils/logger.js"; +import { sanitizeLogArgs } from "@/utils/security.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 ConfigurationError('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 ConfigurationError("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 ConfigurationError( + "Cannot use both --data and --json. Use --data for inline JSON string or --json for JSON file path.", + ); + } + + if (!dataFlag && !jsonFlag) { + throw new ConfigurationError( + '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 { + 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) { + 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: ${msg}`)); + } + } else if (error instanceof ZodError) { + console.error(chalk.red(`❌ Operation failed:`)); + console.error(chalk.red(z.prettifyError(error))); + } else { + 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: path to a YAML file + */ +export async function parseConfigInput( + configFlag: string | undefined, +): Promise { + if (!configFlag) { + throw new ConfigurationError( + "No config provided. Use --config path/to/file.yaml", + ); + } + + 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 new file mode 100644 index 00000000..abe1e5a1 --- /dev/null +++ b/src/cli/commands/sdk/utils/datasource-types.ts @@ -0,0 +1,69 @@ +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", + }, + { + command: "file", + type: "knowledge_base_file", + description: "File datasource (use --file flags for local files)", + }, + { + command: "code", + type: "code", + description: "Code repository datasource", + }, + { + command: "google", + type: "llm_routing_google", + description: "Google Docs datasource", + }, + { + command: "provider", + type: "provider", + description: "Provider datasource", + }, + { + command: "azure-devops-wiki", + serviceKey: "azureDevOpsWiki", + type: "knowledge_base_azure_devops_wiki", + description: "Azure DevOps Wiki datasource", + }, + { + command: "azure-devops-work-item", + serviceKey: "azureDevOpsWorkItem", + type: "knowledge_base_azure_devops_work_item", + description: "Azure DevOps Work Item datasource", + }, + { + command: "xray", + type: "knowledge_base_xray", + description: "Xray test management datasource", + }, + { + command: "sharepoint", + type: "knowledge_base_sharepoint", + description: "SharePoint datasource", + }, + { + command: "platform", + type: "platform_marketplace_assistant", + description: "Platform marketplace assistant datasource", + }, +]; 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..9c9b63a1 --- /dev/null +++ b/src/cli/commands/sdk/utils/file-utils.ts @@ -0,0 +1,22 @@ +import { readFile } from "node:fs/promises"; +import { basename } from "node: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 { + 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 }; + }), + ); +} diff --git a/src/cli/commands/sdk/utils/render.ts b/src/cli/commands/sdk/utils/render.ts new file mode 100644 index 00000000..3e7abd87 --- /dev/null +++ b/src/cli/commands/sdk/utils/render.ts @@ -0,0 +1,193 @@ +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({ + style: { + head: columns.map(() => "white"), + }, + 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..30379d87 --- /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 ", + "Path to workflow YAML config file", + ) + .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 ", + "Path to workflow YAML config file", + ) + .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());