@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 +510 -24
- package/dist/scheduler/daemon.js +233 -0
- package/package.json +2 -1
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
|
|
5
|
-
import { readFileSync as
|
|
6
|
-
import { fileURLToPath as
|
|
7
|
-
import { dirname as
|
|
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
|
|
1626
|
-
import { join as
|
|
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 =
|
|
1633
|
-
if (!
|
|
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
|
|
1657
|
-
import { join as
|
|
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 (!
|
|
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) =>
|
|
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 =
|
|
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
|
|
1756
|
-
import { join as
|
|
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 (!
|
|
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) =>
|
|
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 =
|
|
1818
|
-
if (!
|
|
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
|
|
2180
|
-
var
|
|
2181
|
-
var pkg = JSON.parse(
|
|
2182
|
-
var program = new
|
|
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.
|
|
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",
|