@knid/agentx 0.1.8 → 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) {
@@ -822,6 +847,10 @@ Examples:
822
847
  const populated = replaceTemplateVars(content, vars);
823
848
  writeFileSync4(join5(targetDir, file), populated, "utf-8");
824
849
  }
850
+ const claudeCommandsDir = join5(targetDir, ".claude", "commands");
851
+ mkdirSync4(claudeCommandsDir, { recursive: true });
852
+ const skillContent = loadTemplate(templatesDir, "create-agent.md");
853
+ writeFileSync4(join5(claudeCommandsDir, "create-agent.md"), skillContent, "utf-8");
825
854
  p2.outro(`Agent scaffolded at ${colors.cyan(targetDir)}`);
826
855
  console.log();
827
856
  console.log(` Next steps:`);
@@ -829,6 +858,8 @@ Examples:
829
858
  console.log(` ${colors.dim("2.")} Edit system-prompt.md with your agent's instructions`);
830
859
  console.log(` ${colors.dim("3.")} agentx validate`);
831
860
  console.log(` ${colors.dim("4.")} agentx run . "test prompt"`);
861
+ console.log();
862
+ console.log(` ${colors.dim("Tip:")} Use ${colors.cyan("/create-agent")} in Claude Code to generate a production-ready agent from a description.`);
832
863
  } catch (error) {
833
864
  if (error instanceof Error) {
834
865
  console.error(colors.error(`Error: ${error.message}`));
@@ -1622,20 +1653,196 @@ Examples:
1622
1653
 
1623
1654
  // src/commands/uninstall.ts
1624
1655
  import { Command as Command12 } from "commander";
1625
- import { existsSync as existsSync17, rmSync as rmSync2 } from "fs";
1626
- 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
1627
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", `
1628
1821
  Examples:
1629
1822
  $ agentx uninstall data-analyst
1630
1823
  $ agentx uninstall gmail-agent --keep-secrets`).action(async (agentName, options) => {
1631
1824
  try {
1632
- const agentDir = join14(AGENTS_DIR, agentName);
1633
- if (!existsSync17(agentDir)) {
1825
+ const agentDir = join16(AGENTS_DIR, agentName);
1826
+ if (!existsSync19(agentDir)) {
1634
1827
  console.error(
1635
1828
  colors.error(`Agent "${agentName}" is not installed.`)
1636
1829
  );
1637
1830
  process.exit(1);
1638
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
+ }
1639
1846
  rmSync2(agentDir, { recursive: true, force: true });
1640
1847
  if (!options.keepSecrets) {
1641
1848
  await deleteSecrets(agentName);
@@ -1653,8 +1860,8 @@ Examples:
1653
1860
 
1654
1861
  // src/commands/list.ts
1655
1862
  import { Command as Command13 } from "commander";
1656
- import { existsSync as existsSync18, readdirSync } from "fs";
1657
- import { join as join15 } from "path";
1863
+ import { existsSync as existsSync20, readdirSync } from "fs";
1864
+ import { join as join17 } from "path";
1658
1865
 
1659
1866
  // src/ui/table.ts
1660
1867
  function formatTable(columns, rows) {
@@ -1682,7 +1889,7 @@ Examples:
1682
1889
  $ agentx list
1683
1890
  $ agentx ls --json`).action((options) => {
1684
1891
  try {
1685
- if (!existsSync18(AGENTS_DIR)) {
1892
+ if (!existsSync20(AGENTS_DIR)) {
1686
1893
  if (options.json) {
1687
1894
  console.log(JSON.stringify([]));
1688
1895
  } else {
@@ -1693,7 +1900,7 @@ Examples:
1693
1900
  }
1694
1901
  return;
1695
1902
  }
1696
- 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")));
1697
1904
  if (entries.length === 0) {
1698
1905
  if (options.json) {
1699
1906
  console.log(JSON.stringify([]));
@@ -1706,7 +1913,7 @@ Examples:
1706
1913
  return;
1707
1914
  }
1708
1915
  const agentData = entries.map((entry) => {
1709
- const agentDir = join15(AGENTS_DIR, entry.name);
1916
+ const agentDir = join17(AGENTS_DIR, entry.name);
1710
1917
  try {
1711
1918
  const manifest = loadAgentYaml(agentDir);
1712
1919
  return {
@@ -1752,8 +1959,8 @@ Examples:
1752
1959
 
1753
1960
  // src/commands/update.ts
1754
1961
  import { Command as Command14 } from "commander";
1755
- import { existsSync as existsSync19, readdirSync as readdirSync2 } from "fs";
1756
- import { join as join16 } from "path";
1962
+ import { existsSync as existsSync21, readdirSync as readdirSync2 } from "fs";
1963
+ import { join as join18 } from "path";
1757
1964
 
1758
1965
  // src/utils/semver.ts
1759
1966
  var SEMVER_REGEX2 = /^(\d+)\.(\d+)\.(\d+)(?:-([a-zA-Z0-9.]+))?(?:\+[a-zA-Z0-9.]+)?$/;
@@ -1799,11 +2006,11 @@ Examples:
1799
2006
  }
1800
2007
  const agentsToUpdate = [];
1801
2008
  if (options?.all) {
1802
- if (!existsSync19(AGENTS_DIR)) {
2009
+ if (!existsSync21(AGENTS_DIR)) {
1803
2010
  console.log(colors.dim("No agents installed."));
1804
2011
  return;
1805
2012
  }
1806
- 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")));
1807
2014
  agentsToUpdate.push(...entries.map((e) => e.name));
1808
2015
  } else if (agentName) {
1809
2016
  agentsToUpdate.push(agentName);
@@ -1814,8 +2021,8 @@ Examples:
1814
2021
  }
1815
2022
  let updatedCount = 0;
1816
2023
  for (const name of agentsToUpdate) {
1817
- const agentDir = join16(AGENTS_DIR, name);
1818
- if (!existsSync19(agentDir)) {
2024
+ const agentDir = join18(AGENTS_DIR, name);
2025
+ if (!existsSync21(agentDir)) {
1819
2026
  console.log(colors.warn(`Agent "${name}" is not installed. Skipping.`));
1820
2027
  continue;
1821
2028
  }
@@ -2175,11 +2382,289 @@ configCommand.command("reset").description("Reset configuration to defaults").ac
2175
2382
  }
2176
2383
  });
2177
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
+
2178
2663
  // src/index.ts
2179
- var __filename3 = fileURLToPath2(import.meta.url);
2180
- var __dirname3 = dirname2(__filename3);
2181
- var pkg = JSON.parse(readFileSync12(join17(__dirname3, "..", "package.json"), "utf-8"));
2182
- 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();
2183
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) => {
2184
2669
  const opts = program.opts();
2185
2670
  if (opts.verbose) {
@@ -2208,6 +2693,7 @@ program.addCommand(infoCommand);
2208
2693
  program.addCommand(searchCommand);
2209
2694
  program.addCommand(trendingCommand);
2210
2695
  program.addCommand(configCommand);
2696
+ program.addCommand(scheduleCommand);
2211
2697
  program.parse();
2212
2698
  export {
2213
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.8",
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",