UNPKG

@agentled/cli

Version:

CLI for Agentled — manage workflows, apps, and knowledge from the command line. Zero context-window cost for AI agents.

488 lines 23.1 kB
/* eslint-disable no-console */ import { existsSync, readFileSync, writeFileSync } from 'node:fs'; function validateManifest(obj) { const errors = []; function err(path, msg) { errors.push({ path, message: msg, severity: 'error' }); } function warn(path, msg) { errors.push({ path, message: msg, severity: 'warning' }); } if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) { err('$', 'Manifest must be a JSON object'); return errors; } // Required top-level fields if (!obj.manifestVersion) err('manifestVersion', 'Required. Use semantic version format e.g. "0.1.0"'); if (obj.manifestVersion && !/^\d+\.\d+\.\d+$/.test(obj.manifestVersion)) { err('manifestVersion', 'Must be a semantic version string (e.g. "0.1.0")'); } if (!obj.group) { err('group', 'Required. Must include at minimum { id, name }'); return errors; // can't continue without group } if (!obj.group.id) err('group.id', 'Required. Use a lowercase-slug like "founder-showcase"'); if (obj.group.id && !/^[a-z0-9-]+$/.test(obj.group.id)) { err('group.id', 'Must be lowercase alphanumeric with hyphens (e.g. "founder-showcase")'); } if (!obj.group.name) err('group.name', 'Required. Human-readable name for the group'); if (!obj.group.goal) warn('group.goal', 'Missing. Document what business objective this group optimizes for'); if (!obj.group.description) warn('group.description', 'Missing. Add a one-sentence description'); // Collect declared local IDs for cross-reference checks const workflowLocalIds = new Set(); const existingWorkflowIds = new Set(); const knowledgeListKeys = new Set(); const stateMachineKeys = new Set(); // knowledgeLists if (obj.knowledgeLists !== undefined) { if (typeof obj.knowledgeLists !== 'object' || Array.isArray(obj.knowledgeLists)) { err('knowledgeLists', 'Must be an object (keys = listKeys)'); } else { for (const [listKey, listDef] of Object.entries(obj.knowledgeLists)) { knowledgeListKeys.add(listKey); const ld = listDef; if (!ld.description) warn(`knowledgeLists.${listKey}.description`, 'Missing. Describe this list\'s purpose'); if (!ld.userKeyField) warn(`knowledgeLists.${listKey}.userKeyField`, 'Missing. Specify the field used as dedup key in kg.upsert-rows'); if (!ld.producers || ld.producers.length === 0) warn(`knowledgeLists.${listKey}.producers`, 'No producers listed. Which workflow(s) write to this list?'); if (!ld.consumers || ld.consumers.length === 0) warn(`knowledgeLists.${listKey}.consumers`, 'No consumers listed. Which workflow(s) read from this list?'); } } } // stateMachines if (obj.stateMachines !== undefined) { if (typeof obj.stateMachines !== 'object' || Array.isArray(obj.stateMachines)) { err('stateMachines', 'Must be an object (keys = listKeys)'); } else { for (const [listKey, sm] of Object.entries(obj.stateMachines)) { stateMachineKeys.add(listKey); const s = sm; const stateSet = new Set(s.states ?? []); if (!s.states || s.states.length === 0) { err(`stateMachines.${listKey}.states`, 'Required. List all valid status values'); } if (s.initialState && !stateSet.has(s.initialState)) { err(`stateMachines.${listKey}.initialState`, `"${s.initialState}" is not in states[]`); } if (s.transitions) { for (let i = 0; i < s.transitions.length; i++) { const t = s.transitions[i]; if (!stateSet.has(t.from)) err(`stateMachines.${listKey}.transitions[${i}].from`, `"${t.from}" is not in states[]`); if (!stateSet.has(t.to)) err(`stateMachines.${listKey}.transitions[${i}].to`, `"${t.to}" is not in states[]`); } } // State machine references a listKey not declared in knowledgeLists if (obj.knowledgeLists && !knowledgeListKeys.has(listKey)) { warn(`stateMachines.${listKey}`, `List key "${listKey}" is not declared in knowledgeLists`); } } } } // workflows if (obj.workflows !== undefined) { if (!Array.isArray(obj.workflows)) { err('workflows', 'Must be an array'); } else { for (let i = 0; i < obj.workflows.length; i++) { const wf = obj.workflows[i]; const loc = `workflows[${i}]`; if (typeof wf !== 'object' || wf === null || Array.isArray(wf)) { err(loc, 'Each workflows[] entry must be an object'); continue; } if (!wf.id) err(`${loc}.id`, 'Required. Use a local slug for cross-references in this manifest'); if (wf.id) workflowLocalIds.add(wf.id); if (!wf.name) err(`${loc}.name`, 'Required'); if (!wf.purpose) warn(`${loc}.purpose`, 'Missing. Add a one-sentence purpose statement'); // Check reads/writes reference declared KG lists if (obj.knowledgeLists) { for (const listKey of (wf.reads ?? [])) { if (!knowledgeListKeys.has(listKey)) { err(`${loc}.reads`, `References list "${listKey}" not declared in knowledgeLists`); } } for (const listKey of (wf.writes ?? [])) { if (!knowledgeListKeys.has(listKey)) { err(`${loc}.writes`, `References list "${listKey}" not declared in knowledgeLists`); } } } } } } // existingWorkflows if (obj.existingWorkflows !== undefined) { if (!Array.isArray(obj.existingWorkflows)) { err('existingWorkflows', 'Must be an array'); } else { const deprecations = []; for (let i = 0; i < obj.existingWorkflows.length; i++) { const ew = obj.existingWorkflows[i]; const loc = `existingWorkflows[${i}]`; if (!ew.workflowId) err(`${loc}.workflowId`, 'Required'); if (ew.workflowId) existingWorkflowIds.add(ew.workflowId); if (!ew.action) err(`${loc}.action`, 'Required. One of: reuse, extend, deprecate, replace'); if (!['reuse', 'extend', 'deprecate', 'replace'].includes(ew.action)) { err(`${loc}.action`, `Unknown action "${ew.action}". Must be one of: reuse, extend, deprecate, replace`); } if (ew.action === 'deprecate' || ew.action === 'replace') { deprecations.push(ew.workflowId ?? loc); } } if (deprecations.length > 0) { warn('existingWorkflows', `${deprecations.length} workflow(s) marked for deprecation/replace: ${deprecations.join(', ')}. Confirm this is intentional.`); } } } // buildOrder if (obj.buildOrder !== undefined) { if (!Array.isArray(obj.buildOrder)) { err('buildOrder', 'Must be an array of workflow IDs/slugs'); } else { const declared = new Set([...workflowLocalIds]); for (let i = 0; i < obj.buildOrder.length; i++) { const ref = obj.buildOrder[i]; // callsWorkflows references that come before their dependency if (!declared.has(ref)) { warn(`buildOrder[${i}]`, `"${ref}" is not declared in workflows[].id — verify the slug matches`); } } } } // integrations if (obj.integrations !== undefined) { if (typeof obj.integrations !== 'object' || Array.isArray(obj.integrations)) { err('integrations', 'Must be an object (keys = app IDs)'); } } // Check for customer-facing send steps without approvalGates if (Array.isArray(obj.workflows)) { for (const wf of obj.workflows) { if (!wf.approvalGates || wf.approvalGates.length === 0) { const hasOutreach = (wf.purpose ?? '').toLowerCase().match(/email|outreach|message|send|notify/); if (hasOutreach) { warn(`workflows[${wf.id}].approvalGates`, 'Workflow appears to send messages but has no approvalGates defined. Customer-facing sends should require human approval.'); } } } } // openQuestions if (Array.isArray(obj.openQuestions)) { const unanswered = obj.openQuestions.filter((q) => q.status !== 'resolved'); if (unanswered.length > 0) { warn('openQuestions', `${unanswered.length} open question(s) unresolved. Answer before building.`); } } return errors; } const SCAFFOLDS = { 'event-driven-funnel': () => ({ $schema: 'https://schemas.agentled.app/group-manifest/v0.json', manifestVersion: '0.1.0', group: { id: 'my-funnel', name: 'My Event-Driven Funnel', description: 'Continuous sourcing → KG enrichment → per-entity activation.', goal: 'Optimize for conversion, not curation.', style: { color: '#6366f1', iconName: 'Sparkles' }, }, operatingPrinciples: [ 'Sourcing is continuous and broad — not batch per theme.', 'Processing fires per entity when it enters the KG, not on a fixed schedule.', 'Customer-facing messages are approval-gated.', ], knowledgeLists: { entities: { description: 'Entities sourced and tracked through the funnel.', userKeyField: 'url', fields: [ { name: 'name', type: 'string' }, { name: 'url', type: 'string' }, { name: 'score', type: 'number' }, { name: 'status', type: 'string' }, ], producers: ['source-entities'], consumers: ['enrich-and-score'], }, }, stateMachines: { entities: { states: ['new', 'enriched', 'scored', 'outreached', 'converted', 'rejected'], initialState: 'new', transitions: [ { from: 'new', to: 'enriched', trigger: 'enrich-and-score', description: 'Enrichment completed' }, { from: 'enriched', to: 'scored', trigger: 'enrich-and-score', description: 'Scoring completed' }, { from: 'scored', to: 'outreached', trigger: 'send-outreach', description: 'Outreach sent' }, { from: 'outreached', to: 'converted', trigger: 'process-reply', description: 'Positive reply received' }, { from: 'scored', to: 'rejected', trigger: 'enrich-and-score', description: 'Score below threshold' }, ], }, }, workflows: [ { id: 'source-entities', name: 'Source Entities', purpose: 'Find entities from external sources and write them to the KG with status: new.', trigger: 'schedule', schedule: 'daily', writes: ['entities'], creditEstimate: '5-15', status: 'planned', }, { id: 'enrich-and-score', name: 'Enrich and Score', purpose: 'Enrich new entities, score against ICP, and update status in KG.', trigger: 'schedule', schedule: 'daily', reads: ['entities'], writes: ['entities'], creditEstimate: '10-30', status: 'planned', }, { id: 'send-outreach', name: 'Send Outreach', purpose: 'Draft personalized outreach for scored entities and send after approval.', trigger: 'schedule', schedule: 'weekly', reads: ['entities'], writes: ['entities'], approvalGates: ['draft-outreach-email'], creditEstimate: '5-10', status: 'planned', }, ], existingWorkflows: [], integrations: { 'agentled': { required: true, purpose: 'LinkedIn enrichment' }, 'kg': { required: true, purpose: 'KG list reads/writes' }, 'gmail': { required: true, purpose: 'Outreach email sending' }, }, buildOrder: ['source-entities', 'enrich-and-score', 'send-outreach'], openQuestions: [ { question: 'What is the primary success metric: conversion rate, entities contacted per week, or operator time saved?', impact: 'Determines which step outputs to track in analytics.', status: 'open', }, { question: 'What is the system of record for conversions — a KG status update, a Stripe webhook, or a manual update?', impact: 'Determines whether a separate webhook listener workflow is needed.', status: 'open', }, ], }), 'sourcing-kit': () => ({ $schema: 'https://schemas.agentled.app/group-manifest/v0.json', manifestVersion: '0.1.0', group: { id: 'my-sourcing-kit', name: 'My Sourcing Kit', description: 'Multiple source workflows feeding a single canonical KG list.', goal: 'Build a deduplicated, always-fresh entity list from multiple sources.', }, knowledgeLists: { 'sourced-entities': { description: 'Canonical entity list from all sources.', userKeyField: 'url', fields: [ { name: 'name', type: 'string' }, { name: 'url', type: 'string' }, { name: 'source', type: 'string' }, { name: 'status', type: 'string' }, ], producers: ['source-a', 'source-b'], consumers: ['process-entities'], }, }, stateMachines: { 'sourced-entities': { states: ['new', 'processed', 'rejected'], initialState: 'new', transitions: [ { from: 'new', to: 'processed', trigger: 'process-entities' }, { from: 'new', to: 'rejected', trigger: 'process-entities' }, ], }, }, workflows: [ { id: 'source-a', name: 'Source A', purpose: 'Source entities from channel A.', trigger: 'schedule', writes: ['sourced-entities'], status: 'planned' }, { id: 'source-b', name: 'Source B', purpose: 'Source entities from channel B.', trigger: 'schedule', writes: ['sourced-entities'], status: 'planned' }, { id: 'process-entities', name: 'Process Entities', purpose: 'Enrich and act on new entities.', trigger: 'schedule', reads: ['sourced-entities'], writes: ['sourced-entities'], status: 'planned' }, ], buildOrder: ['source-a', 'source-b', 'process-entities'], openQuestions: [], }), 'intake-approval': () => ({ $schema: 'https://schemas.agentled.app/group-manifest/v0.json', manifestVersion: '0.1.0', group: { id: 'my-intake-approval', name: 'My Intake + Approval', description: 'Inbound intake → triage → human approval → action.', goal: 'Process inbound items quickly while keeping humans in the loop for customer-facing actions.', }, knowledgeLists: { 'intake-items': { description: 'Inbound items awaiting triage and approval.', userKeyField: 'id', fields: [ { name: 'id', type: 'string' }, { name: 'source', type: 'string' }, { name: 'content', type: 'string' }, { name: 'priority', type: 'string' }, { name: 'status', type: 'string' }, ], producers: ['intake'], consumers: ['triage', 'respond'], }, }, stateMachines: { 'intake-items': { states: ['new', 'triaged', 'approved', 'responded', 'dismissed'], initialState: 'new', transitions: [ { from: 'new', to: 'triaged', trigger: 'triage' }, { from: 'triaged', to: 'approved', trigger: 'respond', description: 'Human approves response' }, { from: 'triaged', to: 'dismissed', trigger: 'respond', description: 'Human dismisses item' }, { from: 'approved', to: 'responded', trigger: 'respond', description: 'Response sent' }, ], }, }, workflows: [ { id: 'intake', name: 'Intake', purpose: 'Collect inbound items from email, webhook, or form.', trigger: 'schedule', writes: ['intake-items'], status: 'planned' }, { id: 'triage', name: 'Triage', purpose: 'Classify priority and draft a response for each new item.', trigger: 'schedule', reads: ['intake-items'], writes: ['intake-items'], status: 'planned' }, { id: 'respond', name: 'Respond', purpose: 'Present drafted responses for approval, then send.', trigger: 'schedule', reads: ['intake-items'], writes: ['intake-items'], approvalGates: ['send-response'], status: 'planned' }, ], buildOrder: ['intake', 'triage', 'respond'], openQuestions: [ { question: 'What is the intake channel: email, webhook, form, or a combination?', impact: 'Determines trigger type and intake step app action.', status: 'open', }, ], }), }; const PATTERN_DESCRIPTIONS = { 'event-driven-funnel': 'Continuous sourcing → KG enrichment → per-entity activation (outreach, booking, etc.)', 'sourcing-kit': 'Multiple source workflows feeding a single deduplicated canonical KG list', 'intake-approval': 'Inbound intake → AI triage → human approval gate → action', }; // --------------------------------------------------------------------------- // Command registration // --------------------------------------------------------------------------- export function registerGroupManifestCommands(program) { const gm = program .command('group-manifest') .alias('gm') .description('Local validation and scaffolding for multi-workflow group manifests'); gm.command('validate <file>') .description('Validate a group manifest JSON file against the schema') .action((file) => { if (!existsSync(file)) { console.error(`Error: file not found: ${file}`); process.exit(1); } let raw; try { raw = readFileSync(file, 'utf-8'); } catch (e) { console.error(`Error reading file: ${e.message}`); process.exit(1); } let obj; try { obj = JSON.parse(raw); } catch (e) { console.error(`Invalid JSON: ${e.message}`); process.exit(1); } const results = validateManifest(obj); const errors = results.filter(r => r.severity === 'error'); const warnings = results.filter(r => r.severity === 'warning'); if (errors.length === 0 && warnings.length === 0) { console.log(`✓ ${file} — valid group manifest`); return; } if (errors.length > 0) { console.error(`\n${errors.length} error(s):\n`); for (const e of errors) { console.error(` [error] ${e.path}: ${e.message}`); } } if (warnings.length > 0) { console.warn(`\n${warnings.length} warning(s):\n`); for (const w of warnings) { console.warn(` [warn] ${w.path}: ${w.message}`); } } if (errors.length > 0) { console.error(`\nValidation failed with ${errors.length} error(s).`); process.exit(1); } else { console.log(`\nValid with ${warnings.length} warning(s). Review warnings before building.`); } }); gm.command('scaffold [pattern]') .description('Emit a starter group manifest for a named pattern') .option('--out <file>', 'Write output to a file instead of stdout') .option('--list', 'List available scaffold patterns') .action((pattern, opts) => { if (opts.list || !pattern || pattern === 'list') { console.log('Available group-manifest scaffold patterns:\n'); for (const [key, desc] of Object.entries(PATTERN_DESCRIPTIONS)) { console.log(` ${key.padEnd(24)} ${desc}`); } console.log('\nUsage: agentled group-manifest scaffold <pattern> [--out <file>]'); return; } const key = pattern; if (!SCAFFOLDS[key]) { console.error(`Unknown pattern "${pattern}". Run \`agentled group-manifest scaffold --list\` to see available patterns.`); process.exit(1); } const manifest = SCAFFOLDS[key](); const json = JSON.stringify(manifest, null, 2); if (opts.out) { try { writeFileSync(opts.out, json + '\n', 'utf-8'); console.log(`Scaffold written to ${opts.out}`); console.log(`\nNext steps:`); console.log(` 1. Edit ${opts.out} — fill in workspace IDs, list schemas, and open questions`); console.log(` 2. agentled group-manifest validate ${opts.out}`); console.log(` 3. Build workflows in the order listed in buildOrder[]`); } catch (e) { console.error(`Error writing file: ${e.message}`); process.exit(1); } } else { console.log(json); } }); } //# sourceMappingURL=group-manifest.js.map