moving
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled

This commit is contained in:
Oleg Maslov
2026-09-02 10:10:29 +02:00
commit 0c3e2ead3b
3841 changed files with 970576 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
{
"name": "@waggle/shared",
"version": "0.1.0",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"build": "tsc",
"test": "node ../../node_modules/vitest/vitest.mjs run --root ../.. --config vitest.config.ts packages/shared/tests"
},
"dependencies": {
"zod": "^3.24.0"
},
"main": "dist/index.js",
"types": "dist/index.d.ts"
}

View File

@@ -0,0 +1,66 @@
// Natural-Language Command Bar (Ctrl+K Tier 1 intent layer).
//
// The contract returned by `POST /api/command/interpret`. The resolver maps a
// plain-language request onto the CLOSED action registry (server-side) and
// returns one structured `InterpretResult`. The LLM never emits an endpoint,
// route, or free code — it picks a registry action id; the SERVER derives the
// executable shape below. See docs/nl-command-bar/STEP0-AND-DESIGN.md.
import type { RiskLevel } from './risk.js';
import type { Tier } from './tiers.js';
/** The five resolution outcomes. `plan` is typed but NOT executed in v1. */
export type InterpretKind = 'action' | 'plan' | 'clarify' | 'tier_gated' | 'none';
/**
* A single resolved, server-validated action. Exactly one of `navigate` /
* `endpoint` is set — both are derived server-side from the registry, never
* supplied by the model.
*/
export interface ResolvedAction {
/** Registry action id (closed set), e.g. 'create_workspace'. */
id: string;
/** Human-readable summary for the preview / approval surface. */
label: string;
/** Validated params the action was built from (for display + audit). */
params: Record<string, unknown>;
/** True → the frontend MUST render the approval surface before executing. */
sideEffect: boolean;
/** Risk badge for the approval surface (consistent with confirmation.ts). */
riskLevel: RiskLevel;
/** Navigation target — reuses CommandResult nav semantics (`onNavigate`). */
navigate?: { type: string; id: string };
/** Server-derived endpoint for a create / side-effect execute. */
endpoint?: { method: 'POST' | 'PATCH' | 'DELETE'; path: string; body?: Record<string, unknown> };
}
/** The single structured result of an interpret call. */
export interface InterpretResult {
kind: InterpretKind;
/** kind === 'action'. */
action?: ResolvedAction;
/** kind === 'plan' — typed only; v1 downgrades to clarify/single-action. */
steps?: ResolvedAction[];
/** kind === 'clarify'. */
question?: string;
options?: string[];
/** kind === 'tier_gated'. */
capability?: string;
requiredTier?: Tier;
actualTier?: Tier;
/** User-facing message (kind 'none', and a hint on others). */
message?: string;
/**
* True when resolution failed/degraded (no key, LLM error, parse failure):
* the frontend should fall back to Tier 0 results.
*/
fallback?: boolean;
}
/** Request body for `POST /api/command/interpret`. */
export interface InterpretRequest {
text: string;
workspaceId?: string;
/** Lightweight client hints (current route, etc.). Optional. */
context?: Record<string, unknown>;
}

View File

@@ -0,0 +1,173 @@
/**
* Persona-aware connector recommendations.
*
* Replaces the old "5 skills + 3 connectors at Essential" capability cap
* (per the user's pushback in 2026-05-08 session: hiding 145 of 148
* connectors didn't simplify, it crippled). Instead the catalog stays
* fully reachable — but the connector-landing surface, onboarding step,
* and chat hints SHOW different defaults based on the user's persona +
* template, on the principle that a Sales rep wants HubSpot/Salesforce
* up front while a Coder wants GitHub/Linear/Slack.
*
* This module is the single source of truth: server (for API), web
* (for ConnectorsApp landing tile), and onboarding (for the "which
* tools?" step) all import from here. Every connector ID listed below
* is validated against `mcp-catalog.ts` by the unit test
* `connector-recommendations.test.ts`.
*
* Shape:
* recommendConnectors(personaId) → { primary: string[], secondary: string[] }
* - `primary` — 35 IDs shown as the immediate "start here" tile
* - `secondary` — additional context-relevant IDs shown one tier down
*
* Capability is NEVER reduced: all 148 connectors remain reachable via
* search + browse-all in ConnectorsApp regardless of recommendation.
*/
/**
* Universal-default connectors used as fallback for any persona without
* a specific override (e.g. general-purpose, planner, verifier,
* coordinator). Picked because these three are the most commonly-used
* tools across professional roles globally.
*/
const UNIVERSAL_PRIMARY = ['gdrive-mcp', 'gmail-mcp', 'notion-mcp'] as const;
/** Secondary defaults that round out the universal set without overwhelming. */
const UNIVERSAL_SECONDARY = ['slack-mcp', 'microsoft-365'] as const;
export interface ConnectorRecommendation {
/** 35 IDs to display in the immediate connector landing tile. */
primary: readonly string[];
/** Additional IDs that show in a "also useful for your role" section. */
secondary: readonly string[];
}
/**
* Per-persona connector recommendations.
*
* The keys are the canonical persona IDs from
* `packages/agent/src/persona-data.ts`. Adding a new persona without
* updating this map is allowed — `recommendConnectors` falls back to
* the universal defaults so the surface never breaks.
*/
export const CONNECTOR_RECOMMENDATIONS: Readonly<Record<string, ConnectorRecommendation>> = {
// ── Universal / reasoning personas — use the universal defaults ──────
'general-purpose': { primary: UNIVERSAL_PRIMARY, secondary: UNIVERSAL_SECONDARY },
'planner': { primary: UNIVERSAL_PRIMARY, secondary: UNIVERSAL_SECONDARY },
'verifier': { primary: UNIVERSAL_PRIMARY, secondary: UNIVERSAL_SECONDARY },
'coordinator': { primary: UNIVERSAL_PRIMARY, secondary: UNIVERSAL_SECONDARY },
// ── Knowledge-tier personas ──────────────────────────────────────────
'researcher': {
primary: ['perplexity', 'exa', 'gdrive-mcp', 'notion-mcp', 'gmail-mcp'],
secondary: ['firecrawl', 'tavily', 'brave-search', 'jina'],
},
'writer': {
primary: ['gdrive-mcp', 'notion-mcp', 'gmail-mcp', 'figma'],
secondary: ['confluence-mcp', 'microsoft-365'],
},
'analyst': {
primary: ['bigquery', 'postgres', 'gdrive-mcp', 'notion-mcp', 'excel'],
secondary: ['snowflake', 'clickhouse', 'duckdb', 'airtable-mcp'],
},
'coder': {
primary: ['github-mcp', 'slack-mcp', 'linear-mcp', 'sentry', 'gdrive-mcp'],
secondary: ['gitlab-mcp', 'docker', 'vercel', 'datadog', 'postgres'],
},
// ── Domain-tier personas ─────────────────────────────────────────────
'project-manager': {
primary: ['linear-mcp', 'jira-mcp', 'slack-mcp', 'notion-mcp', 'gdrive-mcp'],
secondary: ['asana-mcp', 'monday-mcp', 'clickup', 'atlassian'],
},
'executive-assistant': {
primary: ['gmail-mcp', 'gdrive-mcp', 'notion-mcp', 'slack-mcp'],
secondary: ['microsoft-365', 'asana-mcp'],
},
'sales-rep': {
primary: ['hubspot-mcp', 'salesforce-mcp', 'slack-mcp', 'gmail-mcp', 'notion-mcp'],
secondary: ['linear-mcp', 'gdrive-mcp', 'airtable-mcp'],
},
'marketer': {
primary: ['hubspot-mcp', 'notion-mcp', 'gdrive-mcp', 'slack-mcp', 'figma'],
secondary: ['airtable-mcp', 'salesforce-mcp', 'firecrawl'],
},
'product-manager-senior': {
primary: ['linear-mcp', 'jira-mcp', 'github-mcp', 'figma', 'slack-mcp', 'notion-mcp'],
secondary: ['atlassian', 'sentry', 'gdrive-mcp'],
},
'hr-manager': {
primary: ['gmail-mcp', 'gdrive-mcp', 'slack-mcp', 'notion-mcp'],
secondary: ['microsoft-365', 'asana-mcp', 'airtable-mcp'],
},
'legal-professional': {
primary: ['gdrive-mcp', 'notion-mcp', 'gmail-mcp'],
secondary: ['microsoft-365', 'box', 'confluence-mcp'],
},
'finance-owner': {
primary: ['stripe-mcp', 'gdrive-mcp', 'excel', 'gmail-mcp', 'notion-mcp'],
secondary: ['microsoft-365', 'bigquery', 'airtable-mcp'],
},
'consultant': {
primary: ['notion-mcp', 'gdrive-mcp', 'gmail-mcp', 'slack-mcp', 'figma'],
secondary: ['microsoft-365', 'confluence-mcp', 'airtable-mcp'],
},
'support-agent': {
primary: ['zendesk', 'intercom', 'slack-mcp', 'gmail-mcp', 'notion-mcp'],
secondary: ['linear-mcp', 'jira-mcp', 'gdrive-mcp'],
},
'ops-manager': {
primary: ['notion-mcp', 'slack-mcp', 'gdrive-mcp', 'atlassian'],
secondary: ['asana-mcp', 'monday-mcp', 'jira-mcp', 'airtable-mcp'],
},
'data-engineer': {
primary: ['bigquery', 'postgres', 'snowflake', 'github-mcp', 'slack-mcp'],
secondary: ['clickhouse', 'duckdb', 'mongodb', 'gdrive-mcp'],
},
'recruiter': {
primary: ['gmail-mcp', 'gdrive-mcp', 'slack-mcp', 'notion-mcp'],
secondary: ['microsoft-365', 'airtable-mcp'],
// Note: 'linkedin' would be the obvious add here but is not in the
// MCP catalog as of 2026-05-08. When/if added, slot it ahead of
// airtable-mcp in secondary.
},
'creative-director': {
primary: ['figma', 'gdrive-mcp', 'slack-mcp', 'notion-mcp'],
secondary: ['microsoft-365', 'asana-mcp'],
},
};
/**
* Get the recommended connectors for a persona.
*
* Falls back to universal defaults for unknown persona IDs (custom
* personas, future additions, typos) so the surface never goes empty.
*/
export function recommendConnectors(personaId: string): ConnectorRecommendation {
const exact = CONNECTOR_RECOMMENDATIONS[personaId];
if (exact) return exact;
return { primary: UNIVERSAL_PRIMARY, secondary: UNIVERSAL_SECONDARY };
}
/**
* Convenience: flatten a recommendation into a single ranked list
* (primary first, then secondary). Useful for the onboarding step's
* "pick your tools" multi-select where ordering matters more than
* sectioning.
*/
export function flattenRecommendation(rec: ConnectorRecommendation): string[] {
return [...rec.primary, ...rec.secondary];
}
/** All connector IDs referenced anywhere in this file. Used by the test
* suite to verify catalog membership without enumerating personas. */
export function allReferencedConnectorIds(): string[] {
const seen = new Set<string>();
for (const id of UNIVERSAL_PRIMARY) seen.add(id);
for (const id of UNIVERSAL_SECONDARY) seen.add(id);
for (const rec of Object.values(CONNECTOR_RECOMMENDATIONS)) {
for (const id of rec.primary) seen.add(id);
for (const id of rec.secondary) seen.add(id);
}
return [...seen].sort();
}

View File

@@ -0,0 +1,24 @@
// @waggle/shared — Constants for M3 Team Pilot
export const TEAM_ROLES = ['owner', 'admin', 'member'] as const;
export const TASK_STATUSES = ['open', 'claimed', 'in_progress', 'done', 'cancelled'] as const;
export const TASK_PRIORITIES = ['critical', 'high', 'normal', 'low'] as const;
export const MESSAGE_TYPES = ['broadcast', 'request', 'response'] as const;
export const JOB_TYPES = ['chat', 'task', 'cron', 'waggle'] as const;
export const JOB_STATUSES = ['queued', 'running', 'completed', 'failed'] as const;
export const AGENT_GROUP_STRATEGIES = ['parallel', 'sequential', 'coordinator'] as const;
export const RESOURCE_TYPES = ['model_recipe', 'skill', 'tool_config', 'prompt_template'] as const;
export const SUGGESTION_TYPES = ['dashboard', 'cron', 'share', 'skill', 'upgrade'] as const;
export const MAX_SUGGESTIONS_PER_INTERACTION = 1;
export const SCOUT_DEFAULT_INTERVAL_MS = 24 * 60 * 60 * 1000; // daily
export const SUBCONSCIOUS_INTERACTION_THRESHOLD = 10; // reflect every N tasks
export const HIVE_MIND_CRON = '0 9 * * 1'; // weekly Monday 9am
/**
* W2G — the prefix the chat route prepends to a persisted assistant turn when
* generation fails. Shared so the server (persist) and the web client (decode a
* reloaded failed turn back into an error block) can never drift. The bare error
* message follows the prefix, matching the live SSE 'error' event payload.
*/
export const GENERATION_FAILED_PREFIX = 'Generation failed: ';

View File

@@ -0,0 +1,11 @@
// @waggle/shared
export * from './types.js';
export * from './schemas.js';
export * from './constants.js';
export * from './tiers.js';
export * from './mcp-catalog.js';
export * from './connector-recommendations.js';
export * from './tool-detection.js';
export * from './risk.js';
export * from './command-intent.js';
export * from './loop-templates.js';

View File

@@ -0,0 +1,144 @@
/**
* Knowledge-worker Loop templates — preset, report-only (L1) automations that
* ride the `job_type:'loop'` executor (packages/server/src/local/loop-executor.ts).
*
* Each template is a ready-to-POST automation: a sensible default cadence plus
* a `jobConfig` matching the executor's LoopSpec ({ prompt, query?, rubric? }).
* They translate the recurring rhythms of knowledge work — the morning brief,
* the weekly digest, the "who have I gone quiet on" sweep — into scheduled,
* memory-grounded reports. All are report-only: a Loop tick generates a report
* grounded in the workspace's memory and notifies; it never sends, writes, or
* acts on an external system.
*
* Cadences are daily/weekly by design — a Loop is several LLM round-trips, so
* tight crons are wasteful (the executor also enforces a per-loop cost floor).
*/
export interface LoopTemplate {
/** Stable id (used as the picker key). */
id: string;
/** User-facing title — becomes the automation name. */
name: string;
/** One-line description shown in the picker. */
description: string;
/** lucide-react icon name for the UI to resolve (falls back gracefully). */
icon: string;
/** The knowledge-worker role this rhythm belongs to (picker grouping hint). */
role: string;
/** Default cron expression (5-field). Daily/weekly by design. */
defaultCron: string;
/** Ready-to-persist loop config — shape matches the executor's LoopSpec. */
jobConfig: {
prompt: string;
query?: string;
rubric?: string;
};
}
export const LOOP_TEMPLATES: LoopTemplate[] = [
{
id: 'daily-desk-brief',
name: 'Daily Desk Brief',
description: 'Each morning, a short brief of what needs your attention today, grounded in this workspace.',
icon: 'Sunrise',
role: 'Executive assistant',
defaultCron: '0 8 * * *',
jobConfig: {
prompt:
'Brief me on what needs my attention today in this workspace. Pull from recent memory: ' +
'open tasks, commitments I made, threads awaiting a reply, and anything time-sensitive. ' +
'Lead with the 3 most important items. Be concise.',
query: 'open tasks commitments awaiting reply deadlines today',
},
},
{
id: 'weekly-wins',
name: 'Weekly Wins Digest',
description: 'A Friday digest of what shipped, closed, or moved this week — ready to share or paste into a status update.',
icon: 'Trophy',
role: 'Project manager',
defaultCron: '0 16 * * 5',
jobConfig: {
prompt:
'Summarise what shipped, closed, or moved forward in this workspace over the past week. ' +
'Group by theme. Write it as a short status update I could share with my team or manager.',
query: 'shipped completed closed progress this week milestones',
},
},
{
id: 'relationship-decay',
name: 'Relationship Decay Sweep',
description: 'Weekly: contacts and deals you have gone quiet on, ranked by importance, so nothing slips.',
icon: 'HeartHandshake',
role: 'Sales / Account management',
defaultCron: '0 9 * * 1',
jobConfig: {
prompt:
'From this workspace\'s memory, identify the contacts, accounts, or deals I have gone quiet on. ' +
'Rank them by importance and how long since the last touch. For each, suggest a one-line reason to reconnect. ' +
'Report only — do not draft or send anything.',
query: 'contacts accounts deals last contact follow up gone quiet',
rubric: 'Reward concrete, specific contacts grounded in memory with a clear last-touch rationale. ' +
'Penalise vague or invented names.',
},
},
{
id: 'inbound-triage',
name: 'Inbound Triage Report',
description: 'Weekday mornings: classify and summarise new inbound items (requests, leads, tickets) — propose-only.',
icon: 'Inbox',
role: 'Support / Operations',
defaultCron: '0 7 * * 1-5',
jobConfig: {
prompt:
'Triage the new inbound items captured in this workspace since the last run. ' +
'Classify each (e.g. request, lead, question, FYI), flag anything urgent, and suggest who or what should handle it. ' +
'This is a report and proposal only — take no action.',
query: 'new inbound requests tickets leads questions to triage',
},
},
{
id: 'renewal-radar',
name: 'Renewal & Expiry Radar',
description: 'Weekly scan for contracts, licenses, or subscriptions coming up for renewal or expiry.',
icon: 'CalendarClock',
role: 'Operations / Legal / Finance',
defaultCron: '0 9 * * 1',
jobConfig: {
prompt:
'Scan this workspace\'s memory for contracts, licenses, subscriptions, or commitments with an upcoming ' +
'renewal, expiry, or review date. List them soonest-first with the date and what action they need. ' +
'If you cannot determine a date, say so rather than guessing.',
query: 'contract license subscription renewal expiry review date deadline',
},
},
{
id: 'commitment-tracker',
name: 'Commitment Tracker',
description: 'End of each weekday: surface the promises you made ("I\'ll send X by Friday") so none are dropped.',
icon: 'CheckSquare',
role: 'Executive assistant',
defaultCron: '0 17 * * 1-5',
jobConfig: {
prompt:
'Review this workspace\'s recent memory for commitments I made — things I said I would do, send, or follow up on. ' +
'List each with who it is owed to and any deadline. Flag any that look overdue. Report only.',
query: 'I will send follow up by promised commitment owe deadline',
},
},
{
id: 'competitor-watch',
name: 'Competitor Watch Digest',
description: 'Weekly: what changed about the competitors and market signals tracked in this workspace.',
icon: 'Telescope',
role: 'Marketing / Product',
defaultCron: '0 9 * * 1',
jobConfig: {
prompt:
'Summarise what is new or changed about the competitors and market signals tracked in this workspace ' +
'since the previous run. Emphasise concrete, sourced developments. If nothing materially changed, say so.',
query: 'competitor market signal launch pricing announcement change',
rubric: 'Reward concrete, novel, sourced items grounded in memory. Penalise speculation or repetition of prior reports.',
},
},
];

View File

@@ -0,0 +1,319 @@
/**
* MCP Server Registry — curated catalog of community MCP servers.
*
* Sources cross-referenced:
* - modelcontextprotocol/servers (official Anthropic reference)
* - punkpeye/awesome-mcp-servers (77K stars)
* - appcypher/awesome-mcp-servers
* - tolkonepiu/best-of-mcp-servers (450 ranked, 34 categories)
*
* Deduplication is enforced at module load time via `assertCatalogUnique`
* at the bottom of this file — adding a server with a colliding normalized
* id or url will crash the build.
*
* Organized by category for the ConnectorsApp MCP tab.
*/
export interface McpServer {
id: string;
name: string;
description: string;
author: string;
category: string;
url: string;
installCmd: string;
capabilities: string[];
official?: boolean;
logo?: string; // Emoji or brand initial
}
export const MCP_CATEGORIES = [
'Database', 'Files', 'Web', 'Code', 'Communication',
'Productivity', 'Analytics', 'Cloud', 'DevTools',
'Business', 'AI & ML', 'Security', 'Media', 'Utilities',
] as const;
export const CATEGORY_EMOJI: Record<string, string> = {
'Database': '\u{1F4BE}', // floppy
'Files': '\u{1F4C1}', // folder
'Web': '\u{1F310}', // globe
'Code': '\u{1F4BB}', // laptop
'Communication': '\u{1F4AC}', // speech
'Productivity': '\u{1F4CB}', // clipboard
'Analytics': '\u{1F4CA}', // chart
'Cloud': '\u{2601}', // cloud
'DevTools': '\u{1F527}', // wrench
'Business': '\u{1F4BC}', // briefcase
'AI & ML': '\u{1F916}', // robot
'Security': '\u{1F512}', // lock
'Media': '\u{1F3A8}', // palette
'Utilities': '\u{2699}', // gear
};
export const MCP_CATALOG: McpServer[] = [
// ═══════════════════════════════════════════════════════════════════
// DATABASE (12)
// ═══════════════════════════════════════════════════════════════════
{ id: 'postgres', name: 'PostgreSQL', description: 'Query databases, inspect schemas, run migrations', author: 'MCP', category: 'Database', url: 'https://github.com/modelcontextprotocol/servers/tree/main/src/postgres', installCmd: 'npx @modelcontextprotocol/server-postgres', capabilities: ['query', 'schema', 'migrations'], official: true, logo: 'PG' },
{ id: 'sqlite', name: 'SQLite', description: 'Read and query SQLite databases', author: 'MCP', category: 'Database', url: 'https://github.com/modelcontextprotocol/servers/tree/main/src/sqlite', installCmd: 'npx @modelcontextprotocol/server-sqlite', capabilities: ['query', 'schema'], official: true, logo: 'SQ' },
{ id: 'mysql', name: 'MySQL', description: 'Query MySQL/MariaDB databases', author: 'Community', category: 'Database', url: 'https://github.com/benborla/mcp-server-mysql', installCmd: 'npx mcp-server-mysql', capabilities: ['query', 'schema', 'tables'] },
{ id: 'mongodb', name: 'MongoDB', description: 'Query MongoDB collections, inspect schemas', author: 'Community', category: 'Database', url: 'https://github.com/mongodb-labs/mongodb-mcp-server', installCmd: 'npx mongodb-mcp-server', capabilities: ['query', 'aggregate', 'collections'] },
{ id: 'redis', name: 'Redis', description: 'Interact with Redis key-value store', author: 'Community', category: 'Database', url: 'https://github.com/redis/mcp-redis', installCmd: 'npx @mcp/redis-server', capabilities: ['get', 'set', 'query'] },
{ id: 'neo4j', name: 'Neo4j', description: 'Query graph databases with Cypher', author: 'Community', category: 'Database', url: 'https://github.com/neo4j-contrib/mcp-neo4j', installCmd: 'npx mcp-neo4j', capabilities: ['cypher', 'nodes', 'relations'] },
{ id: 'supabase', name: 'Supabase', description: 'Manage Supabase projects, query data, auth', author: 'Community', category: 'Database', url: 'https://github.com/supabase-community/supabase-mcp', installCmd: 'npx supabase-mcp-server', capabilities: ['query', 'auth', 'storage', 'functions'] },
{ id: 'neon', name: 'Neon', description: 'Serverless Postgres — create databases, query, branch', author: 'Community', category: 'Database', url: 'https://github.com/neondatabase/mcp-server-neon', installCmd: 'npx mcp-server-neon', capabilities: ['query', 'branches', 'databases'] },
{ id: 'qdrant', name: 'Qdrant', description: 'Vector database — search, upsert, collections', author: 'Community', category: 'Database', url: 'https://github.com/qdrant/mcp-server-qdrant', installCmd: 'npx mcp-server-qdrant', capabilities: ['search', 'upsert', 'collections'] },
{ id: 'turso', name: 'Turso', description: 'Edge SQLite database (libSQL)', author: 'Community', category: 'Database', url: 'https://github.com/turso-extended/mcp-server-turso', installCmd: 'npx mcp-server-turso', capabilities: ['query', 'schema'] },
{ id: 'planetscale', name: 'PlanetScale', description: 'MySQL-compatible serverless database', author: 'Community', category: 'Database', url: 'https://github.com/planetscale/mcp-server', installCmd: 'npx @planetscale/mcp-server', capabilities: ['query', 'schema', 'branches'] },
{ id: 'clickhouse', name: 'ClickHouse', description: 'Analytics database — fast SQL queries', author: 'Community', category: 'Database', url: 'https://github.com/ClickHouse/mcp-server', installCmd: 'npx @clickhouse/mcp-server', capabilities: ['query', 'tables', 'analytics'] },
{ id: 'bigquery', name: 'BigQuery', description: 'Google BigQuery — serverless data warehouse and analytics', author: 'Community', category: 'Database', url: 'https://github.com/LucasHild/mcp-server-bigquery', installCmd: 'npx mcp-server-bigquery', capabilities: ['query', 'schema', 'datasets'] },
{ id: 'snowflake', name: 'Snowflake', description: 'Cloud data warehouse — read/write with insight tracking', author: 'Community', category: 'Database', url: 'https://github.com/Snowflake-Labs/mcp', installCmd: 'uvx mcp-snowflake', capabilities: ['query', 'schema', 'warehouses'] },
{ id: 'duckdb', name: 'DuckDB', description: 'In-process analytical SQL database', author: 'Community', category: 'Database', url: 'https://github.com/ktanaka101/mcp-server-duckdb', installCmd: 'uvx mcp-server-duckdb', capabilities: ['query', 'schema', 'analytics'] },
{ id: 'couchbase', name: 'Couchbase', description: 'Distributed NoSQL — natural language querying', author: 'Community', category: 'Database', url: 'https://github.com/Couchbase-Ecosystem/mcp-server-couchbase', installCmd: 'npx mcp-server-couchbase', capabilities: ['query', 'buckets', 'n1ql'] },
{ id: 'tidb', name: 'TiDB', description: 'Distributed MySQL-compatible serverless database', author: 'Community', category: 'Database', url: 'https://github.com/pingcap/pytidb', installCmd: 'uvx pytidb-mcp', capabilities: ['query', 'schema', 'vector_search'] },
{ id: 'excel', name: 'Microsoft Excel', description: 'Read/write Excel workbooks — cells, worksheets, charts', author: 'Community', category: 'Database', url: 'https://github.com/haris-musa/excel-mcp-server', installCmd: 'uvx excel-mcp-server', capabilities: ['read', 'write', 'charts', 'pivot'] },
{ id: 'nocodb', name: 'NocoDB', description: 'Open-source Airtable alternative — records, tables, views', author: 'Community', category: 'Database', url: 'https://github.com/edwinbernadus/nocodb-mcp-server', installCmd: 'npx nocodb-mcp-server', capabilities: ['read', 'write', 'tables'] },
// ═══════════════════════════════════════════════════════════════════
// FILES & STORAGE (8)
// ═══════════════════════════════════════════════════════════════════
{ id: 'filesystem', name: 'Filesystem', description: 'Read, write, search files on local filesystem', author: 'MCP', category: 'Files', url: 'https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem', installCmd: 'npx @modelcontextprotocol/server-filesystem /path', capabilities: ['read', 'write', 'search', 'directory'], official: true },
{ id: 'gdrive-mcp', name: 'Google Drive', description: 'Search and read Google Drive files', author: 'MCP', category: 'Files', url: 'https://github.com/modelcontextprotocol/servers/tree/main/src/gdrive', installCmd: 'npx @modelcontextprotocol/server-gdrive', capabilities: ['search', 'read', 'list'], official: true },
{ id: 's3', name: 'AWS S3', description: 'List, read, and manage S3 objects', author: 'Community', category: 'Files', url: 'https://github.com/aws/mcp-server-s3', installCmd: 'npx @aws/mcp-server-s3', capabilities: ['list', 'read', 'write'] },
{ id: 'onedrive-mcp', name: 'OneDrive', description: 'Access Microsoft OneDrive files', author: 'Community', category: 'Files', url: 'https://github.com/microsoft/mcp-server-onedrive', installCmd: 'npx mcp-server-onedrive', capabilities: ['read', 'list', 'search'] },
{ id: 'box', name: 'Box', description: 'Enterprise file sharing and management', author: 'Community', category: 'Files', url: 'https://github.com/box/mcp-server-box', installCmd: 'npx @box/mcp-server', capabilities: ['read', 'upload', 'search'] },
{ id: 'dropbox-mcp', name: 'Dropbox', description: 'Access Dropbox files and folders', author: 'Community', category: 'Files', url: 'https://github.com/dropbox/mcp-server', installCmd: 'npx mcp-server-dropbox', capabilities: ['read', 'list', 'search'] },
{ id: 'minio', name: 'MinIO', description: 'S3-compatible object storage', author: 'Community', category: 'Files', url: 'https://github.com/minio/mcp-server-minio', installCmd: 'npx mcp-server-minio', capabilities: ['list', 'read', 'write', 'buckets'] },
{ id: 'gcs', name: 'Google Cloud Storage', description: 'Access GCS buckets and objects', author: 'Community', category: 'Files', url: 'https://github.com/GoogleCloudPlatform/mcp-server-gcs', installCmd: 'npx mcp-server-gcs', capabilities: ['list', 'read', 'write'] },
// ═══════════════════════════════════════════════════════════════════
// WEB & SEARCH (10)
// ═══════════════════════════════════════════════════════════════════
{ id: 'brave-search', name: 'Brave Search', description: 'Web and local search via Brave API', author: 'MCP', category: 'Web', url: 'https://github.com/modelcontextprotocol/servers/tree/main/src/brave-search', installCmd: 'npx @modelcontextprotocol/server-brave-search', capabilities: ['web_search', 'local_search'], official: true },
{ id: 'fetch', name: 'Fetch', description: 'Fetch URLs and convert to markdown', author: 'MCP', category: 'Web', url: 'https://github.com/modelcontextprotocol/servers/tree/main/src/fetch', installCmd: 'npx @modelcontextprotocol/server-fetch', capabilities: ['fetch', 'convert'], official: true },
{ id: 'puppeteer', name: 'Puppeteer', description: 'Browser automation — navigate, screenshot, interact', author: 'MCP', category: 'Web', url: 'https://github.com/modelcontextprotocol/servers/tree/main/src/puppeteer', installCmd: 'npx @modelcontextprotocol/server-puppeteer', capabilities: ['navigate', 'screenshot', 'click'], official: true },
{ id: 'playwright-mcp', name: 'Playwright', description: 'Cross-browser automation and testing', author: 'Community', category: 'Web', url: 'https://github.com/playwright-community/mcp-server-playwright', installCmd: 'npx @playwright/mcp-server', capabilities: ['navigate', 'screenshot', 'test'] },
{ id: 'tavily', name: 'Tavily', description: 'AI-optimized web search API', author: 'Community', category: 'Web', url: 'https://github.com/tavily-ai/mcp-server-tavily', installCmd: 'npx mcp-server-tavily', capabilities: ['search', 'extract'] },
{ id: 'exa', name: 'Exa', description: 'Neural search engine — semantic web search', author: 'Community', category: 'Web', url: 'https://github.com/exa-labs/exa-mcp-server', installCmd: 'npx exa-mcp-server', capabilities: ['search', 'contents', 'similar'] },
{ id: 'firecrawl', name: 'Firecrawl', description: 'Web scraping and crawling with markdown output', author: 'Community', category: 'Web', url: 'https://github.com/firecrawl/mcp-server-firecrawl', installCmd: 'npx mcp-server-firecrawl', capabilities: ['scrape', 'crawl', 'extract'] },
{ id: 'serper', name: 'Serper', description: 'Google Search API results', author: 'Community', category: 'Web', url: 'https://github.com/nichochar/mcp-server-serper', installCmd: 'npx mcp-server-serper', capabilities: ['search', 'news', 'images'] },
{ id: 'browserbase', name: 'Browserbase', description: 'Cloud browser automation platform', author: 'Community', category: 'Web', url: 'https://github.com/browserbase/mcp-server', installCmd: 'npx @browserbase/mcp-server', capabilities: ['navigate', 'screenshot', 'session'] },
{ id: 'jina', name: 'Jina Reader', description: 'Extract content from any URL as clean text', author: 'Community', category: 'Web', url: 'https://github.com/jina-ai/mcp-server', installCmd: 'npx mcp-server-jina', capabilities: ['read', 'extract', 'summarize'] },
{ id: 'perplexity', name: 'Perplexity', description: 'Perplexity AI — real-time web search with citations', author: 'Community', category: 'Web', url: 'https://github.com/ppl-ai/modelcontextprotocol', installCmd: 'npx @perplexity-ai/mcp-server', capabilities: ['search', 'ask', 'citations'] },
{ id: 'kagi', name: 'Kagi Search', description: 'Privacy-focused premium search engine', author: 'Community', category: 'Web', url: 'https://github.com/kagisearch/kagimcp', installCmd: 'uvx kagimcp', capabilities: ['search', 'summarize', 'universal'] },
{ id: 'searxng', name: 'SearXNG', description: 'Self-hosted privacy-respecting metasearch engine', author: 'Community', category: 'Web', url: 'https://github.com/ihor-sokoliuk/mcp-searxng', installCmd: 'npx mcp-searxng', capabilities: ['search', 'images', 'news'] },
{ id: 'apify', name: 'Apify', description: 'Web scraping platform — run 4000+ pre-built actors', author: 'Community', category: 'Web', url: 'https://github.com/apify/actors-mcp-server', installCmd: 'npx @apify/actors-mcp-server', capabilities: ['scrape', 'crawl', 'actors'] },
// ═══════════════════════════════════════════════════════════════════
// CODE & DEVTOOLS (12)
// ═══════════════════════════════════════════════════════════════════
{ id: 'github-mcp', name: 'GitHub', description: 'Repos, issues, PRs, code search, actions', author: 'MCP', category: 'Code', url: 'https://github.com/modelcontextprotocol/servers/tree/main/src/github', installCmd: 'npx @modelcontextprotocol/server-github', capabilities: ['repos', 'issues', 'prs', 'search', 'actions'], official: true },
{ id: 'gitlab-mcp', name: 'GitLab', description: 'Projects, issues, merge requests, pipelines', author: 'Community', category: 'Code', url: 'https://github.com/theanhne/mcp-server-gitlab', installCmd: 'npx mcp-server-gitlab', capabilities: ['projects', 'issues', 'mrs', 'pipelines'] },
{ id: 'sentry', name: 'Sentry', description: 'Error tracking — issues, events, releases', author: 'MCP', category: 'DevTools', url: 'https://github.com/modelcontextprotocol/servers/tree/main/src/sentry', installCmd: 'npx @modelcontextprotocol/server-sentry', capabilities: ['issues', 'events', 'projects'], official: true },
{ id: 'docker', name: 'Docker', description: 'Manage containers, images, compose stacks', author: 'Community', category: 'DevTools', url: 'https://github.com/docker/mcp-server-docker', installCmd: 'npx mcp-server-docker', capabilities: ['containers', 'images', 'compose', 'logs'] },
{ id: 'kubernetes', name: 'Kubernetes', description: 'Manage K8s clusters, pods, deployments', author: 'Community', category: 'DevTools', url: 'https://github.com/kubernetes/mcp-server', installCmd: 'npx mcp-server-kubernetes', capabilities: ['pods', 'deployments', 'services', 'logs'] },
{ id: 'vercel', name: 'Vercel', description: 'Deployments, domains, environment variables', author: 'Community', category: 'DevTools', url: 'https://github.com/vercel/mcp-server', installCmd: 'npx @vercel/mcp-server', capabilities: ['deployments', 'domains', 'env', 'logs'] },
{ id: 'npm', name: 'npm', description: 'Search packages, view details, check versions', author: 'Community', category: 'Code', url: 'https://github.com/nichochar/mcp-server-npm', installCmd: 'npx mcp-server-npm', capabilities: ['search', 'info', 'versions'] },
{ id: 'grafana', name: 'Grafana', description: 'Query dashboards, alerts, and datasources', author: 'Community', category: 'DevTools', url: 'https://github.com/grafana/mcp-server-grafana', installCmd: 'npx mcp-server-grafana', capabilities: ['dashboards', 'alerts', 'queries'] },
{ id: 'datadog', name: 'Datadog', description: 'Metrics, logs, monitors, and incidents', author: 'Community', category: 'DevTools', url: 'https://github.com/DataDog/mcp-server', installCmd: 'npx @datadog/mcp-server', capabilities: ['metrics', 'logs', 'monitors'] },
{ id: 'circleci', name: 'CircleCI', description: 'Pipelines, jobs, artifacts', author: 'Community', category: 'DevTools', url: 'https://github.com/CircleCI-Public/mcp-server-circleci', installCmd: 'npx mcp-server-circleci', capabilities: ['pipelines', 'jobs', 'artifacts'] },
{ id: 'terraform', name: 'Terraform', description: 'Infrastructure as code — plan, apply, state', author: 'Community', category: 'DevTools', url: 'https://github.com/hashicorp/mcp-server-terraform', installCmd: 'npx mcp-server-terraform', capabilities: ['plan', 'state', 'modules'] },
{ id: 'cloudflare', name: 'Cloudflare', description: 'Workers, DNS, KV, R2, analytics', author: 'Community', category: 'DevTools', url: 'https://github.com/cloudflare/mcp-server-cloudflare', installCmd: 'npx @cloudflare/mcp-server', capabilities: ['workers', 'dns', 'kv', 'r2'] },
{ id: 'git', name: 'Git', description: 'Read, search, and manipulate local Git repositories', author: 'MCP', category: 'Code', url: 'https://github.com/modelcontextprotocol/servers/tree/main/src/git', installCmd: 'uvx mcp-server-git', capabilities: ['log', 'diff', 'status', 'blame'], official: true },
{ id: 'postman', name: 'Postman', description: 'API development — collections, requests, environments', author: 'Community', category: 'DevTools', url: 'https://github.com/shannonlal/mcp-postman', installCmd: 'npx mcp-postman', capabilities: ['collections', 'requests', 'environments'] },
{ id: 'pulumi', name: 'Pulumi', description: 'Infrastructure as code with real languages — preview, up, state', author: 'Community', category: 'DevTools', url: 'https://github.com/pulumi/mcp-server', installCmd: 'npx @pulumi/mcp-server', capabilities: ['preview', 'up', 'state', 'stacks'] },
{ id: 'gitkraken', name: 'GitKraken', description: 'Git client — workspaces, PRs, issues across platforms', author: 'Community', category: 'DevTools', url: 'https://github.com/gitkraken/gk-cli', installCmd: 'gk mcp', capabilities: ['workspaces', 'prs', 'issues', 'focus'] },
{ id: 'semgrep', name: 'Semgrep', description: 'Static analysis — security and code quality scans', author: 'Community', category: 'DevTools', url: 'https://github.com/semgrep/mcp', installCmd: 'uvx semgrep-mcp', capabilities: ['scan', 'rules', 'findings'] },
{ id: 'e2b', name: 'E2B', description: 'Secure cloud sandboxes — run untrusted code in isolated containers', author: 'Community', category: 'DevTools', url: 'https://github.com/e2b-dev/mcp-server', installCmd: 'npx @e2b/mcp-server', capabilities: ['run', 'files', 'sandbox'] },
{ id: 'skyvern', name: 'Skyvern', description: 'Browser automation powered by LLMs — navigate, fill forms, scrape', author: 'Community', category: 'DevTools', url: 'https://github.com/Skyvern-AI/skyvern/tree/main/integrations/mcp', installCmd: 'uvx skyvern-mcp', capabilities: ['navigate', 'fill', 'scrape', 'vision'] },
// ═══════════════════════════════════════════════════════════════════
// COMMUNICATION (8)
// ═══════════════════════════════════════════════════════════════════
{ id: 'slack-mcp', name: 'Slack', description: 'Read/send messages, manage channels, search', author: 'MCP', category: 'Communication', url: 'https://github.com/modelcontextprotocol/servers/tree/main/src/slack', installCmd: 'npx @modelcontextprotocol/server-slack', capabilities: ['read', 'send', 'channels', 'search'], official: true },
{ id: 'gmail-mcp', name: 'Gmail', description: 'Read, search, and send emails', author: 'Community', category: 'Communication', url: 'https://github.com/nichochar/mcp-server-gmail', installCmd: 'npx mcp-server-gmail', capabilities: ['read', 'search', 'send'] },
{ id: 'discord-mcp', name: 'Discord', description: 'Read/send messages, manage channels', author: 'Community', category: 'Communication', url: 'https://github.com/discord/mcp-server', installCmd: 'npx mcp-server-discord', capabilities: ['read', 'send', 'channels'] },
{ id: 'teams-mcp', name: 'Microsoft Teams', description: 'Messages, channels, meetings', author: 'Community', category: 'Communication', url: 'https://github.com/microsoft/mcp-server-teams', installCmd: 'npx mcp-server-teams', capabilities: ['messages', 'channels', 'meetings'] },
{ id: 'telegram', name: 'Telegram', description: 'Send/receive Telegram messages', author: 'Community', category: 'Communication', url: 'https://github.com/nichochar/mcp-server-telegram', installCmd: 'npx mcp-server-telegram', capabilities: ['send', 'receive', 'groups'] },
{ id: 'whatsapp', name: 'WhatsApp', description: 'Send WhatsApp messages via Business API', author: 'Community', category: 'Communication', url: 'https://github.com/nichochar/mcp-server-whatsapp', installCmd: 'npx mcp-server-whatsapp', capabilities: ['send', 'templates'] },
{ id: 'twilio', name: 'Twilio', description: 'SMS, voice calls, WhatsApp messaging', author: 'Community', category: 'Communication', url: 'https://github.com/twilio/mcp-server', installCmd: 'npx @twilio/mcp-server', capabilities: ['sms', 'voice', 'whatsapp'] },
{ id: 'sendgrid', name: 'SendGrid', description: 'Transactional and marketing email', author: 'Community', category: 'Communication', url: 'https://github.com/sendgrid/mcp-server', installCmd: 'npx mcp-server-sendgrid', capabilities: ['send', 'templates', 'stats'] },
{ id: 'bluesky', name: 'Bluesky', description: 'Post, read, and search the Bluesky social network (AT Protocol)', author: 'Community', category: 'Communication', url: 'https://github.com/semioz/bluesky-mcp', installCmd: 'npx bluesky-mcp', capabilities: ['post', 'feed', 'search', 'follow'] },
// ═══════════════════════════════════════════════════════════════════
// PRODUCTIVITY (12)
// ═══════════════════════════════════════════════════════════════════
{ id: 'notion-mcp', name: 'Notion', description: 'Pages, databases, search, create content', author: 'Community', category: 'Productivity', url: 'https://github.com/makenotion/notion-mcp-server', installCmd: 'npx @notionhq/mcp-server', capabilities: ['search', 'read', 'create', 'databases'] },
{ id: 'linear-mcp', name: 'Linear', description: 'Issues, projects, cycles, teams', author: 'Community', category: 'Productivity', url: 'https://github.com/linear/mcp-server-linear', installCmd: 'npx mcp-server-linear', capabilities: ['issues', 'projects', 'cycles'] },
{ id: 'jira-mcp', name: 'Jira', description: 'Issues, projects, sprints, boards', author: 'Community', category: 'Productivity', url: 'https://github.com/atlassian/mcp-server-jira', installCmd: 'npx mcp-server-jira', capabilities: ['issues', 'search', 'projects', 'sprints'] },
{ id: 'confluence-mcp', name: 'Confluence', description: 'Read and search Confluence pages', author: 'Community', category: 'Productivity', url: 'https://github.com/atlassian/mcp-server-confluence', installCmd: 'npx mcp-server-confluence', capabilities: ['search', 'read', 'spaces'] },
{ id: 'asana-mcp', name: 'Asana', description: 'Tasks, projects, teams, workspaces', author: 'Community', category: 'Productivity', url: 'https://github.com/asana/mcp-server', installCmd: 'npx mcp-server-asana', capabilities: ['tasks', 'projects', 'teams'] },
{ id: 'todoist', name: 'Todoist', description: 'Task management — projects, tasks, labels', author: 'Community', category: 'Productivity', url: 'https://github.com/doist/mcp-server-todoist', installCmd: 'npx mcp-server-todoist', capabilities: ['tasks', 'projects', 'labels'] },
{ id: 'google-calendar', name: 'Google Calendar', description: 'Events, scheduling, availability', author: 'Community', category: 'Productivity', url: 'https://github.com/nichochar/mcp-server-gcal', installCmd: 'npx mcp-server-gcal', capabilities: ['events', 'create', 'availability'] },
{ id: 'google-docs', name: 'Google Docs', description: 'Read and edit Google Docs', author: 'Community', category: 'Productivity', url: 'https://github.com/nichochar/mcp-server-gdocs', installCmd: 'npx mcp-server-gdocs', capabilities: ['read', 'edit', 'create'] },
{ id: 'google-sheets', name: 'Google Sheets', description: 'Read, write, and query spreadsheets', author: 'Community', category: 'Productivity', url: 'https://github.com/nichochar/mcp-server-gsheets', installCmd: 'npx mcp-server-gsheets', capabilities: ['read', 'write', 'query'] },
{ id: 'obsidian-mcp', name: 'Obsidian', description: 'Read and search Obsidian vaults', author: 'Community', category: 'Productivity', url: 'https://github.com/obsidian-community/mcp-server', installCmd: 'npx mcp-server-obsidian', capabilities: ['read', 'search', 'backlinks'] },
{ id: 'monday-mcp', name: 'monday.com', description: 'Boards, items, updates, automations', author: 'Community', category: 'Productivity', url: 'https://github.com/mondaycom/mcp-server', installCmd: 'npx mcp-server-monday', capabilities: ['boards', 'items', 'updates'] },
{ id: 'clickup', name: 'ClickUp', description: 'Tasks, spaces, lists, docs', author: 'Community', category: 'Productivity', url: 'https://github.com/clickup/mcp-server', installCmd: 'npx mcp-server-clickup', capabilities: ['tasks', 'spaces', 'docs'] },
{ id: 'atlassian', name: 'Atlassian', description: 'Unified Jira + Confluence access across Cloud and Server', author: 'Community', category: 'Productivity', url: 'https://github.com/sooperset/mcp-atlassian', installCmd: 'uvx mcp-atlassian', capabilities: ['jira', 'confluence', 'search', 'issues'] },
{ id: 'make', name: 'Make', description: 'Run Make.com (Integromat) scenarios — automation orchestration', author: 'Community', category: 'Productivity', url: 'https://github.com/integromat/make-mcp-server', installCmd: 'npx @make/mcp-server', capabilities: ['scenarios', 'triggers', 'webhooks'] },
{ id: 'pipedream', name: 'Pipedream', description: 'Workflow automation with 2000+ integrations', author: 'Community', category: 'Productivity', url: 'https://github.com/PipedreamHQ/pipedream/tree/master/modelcontextprotocol', installCmd: 'npx @pipedream/mcp-server', capabilities: ['workflows', 'apps', 'triggers'] },
{ id: 'microsoft-365', name: 'Microsoft 365', description: 'Full M365 suite via Graph API — mail, files, calendar, Excel', author: 'Community', category: 'Productivity', url: 'https://github.com/softeria/ms-365-mcp-server', installCmd: 'npx ms-365-mcp-server', capabilities: ['mail', 'files', 'calendar', 'excel'] },
// ═══════════════════════════════════════════════════════════════════
// BUSINESS & CRM (8)
// ═══════════════════════════════════════════════════════════════════
{ id: 'stripe-mcp', name: 'Stripe', description: 'Payments, subscriptions, customers, invoices', author: 'Community', category: 'Business', url: 'https://github.com/stripe/agent-toolkit', installCmd: 'npx @stripe/mcp-server', capabilities: ['payments', 'customers', 'subscriptions', 'invoices'] },
{ id: 'salesforce-mcp', name: 'Salesforce', description: 'CRM — accounts, contacts, opportunities', author: 'Community', category: 'Business', url: 'https://github.com/salesforce/mcp-server', installCmd: 'npx mcp-server-salesforce', capabilities: ['accounts', 'contacts', 'opportunities', 'soql'] },
{ id: 'hubspot-mcp', name: 'HubSpot', description: 'CRM, marketing, sales, service hub', author: 'Community', category: 'Business', url: 'https://github.com/hubspot/mcp-server', installCmd: 'npx mcp-server-hubspot', capabilities: ['contacts', 'deals', 'tickets', 'email'] },
{ id: 'shopify', name: 'Shopify', description: 'Products, orders, customers, inventory', author: 'Community', category: 'Business', url: 'https://github.com/shopify/mcp-server', installCmd: 'npx mcp-server-shopify', capabilities: ['products', 'orders', 'customers'] },
{ id: 'airtable-mcp', name: 'Airtable', description: 'Bases, tables, records, views', author: 'Community', category: 'Business', url: 'https://github.com/airtable/mcp-server', installCmd: 'npx mcp-server-airtable', capabilities: ['records', 'tables', 'views'] },
{ id: 'intercom', name: 'Intercom', description: 'Customer messaging, tickets, articles', author: 'Community', category: 'Business', url: 'https://github.com/intercom/mcp-server', installCmd: 'npx mcp-server-intercom', capabilities: ['conversations', 'contacts', 'articles'] },
{ id: 'zendesk', name: 'Zendesk', description: 'Support tickets, users, organizations', author: 'Community', category: 'Business', url: 'https://github.com/zendesk/mcp-server', installCmd: 'npx mcp-server-zendesk', capabilities: ['tickets', 'users', 'search'] },
{ id: 'freshdesk', name: 'Freshdesk', description: 'Help desk — tickets, contacts, knowledge base', author: 'Community', category: 'Business', url: 'https://github.com/nichochar/mcp-server-freshdesk', installCmd: 'npx mcp-server-freshdesk', capabilities: ['tickets', 'contacts', 'kb'] },
{ id: 'chargebee', name: 'Chargebee', description: 'Subscription billing — customers, invoices, plans', author: 'Community', category: 'Business', url: 'https://github.com/chargebee/agentkit/tree/main/modelcontextprotocol', installCmd: 'npx @chargebee/mcp-server', capabilities: ['subscriptions', 'invoices', 'customers'] },
{ id: 'google-ads', name: 'Google Ads', description: 'Campaigns, keywords, performance reports', author: 'Community', category: 'Business', url: 'https://github.com/gomarble-ai/google-ads-mcp-server', installCmd: 'npx google-ads-mcp', capabilities: ['campaigns', 'keywords', 'reports'] },
{ id: 'facebook-ads', name: 'Facebook Ads', description: 'Meta ad accounts, campaigns, creatives, insights', author: 'Community', category: 'Business', url: 'https://github.com/gomarble-ai/facebook-ads-mcp-server', installCmd: 'npx facebook-ads-mcp', capabilities: ['campaigns', 'creatives', 'insights'] },
{ id: 'google-maps', name: 'Google Maps', description: 'Geocoding, places, directions, distance matrix', author: 'Community', category: 'Business', url: 'https://github.com/cablate/mcp-google-map', installCmd: 'npx mcp-google-map', capabilities: ['geocode', 'places', 'directions'] },
// ═══════════════════════════════════════════════════════════════════
// CLOUD & INFRASTRUCTURE (8)
// ═══════════════════════════════════════════════════════════════════
{ id: 'aws', name: 'AWS', description: 'EC2, Lambda, CloudWatch, IAM, and more', author: 'Community', category: 'Cloud', url: 'https://github.com/aws/mcp-server-aws', installCmd: 'npx @aws/mcp-server', capabilities: ['ec2', 'lambda', 'cloudwatch', 'iam'] },
{ id: 'gcp', name: 'Google Cloud', description: 'Compute, BigQuery, Cloud Run, IAM', author: 'Community', category: 'Cloud', url: 'https://github.com/GoogleCloudPlatform/mcp-server', installCmd: 'npx mcp-server-gcp', capabilities: ['compute', 'bigquery', 'run', 'iam'] },
{ id: 'azure', name: 'Azure', description: 'VMs, Functions, CosmosDB, Active Directory', author: 'Community', category: 'Cloud', url: 'https://github.com/microsoft/mcp-server-azure', installCmd: 'npx mcp-server-azure', capabilities: ['vms', 'functions', 'cosmosdb'] },
{ id: 'fly', name: 'Fly.io', description: 'Deploy and manage Fly.io applications', author: 'Community', category: 'Cloud', url: 'https://github.com/fly-io/mcp-server', installCmd: 'npx mcp-server-fly', capabilities: ['deploy', 'machines', 'secrets'] },
{ id: 'railway', name: 'Railway', description: 'Deploy apps, manage services and databases', author: 'Community', category: 'Cloud', url: 'https://github.com/railwayapp/mcp-server', installCmd: 'npx mcp-server-railway', capabilities: ['deploy', 'services', 'variables'] },
{ id: 'render', name: 'Render', description: 'Web services, databases, cron jobs', author: 'Community', category: 'Cloud', url: 'https://github.com/render-oss/mcp-server', installCmd: 'npx mcp-server-render', capabilities: ['services', 'databases', 'deploys'] },
{ id: 'digitalocean', name: 'DigitalOcean', description: 'Droplets, databases, Kubernetes', author: 'Community', category: 'Cloud', url: 'https://github.com/digitalocean/mcp-server', installCmd: 'npx mcp-server-digitalocean', capabilities: ['droplets', 'databases', 'k8s'] },
{ id: 'hetzner', name: 'Hetzner', description: 'Servers, networks, firewalls', author: 'Community', category: 'Cloud', url: 'https://github.com/nichochar/mcp-server-hetzner', installCmd: 'npx mcp-server-hetzner', capabilities: ['servers', 'networks', 'firewalls'] },
{ id: 'edgeone', name: 'EdgeOne Pages', description: 'Tencent EdgeOne — deploy static sites to global edge', author: 'Community', category: 'Cloud', url: 'https://github.com/TencentEdgeOne/edgeone-pages-mcp', installCmd: 'npx edgeone-pages-mcp', capabilities: ['deploy', 'domains', 'edge'] },
// ═══════════════════════════════════════════════════════════════════
// AI & ML (8)
// ═══════════════════════════════════════════════════════════════════
{ id: 'openai-mcp', name: 'OpenAI', description: 'Chat, embeddings, image generation, moderation', author: 'Community', category: 'AI & ML', url: 'https://github.com/openai/mcp-server', installCmd: 'npx mcp-server-openai', capabilities: ['chat', 'embeddings', 'images'] },
{ id: 'huggingface', name: 'Hugging Face', description: 'Models, datasets, spaces, inference', author: 'Community', category: 'AI & ML', url: 'https://github.com/huggingface/mcp-server', installCmd: 'npx mcp-server-huggingface', capabilities: ['models', 'datasets', 'inference'] },
{ id: 'replicate', name: 'Replicate', description: 'Run ML models via API — image, video, audio', author: 'Community', category: 'AI & ML', url: 'https://github.com/replicate/mcp-server', installCmd: 'npx mcp-server-replicate', capabilities: ['predict', 'models', 'deployments'] },
{ id: 'stability', name: 'Stability AI', description: 'Image generation — Stable Diffusion API', author: 'Community', category: 'AI & ML', url: 'https://github.com/stability-ai/mcp-server', installCmd: 'npx mcp-server-stability', capabilities: ['generate', 'edit', 'upscale'] },
{ id: 'langchain', name: 'LangChain', description: 'Chain tools, retrievers, and agents', author: 'Community', category: 'AI & ML', url: 'https://github.com/langchain-ai/mcp-server', installCmd: 'npx mcp-server-langchain', capabilities: ['chains', 'retrievers', 'tools'] },
{ id: 'pinecone', name: 'Pinecone', description: 'Vector database for embeddings search', author: 'Community', category: 'AI & ML', url: 'https://github.com/pinecone-io/mcp-server', installCmd: 'npx mcp-server-pinecone', capabilities: ['upsert', 'query', 'namespaces'] },
{ id: 'weaviate', name: 'Weaviate', description: 'Vector search engine with ML models', author: 'Community', category: 'AI & ML', url: 'https://github.com/weaviate/mcp-server', installCmd: 'npx mcp-server-weaviate', capabilities: ['search', 'objects', 'schema'] },
{ id: 'elevenlabs-mcp', name: 'ElevenLabs', description: 'Text-to-speech, voice cloning', author: 'Community', category: 'AI & ML', url: 'https://github.com/elevenlabs/mcp-server', installCmd: 'npx mcp-server-elevenlabs', capabilities: ['tts', 'voices', 'clone'] },
{ id: 'llamacloud', name: 'LlamaCloud', description: 'LlamaIndex managed RAG — parse, index, query documents', author: 'Community', category: 'AI & ML', url: 'https://github.com/run-llama/mcp-server-llamacloud', installCmd: 'npx mcp-server-llamacloud', capabilities: ['parse', 'index', 'query'] },
{ id: 'fastmcp', name: 'FastMCP', description: 'Framework for building MCP servers in Python — meta server', author: 'Community', category: 'AI & ML', url: 'https://github.com/jlowin/fastmcp', installCmd: 'uvx fastmcp', capabilities: ['framework', 'tools', 'prompts'] },
{ id: 'opik', name: 'Opik', description: 'Comet ML — LLM observability, traces, evals', author: 'Community', category: 'AI & ML', url: 'https://github.com/comet-ml/opik-mcp', installCmd: 'npx @comet/opik-mcp', capabilities: ['traces', 'evals', 'datasets'] },
// ═══════════════════════════════════════════════════════════════════
// ANALYTICS (6)
// ═══════════════════════════════════════════════════════════════════
{ id: 'posthog', name: 'PostHog', description: 'Product analytics, feature flags, experiments', author: 'Community', category: 'Analytics', url: 'https://github.com/PostHog/mcp-server', installCmd: 'npx mcp-server-posthog', capabilities: ['events', 'funnels', 'feature_flags'] },
{ id: 'amplitude', name: 'Amplitude', description: 'Product analytics and user behavior', author: 'Community', category: 'Analytics', url: 'https://github.com/amplitude/mcp-server', installCmd: 'npx mcp-server-amplitude', capabilities: ['events', 'cohorts', 'charts'] },
{ id: 'mixpanel', name: 'Mixpanel', description: 'Event analytics, funnels, retention', author: 'Community', category: 'Analytics', url: 'https://github.com/mixpanel/mcp-server', installCmd: 'npx mcp-server-mixpanel', capabilities: ['events', 'funnels', 'reports'] },
{ id: 'plausible', name: 'Plausible', description: 'Privacy-focused web analytics', author: 'Community', category: 'Analytics', url: 'https://github.com/plausible/mcp-server', installCmd: 'npx mcp-server-plausible', capabilities: ['stats', 'pages', 'sources'] },
{ id: 'prometheus', name: 'Prometheus', description: 'Metrics, alerts, targets', author: 'Community', category: 'Analytics', url: 'https://github.com/prometheus/mcp-server', installCmd: 'npx mcp-server-prometheus', capabilities: ['query', 'alerts', 'targets'] },
{ id: 'google-analytics', name: 'Google Analytics', description: 'GA4 reports, realtime, audiences', author: 'Community', category: 'Analytics', url: 'https://github.com/nichochar/mcp-server-ga4', installCmd: 'npx mcp-server-ga4', capabilities: ['reports', 'realtime', 'audiences'] },
{ id: 'tinybird', name: 'Tinybird', description: 'Real-time analytics — ClickHouse-powered data pipelines', author: 'Community', category: 'Analytics', url: 'https://github.com/tinybirdco/mcp-tinybird', installCmd: 'npx @tinybird/mcp-server', capabilities: ['query', 'pipes', 'data_sources'] },
{ id: 'victoriametrics', name: 'VictoriaMetrics', description: 'High-performance time-series database — queries, alerts', author: 'Community', category: 'Analytics', url: 'https://github.com/VictoriaMetrics-Community/mcp-victoriametrics', installCmd: 'npx mcp-victoriametrics', capabilities: ['query', 'metrics', 'alerts'] },
// ═══════════════════════════════════════════════════════════════════
// SECURITY (4)
// ═══════════════════════════════════════════════════════════════════
{ id: 'vault', name: 'HashiCorp Vault', description: 'Secrets management — read, list, manage', author: 'Community', category: 'Security', url: 'https://github.com/hashicorp/mcp-server-vault', installCmd: 'npx mcp-server-vault', capabilities: ['secrets', 'policies', 'auth'] },
{ id: 'snyk', name: 'Snyk', description: 'Security scanning — vulnerabilities, licenses', author: 'Community', category: 'Security', url: 'https://github.com/snyk/mcp-server', installCmd: 'npx @snyk/mcp-server', capabilities: ['scan', 'vulnerabilities', 'licenses'] },
{ id: 'onepassword', name: '1Password', description: 'Password and secret management', author: 'Community', category: 'Security', url: 'https://github.com/1Password/mcp-server', installCmd: 'npx mcp-server-1password', capabilities: ['items', 'vaults', 'secrets'] },
{ id: 'bitwarden', name: 'Bitwarden', description: 'Password manager — items, folders, organizations', author: 'Community', category: 'Security', url: 'https://github.com/bitwarden/mcp-server', installCmd: 'npx mcp-server-bitwarden', capabilities: ['items', 'folders', 'generate'] },
{ id: 'keycloak', name: 'Keycloak', description: 'Identity and access management — users, roles, realms', author: 'Community', category: 'Security', url: 'https://github.com/idoyudha/mcp-keycloak', installCmd: 'npx mcp-keycloak', capabilities: ['users', 'roles', 'realms', 'sso'] },
// ═══════════════════════════════════════════════════════════════════
// MEDIA (6)
// ═══════════════════════════════════════════════════════════════════
{ id: 'figma', name: 'Figma', description: 'Read designs, components, variables, comments', author: 'Community', category: 'Media', url: 'https://github.com/nichochar/mcp-server-figma', installCmd: 'npx mcp-server-figma', capabilities: ['files', 'components', 'comments'] },
{ id: 'canva', name: 'Canva', description: 'Designs, templates, brand assets', author: 'Community', category: 'Media', url: 'https://github.com/canva/mcp-server', installCmd: 'npx mcp-server-canva', capabilities: ['designs', 'templates', 'export'] },
{ id: 'youtube', name: 'YouTube', description: 'Video search, transcripts, channel data', author: 'Community', category: 'Media', url: 'https://github.com/nichochar/mcp-server-youtube', installCmd: 'npx mcp-server-youtube', capabilities: ['search', 'transcripts', 'channels'] },
{ id: 'spotify', name: 'Spotify', description: 'Search tracks, playlists, playback control', author: 'Community', category: 'Media', url: 'https://github.com/nichochar/mcp-server-spotify', installCmd: 'npx mcp-server-spotify', capabilities: ['search', 'playlists', 'playback'] },
{ id: 'unsplash', name: 'Unsplash', description: 'Search and download stock photos', author: 'Community', category: 'Media', url: 'https://github.com/nichochar/mcp-server-unsplash', installCmd: 'npx mcp-server-unsplash', capabilities: ['search', 'download', 'collections'] },
{ id: 'dall-e', name: 'DALL-E', description: 'AI image generation via OpenAI', author: 'Community', category: 'Media', url: 'https://github.com/nichochar/mcp-server-dalle', installCmd: 'npx mcp-server-dalle', capabilities: ['generate', 'edit', 'variations'] },
{ id: 'videodb', name: 'VideoDB', description: 'Serverless video database — index, search, stream, auto-edit', author: 'Community', category: 'Media', url: 'https://github.com/video-db/agent-toolkit/tree/main/modelcontextprotocol', installCmd: 'npx @videodb/mcp-server', capabilities: ['index', 'search', 'stream', 'edit'] },
{ id: 'echarts', name: 'Apache ECharts', description: 'Generate charts from data — bar, line, pie, heatmap, radar', author: 'Community', category: 'Media', url: 'https://github.com/hustcc/mcp-echarts', installCmd: 'npx mcp-echarts', capabilities: ['charts', 'visualization', 'export'] },
// ═══════════════════════════════════════════════════════════════════
// UTILITIES (8)
// ═══════════════════════════════════════════════════════════════════
{ id: 'memory-mcp', name: 'Memory', description: 'Persistent key-value memory for agents', author: 'MCP', category: 'Utilities', url: 'https://github.com/modelcontextprotocol/servers/tree/main/src/memory', installCmd: 'npx @modelcontextprotocol/server-memory', capabilities: ['store', 'retrieve', 'search'], official: true },
{ id: 'time', name: 'Time', description: 'Current time, timezone conversions, countdowns', author: 'MCP', category: 'Utilities', url: 'https://github.com/modelcontextprotocol/servers/tree/main/src/time', installCmd: 'npx @modelcontextprotocol/server-time', capabilities: ['current', 'convert', 'diff'], official: true },
{ id: 'sequentialthinking', name: 'Sequential Thinking', description: 'Chain-of-thought reasoning with revision', author: 'MCP', category: 'Utilities', url: 'https://github.com/modelcontextprotocol/servers/tree/main/src/sequentialthinking', installCmd: 'npx @modelcontextprotocol/server-sequentialthinking', capabilities: ['think', 'revise'], official: true },
{ id: 'context7', name: 'Context7', description: 'Library documentation lookup', author: 'Community', category: 'Utilities', url: 'https://github.com/upstash/context7', installCmd: 'npx @upstash/context7-mcp', capabilities: ['docs', 'examples', 'api_ref'] },
{ id: 'magic-mcp', name: 'Magic MCP', description: 'Generate UI components and previews', author: 'Community', category: 'Utilities', url: 'https://github.com/nichochar/magic-mcp', installCmd: 'npx magic-mcp', capabilities: ['generate_ui', 'preview'] },
{ id: 'mcp-shell', name: 'Shell', description: 'Safe shell command runner with allowlists', author: 'Community', category: 'Utilities', url: 'https://github.com/nichochar/mcp-server-shell', installCmd: 'npx mcp-server-shell', capabilities: ['run', 'scripts'] },
{ id: 'everything', name: 'Everything', description: 'MCP protocol test server — all resource types', author: 'MCP', category: 'Utilities', url: 'https://github.com/modelcontextprotocol/servers/tree/main/src/everything', installCmd: 'npx @modelcontextprotocol/server-everything', capabilities: ['resources', 'tools', 'prompts'], official: true },
{ id: 'calculator', name: 'Calculator', description: 'Math operations, unit conversions', author: 'Community', category: 'Utilities', url: 'https://github.com/nichochar/mcp-server-calculator', installCmd: 'npx mcp-server-calculator', capabilities: ['math', 'convert', 'statistics'] },
{ id: 'gitingest', name: 'GitIngest', description: 'Turn any Git repo into prompt-friendly context summaries', author: 'Community', category: 'Utilities', url: 'https://github.com/cyclotruc/gitingest', installCmd: 'uvx gitingest-mcp', capabilities: ['summarize', 'ingest', 'context'] },
{ id: 'xcode', name: 'Xcode', description: 'Drive Xcode builds, simulators, and iOS/macOS projects', author: 'Community', category: 'Utilities', url: 'https://github.com/ShenghaiWang/xcodebuild', installCmd: 'npx xcodebuild-mcp', capabilities: ['build', 'test', 'simulator'] },
];
// ═══════════════════════════════════════════════════════════════════════
// Dedup guard — normalize ids + url check, fires at module load time.
// ═══════════════════════════════════════════════════════════════════════
/**
* Normalize an MCP server identifier or name to a canonical dedup key.
* Strips common MCP affixes, npm scopes, and punctuation so that
* "GitHub" / "github-mcp" / "mcp-server-github" / "@modelcontextprotocol/server-github"
* all collapse to the same key ("github").
*
* Used both by the internal dedup guard and by any caller that needs to
* match a server name against an external source (awesome-list, composio, etc).
*/
export function normalizeMcpId(raw: string): string {
return raw
.toLowerCase()
.replace(/^@[^/]+\//, '') // strip npm scope
.replace(/[.()[\]/,]/g, ' ') // punctuation → space
.replace(/\s+/g, ' ')
.replace(/\bmcp[- ]?server\b/g, '')
.replace(/\bserver[- ]?mcp\b/g, '')
.replace(/\bmcp\b/g, '')
.replace(/^server[- ]/, '') // leading "server-" (post-scope)
.replace(/[- ]server$/, '') // trailing "-server"
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
.trim();
}
/**
* Enforces catalog uniqueness. Throws a descriptive error if two servers
* collide on either:
* 1. normalized id (catches "github-mcp" + "github" + "mcp-server-github")
* 2. repository url (catches accidental copy-paste of an existing entry)
*
* Runs once at module load so Vite / tsc builds fail fast when a contributor
* adds a duplicate. O(n) with early exit.
*/
function assertCatalogUnique(catalog: readonly McpServer[]): void {
const seenIds = new Map<string, string>();
const seenUrls = new Map<string, string>();
for (const server of catalog) {
const normalizedId = normalizeMcpId(server.id);
const existingId = seenIds.get(normalizedId);
if (existingId && existingId !== server.id) {
throw new Error(
`[mcp-registry] duplicate server id: "${server.id}" collides with ` +
`"${existingId}" (normalized: "${normalizedId}"). ` +
`If these are genuinely different servers, rename one so their ` +
`normalized keys don't collide.`
);
}
seenIds.set(normalizedId, server.id);
const existingUrl = seenUrls.get(server.url);
if (existingUrl && existingUrl !== server.id) {
throw new Error(
`[mcp-registry] duplicate repository url: ` +
`"${server.id}" and "${existingUrl}" both point at ${server.url}`
);
}
seenUrls.set(server.url, server.id);
}
}
assertCatalogUnique(MCP_CATALOG);

View File

@@ -0,0 +1,82 @@
/**
* Canonical risk / approval / audit taxonomy (P7/D15 Track A, binding A1).
*
* Before this module the vocabulary was redeclared in five places that had
* drifted apart — trust-model.ts (RiskLevel without 'critical'), confirmation.ts
* (a hardcoded ApprovalClass mapper), install-audit.ts (the widest set, plus a
* hand-duplicated SQLite CHECK in hive-mind-core/schema.ts), and
* team-capability-governance.ts (a 4th set keyed on 'none'). A producer narrower
* than its store meant a `critical` risk could only ever be hand-written, and the
* FE modal couldn't represent the `critical`/`blocked` levels the audit recorded.
*
* This is the SINGLE SOURCE. Each enum is the WIDEST of the prior sets so no
* producer/store mismatch remains. Every other module imports from here.
*
* Two axes, one rule:
* - `RiskLevel` = how dangerous the action is (severity → display colour).
* - `ApprovalClass` = how strongly to gate it (standard < elevated < critical < blocked).
* They are NOT the same axis; surfaces that collapsed them must keep them distinct.
*
* Each type is derived from a runtime `const` array so the array can also drive
* the SQLite CHECK constraints (A3) and parity tests, with zero chance of the
* type and the value list drifting.
*/
/** Severity of an action. Widest prior set = install-audit's (adds 'critical'). */
export const RISK_LEVELS = ['low', 'medium', 'high', 'critical'] as const;
export type RiskLevel = (typeof RISK_LEVELS)[number];
/** Gating strength. Widest prior set = install-audit's (adds 'blocked'). */
export const APPROVAL_CLASSES = ['standard', 'elevated', 'critical', 'blocked'] as const;
export type ApprovalClass = (typeof APPROVAL_CLASSES)[number];
/** Provenance of a capability. Widest prior set = install-audit's (adds 'security-gate'). */
export const TRUST_SOURCES = [
'builtin', // First-party, ships with Waggle
'starter_pack', // Curated starter skills
'local_user', // User-created via create_skill
'third_party_verified', // Verified registry
'third_party_unverified', // Unknown registry source
'unknown', // No provenance information
'security-gate', // Set by the SecurityGate scan (blocked/flagged)
] as const;
export type TrustSource = (typeof TRUST_SOURCES)[number];
/** How a risk assessment was derived. */
export const ASSESSMENT_MODES = ['declared', 'heuristic', 'mixed'] as const;
export type AssessmentMode = (typeof ASSESSMENT_MODES)[number];
/** Lifecycle action recorded in install_audit. Includes 'uninstalled' (P5/D4). */
export const AUDIT_ACTIONS = [
'proposed', 'approved', 'installed', 'rejected', 'failed', 'blocked', 'uninstalled',
] as const;
export type AuditAction = (typeof AUDIT_ACTIONS)[number];
/** Kind of capability an audit row concerns. */
export const AUDIT_CAPABILITY_TYPES = [
'native', 'skill', 'plugin', 'mcp', 'connector', 'marketplace',
] as const;
export type AuditCapabilityType = (typeof AUDIT_CAPABILITY_TYPES)[number];
/** Who initiated an audited action. */
export const AUDIT_INITIATORS = ['agent', 'user', 'system'] as const;
export type AuditInitiator = (typeof AUDIT_INITIATORS)[number];
/** Ascending severity order — index = rank. Used to compare/sort risk levels so a
* `critical` always outranks `low` (the team-governance bug was a `?? 0` that
* sorted `critical` BELOW `low`). */
export function riskRank(level: RiskLevel): number {
return RISK_LEVELS.indexOf(level);
}
/** True when `level` meets or exceeds `threshold` on the canonical severity scale. */
export function riskAtLeast(level: RiskLevel, threshold: RiskLevel): boolean {
return riskRank(level) >= riskRank(threshold);
}
/** Build a SQLite `CHECK (col IN (...))` clause body from a const value list, so
* the DDL constraint and the TS union can never drift (A3 single-sources both
* install-audit.ts and hive-mind-core/schema.ts from these arrays). */
export function sqlInList(values: readonly string[]): string {
return values.map((v) => `'${v}'`).join(', ');
}

View File

@@ -0,0 +1,138 @@
// @waggle/shared — Zod validation schemas for API requests
import { z } from 'zod';
import { AGENT_RUN_STATES } from './types.js';
export const createTeamSchema = z.object({
name: z.string().min(1).max(100),
slug: z.string().min(1).max(50).regex(/^[a-z0-9-]+$/),
});
export const inviteMemberSchema = z.object({
email: z.string().email(),
role: z.enum(['admin', 'member']),
});
export const updateMemberSchema = z.object({
role: z.enum(['admin', 'member']).optional(),
roleDescription: z.string().max(500).optional(),
interests: z.array(z.string()).optional(),
});
export const createTaskSchema = z.object({
title: z.string().min(1).max(200),
description: z.string().max(5000).optional(),
priority: z.enum(['critical', 'high', 'normal', 'low']).default('normal'),
parentTaskId: z.string().uuid().optional(),
});
export const updateTaskSchema = z.object({
title: z.string().min(1).max(200).optional(),
description: z.string().max(5000).optional(),
status: z.enum(['open', 'claimed', 'in_progress', 'done', 'cancelled']).optional(),
priority: z.enum(['critical', 'high', 'normal', 'low']).optional(),
assignedTo: z.string().uuid().nullable().optional(),
});
export const sendMessageSchema = z.object({
type: z.enum(['broadcast', 'request', 'response']),
subtype: z.enum([
'knowledge_check', 'task_delegation', 'skill_request', 'model_recommendation',
'knowledge_match', 'task_claim', 'discovery', 'routed_share', 'skill_share', 'model_recipe',
]),
content: z.record(z.unknown()),
referenceId: z.string().uuid().optional(),
routing: z.array(z.object({ userId: z.string().uuid(), reason: z.string() })).optional(),
});
// UX-Refactor Phase 3 (PRD §15.5): shared enum fragments for the Agent entity.
// Ref-id arrays use plain min(1) strings — workspace/agent ids in this repo are
// NOT all UUIDs (cron ids are numeric, artifact ids are `art_${uuid}`).
// status derives from the §14.5 AGENT_RUN_STATES tuple in types.ts (the
// vocabulary the sidecar agents-store actually persists) — single source.
const agentTypeEnum = z.enum(['personal', 'workspace', 'team', 'autonomous']);
const autonomyLevelEnum = z.enum(['manual', 'guided', 'medium', 'high']);
const scopeEnum = z.enum(['personal', 'workspace', 'team', 'organization']);
const agentStatusEnum = z.enum(AGENT_RUN_STATES);
// NOTE: this schema is consumed by the Clerk-gated CLOUD route
// (packages/server/src/routes/agents.ts). The cloud AgentService persists only
// the legacy fields (name/role/systemPrompt/model/tools/config/teamId) — the
// §15.5 fields below validate but are NOT stored there yet. The local sidecar
// surface (local/routes/agents.ts) is the §15.5 system of record.
export const createAgentSchema = z.object({
name: z.string().min(1).max(100),
role: z.string().max(500).optional(),
systemPrompt: z.string().max(10000).optional(),
model: z.string().min(1).default('claude-haiku-4-5'),
tools: z.array(z.string()).default([]),
config: z.record(z.unknown()).default({}),
teamId: z.string().uuid().optional(),
// §15.5 optional Agent-entity fields (Phase 3) — all optional for back-compat.
type: agentTypeEnum.optional(),
goal: z.string().max(4000).optional(),
description: z.string().max(2000).optional(),
personaId: z.string().min(1).max(200).optional(),
autonomyLevel: autonomyLevelEnum.optional(),
workspaceIds: z.array(z.string().min(1)).optional(),
memoryScopes: z.array(scopeEnum).optional(),
skillIds: z.array(z.string().min(1)).optional(),
connectorIds: z.array(z.string().min(1)).optional(),
mcpIds: z.array(z.string().min(1)).optional(),
permissions: z.record(z.unknown()).optional(),
status: agentStatusEnum.optional(),
});
// NOTE (Phase 3A review): speculative updateAgent/createSkill/updateSkill/
// createAutomation/updateAutomation schemas were removed here — no route
// consumed them and their shapes contradicted the implemented wire contracts
// (the local routes validate inline; skills mandate steps[]; automations use a
// nested trigger object). Re-add a schema only together with a route that
// parses with it.
export const createAgentGroupSchema = z.object({
name: z.string().min(1).max(100),
description: z.string().max(500).optional(),
strategy: z.enum(['parallel', 'sequential', 'coordinator']),
members: z.array(z.object({
agentId: z.string().uuid(),
roleInGroup: z.enum(['lead', 'worker']).default('worker'),
executionOrder: z.number().int().min(0).default(0),
})),
});
export const createEntitySchema = z.object({
entityType: z.string().min(1).max(100),
name: z.string().min(1).max(200),
properties: z.record(z.unknown()).default({}),
validFrom: z.string().datetime().optional(),
validTo: z.string().datetime().optional(),
});
export const createRelationSchema = z.object({
sourceId: z.string().uuid(),
targetId: z.string().uuid(),
relationType: z.string().min(1).max(100),
confidence: z.number().min(0).max(1).default(1.0),
properties: z.record(z.unknown()).default({}),
});
export const createResourceSchema = z.object({
resourceType: z.enum(['model_recipe', 'skill', 'tool_config', 'prompt_template']),
name: z.string().min(1).max(200),
description: z.string().max(1000).optional(),
config: z.record(z.unknown()),
});
export const createCronSchema = z.object({
name: z.string().min(1).max(200),
cronExpr: z.string().min(1),
jobType: z.string().min(1),
jobConfig: z.record(z.unknown()).default({}),
});
export const queueJobSchema = z.object({
jobType: z.enum(['chat', 'task', 'cron', 'waggle', 'group']),
input: z.record(z.unknown()),
teamId: z.string().uuid().optional(),
});

View File

@@ -0,0 +1,244 @@
/**
* Tier Architecture — canonical tier definitions, capabilities, and enforcement.
*
* CANONICAL TIER NAMES: TRIAL, FREE, TEAMS, ENTERPRISE (4-tier)
* These are the only valid values for the Tier type.
* Note: FREE displays as "Solo" (see TIER_LABELS); the stored value stays `FREE`.
*
* Pricing (confirmed July 5, 2026):
* TRIAL — $0 / 15 days (all features unlocked)
* FREE — Solo — $0 forever (all personal features)
* TEAMS — $49/mo per seat (shared workspaces, WaggleDance, governance)
* ENTERPRISE — Consultative (KVARK sovereign on-prem)
*
* Strategy: Memory + Harvest is free forever (lock-in moat).
* Agents are free (they generate memory).
* Skills/connectors are the upgrade trigger.
* Trial → Free fallback after 15 days (painful but not destructive).
*/
export const TIERS = ['TRIAL', 'FREE', 'TEAMS', 'ENTERPRISE'] as const;
export type Tier = typeof TIERS[number];
export const TRIAL_DURATION_DAYS = 15;
/**
* Read an env var safely from either Node (`process.env`) or the browser
* (where `process` is undefined). Used by shared tier config so this module
* can be imported by both the sidecar and the web bundle.
*/
const readEnv = (key: string): string | null => {
const proc = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process;
return proc?.env?.[key] ?? null;
};
export type EmbeddingProviderType = 'inprocess' | 'ollama' | 'voyage' | 'openai' | 'litellm' | 'mock';
export type ExportFormat = 'txt' | 'md' | 'pdf' | 'json';
export interface TierCapabilities {
connectorLimit: number;
workspaceLimit: number;
embeddingProviders: EmbeddingProviderType[];
embeddingQuotaPerMonth: number;
messageHistoryLimit: number;
spawnAgents: boolean;
customSkills: boolean;
teamSkillLibrary: boolean;
cloudSync: boolean;
exportFormats: ExportFormat[];
teamMembersLimit: number;
sharedWorkspaces: boolean;
adminPanel: boolean;
auditLog: 'none' | 'basic' | 'full';
selfHosted: boolean;
managedModelPool: boolean;
priorityModels: boolean;
kvarkCta: 'none' | 'subtle' | 'active';
stripePriceId: string | null;
}
export const TIER_CAPABILITIES: Record<Tier, TierCapabilities> = {
TRIAL: {
connectorLimit: -1,
workspaceLimit: -1,
embeddingProviders: ['inprocess', 'mock', 'ollama', 'voyage', 'openai', 'litellm'],
embeddingQuotaPerMonth: -1,
messageHistoryLimit: -1,
spawnAgents: true,
customSkills: true,
teamSkillLibrary: true,
cloudSync: true,
exportFormats: ['txt', 'md', 'pdf', 'json'],
teamMembersLimit: -1,
sharedWorkspaces: true,
adminPanel: true,
auditLog: 'full',
selfHosted: false,
managedModelPool: true,
priorityModels: true,
kvarkCta: 'subtle',
stripePriceId: null,
},
FREE: {
connectorLimit: -1,
workspaceLimit: -1,
embeddingProviders: ['inprocess', 'mock', 'ollama', 'voyage', 'openai'],
embeddingQuotaPerMonth: -1,
messageHistoryLimit: -1,
spawnAgents: true,
customSkills: true,
teamSkillLibrary: false,
cloudSync: false,
exportFormats: ['txt', 'md', 'pdf', 'json'],
teamMembersLimit: 1,
sharedWorkspaces: false,
adminPanel: false,
auditLog: 'basic',
selfHosted: false,
managedModelPool: false,
priorityModels: false,
kvarkCta: 'subtle',
stripePriceId: null,
},
TEAMS: {
connectorLimit: -1,
workspaceLimit: -1,
embeddingProviders: ['inprocess', 'mock', 'ollama', 'voyage', 'openai', 'litellm'],
embeddingQuotaPerMonth: -1,
messageHistoryLimit: -1,
spawnAgents: true,
customSkills: true,
teamSkillLibrary: true,
cloudSync: true,
exportFormats: ['txt', 'md', 'pdf', 'json'],
teamMembersLimit: -1,
sharedWorkspaces: true,
adminPanel: true,
auditLog: 'full',
selfHosted: true,
managedModelPool: true,
priorityModels: true,
kvarkCta: 'active',
stripePriceId: readEnv('STRIPE_PRICE_TEAMS'),
},
ENTERPRISE: {
connectorLimit: -1,
workspaceLimit: -1,
embeddingProviders: ['inprocess', 'mock', 'ollama', 'voyage', 'openai', 'litellm'],
embeddingQuotaPerMonth: -1,
messageHistoryLimit: -1,
spawnAgents: true,
customSkills: true,
teamSkillLibrary: true,
cloudSync: true,
exportFormats: ['txt', 'md', 'pdf', 'json'],
teamMembersLimit: -1,
sharedWorkspaces: true,
adminPanel: true,
auditLog: 'full',
selfHosted: true,
managedModelPool: true,
priorityModels: true,
kvarkCta: 'none',
stripePriceId: null,
},
};
// Tier ordering — higher index = more capable
// TRIAL has max capabilities but is time-limited, so it ranks above TEAMS
const TIER_ORDER: Record<Tier, number> = {
FREE: 0, TEAMS: 2, ENTERPRISE: 3, TRIAL: 3,
};
/** Map legacy tier names to new canonical names. */
const LEGACY_TIER_MAP: Record<string, Tier> = {
solo: 'FREE',
basic: 'FREE',
pro: 'FREE',
business: 'TEAMS',
enterprise: 'ENTERPRISE',
trial: 'TRIAL',
};
/** Display names — single source of truth for user-facing tier labels. */
export const TIER_LABELS: Record<Tier, string> = {
TRIAL: 'Trial',
FREE: 'Solo',
TEAMS: 'Team',
ENTERPRISE: 'Enterprise',
};
/** Get the user-facing display name for a tier. */
export function tierLabel(t: Tier): string {
return TIER_LABELS[t];
}
/** Parse a tier string (handles both legacy and canonical names). */
export function parseTier(raw: string): Tier | null {
const upper = raw.toUpperCase();
if (TIERS.includes(upper as Tier)) return upper as Tier;
return LEGACY_TIER_MAP[raw.toLowerCase()] ?? null;
}
/** Check if a trial has expired. `trialStartedAt` is an ISO date string. */
export function isTrialExpired(trialStartedAt: string | null | undefined): boolean {
if (!trialStartedAt) return true;
const start = new Date(trialStartedAt).getTime();
if (isNaN(start)) return true;
const now = Date.now();
const elapsed = now - start;
return elapsed > TRIAL_DURATION_DAYS * 24 * 60 * 60 * 1000;
}
/** Get the effective tier — downgrades TRIAL to FREE if expired. */
export function getEffectiveTier(tier: Tier, trialStartedAt?: string | null): Tier {
if (tier === 'TRIAL' && isTrialExpired(trialStartedAt)) return 'FREE';
return tier;
}
/** Days remaining in trial (0 if expired or not on trial). */
export function trialDaysRemaining(trialStartedAt: string | null | undefined): number {
if (!trialStartedAt) return 0;
const start = new Date(trialStartedAt).getTime();
if (isNaN(start)) return 0;
const elapsed = Date.now() - start;
const remaining = TRIAL_DURATION_DAYS - elapsed / (24 * 60 * 60 * 1000);
return Math.max(0, Math.ceil(remaining));
}
export class TierError extends Error {
constructor(
public readonly required: Tier,
public readonly actual: Tier,
) {
super(`Tier insufficient: requires ${required}, actual is ${actual}`);
this.name = 'TierError';
}
}
export function tierSatisfies(actual: Tier, required: Tier): boolean {
return TIER_ORDER[actual] >= TIER_ORDER[required];
}
export function assertTierCapability(actual: Tier, required: Tier): void {
if (!tierSatisfies(actual, required)) {
throw new TierError(required, actual);
}
}
export function getCapabilities(tier: Tier): TierCapabilities {
return TIER_CAPABILITIES[tier];
}
export function hasCapability<K extends keyof TierCapabilities>(
tier: Tier,
capability: K,
minimumValue?: TierCapabilities[K],
): boolean {
const cap = TIER_CAPABILITIES[tier][capability];
if (minimumValue === undefined) return Boolean(cap);
if (typeof cap === 'number' && typeof minimumValue === 'number') {
return cap === -1 || cap >= minimumValue;
}
return cap === minimumValue;
}

View File

@@ -0,0 +1,262 @@
/**
* Tool Detection — shared types for AI-OS Phase 0.
*
* Mission: report which external AI tools are installed on the user's
* machine, with version and hive-mind hook status. Foundation for the
* launcher dock (Phase 2), hook auto-installer UX (Phase 2), and the
* Mission Control inventory tile (Phase 4).
*
* The detection itself runs in the sidecar (agent package); these types
* live in @waggle/shared so both the sidecar and the web bundle can
* consume the same envelope without duplicating shapes.
*
* Out of scope for these types:
* - Hook installation (Phase 2)
* - Tool spawn / process management (Phase 2)
* - Workspace context injection (Phase 2)
*/
/**
* Canonical AI-tool IDs that hive-mind has shipped hook packages for.
* See packages/hive-mind-hooks-* for the matching installers.
*/
export const SUPPORTED_TOOLS = [
'claude-code',
'claude-desktop',
'cursor',
'codex',
'codex-desktop',
'hermes',
'openclaw',
] as const;
export type ToolId = typeof SUPPORTED_TOOLS[number];
/**
* AI-OS #5 — how a tool is detected. `path` = PATH lookup by binary name (CLI
* tools; the only kind a third-party JSON adapter may declare). `candidates` =
* platform-branching candidate-path probe, resolved by built-in agent code
* (GUI/desktop apps) — not expressible declaratively, so built-in only.
*/
export type ToolDetectSpec =
| { kind: 'path'; binaryName: string }
| { kind: 'candidates' };
export type ExternalToolAccess = 'read-only' | 'workspace-write' | 'native';
export type ToolPromptTransport = 'stdin' | 'arg' | 'temp-file';
export type ToolOutputDialect =
| 'claude-stream-json'
| 'codex-jsonl'
| 'hermes-text'
| 'openclaw-json'
| 'text'
| 'json'
| 'jsonl';
export type ToolWorkspaceBinding = 'cwd' | 'flag' | 'managed-agent';
/** Declarative, shell-free contract for one capturable agent task. */
export interface ToolTaskSpec {
argvTemplate: readonly string[];
resumeArgvTemplate?: readonly string[];
accessArgs: Partial<Record<ExternalToolAccess, readonly string[]>>;
promptTransport: ToolPromptTransport;
outputDialect: ToolOutputDialect;
workspaceBinding: ToolWorkspaceBinding;
permissionModes: readonly ExternalToolAccess[];
resumable: boolean;
}
export interface ToolCapabilities {
interactiveLaunch: boolean;
headlessTask: boolean;
structuredProgress: boolean;
resumable: boolean;
liveWaggleDance: boolean;
}
/**
* AI-OS #5 — declarative descriptor for one external tool. The single source of
* truth for the per-tool facts that used to be duplicated across SUPPORTED_TOOLS
* / LAUNCH_COHORT / TOOL_DISPLAY_NAMES / HOOK_POINTER_BY_TOOL / HOOKS_COHORT /
* the per-tool detectors. Built-ins live in BUILTIN_TOOL_MANIFESTS; third-party
* adapters are loaded (data-only) from ~/.waggle/adapters/*.json.
*/
export interface ToolManifest {
id: string;
displayName: string;
launchable: boolean;
hookCapable: boolean;
hookPointer: string;
detect: ToolDetectSpec;
/**
* Declarative inline-prompt arg template for THIRD-PARTY path adapters
* (e.g. ['--print', '{prompt}']). Built-ins keep their logic in
* launcher-prompt-args.ts. Captured in v1; application is a fast-follow.
*/
promptArgTemplate?: string[];
/** Explicit capability split: opening an app is not the same as running a task. */
capabilities?: ToolCapabilities;
/** Present only when the adapter has a verified, capturable headless lane. */
task?: ToolTaskSpec;
/** true = first-party (the 7); false/absent = loaded third-party. */
builtin?: boolean;
}
/**
* The canonical 7 built-in tools — the source of truth for their per-tool data.
* SUPPORTED_TOOLS (above) stays the `as const` type anchor; the cohort/name/
* pointer consts derive from these manifests.
*/
export const BUILTIN_TOOL_MANIFESTS: readonly ToolManifest[] = [
{
id: 'claude-code', displayName: 'Claude Code', launchable: true, hookCapable: true,
hookPointer: '.claude/hive-mind-install.json', detect: { kind: 'path', binaryName: 'claude' }, builtin: true,
capabilities: { interactiveLaunch: true, headlessTask: true, structuredProgress: true, resumable: true, liveWaggleDance: false },
task: {
argvTemplate: ['-p', '--safe-mode', '--disable-slash-commands', '--no-session-persistence', '--max-budget-usd', '0.25', '--input-format', 'text', '--output-format', 'stream-json', '--verbose', '{accessArgs}'],
resumeArgvTemplate: ['-p', '--safe-mode', '--disable-slash-commands', '--resume', '{sessionId}', '--max-budget-usd', '0.25', '--input-format', 'text', '--output-format', 'stream-json', '--verbose', '{accessArgs}'],
accessArgs: {
'read-only': ['--permission-mode', 'plan'],
'workspace-write': ['--permission-mode', 'acceptEdits'],
native: [],
},
promptTransport: 'stdin', outputDialect: 'claude-stream-json', workspaceBinding: 'cwd',
permissionModes: ['read-only', 'workspace-write', 'native'], resumable: true,
},
},
{
id: 'claude-desktop', displayName: 'Claude Desktop', launchable: true, hookCapable: true,
hookPointer: '.waggle/claude-desktop/hive-mind-install.json', detect: { kind: 'candidates' }, builtin: true,
capabilities: { interactiveLaunch: true, headlessTask: false, structuredProgress: false, resumable: false, liveWaggleDance: false },
},
{
id: 'cursor', displayName: 'Cursor', launchable: true, hookCapable: true,
hookPointer: '.cursor/hive-mind-install.json', detect: { kind: 'candidates' }, builtin: true,
capabilities: { interactiveLaunch: true, headlessTask: false, structuredProgress: false, resumable: false, liveWaggleDance: false },
},
{
id: 'codex', displayName: 'Codex CLI', launchable: true, hookCapable: true,
hookPointer: '.codex/hive-mind-install.json', detect: { kind: 'path', binaryName: 'codex' }, builtin: true,
capabilities: { interactiveLaunch: true, headlessTask: true, structuredProgress: true, resumable: true, liveWaggleDance: false },
task: {
argvTemplate: ['{accessArgs}', 'exec', '--ignore-user-config', '--ignore-rules', '--ephemeral', '--skip-git-repo-check', '--json', '--color', 'never', '-C', '{workspacePath}', '-'],
resumeArgvTemplate: ['{accessArgs}', 'exec', '--ignore-user-config', '--ignore-rules', '--ephemeral', '--skip-git-repo-check', 'resume', '{sessionId}', '--json', '--color', 'never', '-C', '{workspacePath}', '-'],
accessArgs: {
'read-only': ['--ask-for-approval', 'never', '--sandbox', 'read-only'],
'workspace-write': ['--ask-for-approval', 'never', '--sandbox', 'workspace-write'],
native: [],
},
promptTransport: 'stdin', outputDialect: 'codex-jsonl', workspaceBinding: 'flag',
permissionModes: ['read-only', 'workspace-write', 'native'], resumable: true,
},
},
{
id: 'codex-desktop', displayName: 'Codex Desktop', launchable: true, hookCapable: true,
hookPointer: '.codex/hive-mind-install.json', detect: { kind: 'candidates' }, builtin: true,
capabilities: { interactiveLaunch: true, headlessTask: false, structuredProgress: false, resumable: false, liveWaggleDance: false },
},
{
id: 'hermes', displayName: 'Hermes Agent', launchable: true, hookCapable: true,
hookPointer: '.hermes/hive-mind-install.json', detect: { kind: 'path', binaryName: 'hermes' }, builtin: true,
capabilities: { interactiveLaunch: true, headlessTask: true, structuredProgress: false, resumable: true, liveWaggleDance: false },
task: {
argvTemplate: ['chat', '-q', '{prompt}', '-Q', '--source', 'tool', '--ignore-rules', '--max-turns', '12', '--checkpoints'],
resumeArgvTemplate: ['chat', '--resume', '{sessionId}', '-q', '{prompt}', '-Q', '--source', 'tool', '--ignore-rules', '--max-turns', '12', '--checkpoints'],
accessArgs: { native: [] }, promptTransport: 'arg', outputDialect: 'hermes-text', workspaceBinding: 'cwd',
permissionModes: ['native'], resumable: true,
},
},
{
id: 'openclaw', displayName: 'OpenClaw', launchable: true, hookCapable: true,
hookPointer: '.openclaw/hive-mind-install.json', detect: { kind: 'path', binaryName: 'openclaw' }, builtin: true,
capabilities: { interactiveLaunch: true, headlessTask: true, structuredProgress: true, resumable: true, liveWaggleDance: false },
task: {
argvTemplate: ['agent', '--agent', '{agentId}', '--session-key', 'agent:{agentId}:waggle:{runId}', '--message-file', '{promptFile}', '--json', '--timeout', '{timeoutSeconds}'],
resumeArgvTemplate: ['agent', '--agent', '{agentId}', '--session-key', 'agent:{agentId}:waggle:{sessionId}', '--message-file', '{promptFile}', '--json', '--timeout', '{timeoutSeconds}'],
accessArgs: { native: [] }, promptTransport: 'temp-file', outputDialect: 'openclaw-json', workspaceBinding: 'managed-agent',
permissionModes: ['native'], resumable: true,
},
},
] as const;
/**
* Tools the launcher dock + hook installer support end-to-end.
*
* Phase 1 shipped with 3 entries (Claude Code, Cursor, Claude
* Desktop — D3). Phase 4 extends to all 7 because (a) each tool
* already has a published hook-installer package
* (@waggle/hive-mind-hooks-<id>), and (b) the marginal cost per
* additional detector is one PATH lookup or candidate-path entry.
*/
export const LAUNCH_COHORT: readonly ToolId[] =
BUILTIN_TOOL_MANIFESTS.filter((m) => m.launchable).map((m) => m.id as ToolId);
/**
* AI-OS #5 — apply a third-party adapter's `promptArgTemplate` to a prompt by
* substituting every `{prompt}` placeholder in each entry. Built-ins use their
* own promptArgsForTool (web); this is the declarative form for loaded adapters.
*/
export function applyPromptArgTemplate(template: readonly string[], prompt: string): string[] {
return template.map((a) => a.split('{prompt}').join(prompt));
}
/**
* Per-tool human-readable display name. Centralized so the launcher
* UI, sidecar logs, and KVARK governance reports all agree.
*/
export const TOOL_DISPLAY_NAMES = Object.fromEntries(
BUILTIN_TOOL_MANIFESTS.map((m) => [m.id, m.displayName]),
) as Record<ToolId, string>;
/**
* Result of detecting a single tool on the user's machine.
*
* Field semantics:
* - `installed` : true iff the binary was found at a known path.
* - `installedPath` : absolute path to the binary, or null.
* - `version` : version string (best-effort; null if exec failed).
* - `hooksInstalled` : true iff a hive-mind hook pointer file
* was found AND its referenced backup file still exists
* (so a partially-rolled-back install reports false).
* - `hookPointerPath` : the pointer file we read (or attempted),
* for diagnostics.
* - `diagnostic` : optional human-readable reason for any
* partial-failure case (e.g. "claude-code installed but --version
* returned non-zero").
*/
export interface DetectedTool {
/** Tool id — a built-in ToolId or a loaded third-party adapter id (#5). */
id: string;
displayName: string;
/** True when the tool manifest allows launching from the dock. */
launchable?: boolean;
/** True when the tool manifest declares hook support. */
hookCapable?: boolean;
/** True for the built-in seven tools; false for loaded adapters. */
builtin?: boolean;
/** True when the manifest can accept the launch prompt inline. */
acceptsInlinePrompt?: boolean;
/** Canonical split between opening the app and running a captured task. */
capabilities?: ToolCapabilities;
/** Access modes supported by the manifest's captured-task contract. */
permissionModes?: readonly ExternalToolAccess[];
installed: boolean;
installedPath: string | null;
version: string | null;
hooksInstalled: boolean;
hookPointerPath: string | null;
diagnostic?: string;
}
/**
* Top-level detection envelope. Stable shape across all callers
* (sidecar route, web UI, Tauri command).
*/
export interface ToolDetectionResult {
/** Platform the detection ran on. */
platform: NodeJS.Platform | 'other';
/** ISO timestamp at which the detection completed. */
detectedAt: string;
/** Per-tool detection result. Registry order: built-ins first, then adapters. */
tools: DetectedTool[];
}

View File

@@ -0,0 +1,780 @@
// @waggle/shared — Domain types for M3 Team Pilot
// === Auth & Users ===
export interface User {
id: string;
clerkId: string;
displayName: string;
email: string;
avatarUrl: string | null;
mindPath: string | null;
createdAt: Date;
updatedAt: Date;
}
// === Teams ===
export interface Team {
id: string;
name: string;
slug: string;
ownerId: string;
createdAt: Date;
}
export type TeamRole = 'owner' | 'admin' | 'member';
export interface TeamMember {
teamId: string;
userId: string;
role: TeamRole;
roleDescription: string | null;
interests: string[] | null;
joinedAt: Date;
}
// === Agent Configuration ===
// UX-Refactor Phase 3 (PRD §15.5, gate B3): the legacy 10 members stay untouched
// (cloud callers depend on userId/role/systemPrompt/config); the optional fields
// below carry the Agent-entity vocabulary. Semantics: `tools` remains the raw
// tool allowlist while skillIds/connectorIds/mcpIds are entity references;
// `role` stays the free-text display label while `personaId` is the canonical
// persona reference. lastRunAt/successRate are DERIVED at read from
// execution_traces (B3) — route layers must not persist them.
export interface AgentDef {
id: string;
userId: string;
teamId: string | null;
name: string;
role: string | null;
systemPrompt: string | null;
model: string;
tools: string[];
config: Record<string, unknown>;
createdAt: Date;
type?: AgentType;
goal?: string;
description?: string;
personaId?: string;
autonomyLevel?: AutonomyLevel;
workspaceIds?: string[];
memoryScopes?: Scope[];
skillIds?: string[];
connectorIds?: string[];
mcpIds?: string[];
permissions?: Record<string, unknown>;
status?: AgentRunState;
/** ISO timestamp — derived at read, never persisted (B3). */
lastRunAt?: string;
/** 0-1 — derived at read from execution_traces outcomes (B3). */
successRate?: number;
}
export type AgentGroupStrategy = 'parallel' | 'sequential' | 'coordinator';
export interface AgentGroup {
id: string;
userId: string;
name: string;
description: string | null;
strategy: AgentGroupStrategy;
createdAt: Date;
}
export interface AgentGroupMember {
groupId: string;
agentId: string;
roleInGroup: 'lead' | 'worker';
executionOrder: number;
}
// === Tasks ===
export type TaskStatus = 'open' | 'claimed' | 'in_progress' | 'done' | 'cancelled';
export type TaskPriority = 'critical' | 'high' | 'normal' | 'low';
export interface Task {
id: string;
teamId: string;
title: string;
description: string | null;
status: TaskStatus;
priority: TaskPriority;
createdBy: string;
assignedTo: string | null;
parentTaskId: string | null;
createdAt: Date;
updatedAt: Date;
}
/**
* AI-OS #6 — durable "why" injected into the agent system prompt each run
* (the purpose above the current turn; complements recall + live awareness).
* All levels optional. Today `project` (workspace) and `goal` (agent goal) are
* populated; `mission` (no workspace-charter field yet) and `task` (already in
* the self-awareness section) are reserved/omitted.
*/
export interface GoalAncestry {
mission?: string;
project?: string;
goal?: string;
task?: string;
}
// === Waggle Dance Messages ===
export type MessageType = 'broadcast' | 'request' | 'response';
export type MessageSubtype =
| 'knowledge_check' | 'task_delegation' | 'skill_request'
| 'model_recommendation' | 'knowledge_match' | 'task_claim'
| 'discovery' | 'routed_share' | 'skill_share' | 'model_recipe';
export interface WaggleMessage {
id: string;
teamId: string;
senderId: string;
type: MessageType;
subtype: MessageSubtype;
content: Record<string, unknown>;
referenceId: string | null;
routing: Array<{ userId: string; reason: string }> | null;
createdAt: Date;
}
// === Team Knowledge Graph ===
export interface TeamEntity {
id: string;
teamId: string;
entityType: string;
name: string;
properties: Record<string, unknown>;
sharedBy: string;
validFrom: Date;
validTo: Date | null;
createdAt: Date;
}
export interface TeamRelation {
id: string;
teamId: string;
sourceId: string;
targetId: string;
relationType: string;
confidence: number;
properties: Record<string, unknown>;
createdAt: Date;
}
// === Team Resources ===
export type ResourceType = 'model_recipe' | 'skill' | 'tool_config' | 'prompt_template';
export interface TeamResource {
id: string;
teamId: string;
resourceType: ResourceType;
name: string;
description: string | null;
config: Record<string, unknown>;
sharedBy: string;
rating: number;
useCount: number;
createdAt: Date;
}
// === Jobs ===
export type JobType = 'chat' | 'task' | 'cron' | 'waggle';
export type JobStatus = 'queued' | 'running' | 'completed' | 'failed';
export interface AgentJob {
id: string;
teamId: string;
userId: string;
jobType: JobType;
status: JobStatus;
input: Record<string, unknown>;
output: Record<string, unknown> | null;
startedAt: Date | null;
completedAt: Date | null;
createdAt: Date;
}
// === Cron ===
export interface CronSchedule {
id: string;
teamId: string;
createdBy: string;
name: string;
cronExpr: string;
jobType: string;
jobConfig: Record<string, unknown>;
enabled: boolean;
lastRunAt: Date | null;
nextRunAt: Date | null;
createdAt: Date;
}
// === Intelligence ===
export type ScoutSource = 'marketplace' | 'mcp_registry' | 'model_provider' | 'team';
export type ScoutCategory = 'skill' | 'mcp' | 'model' | 'feature' | 'practice';
export type FindingStatus = 'new' | 'presented' | 'adopted' | 'dismissed';
export interface ScoutFinding {
id: string;
userId: string | null;
teamId: string | null;
source: ScoutSource;
category: ScoutCategory;
title: string;
summary: string | null;
relevanceScore: number;
url: string | null;
status: FindingStatus;
createdAt: Date;
}
export type SuggestionType = 'dashboard' | 'cron' | 'share' | 'skill' | 'upgrade';
export type SuggestionStatus = 'pending' | 'accepted' | 'dismissed' | 'snoozed';
export interface ProactivePattern {
id: string;
name: string;
trigger: Record<string, unknown>;
suggestionType: SuggestionType;
template: string;
enabled: boolean;
}
export interface SuggestionEntry {
id: string;
userId: string;
patternId: string;
context: Record<string, unknown>;
status: SuggestionStatus;
createdAt: Date;
}
// === Audit ===
export interface AuditEntry {
id: string;
userId: string;
teamId: string | null;
agentName: string;
actionType: string;
description: string;
beforeState: Record<string, unknown> | null;
afterState: Record<string, unknown> | null;
requiresApproval: boolean;
approved: boolean | null;
approvedBy: string | null;
createdAt: Date;
}
// === WebSocket Events ===
export type WsClientEvent =
| { type: 'authenticate'; token: string }
| { type: 'join_team'; teamSlug: string }
| { type: 'send_message'; teamSlug: string; messageType: MessageType; subtype: MessageSubtype; content: Record<string, unknown> };
export type WsServerEvent =
| { type: 'waggle_message'; message: WaggleMessage }
| { type: 'task_update'; task: Task }
| { type: 'agent_status'; userId: string; status: 'running' | 'idle' | 'completed' }
| { type: 'suggestion'; suggestion: SuggestionEntry }
| { type: 'scout_finding'; finding: ScoutFinding }
| { type: 'job_progress'; jobId: string; progress: Record<string, unknown> };
// ─── Connector Types ────────────────────────────────────────────────────
/** Connector credential type stored in vault */
export interface ConnectorCredential {
type: 'api_key' | 'oauth2' | 'bearer' | 'basic';
/** For oauth2: access token */
accessToken?: string;
/** For oauth2: refresh token */
refreshToken?: string;
/** ISO timestamp when accessToken expires */
expiresAt?: string;
/** OAuth scopes granted */
scopes?: string[];
/** For api_key/bearer: the key or token value */
apiKey?: string;
/** For basic: username */
username?: string;
}
/** Connector status in the system */
export type ConnectorStatus = 'connected' | 'disconnected' | 'expired' | 'error';
/** Rich action metadata for SDK-backed connectors */
export interface ConnectorActionMeta {
name: string;
description: string;
riskLevel: 'low' | 'medium' | 'high';
}
/** Connector definition — what the user sees */
export interface ConnectorDefinition {
id: string;
name: string;
description: string;
/** Which service this connects to */
service: string;
/** What auth method is needed */
authType: 'api_key' | 'oauth2' | 'bearer' | 'basic';
/** Whether credentials exist in vault */
status: ConnectorStatus;
/** What the connector can do */
capabilities: ('read' | 'write' | 'search')[];
/** Which substrate manages this connector */
substrate: 'waggle' | 'kvark';
/** Agent tools this connector provides when connected */
tools: string[];
/** Connector-specific config */
config?: Record<string, unknown>;
/** Rich action metadata (optional — available when SDK connector is loaded) */
actions?: ConnectorActionMeta[];
/** CDN URL for SVG logo */
logoUrl?: string;
/** Connector category */
category?: 'productivity' | 'development' | 'crm' | 'data' | 'communication' | 'storage' | 'integration';
/** 1-2 sentences: what credential is needed and where to get it */
setupGuide?: string;
/** When the connector was last manually synced (C16). GET /api/connectors
* enriches each definition with this from the vault stamp. */
lastSyncAt?: string;
}
/** Connector health for cockpit display */
export interface ConnectorHealth {
id: string;
name: string;
status: ConnectorStatus;
lastChecked: string;
error?: string;
tokenExpiresAt?: string;
/** When the connector was last manually synced (C16: sync-now = health
* re-probe + stamp). Stamped by POST /api/connectors/:id/sync; merged into
* GET /api/connectors/:id/health (and the fallback path) from the vault. */
lastSyncAt?: string;
}
// === UX-Refactor vocabulary (PRD §15.2) ===
// Domain literal unions for the workspace-first Agent Desktop refactor.
// Single source of truth — the sidecar route layer and apps/web both import these
// (no per-file union duplication; see docs/ux-refactor/deltas/shared-types-delta.md §0).
export type WorkspaceType =
| 'project' | 'client' | 'research' | 'personal' | 'team' | 'organization';
export type Scope = 'personal' | 'workspace' | 'team' | 'organization';
/** 0-100 confidence score for a memory / provenance signal. */
export type Confidence = number;
export type MemoryKind =
| 'fact' | 'decision' | 'task' | 'preference'
| 'strategy' | 'learning' | 'goal' | 'entity';
export type ArtifactKind =
| 'document' | 'presentation' | 'spreadsheet' | 'dashboard'
| 'research' | 'code' | 'media' | 'design' | 'other';
export type AgentType = 'personal' | 'workspace' | 'team' | 'autonomous';
export type AutonomyLevel = 'manual' | 'guided' | 'medium' | 'high';
/** PRD §14.5 agent lifecycle states — SINGLE source of truth. The sidecar
* store (packages/server/src/local/agents-store.ts) and the FE view-model
* (apps/web/src/lib/types.ts) re-export this union; do not redeclare it.
* Declared as a const tuple so schemas.ts derives `agentStatusEnum` from it
* (z.enum) — the type and the runtime list cannot drift. */
export const AGENT_RUN_STATES = [
'draft', 'idle', 'running', 'paused', 'failed',
'waiting_for_approval', 'completed', 'archived',
] as const;
export type AgentRunState = (typeof AGENT_RUN_STATES)[number];
/**
* Canonical runtime record for work performed inside the Room. This is
* intentionally separate from `AgentRunState` above: that union describes the
* lifecycle of a saved Agent blueprint, while collaboration runs are concrete
* executions by Waggle agents or external tools.
*/
export const COLLABORATION_RUN_SOURCES = [
'external_tool', 'fleet', 'chat_subagent', 'workflow', 'agent_group',
] as const;
export type CollaborationRunSource = (typeof COLLABORATION_RUN_SOURCES)[number];
export const COLLABORATION_RUN_STATUSES = [
'queued', 'starting', 'running', 'waiting_for_approval', 'paused',
'cancelling', 'completed', 'failed', 'cancelled', 'interrupted',
] as const;
export type CollaborationRunStatus = (typeof COLLABORATION_RUN_STATUSES)[number];
export const COLLABORATION_RUN_CONTROLS = ['cancel', 'pause', 'resume', 'message'] as const;
export type CollaborationRunControl = (typeof COLLABORATION_RUN_CONTROLS)[number];
export interface CollaborationRunExecutor {
kind: 'external_tool' | 'waggle_agent' | 'coordinator';
toolId?: string;
pid?: number;
agentId?: string;
personaId?: string;
model?: string;
}
export interface CollaborationRunProgress {
message: string;
phase?: string;
current?: number;
total?: number;
}
export interface CollaborationRunResult {
summary?: string;
sessionId?: string;
traceId?: string;
artifacts?: string[];
exitCode?: number | null;
error?: string;
}
export interface CollaborationRunMetrics {
toolsUsed?: string[];
inputTokens?: number;
outputTokens?: number;
costUsd?: number;
}
export interface CollaborationRunMemoryRefs {
status: 'pending' | 'complete' | 'partial' | 'failed';
personalFrameIds: number[];
workspaceFrameIds: Record<string, number[]>;
}
export interface CollaborationRunCapabilities {
cancel: boolean;
pause: boolean;
resume: boolean;
message: boolean;
}
export interface CollaborationRunAttribution {
routeDecisionId: string;
/** Absent when the run was dispatched without a memory brief. */
briefHash?: string;
}
/**
* One executable leaf is bound to exactly one `workspaceId`. A Room/root run
* may coordinate several leaves and therefore carries only `workspaceIds`.
*/
interface CollaborationRunBase {
schemaVersion: 1;
id: string;
roomId: string;
rootRunId: string;
source: CollaborationRunSource;
executor: CollaborationRunExecutor;
title: string;
task: string;
attribution?: CollaborationRunAttribution;
status: CollaborationRunStatus;
progress?: CollaborationRunProgress;
result?: CollaborationRunResult;
metrics?: CollaborationRunMetrics;
memoryRefs: CollaborationRunMemoryRefs;
capabilities: CollaborationRunCapabilities;
revision: number;
createdAt: string;
updatedAt: string;
startedAt?: string;
completedAt?: string;
}
/** A Room is the durable coordination root and may span many workspaces. */
export interface CollaborationRoomRun extends CollaborationRunBase {
kind: 'room';
parentRunId: null;
workspaceIds: string[];
}
/** An executable participant is always bound to one concrete workspace. */
export interface CollaborationWorkerRun extends CollaborationRunBase {
kind: 'worker';
parentRunId: string;
workspaceId: string;
retryOfRunId?: string;
}
export type CollaborationRun = CollaborationRoomRun | CollaborationWorkerRun;
export interface CollaborationRunEvent {
seq: number;
type: 'upsert';
run: CollaborationRun;
timestamp: string;
}
export interface CollaborationRunSnapshot {
lastSeq: number;
runs: CollaborationRun[];
}
/** PRD §15.2 / §12.13 extension domains (B7 ratified: drop 'external_tool' —
* external tools surface via connectors/MCPs — and add 'agent'). Const tuple
* so the Extend routes validate the `type` facet against the runtime list. */
export const EXTENSION_TYPES = [
'skill', 'agent', 'connector', 'mcp', 'model', 'template',
] as const;
export type ExtensionType = (typeof EXTENSION_TYPES)[number];
/**
* Installed MCP-server instance state (shared-types-delta §8b). The CATALOG
* entry stays `McpServer` (mcp-catalog.ts) — `McpInstance.id` references
* `McpServer.id` for catalog installs, or the custom server name for
* user-added servers (POST /api/mcps). Produced by GET /api/mcps.
*
* v1 carries ONLY fields the route actually emits. The delta's
* version/riskLevel/permissions/lastUsedAt fields are deferred (no producer
* yet); per-tool `permissions` granularity is deferred together with C19's
* single-workspaceId scope model — PATCH /api/mcps/:id/permissions accepts
* `{ workspaceId }` or `{ scope: 'personal' }` only.
*/
export interface McpInstance {
id: string;
name: string;
status: 'installed' | 'running' | 'stopped' | 'error';
/** Locality (§17.3): 'workspace' when pinned to a workspaceId, else 'personal'. */
scope: Scope;
/** Workspace/agent ids this server is pinned to (C19: single workspaceId v1). */
connectedTo?: string[];
}
/**
* PRD §15.3 workspace contract — the normalized shape the sidecar route layer
* exposes to the new UI. The PERSISTED struct lives in `@waggle/hive-mind-core`
* (`workspace-manager.ts` `WorkspaceConfig`, a superset carrying legacy fields).
* A route-layer normalizer (Phase 1) bridges the struct to this contract, filling
* defaults for pre-V2 workspaces (type from templateId/group, status 'active',
* updatedAt from created). Kept separate (not `extends`) so the additive fields on
* the persistence struct stay optional and existing workspace literals don't break.
*/
export interface WorkspaceConfigV2 {
id: string;
name: string;
description?: string;
type: WorkspaceType;
group: string;
icon?: string;
status: 'active' | 'paused' | 'archived';
model?: string;
personaId?: string;
templateId?: string;
tools?: string[];
skills?: string[];
agentIds?: string[];
connectorIds?: string[];
mcpIds?: string[];
storageType?: 'virtual' | 'local' | 'team';
storagePath?: string;
teamId?: string;
teamRole?: 'owner' | 'admin' | 'member' | 'viewer';
riskLevel?: 'minimal' | 'limited' | 'high-risk' | 'unacceptable';
created: string;
updatedAt: string;
lastActiveAt?: string;
}
// === UX-Refactor Command vocabulary (PRD §12.3 / shared-types-delta §9) ===
// Command Center (Ctrl+K) result/command shapes. Single source of truth — the
// sidecar `command.ts` route layer and apps/web both import these. See
// docs/ux-refactor/deltas/shared-types-delta.md §9.
/** The six verb sections of the Command Center (PRD §12.3). */
export type CommandCategory =
| 'search' | 'launch' | 'create' | 'run' | 'navigate' | 'extend';
/**
* Every searchable object class the palette federates over (PRD §12.3 FR:
* "search across workspaces, memory, artifacts, sessions, people, agents,
* skills, commands, connectors, MCPs"). Artifact/agent rows are gated until
* those screens (S05/S09) land — the type carries them so the union is stable.
*/
export type CommandResultType =
| 'workspace' | 'memory' | 'artifact' | 'session' | 'person'
| 'agent' | 'skill' | 'command' | 'connector' | 'mcp' | 'automation';
/**
* What a `run`/`create`/`navigate`/`extend` result does when executed. A
* Navigate result carries a `route`; a server-dispatched action carries an
* `endpoint` + `payload`. All optional so a pure Search result needs none.
*/
export interface CommandAction {
route?: string;
endpoint?: string;
payload?: Record<string, unknown>;
}
/** One row in the Command Center result list. */
export interface CommandResult {
id: string;
type: CommandResultType;
title: string;
subtitle?: string;
category: CommandCategory;
icon?: string;
/** §12.3 permission-gated → renders the approval prompt before execution. */
requiresApproval?: boolean;
action?: CommandAction;
}
/**
* The execute request the palette posts to `POST /api/command/execute`. A
* structured command resolves via `id`; a natural-language command rides in
* `input` (PRD §12.3 "natural-language command input").
*/
export interface Command {
id?: string;
input?: string;
category?: CommandCategory;
type?: CommandResultType;
workspaceId?: string;
payload?: Record<string, unknown>;
}
// === UX-Refactor Memory entity (PRD §15.4 / shared-types-delta §3b) ===
// The normalized contract the sidecar `memory.ts` route layer exposes to the
// Memory Center. The PERSISTED row lives in `@waggle/hive-mind-core`
// (`memory_frames`); a route-layer normalizer (Phase 2B) projects the row +
// its JSON `metadata` blob into this shape. `kind/confidence/scope/status/
// sourceId/tags/evidence/related*` ride `memory_frames.metadata` (added by the
// idempotent ADD-COLUMN migration M1 in Phase 2B); the base columns map 1:1
// (`content`, `created_at`, `last_accessed`, `importance`, `source`).
/**
* Memory lifecycle state (PRD §12.4 / §14.4), reconciled with the Phase-2 gate
* ratifications (2026-06-09):
* - `unreviewed` — freshly imported, pending non-blocking review (C33: import
* commits immediately but lands unreviewed; Memory Center "needs review" filter).
* - `archived` — reversible soft-status (A8 Archive).
* - `deprecated` — superseded (A8 Deprecate; mirrors the existing `importance`).
* - `low_confidence` / `conflict` — surfaced for review; may be derived at
* recall-time (S04 C10) rather than persisted.
* - Delete is a HARD delete (A8) — there is no `trash`/tombstone state in v1.
*/
export type MemoryStatus =
| 'active' | 'unreviewed' | 'low_confidence' | 'conflict' | 'deprecated' | 'archived';
export interface Memory {
id: string;
kind: MemoryKind;
title: string;
content: string;
scope: Scope;
workspaceId?: string;
teamId?: string | null;
/** Provenance class — maps from `memory_frames.source` (FrameSource). */
source: string;
sourceId?: string | null;
sourceUrl?: string | null;
/** #7: whether a verbatim raw_archive row is linked (metadata.archiveUids), i.e.
* whether "View original source" can actually load. Distinct from sourceId — an
* auto-synced harvest summary carries a sourceId but no archive link, so the
* affordance gates on THIS, not on sourceId presence. */
hasOriginalSource?: boolean;
/** 0-100; B2 heuristic at import (source-trust × adapter × dedup). */
confidence?: Confidence;
/** Reuses the substrate `Importance` union (`frames.ts`). */
importance: 'critical' | 'important' | 'normal' | 'temporary' | 'deprecated';
evidence?: string[];
tags?: string[];
relatedMemoryIds?: string[];
relatedArtifactIds?: string[];
status: MemoryStatus;
createdAt: string;
updatedAt?: string;
lastAccessedAt?: string;
}
// === UX-Refactor Artifact entity (PRD §15.6 / shared-types-delta §4a) ===
// Artifacts are first-class produced OUTCOMES (decks/docs/sheets/dashboards/
// research), NOT raw file attachments. Per the Phase-2 gate ratification (A6),
// the backing store is a per-workspace `artifacts.json` index over the existing
// StorageProvider (NO new SQLite table) — assigned a stable id + status/tags/
// relations. Classification rule: an artifact is an EXPLICIT produced output
// (generated doc or user-promoted file), not every ingested input.
export type ArtifactStatus = 'draft' | 'ready' | 'in_review' | 'final' | 'archived';
export interface Artifact {
id: string;
title: string;
kind: ArtifactKind;
workspaceId: string;
teamId?: string | null;
createdBy: string;
/** agent | user | import | automation */
source: string;
status: ArtifactStatus;
/** Pre-archive status, stashed by the server on Archive so Unarchive restores
* the prior lifecycle state faithfully (A8 reversibility), not a flat 'draft'. */
prevStatus?: ArtifactStatus;
mimeType?: string;
/** StorageProvider path (virtual | local | team). */
storagePath?: string;
previewUrl?: string;
tags?: string[];
relatedMemoryIds?: string[];
relatedSessionIds?: string[];
relatedTaskIds?: string[];
relatedAgentIds?: string[];
createdAt: string;
updatedAt: string;
}
// === Federated "search-related" envelope (S05 headline, PRD §16.6 / line 532) ===
// The Artifact Center's defining endpoint returns an artifact PLUS its related
// memories/sessions/tasks/agents — "outcomes with relations, not files". Memories
// reuse the canonical `Memory` shape; the other three are lightweight references
// (the full Session/Task/Agent contracts are not part of this envelope on purpose).
export interface RelatedRef {
id: string;
title: string;
/** Owning workspace, when the item is workspace-scoped. */
workspaceId?: string;
/** Short text excerpt for display, when available. */
snippet?: string;
/** Sub-classification (e.g. session status, task state, agent type). */
kind?: string;
}
export interface RelatedSearchResult {
artifacts: Artifact[];
memories: Memory[];
sessions: RelatedRef[];
tasks: RelatedRef[];
agents: RelatedRef[];
}
// NOTE (Phase 3A review): a speculative shared `Skill` interface was removed
// here — no route produces it (GET /api/skills returns {name,length,preview};
// POST /api/skills/create takes {name,description,steps[],tools,category}).
// Re-introduce a Skill contract only together with a route that emits it.
// === UX-Refactor Automation entity (PRD §16.10 / shared-types-delta §7, Phase 3) ===
// "Automations" is the PRD-vocabulary alias over the existing cron substrate
// (B4 — alias, never rename; `/api/cron/*` callers keep working). Trigger/
// condition/actions ride the existing `cron_schedules.job_config` TEXT blob —
// NO migration. C24: schedule-only triggers v1 ('event' stays in the union but
// is rejected by the route layer). C25: `condition` is an ADVISORY string —
// no evaluation engine in v1.
export type AutomationTriggerType = 'schedule' | 'event' | 'manual';
export interface Automation {
id: string;
name: string;
triggerType: AutomationTriggerType;
/** Cron expression when triggerType === 'schedule'. */
schedule?: string;
/** Advisory only (C25) — stored, surfaced, never evaluated in v1. */
condition?: string;
actions: string[];
agentId?: string;
notify?: boolean;
workspaceId: string;
status: 'active' | 'paused' | 'running' | 'failed';
lastRun?: string;
nextRun?: string;
}

View File

@@ -0,0 +1,113 @@
/**
* Persona-aware connector recommendations.
*
* Two layers of guards:
* 1. SHAPE — primary lengths in the agreed 35 range, no duplicates,
* fallback for unknown personas, every onboarding persona has a
* matching map entry.
* 2. CATALOG MEMBERSHIP — every connector ID referenced by the
* recommendations actually exists in mcp-catalog.ts. This is the
* bug class we fix here: typos / stale IDs / forward-references
* that ship a recommendation pointing nowhere.
*/
import { describe, it, expect } from 'vitest';
import {
recommendConnectors,
flattenRecommendation,
allReferencedConnectorIds,
CONNECTOR_RECOMMENDATIONS,
} from '../src/connector-recommendations.js';
import { MCP_CATALOG } from '../src/mcp-catalog.js';
const CATALOG_IDS = new Set(MCP_CATALOG.map(s => s.id));
describe('recommendConnectors', () => {
it('returns universal defaults for an unknown persona id', () => {
const r = recommendConnectors('not-a-real-persona-id');
expect(r.primary).toContain('gdrive-mcp');
expect(r.primary).toContain('gmail-mcp');
expect(r.primary).toContain('notion-mcp');
});
it('returns the persona-specific recommendation when the id matches', () => {
const sales = recommendConnectors('sales-rep');
expect(sales.primary).toContain('hubspot-mcp');
expect(sales.primary).toContain('salesforce-mcp');
const coder = recommendConnectors('coder');
expect(coder.primary).toContain('github-mcp');
expect(coder.primary).toContain('linear-mcp');
const consultant = recommendConnectors('consultant');
expect(consultant.primary).toContain('notion-mcp');
expect(consultant.primary).toContain('gdrive-mcp');
});
it('always returns a non-empty primary list (never strands the UI on empty)', () => {
const personaIds = [...Object.keys(CONNECTOR_RECOMMENDATIONS), 'unknown-persona', ''];
for (const id of personaIds) {
const r = recommendConnectors(id);
expect(r.primary.length).toBeGreaterThan(0);
}
});
});
describe('CONNECTOR_RECOMMENDATIONS shape', () => {
it('every recommendation has 3-6 primary entries (Ljiljana-bar cognitive ceiling)', () => {
for (const [personaId, rec] of Object.entries(CONNECTOR_RECOMMENDATIONS)) {
expect(rec.primary.length, `persona "${personaId}" primary out of range`).toBeGreaterThanOrEqual(3);
expect(rec.primary.length, `persona "${personaId}" primary out of range`).toBeLessThanOrEqual(6);
}
});
it('no recommendation has duplicate ids within primary or secondary', () => {
for (const [personaId, rec] of Object.entries(CONNECTOR_RECOMMENDATIONS)) {
const allIds = [...rec.primary, ...rec.secondary];
const dupes = allIds.filter((id, i) => allIds.indexOf(id) !== i);
expect(dupes, `persona "${personaId}" has duplicate connector ids: ${dupes.join(', ')}`).toEqual([]);
}
});
it('every recommended id resolves to a real entry in mcp-catalog.ts', () => {
const referenced = allReferencedConnectorIds();
const missing = referenced.filter(id => !CATALOG_IDS.has(id));
expect(missing, `connector recommendation references ids not in mcp-catalog.ts: ${missing.join(', ')}`).toEqual([]);
});
it('covers every persona id from persona-data.ts (no recommendation drift after a persona is added)', () => {
// The persona list is duplicated here intentionally — importing from
// packages/agent would couple shared->agent, which we don't want for
// a leaf data module. If a persona is added in persona-data.ts but
// not here, this test fails and the fix is a one-line entry.
const ALL_PERSONA_IDS = [
'researcher', 'writer', 'analyst', 'coder',
'project-manager', 'executive-assistant', 'sales-rep', 'marketer',
'product-manager-senior', 'hr-manager', 'legal-professional',
'finance-owner', 'consultant',
'general-purpose', 'planner', 'verifier', 'coordinator',
'support-agent', 'ops-manager', 'data-engineer', 'recruiter',
'creative-director',
];
const recommendedIds = new Set(Object.keys(CONNECTOR_RECOMMENDATIONS));
const missingFromMap = ALL_PERSONA_IDS.filter(id => !recommendedIds.has(id));
expect(missingFromMap, `personas without a CONNECTOR_RECOMMENDATIONS entry: ${missingFromMap.join(', ')}`).toEqual([]);
});
});
describe('flattenRecommendation', () => {
it('returns primary IDs in order, then secondary', () => {
const flat = flattenRecommendation({
primary: ['a', 'b', 'c'],
secondary: ['d', 'e'],
});
expect(flat).toEqual(['a', 'b', 'c', 'd', 'e']);
});
it('preserves primary-first ordering for a real persona', () => {
const flat = flattenRecommendation(recommendConnectors('coder'));
// First five must be the primary list (anchored to github-mcp on top
// because that's the most-load-bearing connector for the coder persona).
expect(flat[0]).toBe('github-mcp');
});
});

View File

@@ -0,0 +1,41 @@
import { describe, it, expect } from 'vitest';
import { LOOP_TEMPLATES } from '../src/loop-templates.js';
describe('LOOP_TEMPLATES (knowledge-worker Loop catalog)', () => {
it('ships a useful set of templates', () => {
expect(LOOP_TEMPLATES.length).toBeGreaterThanOrEqual(5);
});
it('every template id is unique', () => {
const ids = LOOP_TEMPLATES.map(t => t.id);
expect(new Set(ids).size).toBe(ids.length);
});
it('every template has a non-empty name, description, role and prompt', () => {
for (const t of LOOP_TEMPLATES) {
expect(t.name.trim().length, t.id).toBeGreaterThan(0);
expect(t.description.trim().length, t.id).toBeGreaterThan(0);
expect(t.role.trim().length, t.id).toBeGreaterThan(0);
expect(t.jobConfig.prompt.trim().length, t.id).toBeGreaterThan(0);
}
});
it('every defaultCron is a valid 5-field cron expression', () => {
for (const t of LOOP_TEMPLATES) {
const fields = t.defaultCron.trim().split(/\s+/);
expect(fields.length, `${t.id}: "${t.defaultCron}"`).toBe(5);
for (const f of fields) {
expect(/^[\d*,/-]+$/.test(f), `${t.id} field "${f}"`).toBe(true);
}
}
});
it('cadences are daily-or-slower — never a minute/sub-hourly wildcard', () => {
// A Loop is several LLM round-trips; a '*' or '*/n' minute field would burn
// tokens every minute. Templates must use a concrete minute.
for (const t of LOOP_TEMPLATES) {
const minute = t.defaultCron.trim().split(/\s+/)[0];
expect(minute === '*' || minute.includes('/'), `${t.id} fires too often (minute="${minute}")`).toBe(false);
}
});
});

View File

@@ -0,0 +1,54 @@
import { describe, it, expect } from 'vitest';
import {
RISK_LEVELS, APPROVAL_CLASSES, TRUST_SOURCES, ASSESSMENT_MODES,
AUDIT_ACTIONS, AUDIT_CAPABILITY_TYPES, AUDIT_INITIATORS,
riskRank, riskAtLeast, sqlInList,
} from '../src/risk.js';
describe('canonical risk taxonomy (A1) — widest-set parity', () => {
it('RiskLevel adopts the audit set (adds critical)', () => {
expect([...RISK_LEVELS]).toEqual(['low', 'medium', 'high', 'critical']);
});
it('ApprovalClass adopts the audit set (adds blocked)', () => {
expect([...APPROVAL_CLASSES]).toEqual(['standard', 'elevated', 'critical', 'blocked']);
});
it('TrustSource is the 7-value set incl. security-gate', () => {
expect([...TRUST_SOURCES]).toEqual([
'builtin', 'starter_pack', 'local_user', 'third_party_verified',
'third_party_unverified', 'unknown', 'security-gate',
]);
});
it('AssessmentMode is declared/heuristic/mixed', () => {
expect([...ASSESSMENT_MODES]).toEqual(['declared', 'heuristic', 'mixed']);
});
it('AuditAction includes uninstalled (P5/D4)', () => {
expect([...AUDIT_ACTIONS]).toEqual([
'proposed', 'approved', 'installed', 'rejected', 'failed', 'blocked', 'uninstalled',
]);
});
it('AuditCapabilityType + AuditInitiator match the store', () => {
expect([...AUDIT_CAPABILITY_TYPES]).toEqual(['native', 'skill', 'plugin', 'mcp', 'connector', 'marketplace']);
expect([...AUDIT_INITIATORS]).toEqual(['agent', 'user', 'system']);
});
});
describe('risk ordering helpers', () => {
it('riskRank orders ascending, critical highest', () => {
expect(riskRank('low')).toBe(0);
expect(riskRank('critical')).toBe(3);
expect(riskRank('critical')).toBeGreaterThan(riskRank('low'));
});
it('riskAtLeast compares on the canonical scale (critical >= low, not below)', () => {
expect(riskAtLeast('critical', 'low')).toBe(true);
expect(riskAtLeast('low', 'high')).toBe(false);
expect(riskAtLeast('high', 'high')).toBe(true);
});
});
describe('sqlInList — CHECK-constraint single source', () => {
it('quotes and comma-joins for a SQLite IN clause', () => {
expect(sqlInList(AUDIT_ACTIONS)).toBe(
"'proposed', 'approved', 'installed', 'rejected', 'failed', 'blocked', 'uninstalled'",
);
});
});

View File

@@ -0,0 +1,89 @@
import { describe, it, expect } from 'vitest';
import {
createTeamSchema,
createTaskSchema,
sendMessageSchema,
createAgentSchema,
createAgentGroupSchema,
} from '../src/schemas.js';
describe('createTeamSchema', () => {
it('accepts valid team', () => {
const result = createTeamSchema.safeParse({ name: 'Marketing', slug: 'marketing' });
expect(result.success).toBe(true);
});
it('rejects empty name', () => {
const result = createTeamSchema.safeParse({ name: '', slug: 'ok' });
expect(result.success).toBe(false);
});
it('rejects invalid slug characters', () => {
const result = createTeamSchema.safeParse({ name: 'Ok', slug: 'Has Spaces' });
expect(result.success).toBe(false);
});
});
describe('createTaskSchema', () => {
it('accepts valid task with defaults', () => {
const result = createTaskSchema.safeParse({ title: 'Research competitors' });
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.priority).toBe('normal');
}
});
});
describe('sendMessageSchema', () => {
it('accepts valid waggle dance message', () => {
const result = sendMessageSchema.safeParse({
type: 'request',
subtype: 'knowledge_check',
content: { topic: 'competitor pricing', scope: 'Product Line X' },
});
expect(result.success).toBe(true);
});
it('rejects invalid subtype', () => {
const result = sendMessageSchema.safeParse({
type: 'broadcast',
subtype: 'invalid_type',
content: {},
});
expect(result.success).toBe(false);
});
});
describe('createAgentSchema', () => {
it('accepts agent with defaults', () => {
const result = createAgentSchema.safeParse({ name: 'web-searcher' });
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.model).toBe('claude-haiku-4-5');
expect(result.data.tools).toEqual([]);
}
});
});
describe('createAgentGroupSchema', () => {
it('accepts valid agent group', () => {
const result = createAgentGroupSchema.safeParse({
name: 'Research Team',
strategy: 'parallel',
members: [
{ agentId: '00000000-0000-0000-0000-000000000001', roleInGroup: 'lead' },
{ agentId: '00000000-0000-0000-0000-000000000002' },
],
});
expect(result.success).toBe(true);
});
it('rejects invalid strategy', () => {
const result = createAgentGroupSchema.safeParse({
name: 'Bad',
strategy: 'random',
members: [],
});
expect(result.success).toBe(false);
});
});

View File

@@ -0,0 +1,41 @@
import { describe, it, expect } from 'vitest';
import {
BUILTIN_TOOL_MANIFESTS, SUPPORTED_TOOLS, LAUNCH_COHORT, TOOL_DISPLAY_NAMES,
applyPromptArgTemplate,
} from '../src/tool-detection.js';
describe('BUILTIN_TOOL_MANIFESTS', () => {
it('has one manifest per supported tool, ids matching SUPPORTED_TOOLS', () => {
expect(BUILTIN_TOOL_MANIFESTS.map((m) => m.id).sort()).toEqual([...SUPPORTED_TOOLS].sort());
});
it('marks every built-in as builtin:true and launchable', () => {
for (const m of BUILTIN_TOOL_MANIFESTS) {
expect(m.builtin).toBe(true);
expect(m.launchable).toBe(true);
}
});
it('every built-in is hook-capable', () => {
expect(BUILTIN_TOOL_MANIFESTS.filter((m) => !m.hookCapable)).toEqual([]);
});
it('derives TOOL_DISPLAY_NAMES + LAUNCH_COHORT from the manifests (unchanged values)', () => {
expect(TOOL_DISPLAY_NAMES['claude-code']).toBe('Claude Code');
expect(TOOL_DISPLAY_NAMES['codex']).toBe('Codex CLI');
expect([...LAUNCH_COHORT].sort()).toEqual([...SUPPORTED_TOOLS].sort());
});
it('claude-code detects by PATH binary "claude" (not its id)', () => {
const cc = BUILTIN_TOOL_MANIFESTS.find((m) => m.id === 'claude-code')!;
expect(cc.detect).toEqual({ kind: 'path', binaryName: 'claude' });
});
});
describe('applyPromptArgTemplate (#5 fast-follow)', () => {
it('substitutes {prompt} in each template entry', () => {
expect(applyPromptArgTemplate(['--print', '{prompt}'], 'hello')).toEqual(['--print', 'hello']);
});
it('substitutes within an entry and across multiple entries', () => {
expect(applyPromptArgTemplate(['-m', 'msg={prompt}', '{prompt}'], 'hi')).toEqual(['-m', 'msg=hi', 'hi']);
});
it('leaves entries without the placeholder untouched', () => {
expect(applyPromptArgTemplate(['--yes', '--fast'], 'hi')).toEqual(['--yes', '--fast']);
});
});

View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2022"],
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"composite": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist", "tests"]
}