@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 +504 -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) {
|
|
@@ -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
|
|
1632
|
-
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
|
|
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 =
|
|
1639
|
-
if (!
|
|
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
|
|
1663
|
-
import { join as
|
|
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 (!
|
|
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) =>
|
|
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 =
|
|
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
|
|
1762
|
-
import { join as
|
|
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 (!
|
|
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) =>
|
|
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 =
|
|
1824
|
-
if (!
|
|
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
|
|
2186
|
-
var
|
|
2187
|
-
var pkg = JSON.parse(
|
|
2188
|
-
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();
|
|
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.
|
|
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",
|