@knid/agentx 0.1.9 → 0.2.0

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
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command19 } from "commander";
5
- import { readFileSync as readFileSync12 } from "fs";
6
- import { fileURLToPath as fileURLToPath2 } from "url";
7
- import { dirname as dirname2, join as join17 } from "path";
4
+ import { Command as Command20 } from "commander";
5
+ import { readFileSync as readFileSync16 } from "fs";
6
+ import { fileURLToPath as fileURLToPath3 } from "url";
7
+ import { dirname as dirname4, join as join21 } from "path";
8
8
 
9
9
  // src/commands/run.ts
10
10
  import { Command } from "commander";
@@ -20,6 +20,7 @@ import { parse } from "yaml";
20
20
 
21
21
  // src/schemas/agent-yaml.ts
22
22
  import { z } from "zod";
23
+ import { Cron } from "croner";
23
24
  var VALID_CATEGORIES = [
24
25
  "productivity",
25
26
  "devtools",
@@ -63,6 +64,21 @@ var requiresSchema = z.object({
63
64
  node: z.string().optional(),
64
65
  os: z.array(z.string()).optional()
65
66
  });
67
+ var scheduleEntrySchema = z.object({
68
+ name: z.string().optional(),
69
+ cron: z.string().refine(
70
+ (val) => {
71
+ try {
72
+ new Cron(val);
73
+ return true;
74
+ } catch {
75
+ return false;
76
+ }
77
+ },
78
+ { message: "Invalid cron expression" }
79
+ ),
80
+ prompt: z.string().min(1).max(2e3)
81
+ });
66
82
  var agentYamlSchema = z.object({
67
83
  name: z.string().min(1).max(100).regex(AGENT_NAME_REGEX, "Name must contain only lowercase letters, numbers, and hyphens"),
68
84
  version: z.string().regex(SEMVER_REGEX, "Version must be valid semver (e.g. 1.0.0)"),
@@ -79,7 +95,8 @@ var agentYamlSchema = z.object({
79
95
  permissions: permissionsSchema.optional(),
80
96
  allowed_tools: z.array(z.string()).optional(),
81
97
  config: z.array(configOptionSchema).optional(),
82
- examples: z.array(exampleSchema).optional()
98
+ examples: z.array(exampleSchema).optional(),
99
+ schedule: z.array(scheduleEntrySchema).max(10).optional()
83
100
  });
84
101
 
85
102
  // src/config/paths.ts
@@ -92,6 +109,10 @@ var CONFIG_PATH = join(AGENTX_HOME, "config.yaml");
92
109
  var AUTH_PATH = join(AGENTX_HOME, "auth.json");
93
110
  var CACHE_DIR = join(AGENTX_HOME, "cache");
94
111
  var LOGS_DIR = join(AGENTX_HOME, "logs");
112
+ var SCHEDULER_DIR = join(AGENTX_HOME, "scheduler");
113
+ var SCHEDULER_PID = join(SCHEDULER_DIR, "scheduler.pid");
114
+ var SCHEDULER_STATE = join(SCHEDULER_DIR, "state.json");
115
+ var SCHEDULER_LOGS_DIR = join(SCHEDULER_DIR, "logs");
95
116
 
96
117
  // src/utils/errors.ts
97
118
  var AgentxError = class extends Error {
@@ -502,6 +523,10 @@ async function deleteSecrets(agentName, secretsDir = SECRETS_DIR) {
502
523
  unlinkSync(filePath);
503
524
  }
504
525
  }
526
+ async function hasSecrets(agentName, secretsDir = SECRETS_DIR) {
527
+ const filePath = getSecretsFilePath(agentName, secretsDir);
528
+ return existsSync5(filePath);
529
+ }
505
530
 
506
531
  // src/runtime/runner.ts
507
532
  function buildClaudeArgs(options) {
@@ -1628,20 +1653,196 @@ Examples:
1628
1653
 
1629
1654
  // src/commands/uninstall.ts
1630
1655
  import { Command as Command12 } from "commander";
1631
- import { existsSync as existsSync17, rmSync as rmSync2 } from "fs";
1632
- import { join as join14 } from "path";
1656
+ import { existsSync as existsSync19, rmSync as rmSync2 } from "fs";
1657
+ import { join as join16 } from "path";
1658
+
1659
+ // src/scheduler/state.ts
1660
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync6, existsSync as existsSync17, mkdirSync as mkdirSync6, renameSync } from "fs";
1661
+ import { dirname as dirname2, join as join14 } from "path";
1662
+ var EMPTY_STATE = {
1663
+ pid: null,
1664
+ startedAt: null,
1665
+ agents: {}
1666
+ };
1667
+ async function loadScheduleState(statePath) {
1668
+ if (!existsSync17(statePath)) {
1669
+ return { ...EMPTY_STATE, agents: {} };
1670
+ }
1671
+ const raw = readFileSync12(statePath, "utf-8");
1672
+ return JSON.parse(raw);
1673
+ }
1674
+ async function saveScheduleState(state, statePath) {
1675
+ const dir = dirname2(statePath);
1676
+ mkdirSync6(dir, { recursive: true });
1677
+ const tmpPath = join14(dir, `.state.${Date.now()}.tmp`);
1678
+ writeFileSync6(tmpPath, JSON.stringify(state, null, 2), { encoding: "utf-8", mode: 384 });
1679
+ renameSync(tmpPath, statePath);
1680
+ }
1681
+ function addAgentToState(state, agentName, schedules) {
1682
+ const scheduleStates = schedules.map((s) => ({
1683
+ name: s.name ?? s.cron,
1684
+ cron: s.cron,
1685
+ prompt: s.prompt,
1686
+ status: "active",
1687
+ lastRunAt: null,
1688
+ lastRunStatus: null,
1689
+ nextRunAt: null,
1690
+ runCount: 0,
1691
+ errorCount: 0
1692
+ }));
1693
+ return {
1694
+ ...state,
1695
+ agents: {
1696
+ ...state.agents,
1697
+ [agentName]: {
1698
+ agentName,
1699
+ schedules: scheduleStates,
1700
+ registeredAt: (/* @__PURE__ */ new Date()).toISOString()
1701
+ }
1702
+ }
1703
+ };
1704
+ }
1705
+ function removeAgentFromState(state, agentName) {
1706
+ const { [agentName]: _, ...remainingAgents } = state.agents;
1707
+ return {
1708
+ ...state,
1709
+ agents: remainingAgents
1710
+ };
1711
+ }
1712
+
1713
+ // src/scheduler/process.ts
1714
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync7, existsSync as existsSync18, unlinkSync as unlinkSync4, mkdirSync as mkdirSync7 } from "fs";
1715
+ import { dirname as dirname3, join as join15 } from "path";
1716
+ import { fork } from "child_process";
1717
+ import { fileURLToPath as fileURLToPath2 } from "url";
1718
+ var __filename3 = fileURLToPath2(import.meta.url);
1719
+ var __dirname3 = dirname3(__filename3);
1720
+ function isDaemonRunning(pidPath) {
1721
+ if (!existsSync18(pidPath)) {
1722
+ return false;
1723
+ }
1724
+ const pid = getDaemonPid(pidPath);
1725
+ if (pid === null) {
1726
+ return false;
1727
+ }
1728
+ try {
1729
+ process.kill(pid, 0);
1730
+ return true;
1731
+ } catch {
1732
+ unlinkSync4(pidPath);
1733
+ return false;
1734
+ }
1735
+ }
1736
+ function getDaemonPid(pidPath) {
1737
+ if (!existsSync18(pidPath)) {
1738
+ return null;
1739
+ }
1740
+ const raw = readFileSync13(pidPath, "utf-8").trim();
1741
+ const pid = parseInt(raw, 10);
1742
+ return isNaN(pid) ? null : pid;
1743
+ }
1744
+ function startDaemon(pidPath, statePath, logsDir) {
1745
+ if (existsSync18(pidPath)) {
1746
+ const pid2 = getDaemonPid(pidPath);
1747
+ if (pid2 !== null) {
1748
+ try {
1749
+ process.kill(pid2, 0);
1750
+ return pid2;
1751
+ } catch {
1752
+ unlinkSync4(pidPath);
1753
+ }
1754
+ }
1755
+ }
1756
+ const dir = dirname3(pidPath);
1757
+ mkdirSync7(dir, { recursive: true });
1758
+ const candidates = [
1759
+ join15(__dirname3, "..", "scheduler", "daemon.js"),
1760
+ // dist/scheduler/daemon.js from dist/index.js
1761
+ join15(__dirname3, "daemon.js"),
1762
+ // same directory
1763
+ join15(__dirname3, "..", "dist", "scheduler", "daemon.js")
1764
+ // from src/ during dev
1765
+ ];
1766
+ let daemonScript = candidates[0];
1767
+ for (const c of candidates) {
1768
+ if (existsSync18(c)) {
1769
+ daemonScript = c;
1770
+ break;
1771
+ }
1772
+ }
1773
+ const child = fork(daemonScript, [], {
1774
+ detached: true,
1775
+ stdio: "ignore",
1776
+ env: {
1777
+ ...process.env,
1778
+ AGENTX_SCHEDULER_STATE: statePath,
1779
+ AGENTX_SCHEDULER_PID: pidPath,
1780
+ AGENTX_SCHEDULER_LOGS: logsDir
1781
+ }
1782
+ });
1783
+ const pid = child.pid;
1784
+ writeFileSync7(pidPath, String(pid), { encoding: "utf-8", mode: 384 });
1785
+ child.unref();
1786
+ return pid;
1787
+ }
1788
+ function stopDaemon(pidPath) {
1789
+ const pid = getDaemonPid(pidPath);
1790
+ if (pid === null) {
1791
+ return false;
1792
+ }
1793
+ try {
1794
+ process.kill(pid, "SIGTERM");
1795
+ if (existsSync18(pidPath)) {
1796
+ unlinkSync4(pidPath);
1797
+ }
1798
+ return true;
1799
+ } catch {
1800
+ if (existsSync18(pidPath)) {
1801
+ unlinkSync4(pidPath);
1802
+ }
1803
+ return false;
1804
+ }
1805
+ }
1806
+ function signalDaemon(signal, pidPath) {
1807
+ const pid = getDaemonPid(pidPath);
1808
+ if (pid === null) {
1809
+ return false;
1810
+ }
1811
+ try {
1812
+ process.kill(pid, signal);
1813
+ return true;
1814
+ } catch {
1815
+ return false;
1816
+ }
1817
+ }
1818
+
1819
+ // src/commands/uninstall.ts
1633
1820
  var uninstallCommand = new Command12("uninstall").description("Uninstall an agent").argument("<agent>", "Agent name to uninstall").option("--keep-secrets", "Keep secrets (do not delete encrypted secrets)").addHelpText("after", `
1634
1821
  Examples:
1635
1822
  $ agentx uninstall data-analyst
1636
1823
  $ agentx uninstall gmail-agent --keep-secrets`).action(async (agentName, options) => {
1637
1824
  try {
1638
- const agentDir = join14(AGENTS_DIR, agentName);
1639
- if (!existsSync17(agentDir)) {
1825
+ const agentDir = join16(AGENTS_DIR, agentName);
1826
+ if (!existsSync19(agentDir)) {
1640
1827
  console.error(
1641
1828
  colors.error(`Agent "${agentName}" is not installed.`)
1642
1829
  );
1643
1830
  process.exit(1);
1644
1831
  }
1832
+ try {
1833
+ const state = await loadScheduleState(SCHEDULER_STATE);
1834
+ if (state.agents[agentName]) {
1835
+ const updated = removeAgentFromState(state, agentName);
1836
+ await saveScheduleState(updated, SCHEDULER_STATE);
1837
+ if (Object.keys(updated.agents).length === 0) {
1838
+ stopDaemon(SCHEDULER_PID);
1839
+ } else if (isDaemonRunning(SCHEDULER_PID)) {
1840
+ signalDaemon("SIGHUP", SCHEDULER_PID);
1841
+ }
1842
+ console.log(colors.dim(`Stopped schedule for ${agentName}`));
1843
+ }
1844
+ } catch {
1845
+ }
1645
1846
  rmSync2(agentDir, { recursive: true, force: true });
1646
1847
  if (!options.keepSecrets) {
1647
1848
  await deleteSecrets(agentName);
@@ -1659,8 +1860,8 @@ Examples:
1659
1860
 
1660
1861
  // src/commands/list.ts
1661
1862
  import { Command as Command13 } from "commander";
1662
- import { existsSync as existsSync18, readdirSync } from "fs";
1663
- import { join as join15 } from "path";
1863
+ import { existsSync as existsSync20, readdirSync } from "fs";
1864
+ import { join as join17 } from "path";
1664
1865
 
1665
1866
  // src/ui/table.ts
1666
1867
  function formatTable(columns, rows) {
@@ -1688,7 +1889,7 @@ Examples:
1688
1889
  $ agentx list
1689
1890
  $ agentx ls --json`).action((options) => {
1690
1891
  try {
1691
- if (!existsSync18(AGENTS_DIR)) {
1892
+ if (!existsSync20(AGENTS_DIR)) {
1692
1893
  if (options.json) {
1693
1894
  console.log(JSON.stringify([]));
1694
1895
  } else {
@@ -1699,7 +1900,7 @@ Examples:
1699
1900
  }
1700
1901
  return;
1701
1902
  }
1702
- const entries = readdirSync(AGENTS_DIR, { withFileTypes: true }).filter((e) => e.isDirectory()).filter((e) => existsSync18(join15(AGENTS_DIR, e.name, "agent.yaml")));
1903
+ const entries = readdirSync(AGENTS_DIR, { withFileTypes: true }).filter((e) => e.isDirectory()).filter((e) => existsSync20(join17(AGENTS_DIR, e.name, "agent.yaml")));
1703
1904
  if (entries.length === 0) {
1704
1905
  if (options.json) {
1705
1906
  console.log(JSON.stringify([]));
@@ -1712,7 +1913,7 @@ Examples:
1712
1913
  return;
1713
1914
  }
1714
1915
  const agentData = entries.map((entry) => {
1715
- const agentDir = join15(AGENTS_DIR, entry.name);
1916
+ const agentDir = join17(AGENTS_DIR, entry.name);
1716
1917
  try {
1717
1918
  const manifest = loadAgentYaml(agentDir);
1718
1919
  return {
@@ -1758,8 +1959,8 @@ Examples:
1758
1959
 
1759
1960
  // src/commands/update.ts
1760
1961
  import { Command as Command14 } from "commander";
1761
- import { existsSync as existsSync19, readdirSync as readdirSync2 } from "fs";
1762
- import { join as join16 } from "path";
1962
+ import { existsSync as existsSync21, readdirSync as readdirSync2 } from "fs";
1963
+ import { join as join18 } from "path";
1763
1964
 
1764
1965
  // src/utils/semver.ts
1765
1966
  var SEMVER_REGEX2 = /^(\d+)\.(\d+)\.(\d+)(?:-([a-zA-Z0-9.]+))?(?:\+[a-zA-Z0-9.]+)?$/;
@@ -1805,11 +2006,11 @@ Examples:
1805
2006
  }
1806
2007
  const agentsToUpdate = [];
1807
2008
  if (options?.all) {
1808
- if (!existsSync19(AGENTS_DIR)) {
2009
+ if (!existsSync21(AGENTS_DIR)) {
1809
2010
  console.log(colors.dim("No agents installed."));
1810
2011
  return;
1811
2012
  }
1812
- const entries = readdirSync2(AGENTS_DIR, { withFileTypes: true }).filter((e) => e.isDirectory()).filter((e) => existsSync19(join16(AGENTS_DIR, e.name, "agent.yaml")));
2013
+ const entries = readdirSync2(AGENTS_DIR, { withFileTypes: true }).filter((e) => e.isDirectory()).filter((e) => existsSync21(join18(AGENTS_DIR, e.name, "agent.yaml")));
1813
2014
  agentsToUpdate.push(...entries.map((e) => e.name));
1814
2015
  } else if (agentName) {
1815
2016
  agentsToUpdate.push(agentName);
@@ -1820,8 +2021,8 @@ Examples:
1820
2021
  }
1821
2022
  let updatedCount = 0;
1822
2023
  for (const name of agentsToUpdate) {
1823
- const agentDir = join16(AGENTS_DIR, name);
1824
- if (!existsSync19(agentDir)) {
2024
+ const agentDir = join18(AGENTS_DIR, name);
2025
+ if (!existsSync21(agentDir)) {
1825
2026
  console.log(colors.warn(`Agent "${name}" is not installed. Skipping.`));
1826
2027
  continue;
1827
2028
  }
@@ -2181,11 +2382,289 @@ configCommand.command("reset").description("Reset configuration to defaults").ac
2181
2382
  }
2182
2383
  });
2183
2384
 
2385
+ // src/commands/schedule.ts
2386
+ import { Command as Command19 } from "commander";
2387
+ import { existsSync as existsSync23, readFileSync as readFileSync15 } from "fs";
2388
+ import { join as join20 } from "path";
2389
+ import { Cron as Cron2 } from "croner";
2390
+
2391
+ // src/scheduler/log-store.ts
2392
+ import { readFileSync as readFileSync14, writeFileSync as writeFileSync8, existsSync as existsSync22, mkdirSync as mkdirSync8, readdirSync as readdirSync3, unlinkSync as unlinkSync5 } from "fs";
2393
+ import { join as join19 } from "path";
2394
+ function getAgentLogDir(agentName, logsDir) {
2395
+ return join19(logsDir, agentName);
2396
+ }
2397
+ async function readLatestLog(agentName, logsDir) {
2398
+ const agentDir = getAgentLogDir(agentName, logsDir);
2399
+ if (!existsSync22(agentDir)) {
2400
+ return null;
2401
+ }
2402
+ const files = readdirSync3(agentDir).filter((f) => f.endsWith(".json")).sort();
2403
+ if (files.length === 0) {
2404
+ return null;
2405
+ }
2406
+ const latest = files[files.length - 1];
2407
+ const raw = readFileSync14(join19(agentDir, latest), "utf-8");
2408
+ return JSON.parse(raw);
2409
+ }
2410
+ async function readAllLogs(agentName, logsDir) {
2411
+ const agentDir = getAgentLogDir(agentName, logsDir);
2412
+ if (!existsSync22(agentDir)) {
2413
+ return [];
2414
+ }
2415
+ const files = readdirSync3(agentDir).filter((f) => f.endsWith(".json")).sort().reverse();
2416
+ return files.map((f) => JSON.parse(readFileSync14(join19(agentDir, f), "utf-8")));
2417
+ }
2418
+
2419
+ // src/commands/schedule.ts
2420
+ import { parse as parseYaml } from "yaml";
2421
+ var scheduleCommand = new Command19("schedule").description("Manage agent schedules").addHelpText("after", `
2422
+ Examples:
2423
+ $ agentx schedule start slack-agent
2424
+ $ agentx schedule stop slack-agent
2425
+ $ agentx schedule list
2426
+ $ agentx schedule logs slack-agent
2427
+ $ agentx schedule logs slack-agent --all
2428
+ $ agentx schedule resume`);
2429
+ scheduleCommand.command("start").description("Start an agent schedule").argument("<agent>", "Agent name to schedule").addHelpText("after", `
2430
+ Examples:
2431
+ $ agentx schedule start slack-agent`).action(async (agentName) => {
2432
+ try {
2433
+ const agentDir = join20(AGENTS_DIR, agentName);
2434
+ const manifestPath = join20(agentDir, "agent.yaml");
2435
+ if (!existsSync23(manifestPath)) {
2436
+ console.error(colors.error(`Error: Agent "${agentName}" is not installed.`));
2437
+ process.exit(1);
2438
+ }
2439
+ const raw = readFileSync15(manifestPath, "utf-8");
2440
+ const parsed = parseYaml(raw);
2441
+ const manifest = agentYamlSchema.parse(parsed);
2442
+ if (!manifest.schedule || manifest.schedule.length === 0) {
2443
+ console.error(colors.error(`Error: ${agentName} has no schedule block in agent.yaml`));
2444
+ process.exit(1);
2445
+ }
2446
+ if (manifest.secrets && manifest.secrets.length > 0) {
2447
+ const requiredSecrets = manifest.secrets.filter((s) => s.required);
2448
+ if (requiredSecrets.length > 0) {
2449
+ const configured = await hasSecrets(agentName);
2450
+ if (!configured) {
2451
+ const names = requiredSecrets.map((s) => s.name).join(", ");
2452
+ console.error(colors.error(`Error: Missing required secrets for ${agentName}: ${names}`));
2453
+ console.error(`Run: agentx configure ${agentName}`);
2454
+ process.exit(1);
2455
+ }
2456
+ }
2457
+ }
2458
+ let state = await loadScheduleState(SCHEDULER_STATE);
2459
+ state = addAgentToState(state, agentName, manifest.schedule);
2460
+ for (const sched of state.agents[agentName].schedules) {
2461
+ try {
2462
+ const cron = new Cron2(sched.cron);
2463
+ const next = cron.nextRun();
2464
+ if (next) {
2465
+ sched.nextRunAt = next.toISOString();
2466
+ }
2467
+ } catch {
2468
+ }
2469
+ }
2470
+ await saveScheduleState(state, SCHEDULER_STATE);
2471
+ if (isDaemonRunning(SCHEDULER_PID)) {
2472
+ signalDaemon("SIGHUP", SCHEDULER_PID);
2473
+ } else {
2474
+ startDaemon(SCHEDULER_PID, SCHEDULER_STATE, SCHEDULER_LOGS_DIR);
2475
+ }
2476
+ console.log(colors.success(`Schedule started for ${colors.bold(agentName)}`));
2477
+ for (const sched of state.agents[agentName].schedules) {
2478
+ const nextStr = sched.nextRunAt ? new Date(sched.nextRunAt).toLocaleString() : "unknown";
2479
+ console.log(` ${colors.cyan(sched.name)} ${colors.dim(sched.cron)} ${colors.dim(`(next: ${nextStr})`)}`);
2480
+ }
2481
+ } catch (error) {
2482
+ if (error instanceof Error) {
2483
+ console.error(colors.error(`Error: ${error.message}`));
2484
+ }
2485
+ process.exit(1);
2486
+ }
2487
+ });
2488
+ scheduleCommand.command("stop").description("Stop an agent schedule").argument("<agent>", "Agent name to stop").addHelpText("after", `
2489
+ Examples:
2490
+ $ agentx schedule stop slack-agent`).action(async (agentName) => {
2491
+ try {
2492
+ let state = await loadScheduleState(SCHEDULER_STATE);
2493
+ if (!state.agents[agentName]) {
2494
+ console.error(colors.error(`Error: ${agentName} has no active schedule`));
2495
+ console.error(`Run: agentx schedule list`);
2496
+ process.exit(1);
2497
+ }
2498
+ state = removeAgentFromState(state, agentName);
2499
+ await saveScheduleState(state, SCHEDULER_STATE);
2500
+ if (Object.keys(state.agents).length === 0) {
2501
+ stopDaemon(SCHEDULER_PID);
2502
+ console.log(colors.success(`Schedule stopped for ${colors.bold(agentName)}`));
2503
+ console.log(colors.dim("Scheduler daemon shut down (no active schedules)"));
2504
+ } else {
2505
+ signalDaemon("SIGHUP", SCHEDULER_PID);
2506
+ console.log(colors.success(`Schedule stopped for ${colors.bold(agentName)}`));
2507
+ }
2508
+ } catch (error) {
2509
+ if (error instanceof Error) {
2510
+ console.error(colors.error(`Error: ${error.message}`));
2511
+ }
2512
+ process.exit(1);
2513
+ }
2514
+ });
2515
+ scheduleCommand.command("list").description("List all active schedules").addHelpText("after", `
2516
+ Examples:
2517
+ $ agentx schedule list`).action(async () => {
2518
+ try {
2519
+ const state = await loadScheduleState(SCHEDULER_STATE);
2520
+ const agents = Object.values(state.agents);
2521
+ if (agents.length === 0) {
2522
+ console.log("No active schedules.");
2523
+ console.log(colors.dim("Start one with: agentx schedule start <agent-name>"));
2524
+ return;
2525
+ }
2526
+ const header = [
2527
+ "Agent".padEnd(20),
2528
+ "Schedule".padEnd(18),
2529
+ "Status".padEnd(10),
2530
+ "Last Run".padEnd(24),
2531
+ "Next Run"
2532
+ ].join("");
2533
+ console.log(colors.bold(header));
2534
+ for (const agent of agents) {
2535
+ for (const sched of agent.schedules) {
2536
+ const lastRun = sched.lastRunAt ? new Date(sched.lastRunAt).toLocaleString() : "-";
2537
+ const nextRun = sched.nextRunAt ? new Date(sched.nextRunAt).toLocaleString() : "-";
2538
+ const statusColor = sched.status === "errored" ? colors.error : sched.status === "running" ? colors.warn : colors.success;
2539
+ const row = [
2540
+ agent.agentName.padEnd(20),
2541
+ sched.cron.padEnd(18),
2542
+ statusColor(sched.status.padEnd(10)),
2543
+ lastRun.padEnd(24),
2544
+ nextRun
2545
+ ].join("");
2546
+ console.log(row);
2547
+ }
2548
+ }
2549
+ } catch (error) {
2550
+ if (error instanceof Error) {
2551
+ console.error(colors.error(`Error: ${error.message}`));
2552
+ }
2553
+ process.exit(1);
2554
+ }
2555
+ });
2556
+ scheduleCommand.command("logs").description("View execution logs for a scheduled agent").argument("<agent>", "Agent name").option("--all", "Show summary of all past runs").addHelpText("after", `
2557
+ Examples:
2558
+ $ agentx schedule logs slack-agent
2559
+ $ agentx schedule logs slack-agent --all`).action(async (agentName, options) => {
2560
+ try {
2561
+ if (options.all) {
2562
+ const logs = await readAllLogs(agentName, SCHEDULER_LOGS_DIR);
2563
+ if (logs.length === 0) {
2564
+ console.log(`No runs recorded for ${agentName}.`);
2565
+ return;
2566
+ }
2567
+ const header = [
2568
+ "Time".padEnd(24),
2569
+ "Schedule".padEnd(18),
2570
+ "Status".padEnd(10),
2571
+ "Duration"
2572
+ ].join("");
2573
+ console.log(colors.bold(header));
2574
+ for (const log2 of logs) {
2575
+ const time = new Date(log2.timestamp).toLocaleString();
2576
+ const statusColor = log2.status === "failure" ? colors.error : colors.success;
2577
+ const dur = `${(log2.duration / 1e3).toFixed(1)}s`;
2578
+ const row = [
2579
+ time.padEnd(24),
2580
+ log2.scheduleName.padEnd(18),
2581
+ statusColor(log2.status.padEnd(10)),
2582
+ dur
2583
+ ].join("");
2584
+ console.log(row);
2585
+ }
2586
+ } else {
2587
+ const log2 = await readLatestLog(agentName, SCHEDULER_LOGS_DIR);
2588
+ if (!log2) {
2589
+ console.log(`No runs recorded for ${agentName}.`);
2590
+ return;
2591
+ }
2592
+ const statusColor = log2.status === "failure" ? colors.error : colors.success;
2593
+ console.log(`Last run: ${new Date(log2.timestamp).toLocaleString()} (${log2.scheduleName})`);
2594
+ console.log(`Status: ${statusColor(log2.status)}`);
2595
+ console.log(`Duration: ${(log2.duration / 1e3).toFixed(1)}s`);
2596
+ console.log(`Prompt: ${log2.prompt}`);
2597
+ console.log("");
2598
+ if (log2.output) {
2599
+ console.log("Output:");
2600
+ console.log(` ${log2.output.split("\n").join("\n ")}`);
2601
+ }
2602
+ if (log2.status === "failure" && log2.error) {
2603
+ console.log("");
2604
+ console.log(colors.error(`Error: ${log2.error}`));
2605
+ }
2606
+ if (log2.stderr) {
2607
+ console.log(colors.dim(`Stderr: ${log2.stderr}`));
2608
+ }
2609
+ }
2610
+ } catch (error) {
2611
+ if (error instanceof Error) {
2612
+ console.error(colors.error(`Error: ${error.message}`));
2613
+ }
2614
+ process.exit(1);
2615
+ }
2616
+ });
2617
+ scheduleCommand.command("resume").description("Resume all previously active schedules").addHelpText("after", `
2618
+ Examples:
2619
+ $ agentx schedule resume`).action(async () => {
2620
+ try {
2621
+ const state = await loadScheduleState(SCHEDULER_STATE);
2622
+ const agents = Object.values(state.agents);
2623
+ if (agents.length === 0) {
2624
+ console.log("No schedules to resume.");
2625
+ console.log(colors.dim("Start one with: agentx schedule start <agent-name>"));
2626
+ return;
2627
+ }
2628
+ for (const agent of agents) {
2629
+ for (const sched of agent.schedules) {
2630
+ try {
2631
+ const cron = new Cron2(sched.cron);
2632
+ const next = cron.nextRun();
2633
+ if (next) {
2634
+ sched.nextRunAt = next.toISOString();
2635
+ }
2636
+ } catch {
2637
+ }
2638
+ if (sched.status === "errored") {
2639
+ sched.status = "active";
2640
+ }
2641
+ }
2642
+ }
2643
+ await saveScheduleState(state, SCHEDULER_STATE);
2644
+ if (isDaemonRunning(SCHEDULER_PID)) {
2645
+ signalDaemon("SIGHUP", SCHEDULER_PID);
2646
+ console.log(colors.success("Scheduler daemon reloaded."));
2647
+ } else {
2648
+ startDaemon(SCHEDULER_PID, SCHEDULER_STATE, SCHEDULER_LOGS_DIR);
2649
+ console.log(colors.success("Scheduler daemon started."));
2650
+ }
2651
+ console.log(`Resumed ${agents.length} agent(s):`);
2652
+ for (const agent of agents) {
2653
+ console.log(` ${colors.cyan(agent.agentName)} (${agent.schedules.length} schedule(s))`);
2654
+ }
2655
+ } catch (error) {
2656
+ if (error instanceof Error) {
2657
+ console.error(colors.error(`Error: ${error.message}`));
2658
+ }
2659
+ process.exit(1);
2660
+ }
2661
+ });
2662
+
2184
2663
  // src/index.ts
2185
- var __filename3 = fileURLToPath2(import.meta.url);
2186
- var __dirname3 = dirname2(__filename3);
2187
- var pkg = JSON.parse(readFileSync12(join17(__dirname3, "..", "package.json"), "utf-8"));
2188
- var program = new Command19();
2664
+ var __filename4 = fileURLToPath3(import.meta.url);
2665
+ var __dirname4 = dirname4(__filename4);
2666
+ var pkg = JSON.parse(readFileSync16(join21(__dirname4, "..", "package.json"), "utf-8"));
2667
+ var program = new Command20();
2189
2668
  program.name("agentx").description("The package manager for AI agents powered by Claude Code").version(pkg.version).option("--verbose", "Show verbose output").option("--debug", "Show debug output including stack traces").hook("preAction", (_thisCommand, actionCommand) => {
2190
2669
  const opts = program.opts();
2191
2670
  if (opts.verbose) {
@@ -2214,6 +2693,7 @@ program.addCommand(infoCommand);
2214
2693
  program.addCommand(searchCommand);
2215
2694
  program.addCommand(trendingCommand);
2216
2695
  program.addCommand(configCommand);
2696
+ program.addCommand(scheduleCommand);
2217
2697
  program.parse();
2218
2698
  export {
2219
2699
  program
@@ -0,0 +1,233 @@
1
+ // src/scheduler/daemon.ts
2
+ import { Cron } from "croner";
3
+
4
+ // src/scheduler/state.ts
5
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync } from "fs";
6
+ import { dirname, join } from "path";
7
+ var EMPTY_STATE = {
8
+ pid: null,
9
+ startedAt: null,
10
+ agents: {}
11
+ };
12
+ async function loadScheduleState(statePath2) {
13
+ if (!existsSync(statePath2)) {
14
+ return { ...EMPTY_STATE, agents: {} };
15
+ }
16
+ const raw = readFileSync(statePath2, "utf-8");
17
+ return JSON.parse(raw);
18
+ }
19
+ async function saveScheduleState(state, statePath2) {
20
+ const dir = dirname(statePath2);
21
+ mkdirSync(dir, { recursive: true });
22
+ const tmpPath = join(dir, `.state.${Date.now()}.tmp`);
23
+ writeFileSync(tmpPath, JSON.stringify(state, null, 2), { encoding: "utf-8", mode: 384 });
24
+ renameSync(tmpPath, statePath2);
25
+ }
26
+
27
+ // src/scheduler/log-store.ts
28
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, unlinkSync } from "fs";
29
+ import { join as join2 } from "path";
30
+ var MAX_LOG_FILES = 50;
31
+ function getAgentLogDir(agentName, logsDir2) {
32
+ return join2(logsDir2, agentName);
33
+ }
34
+ function timestampToFilename(timestamp) {
35
+ return timestamp.replace(/:/g, "-") + ".json";
36
+ }
37
+ async function writeRunLog(log, logsDir2) {
38
+ const agentDir = getAgentLogDir(log.agentName, logsDir2);
39
+ mkdirSync2(agentDir, { recursive: true });
40
+ const filename = timestampToFilename(log.timestamp);
41
+ const filePath = join2(agentDir, filename);
42
+ writeFileSync2(filePath, JSON.stringify(log, null, 2), "utf-8");
43
+ }
44
+ async function rotateLogs(agentName, logsDir2) {
45
+ const agentDir = getAgentLogDir(agentName, logsDir2);
46
+ if (!existsSync2(agentDir)) {
47
+ return;
48
+ }
49
+ const files = readdirSync(agentDir).filter((f) => f.endsWith(".json")).sort();
50
+ if (files.length <= MAX_LOG_FILES) {
51
+ return;
52
+ }
53
+ const toDelete = files.slice(0, files.length - MAX_LOG_FILES);
54
+ for (const f of toDelete) {
55
+ unlinkSync(join2(agentDir, f));
56
+ }
57
+ }
58
+
59
+ // src/scheduler/daemon.ts
60
+ import { writeFileSync as writeFileSync3, unlinkSync as unlinkSync2, existsSync as existsSync3 } from "fs";
61
+ import { execFile } from "child_process";
62
+ import { promisify } from "util";
63
+ var execFileAsync = promisify(execFile);
64
+ var statePath = process.env.AGENTX_SCHEDULER_STATE;
65
+ var pidPath = process.env.AGENTX_SCHEDULER_PID;
66
+ var logsDir = process.env.AGENTX_SCHEDULER_LOGS;
67
+ var activeJobs = /* @__PURE__ */ new Map();
68
+ var runningAgents = /* @__PURE__ */ new Set();
69
+ var RETRY_DELAYS = [1e4, 3e4];
70
+ async function executeAgent(agentName, schedule, retryAttempt = 0) {
71
+ const runKey = `${agentName}:${schedule.name}`;
72
+ if (runningAgents.has(runKey)) {
73
+ await writeRunLog({
74
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
75
+ agentName,
76
+ scheduleName: schedule.name,
77
+ cron: schedule.cron,
78
+ prompt: schedule.prompt,
79
+ output: "",
80
+ stderr: "",
81
+ status: "success",
82
+ duration: 0,
83
+ error: null,
84
+ retryAttempt,
85
+ skipped: true
86
+ }, logsDir);
87
+ return;
88
+ }
89
+ runningAgents.add(runKey);
90
+ const startTime = Date.now();
91
+ const state = await loadScheduleState(statePath);
92
+ const agentState = state.agents[agentName];
93
+ if (agentState) {
94
+ const schedState = agentState.schedules.find((s) => s.name === schedule.name);
95
+ if (schedState) {
96
+ schedState.status = "running";
97
+ await saveScheduleState(state, statePath);
98
+ }
99
+ }
100
+ try {
101
+ const { stdout, stderr } = await execFileAsync("agentx", ["run", agentName, schedule.prompt], {
102
+ timeout: 3e5,
103
+ // 5 minute timeout
104
+ env: { ...process.env }
105
+ });
106
+ const duration = Date.now() - startTime;
107
+ await writeRunLog({
108
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
109
+ agentName,
110
+ scheduleName: schedule.name,
111
+ cron: schedule.cron,
112
+ prompt: schedule.prompt,
113
+ output: stdout,
114
+ stderr: stderr || "",
115
+ status: "success",
116
+ duration,
117
+ error: null,
118
+ retryAttempt,
119
+ skipped: false
120
+ }, logsDir);
121
+ const updatedState = await loadScheduleState(statePath);
122
+ const updAgent = updatedState.agents[agentName];
123
+ if (updAgent) {
124
+ const sched = updAgent.schedules.find((s) => s.name === schedule.name);
125
+ if (sched) {
126
+ sched.status = "active";
127
+ sched.lastRunAt = (/* @__PURE__ */ new Date()).toISOString();
128
+ sched.lastRunStatus = "success";
129
+ sched.runCount += 1;
130
+ await saveScheduleState(updatedState, statePath);
131
+ }
132
+ }
133
+ await rotateLogs(agentName, logsDir);
134
+ } catch (error) {
135
+ const duration = Date.now() - startTime;
136
+ const errMsg = error instanceof Error ? error.message : String(error);
137
+ const stderr = error && typeof error === "object" && "stderr" in error ? String(error.stderr) : "";
138
+ await writeRunLog({
139
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
140
+ agentName,
141
+ scheduleName: schedule.name,
142
+ cron: schedule.cron,
143
+ prompt: schedule.prompt,
144
+ output: "",
145
+ stderr,
146
+ status: "failure",
147
+ duration,
148
+ error: errMsg,
149
+ retryAttempt,
150
+ skipped: false
151
+ }, logsDir);
152
+ if (retryAttempt < RETRY_DELAYS.length) {
153
+ runningAgents.delete(runKey);
154
+ const delay = RETRY_DELAYS[retryAttempt];
155
+ await new Promise((resolve) => setTimeout(resolve, delay));
156
+ return executeAgent(agentName, schedule, retryAttempt + 1);
157
+ }
158
+ const updatedState = await loadScheduleState(statePath);
159
+ const updAgent = updatedState.agents[agentName];
160
+ if (updAgent) {
161
+ const sched = updAgent.schedules.find((s) => s.name === schedule.name);
162
+ if (sched) {
163
+ sched.status = "errored";
164
+ sched.lastRunAt = (/* @__PURE__ */ new Date()).toISOString();
165
+ sched.lastRunStatus = "failure";
166
+ sched.runCount += 1;
167
+ sched.errorCount += 1;
168
+ await saveScheduleState(updatedState, statePath);
169
+ }
170
+ }
171
+ await rotateLogs(agentName, logsDir);
172
+ } finally {
173
+ runningAgents.delete(runKey);
174
+ }
175
+ }
176
+ function reconcileJobs(state) {
177
+ for (const [, jobs] of activeJobs) {
178
+ for (const job of jobs) {
179
+ job.stop();
180
+ }
181
+ }
182
+ activeJobs.clear();
183
+ for (const [agentName, agentState] of Object.entries(state.agents)) {
184
+ const jobs = [];
185
+ for (const schedule of agentState.schedules) {
186
+ const job = new Cron(schedule.cron, () => {
187
+ executeAgent(agentName, schedule).catch((err) => {
188
+ console.error(`[scheduler] Error executing ${agentName}/${schedule.name}:`, err);
189
+ });
190
+ });
191
+ const nextRun = job.nextRun();
192
+ if (nextRun) {
193
+ schedule.nextRunAt = nextRun.toISOString();
194
+ }
195
+ jobs.push(job);
196
+ }
197
+ activeJobs.set(agentName, jobs);
198
+ }
199
+ saveScheduleState(state, statePath).catch(() => {
200
+ });
201
+ }
202
+ async function startup() {
203
+ writeFileSync3(pidPath, String(process.pid), { encoding: "utf-8", mode: 384 });
204
+ const state = await loadScheduleState(statePath);
205
+ state.pid = process.pid;
206
+ state.startedAt = (/* @__PURE__ */ new Date()).toISOString();
207
+ await saveScheduleState(state, statePath);
208
+ reconcileJobs(state);
209
+ }
210
+ process.on("SIGHUP", async () => {
211
+ try {
212
+ const state = await loadScheduleState(statePath);
213
+ reconcileJobs(state);
214
+ } catch (err) {
215
+ console.error("[scheduler] Error handling SIGHUP:", err);
216
+ }
217
+ });
218
+ process.on("SIGTERM", () => {
219
+ for (const [, jobs] of activeJobs) {
220
+ for (const job of jobs) {
221
+ job.stop();
222
+ }
223
+ }
224
+ activeJobs.clear();
225
+ if (existsSync3(pidPath)) {
226
+ unlinkSync2(pidPath);
227
+ }
228
+ process.exit(0);
229
+ });
230
+ startup().catch((err) => {
231
+ console.error("[scheduler] Fatal startup error:", err);
232
+ process.exit(1);
233
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knid/agentx",
3
- "version": "0.1.9",
3
+ "version": "0.2.0",
4
4
  "description": "The package manager for AI agents powered by Claude Code",
5
5
  "type": "module",
6
6
  "bin": {
@@ -28,6 +28,7 @@
28
28
  "@clack/prompts": "^0.9.0",
29
29
  "chalk": "^5.4.0",
30
30
  "commander": "^12.0.0",
31
+ "croner": "^10.0.1",
31
32
  "execa": "^9.0.0",
32
33
  "marked": "^15.0.12",
33
34
  "marked-terminal": "^7.3.0",