@formigio/fazemos-cli 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -5,9 +5,9 @@ import { config, getEnv, getToken, getActiveOrgId, setActiveOrgId, addEnvironmen
5
5
  import { login, signup, confirmSignup, adminLogin } from './auth.js';
6
6
  import { api } from './api.js';
7
7
  import { execSync } from 'child_process';
8
- import { readFileSync } from 'fs';
8
+ import { readFileSync, readdirSync } from 'fs';
9
9
  import { fileURLToPath } from 'url';
10
- import { dirname, resolve } from 'path';
10
+ import { dirname, resolve, basename } from 'path';
11
11
  const __dirname = dirname(fileURLToPath(import.meta.url));
12
12
  const pkg = JSON.parse(readFileSync(resolve(__dirname, '..', 'package.json'), 'utf-8'));
13
13
  function parseNumber(val) {
@@ -1358,7 +1358,8 @@ executions
1358
1358
  .command('show')
1359
1359
  .description('Show execution detail')
1360
1360
  .argument('<id>', 'Execution ID')
1361
- .action(async (id) => {
1361
+ .option('-c, --context', 'Show full execution context (prompt, model, repos, worksheet)')
1362
+ .action(async (id, opts) => {
1362
1363
  try {
1363
1364
  const data = await api('GET', `/api/executions/${id}`);
1364
1365
  const e = data.execution;
@@ -1382,6 +1383,30 @@ executions
1382
1383
  console.log(` Started: ${e.started_at}`);
1383
1384
  if (e.completed_at)
1384
1385
  console.log(` Completed: ${e.completed_at}`);
1386
+ if (opts.context && e.context) {
1387
+ const ctx = e.context;
1388
+ console.log(chalk.cyan(`\n Context:`));
1389
+ if (ctx.model)
1390
+ console.log(` Model: ${ctx.model}`);
1391
+ if (ctx.maxBudgetUsd)
1392
+ console.log(` Budget: $${ctx.maxBudgetUsd}`);
1393
+ if (ctx.repos?.length)
1394
+ console.log(` Repos: ${ctx.repos.map((r) => r.name || r).join(', ')}`);
1395
+ if (ctx.worksheetName)
1396
+ console.log(` Worksheet: ${ctx.worksheetName}`);
1397
+ if (ctx.worksheetPurpose)
1398
+ console.log(` Purpose: ${ctx.worksheetPurpose}`);
1399
+ if (ctx.actionDescription)
1400
+ console.log(` Action: ${ctx.actionDescription}`);
1401
+ if (ctx.outcomes?.length) {
1402
+ console.log(chalk.cyan(` Outcomes:`));
1403
+ for (const o of ctx.outcomes) {
1404
+ console.log(` ${o.status === 'achieved' ? '✓' : '○'} ${o.description || o.name}`);
1405
+ }
1406
+ }
1407
+ if (ctx.prompt)
1408
+ console.log(` Prompt: ${ctx.prompt}`);
1409
+ }
1385
1410
  }
1386
1411
  catch (err) {
1387
1412
  console.error(chalk.red(err.message));
@@ -1457,6 +1482,141 @@ executions
1457
1482
  process.exit(1);
1458
1483
  }
1459
1484
  });
1485
+ executions
1486
+ .command('logs')
1487
+ .description('Show execution output (polls if running)')
1488
+ .argument('<id>', 'Execution ID')
1489
+ .option('-f, --follow', 'Keep polling until execution completes')
1490
+ .action(async (id, opts) => {
1491
+ try {
1492
+ const poll = async () => {
1493
+ const data = await api('GET', `/api/executions/${id}`);
1494
+ return data.execution;
1495
+ };
1496
+ let e = await poll();
1497
+ const isTerminal = (status) => ['completed', 'failed', 'cancelled'].includes(status);
1498
+ const formatOutput = (ex) => {
1499
+ if (ex.output_data) {
1500
+ return typeof ex.output_data === 'string' ? ex.output_data : JSON.stringify(ex.output_data, null, 2);
1501
+ }
1502
+ if (ex.output_text)
1503
+ return ex.output_text;
1504
+ return '';
1505
+ };
1506
+ // Print current output
1507
+ const initialOutput = formatOutput(e);
1508
+ if (initialOutput)
1509
+ console.log(initialOutput);
1510
+ if (isTerminal(e.status)) {
1511
+ if (!initialOutput)
1512
+ console.log(chalk.yellow('No output'));
1513
+ console.log(chalk.gray(`\n[${e.status}]`));
1514
+ return;
1515
+ }
1516
+ if (!opts.follow) {
1517
+ console.log(chalk.yellow(`\n[${e.status} — use --follow to poll]`));
1518
+ return;
1519
+ }
1520
+ // Poll until complete
1521
+ let lastLen = initialOutput.length;
1522
+ while (!isTerminal(e.status)) {
1523
+ await new Promise(r => setTimeout(r, 3000));
1524
+ e = await poll();
1525
+ const currentOutput = formatOutput(e);
1526
+ if (currentOutput.length > lastLen) {
1527
+ process.stdout.write(currentOutput.slice(lastLen));
1528
+ lastLen = currentOutput.length;
1529
+ }
1530
+ }
1531
+ console.log(chalk.gray(`\n[${e.status}]`));
1532
+ }
1533
+ catch (err) {
1534
+ console.error(chalk.red(err.message));
1535
+ process.exit(1);
1536
+ }
1537
+ });
1538
+ executions
1539
+ .command('cloudwatch-logs')
1540
+ .alias('cw')
1541
+ .description('Show container logs from CloudWatch')
1542
+ .argument('<id>', 'Execution ID')
1543
+ .option('-l, --limit <n>', 'Max log lines', parseNumber, 100)
1544
+ .option('-f, --follow', 'Poll for new lines until execution completes')
1545
+ .option('--since <duration>', 'Show logs since (e.g., 5m, 1h)')
1546
+ .action(async (id, opts) => {
1547
+ try {
1548
+ const isTerminal = (status) => ['completed', 'failed', 'cancelled'].includes(status);
1549
+ const parseSince = (since) => {
1550
+ const match = since.match(/^(\d+)(m|h|d)$/);
1551
+ if (!match)
1552
+ throw new Error('Invalid --since format. Use e.g. 5m, 1h, 2d');
1553
+ const val = parseInt(match[1], 10);
1554
+ const unit = match[2];
1555
+ const multipliers = { m: 60000, h: 3600000, d: 86400000 };
1556
+ return Date.now() - val * multipliers[unit];
1557
+ };
1558
+ const startTime = opts.since ? String(parseSince(opts.since)) : undefined;
1559
+ const fetchLogs = async (nextToken) => {
1560
+ const params = new URLSearchParams();
1561
+ params.set('limit', String(opts.limit));
1562
+ if (startTime)
1563
+ params.set('startTime', startTime);
1564
+ if (nextToken)
1565
+ params.set('nextToken', nextToken);
1566
+ return await api('GET', `/api/executions/${id}/cloudwatch-logs?${params}`);
1567
+ };
1568
+ const formatLine = (event) => {
1569
+ const ts = new Date(event.timestamp);
1570
+ const hh = String(ts.getHours()).padStart(2, '0');
1571
+ const mm = String(ts.getMinutes()).padStart(2, '0');
1572
+ const ss = String(ts.getSeconds()).padStart(2, '0');
1573
+ return `${chalk.gray(`[${hh}:${mm}:${ss}]`)} ${event.message}`;
1574
+ };
1575
+ // Initial fetch
1576
+ let data = await fetchLogs();
1577
+ if (!data.logs?.length && !opts.follow) {
1578
+ console.log(chalk.yellow(data.message || 'No log events'));
1579
+ return;
1580
+ }
1581
+ for (const event of data.logs || []) {
1582
+ console.log(formatLine(event));
1583
+ }
1584
+ if (!opts.follow)
1585
+ return;
1586
+ // Poll loop
1587
+ let token = data.nextToken;
1588
+ while (true) {
1589
+ await new Promise(r => setTimeout(r, 2000));
1590
+ // Check execution status
1591
+ const exData = await api('GET', `/api/executions/${id}`);
1592
+ const status = exData.execution?.status;
1593
+ // Fetch new logs
1594
+ data = await fetchLogs(token);
1595
+ for (const event of data.logs || []) {
1596
+ console.log(formatLine(event));
1597
+ }
1598
+ token = data.nextToken;
1599
+ if (isTerminal(status)) {
1600
+ // Drain remaining log pages
1601
+ let prevToken;
1602
+ while (token && token !== prevToken) {
1603
+ prevToken = token;
1604
+ data = await fetchLogs(token);
1605
+ for (const event of data.logs || []) {
1606
+ console.log(formatLine(event));
1607
+ }
1608
+ token = data.nextToken;
1609
+ }
1610
+ console.log(chalk.gray(`\n[${status}]`));
1611
+ break;
1612
+ }
1613
+ }
1614
+ }
1615
+ catch (err) {
1616
+ console.error(chalk.red(err.message));
1617
+ process.exit(1);
1618
+ }
1619
+ });
1460
1620
  // ── My Work ─────────────────────────────────────────────────
1461
1621
  program
1462
1622
  .command('my-work')
@@ -1536,6 +1696,27 @@ program
1536
1696
  });
1537
1697
  // ── Agents ──────────────────────────────────────────────────
1538
1698
  const agentsCmd = program.command('agents').description('Agent management');
1699
+ /** Resolve agent name or UUID to member ID */
1700
+ async function resolveAgentId(nameOrId) {
1701
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-/.test(nameOrId))
1702
+ return nameOrId;
1703
+ const orgId = getActiveOrgId();
1704
+ if (!orgId) {
1705
+ console.error(chalk.red('No active org'));
1706
+ process.exit(1);
1707
+ }
1708
+ const membersData = await api('GET', `/api/organizations/${orgId}/members?type=agent`);
1709
+ const norm = (s) => s.toLowerCase().replace(/[-_\s]+/g, ' ').trim();
1710
+ const agent = membersData.members.find((m) => norm(m.display_name) === norm(nameOrId));
1711
+ if (!agent) {
1712
+ console.error(chalk.red(`Agent "${nameOrId}" not found. Available:`));
1713
+ for (const m of membersData.members.filter((m) => m.member_type === 'agent')) {
1714
+ console.log(` ${m.display_name} — ${m.id}`);
1715
+ }
1716
+ process.exit(1);
1717
+ }
1718
+ return agent.id;
1719
+ }
1539
1720
  agentsCmd
1540
1721
  .command('register')
1541
1722
  .description('Register an agent')
@@ -1594,9 +1775,10 @@ agentsCmd
1594
1775
  agentsCmd
1595
1776
  .command('show')
1596
1777
  .description('Show agent status and details')
1597
- .argument('<id>', 'Agent member ID')
1778
+ .argument('<id>', 'Agent name or member ID')
1598
1779
  .action(async (id) => {
1599
1780
  try {
1781
+ id = await resolveAgentId(id);
1600
1782
  const data = await api('GET', `/api/agents/${id}/status`);
1601
1783
  console.log(chalk.cyan(`Agent: ${data.display_name}`));
1602
1784
  console.log(` ID: ${data.id}`);
@@ -1625,10 +1807,11 @@ agentsCmd
1625
1807
  agentsCmd
1626
1808
  .command('metrics')
1627
1809
  .description('Show agent execution metrics')
1628
- .argument('<id>', 'Agent member ID')
1810
+ .argument('<id>', 'Agent name or member ID')
1629
1811
  .option('-w, --window <hours>', 'Time window in hours', '24')
1630
1812
  .action(async (id, opts) => {
1631
1813
  try {
1814
+ id = await resolveAgentId(id);
1632
1815
  const data = await api('GET', `/api/agents/${id}/metrics?window_hours=${opts.window}`);
1633
1816
  const e = data.executions;
1634
1817
  console.log(chalk.cyan(`Metrics (last ${data.window_hours}h):`));
@@ -1655,12 +1838,13 @@ agentsCmd
1655
1838
  agentsCmd
1656
1839
  .command('events')
1657
1840
  .description('Show agent event log')
1658
- .argument('<id>', 'Agent member ID')
1841
+ .argument('<id>', 'Agent name or member ID')
1659
1842
  .option('-t, --type <type>', 'Filter by event type')
1660
1843
  .option('-e, --execution <id>', 'Filter by execution ID')
1661
1844
  .option('-l, --limit <n>', 'Max events to show', '20')
1662
1845
  .action(async (id, opts) => {
1663
1846
  try {
1847
+ id = await resolveAgentId(id);
1664
1848
  const params = [`limit=${opts.limit}`];
1665
1849
  if (opts.type)
1666
1850
  params.push(`event_type=${opts.type}`);
@@ -1689,9 +1873,10 @@ agentsCmd
1689
1873
  agentsCmd
1690
1874
  .command('budget')
1691
1875
  .description('Check agent budget status')
1692
- .argument('<id>', 'Agent member ID')
1876
+ .argument('<id>', 'Agent name or member ID')
1693
1877
  .action(async (id) => {
1694
1878
  try {
1879
+ id = await resolveAgentId(id);
1695
1880
  const data = await api('GET', `/api/agents/${id}/budget`);
1696
1881
  const icon = data.allowed ? chalk.green('✓') : chalk.red('✗');
1697
1882
  console.log(` ${icon} Budget: ${data.allowed ? 'OK' : 'EXCEEDED'}`);
@@ -1713,12 +1898,13 @@ agentsCmd
1713
1898
  agentsCmd
1714
1899
  .command('heartbeat')
1715
1900
  .description('Send agent heartbeat')
1716
- .argument('<id>', 'Agent member ID')
1901
+ .argument('<id>', 'Agent name or member ID')
1717
1902
  .option('-s, --status <status>', 'Agent status (idle, busy, error)', 'idle')
1718
1903
  .option('-e, --execution <id>', 'Current execution ID')
1719
1904
  .option('-d, --details <json>', 'Additional details as JSON')
1720
1905
  .action(async (id, opts) => {
1721
1906
  try {
1907
+ id = await resolveAgentId(id);
1722
1908
  const body = { status: opts.status };
1723
1909
  if (opts.execution)
1724
1910
  body.currentExecutionId = opts.execution;
@@ -1735,12 +1921,13 @@ agentsCmd
1735
1921
  agentsCmd
1736
1922
  .command('event')
1737
1923
  .description('Log an agent event')
1738
- .argument('<id>', 'Agent member ID')
1924
+ .argument('<id>', 'Agent name or member ID')
1739
1925
  .requiredOption('-t, --type <type>', 'Event type (status_change, error, budget_warning, etc.)')
1740
1926
  .option('-e, --execution <id>', 'Related execution ID')
1741
1927
  .option('-d, --details <json>', 'Event details as JSON')
1742
1928
  .action(async (id, opts) => {
1743
1929
  try {
1930
+ id = await resolveAgentId(id);
1744
1931
  const body = { eventType: opts.type };
1745
1932
  if (opts.execution)
1746
1933
  body.executionId = opts.execution;
@@ -1757,9 +1944,10 @@ agentsCmd
1757
1944
  agentsCmd
1758
1945
  .command('work')
1759
1946
  .description('Show consolidated work assignments for an agent')
1760
- .argument('<id>', 'Agent member ID')
1947
+ .argument('<id>', 'Agent name or member ID')
1761
1948
  .action(async (id) => {
1762
1949
  try {
1950
+ id = await resolveAgentId(id);
1763
1951
  const data = await api('GET', `/api/agents/${id}/work`);
1764
1952
  console.log(chalk.cyan(`Agent Status: ${data.agent_status || 'unknown'}`));
1765
1953
  // Budget summary
@@ -1798,6 +1986,84 @@ agentsCmd
1798
1986
  process.exit(1);
1799
1987
  }
1800
1988
  });
1989
+ agentsCmd
1990
+ .command('upload-definition')
1991
+ .description('Upload an agent definition file to Fazemos')
1992
+ .argument('<name>', 'Agent name')
1993
+ .argument('<file>', 'Path to agent definition .md file')
1994
+ .action(async (name, file) => {
1995
+ try {
1996
+ // Resolve agent name to member ID
1997
+ const orgId = getActiveOrgId();
1998
+ if (!orgId) {
1999
+ console.error(chalk.red('No active org'));
2000
+ process.exit(1);
2001
+ }
2002
+ const membersData = await api('GET', `/api/organizations/${orgId}/members?type=agent`);
2003
+ const norm = (s) => s.toLowerCase().replace(/[-_\s]+/g, ' ').trim();
2004
+ const agent = membersData.members.find((m) => norm(m.display_name) === norm(name));
2005
+ if (!agent) {
2006
+ console.error(chalk.red(`Agent "${name}" not found`));
2007
+ process.exit(1);
2008
+ }
2009
+ // Read file and strip YAML frontmatter
2010
+ const raw = readFileSync(resolve(file), 'utf-8');
2011
+ const body = raw.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, '').trim();
2012
+ await api('PATCH', `/api/members/${agent.id}/agent-config`, { systemPrompt: body });
2013
+ console.log(chalk.green(`Uploaded definition for ${agent.display_name} (${body.length} chars)`));
2014
+ }
2015
+ catch (err) {
2016
+ console.error(chalk.red(err.message));
2017
+ process.exit(1);
2018
+ }
2019
+ });
2020
+ agentsCmd
2021
+ .command('upload-all')
2022
+ .description('Upload all agent definitions from a directory')
2023
+ .argument('<dir>', 'Directory containing .md agent definition files')
2024
+ .action(async (dir) => {
2025
+ try {
2026
+ const orgId = getActiveOrgId();
2027
+ if (!orgId) {
2028
+ console.error(chalk.red('No active org'));
2029
+ process.exit(1);
2030
+ }
2031
+ const membersData = await api('GET', `/api/organizations/${orgId}/members?type=agent`);
2032
+ const agents = membersData.members.filter((m) => m.member_type === 'agent');
2033
+ const resolvedDir = resolve(dir);
2034
+ const files = readdirSync(resolvedDir).filter(f => f.endsWith('.md'));
2035
+ if (!files.length) {
2036
+ console.log(chalk.yellow(`No .md files found in ${resolvedDir}`));
2037
+ return;
2038
+ }
2039
+ let uploaded = 0;
2040
+ let skipped = 0;
2041
+ const norm = (s) => s.toLowerCase().replace(/[-_\s]+/g, ' ').trim();
2042
+ for (const file of files) {
2043
+ const raw = readFileSync(resolve(resolvedDir, file), 'utf-8');
2044
+ // Extract name from frontmatter (name: field), fall back to filename
2045
+ const frontmatterMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
2046
+ const frontmatter = frontmatterMatch ? frontmatterMatch[1] : '';
2047
+ const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
2048
+ const name = nameMatch ? nameMatch[1].trim() : basename(file, '.md');
2049
+ const agent = agents.find((a) => norm(a.display_name) === norm(name));
2050
+ if (!agent) {
2051
+ console.log(chalk.yellow(` ⊘ ${name} — no matching agent`));
2052
+ skipped++;
2053
+ continue;
2054
+ }
2055
+ const body = raw.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, '').trim();
2056
+ await api('PATCH', `/api/members/${agent.id}/agent-config`, { systemPrompt: body });
2057
+ console.log(chalk.green(` ✓ ${agent.display_name} (${body.length} chars)`));
2058
+ uploaded++;
2059
+ }
2060
+ console.log(`\n${uploaded} uploaded, ${skipped} skipped`);
2061
+ }
2062
+ catch (err) {
2063
+ console.error(chalk.red(err.message));
2064
+ process.exit(1);
2065
+ }
2066
+ });
1801
2067
  // ── Health ──────────────────────────────────────────────────
1802
2068
  program
1803
2069
  .command('health')
@@ -1879,5 +2145,25 @@ apiKeys
1879
2145
  process.exit(1);
1880
2146
  }
1881
2147
  });
2148
+ // ── Monitor ─────────────────────────────────────────────────
2149
+ program
2150
+ .command('monitor')
2151
+ .description('Launch web-based execution monitor (live logs, status)')
2152
+ .option('-p, --port <port>', 'Port to listen on', '4600')
2153
+ .action(async (opts) => {
2154
+ try {
2155
+ const port = parseInt(opts.port, 10);
2156
+ const { startMonitor } = await import('./monitor.js');
2157
+ await startMonitor(port);
2158
+ console.log(chalk.green(`\n ◆ Fazemos Execution Monitor`));
2159
+ console.log(` ${chalk.cyan(`http://localhost:${port}`)}`);
2160
+ console.log(chalk.gray(`\n Proxying API calls with your current auth session`));
2161
+ console.log(chalk.gray(` Press Ctrl+C to stop\n`));
2162
+ }
2163
+ catch (err) {
2164
+ console.error(chalk.red(err.message));
2165
+ process.exit(1);
2166
+ }
2167
+ });
1882
2168
  program.parse();
1883
2169
  //# sourceMappingURL=index.js.map