copilot-agent 0.11.0 → 1.0.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/README.md +91 -3
- package/dist/index.js +1002 -117
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -639,7 +639,7 @@ async function runCopilot(args, options) {
|
|
|
639
639
|
if (!options?.useWorktree) {
|
|
640
640
|
await waitForAgentInDir(dir, "copilot");
|
|
641
641
|
}
|
|
642
|
-
return new Promise((
|
|
642
|
+
return new Promise((resolve8) => {
|
|
643
643
|
const child = spawn("copilot", args, {
|
|
644
644
|
cwd: options?.cwd,
|
|
645
645
|
stdio: "inherit",
|
|
@@ -649,10 +649,10 @@ async function runCopilot(args, options) {
|
|
|
649
649
|
await sleep(3e3);
|
|
650
650
|
const sid = getLatestSessionId();
|
|
651
651
|
const premium = sid ? getSessionPremium(sid) : 0;
|
|
652
|
-
|
|
652
|
+
resolve8({ exitCode: code ?? 1, sessionId: sid, premium });
|
|
653
653
|
});
|
|
654
654
|
child.on("error", () => {
|
|
655
|
-
|
|
655
|
+
resolve8({ exitCode: 1, sessionId: null, premium: 0 });
|
|
656
656
|
});
|
|
657
657
|
});
|
|
658
658
|
}
|
|
@@ -685,7 +685,7 @@ async function runClaude(args, options) {
|
|
|
685
685
|
if (!options?.useWorktree) {
|
|
686
686
|
await waitForAgentInDir(dir, "claude");
|
|
687
687
|
}
|
|
688
|
-
return new Promise((
|
|
688
|
+
return new Promise((resolve8) => {
|
|
689
689
|
const child = spawn("claude", args, {
|
|
690
690
|
cwd: options?.cwd,
|
|
691
691
|
stdio: "inherit",
|
|
@@ -694,10 +694,10 @@ async function runClaude(args, options) {
|
|
|
694
694
|
child.on("close", async (code) => {
|
|
695
695
|
await sleep(2e3);
|
|
696
696
|
const sid = getLatestClaudeSessionId(options?.cwd);
|
|
697
|
-
|
|
697
|
+
resolve8({ exitCode: code ?? 1, sessionId: sid, premium: 0 });
|
|
698
698
|
});
|
|
699
699
|
child.on("error", () => {
|
|
700
|
-
|
|
700
|
+
resolve8({ exitCode: 1, sessionId: null, premium: 0 });
|
|
701
701
|
});
|
|
702
702
|
});
|
|
703
703
|
}
|
|
@@ -1232,9 +1232,9 @@ async function runCommand(dir, opts) {
|
|
|
1232
1232
|
${"\u2550".repeat(60)}`);
|
|
1233
1233
|
log(`${BOLD}${CYAN}Task: ${task.title}${RESET}`);
|
|
1234
1234
|
log(`${"\u2550".repeat(60)}`);
|
|
1235
|
-
const
|
|
1235
|
+
const timestamp2 = Date.now().toString(36);
|
|
1236
1236
|
const random = Math.random().toString(36).substring(2, 6);
|
|
1237
|
-
const branchName = `agent/fix-${completed + 1}-${
|
|
1237
|
+
const branchName = `agent/fix-${completed + 1}-${timestamp2}-${random}`;
|
|
1238
1238
|
if (mainBranch && isGitRepo(dir)) {
|
|
1239
1239
|
if (gitStatus(dir)) gitStash(dir);
|
|
1240
1240
|
gitCheckout(dir, mainBranch);
|
|
@@ -1367,9 +1367,9 @@ ${"\u2550".repeat(60)}`);
|
|
|
1367
1367
|
log(`${BOLD}${CYAN}[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Task ${taskIdx}: ${task.title}${RESET}`);
|
|
1368
1368
|
log(`${DIM}Premium: ${totalPremium}/${opts.maxPremium}${RESET}`);
|
|
1369
1369
|
log(`${"\u2550".repeat(60)}`);
|
|
1370
|
-
const
|
|
1370
|
+
const timestamp2 = Date.now().toString(36);
|
|
1371
1371
|
const random = Math.random().toString(36).substring(2, 6);
|
|
1372
|
-
const branchName = `agent/overnight-${taskIdx}-${
|
|
1372
|
+
const branchName = `agent/overnight-${taskIdx}-${timestamp2}-${random}`;
|
|
1373
1373
|
if (mainBranch && isGitRepo(dir)) {
|
|
1374
1374
|
gitStash(dir);
|
|
1375
1375
|
gitCheckout(dir, mainBranch);
|
|
@@ -1492,8 +1492,8 @@ async function researchCommand(dir, opts) {
|
|
|
1492
1492
|
ok("RESEARCH-PROPOSALS.md generated.");
|
|
1493
1493
|
const backupDir = join6(homedir4(), ".copilot", "research-reports");
|
|
1494
1494
|
mkdirSync3(backupDir, { recursive: true });
|
|
1495
|
-
const
|
|
1496
|
-
const backupFile = join6(backupDir, `${projectName}-${
|
|
1495
|
+
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
1496
|
+
const backupFile = join6(backupDir, `${projectName}-${timestamp2}.md`);
|
|
1497
1497
|
copyFileSync(proposalsFile, backupFile);
|
|
1498
1498
|
ok(`Backup saved: ${backupFile}`);
|
|
1499
1499
|
} else {
|
|
@@ -1779,21 +1779,63 @@ async function loadBlessed() {
|
|
|
1779
1779
|
blessed = mod.default || mod;
|
|
1780
1780
|
}
|
|
1781
1781
|
function registerDashboardCommand(program2) {
|
|
1782
|
-
program2.command("dashboard").alias("tui").description("Real-time terminal dashboard for copilot sessions (htop-style)").option("-r, --refresh <n>", "Refresh interval in seconds", "5").option("-l, --limit <n>", "Number of sessions to show", "20").
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
} else {
|
|
1786
|
-
await loadBlessed();
|
|
1787
|
-
runBlessedDashboard(parseInt(opts.refresh, 10), parseInt(opts.limit, 10));
|
|
1788
|
-
}
|
|
1782
|
+
program2.command("dashboard").alias("tui").description("Real-time terminal dashboard for copilot sessions (htop-style)").option("-r, --refresh <n>", "Refresh interval in seconds", "5").option("-l, --limit <n>", "Number of sessions to show", "20").action(async (opts) => {
|
|
1783
|
+
await loadBlessed();
|
|
1784
|
+
runBlessedDashboard(parseInt(opts.refresh, 10), parseInt(opts.limit, 10));
|
|
1789
1785
|
});
|
|
1790
1786
|
}
|
|
1787
|
+
function createCache() {
|
|
1788
|
+
return { sessions: [], sessionsTs: 0, procs: [], procsTs: 0, details: /* @__PURE__ */ new Map() };
|
|
1789
|
+
}
|
|
1790
|
+
function cacheSessions(cache, limit) {
|
|
1791
|
+
const now = Date.now();
|
|
1792
|
+
if (now - cache.sessionsTs > 2e3) {
|
|
1793
|
+
try {
|
|
1794
|
+
cache.sessions = listAllSessions(limit);
|
|
1795
|
+
} catch {
|
|
1796
|
+
}
|
|
1797
|
+
cache.sessionsTs = now;
|
|
1798
|
+
}
|
|
1799
|
+
return cache.sessions;
|
|
1800
|
+
}
|
|
1801
|
+
function cacheProcs(cache) {
|
|
1802
|
+
const now = Date.now();
|
|
1803
|
+
if (now - cache.procsTs > 3e3) {
|
|
1804
|
+
try {
|
|
1805
|
+
cache.procs = findAgentProcesses();
|
|
1806
|
+
} catch {
|
|
1807
|
+
}
|
|
1808
|
+
cache.procsTs = now;
|
|
1809
|
+
}
|
|
1810
|
+
return cache.procs;
|
|
1811
|
+
}
|
|
1812
|
+
function cacheDetail(cache, s) {
|
|
1813
|
+
const entry = cache.details.get(s.id);
|
|
1814
|
+
if (entry && Date.now() - entry.ts < 1e4) return entry.report;
|
|
1815
|
+
let report = null;
|
|
1816
|
+
try {
|
|
1817
|
+
report = getAgentSessionReport(s.id, s.agent);
|
|
1818
|
+
} catch {
|
|
1819
|
+
}
|
|
1820
|
+
cache.details.set(s.id, { report, ts: Date.now() });
|
|
1821
|
+
if (cache.details.size > 15) {
|
|
1822
|
+
const oldest = [...cache.details.entries()].sort((a, b) => a[1].ts - b[1].ts)[0];
|
|
1823
|
+
if (oldest) cache.details.delete(oldest[0]);
|
|
1824
|
+
}
|
|
1825
|
+
return report;
|
|
1826
|
+
}
|
|
1791
1827
|
function runBlessedDashboard(refreshSec, limit) {
|
|
1828
|
+
const cache = createCache();
|
|
1829
|
+
const origTerm = process.env.TERM;
|
|
1830
|
+
if (origTerm?.includes("256color")) {
|
|
1831
|
+
process.env.TERM = "xterm";
|
|
1832
|
+
}
|
|
1792
1833
|
const screen = blessed.screen({
|
|
1793
1834
|
smartCSR: true,
|
|
1794
1835
|
title: "copilot-agent dashboard",
|
|
1795
1836
|
fullUnicode: true
|
|
1796
1837
|
});
|
|
1838
|
+
if (origTerm) process.env.TERM = origTerm;
|
|
1797
1839
|
const BG = "#0d1117";
|
|
1798
1840
|
const FG = "#e6edf3";
|
|
1799
1841
|
const BORDER_COLOR = "#30363d";
|
|
@@ -1878,13 +1920,7 @@ function runBlessedDashboard(refreshSec, limit) {
|
|
|
1878
1920
|
let procs = [];
|
|
1879
1921
|
let selectedIdx = 0;
|
|
1880
1922
|
let focusedPanel = "sessions";
|
|
1881
|
-
|
|
1882
|
-
try {
|
|
1883
|
-
procs = findAgentProcesses();
|
|
1884
|
-
sessions = listAllSessions(limit);
|
|
1885
|
-
} catch {
|
|
1886
|
-
}
|
|
1887
|
-
}
|
|
1923
|
+
let lastDetailId = "";
|
|
1888
1924
|
function renderHeader() {
|
|
1889
1925
|
const time = (/* @__PURE__ */ new Date()).toLocaleTimeString("en-GB");
|
|
1890
1926
|
const totalPremium = sessions.reduce((s, x) => s + x.premiumRequests, 0);
|
|
@@ -1910,37 +1946,44 @@ ${rows.join("\n")}`);
|
|
|
1910
1946
|
}
|
|
1911
1947
|
sessionList.setLabel(` {cyan-fg}{bold}Sessions (${sessions.length}){/} `);
|
|
1912
1948
|
}
|
|
1913
|
-
function renderDetail2() {
|
|
1949
|
+
function renderDetail2(force = false) {
|
|
1914
1950
|
if (sessions.length === 0 || selectedIdx < 0 || selectedIdx >= sessions.length) {
|
|
1915
1951
|
detailBox.setContent("{gray-fg}No session selected{/}");
|
|
1952
|
+
lastDetailId = "";
|
|
1916
1953
|
return;
|
|
1917
1954
|
}
|
|
1918
1955
|
const s = sessions[selectedIdx];
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
}
|
|
1925
|
-
|
|
1926
|
-
}
|
|
1927
|
-
} catch {
|
|
1928
|
-
detailBox.setContent("{red-fg}Error loading session detail{/}");
|
|
1956
|
+
if (!force && s.id === lastDetailId) return;
|
|
1957
|
+
lastDetailId = s.id;
|
|
1958
|
+
const report = cacheDetail(cache, s);
|
|
1959
|
+
if (report) {
|
|
1960
|
+
detailBox.setContent(detailContent(report));
|
|
1961
|
+
detailBox.setLabel(` {cyan-fg}{bold}Detail \u2014 ${s.id.slice(0, 12)}\u2026{/} `);
|
|
1962
|
+
} else {
|
|
1963
|
+
detailBox.setContent(`{gray-fg}Could not load report for ${s.id.slice(0, 8)}\u2026{/}`);
|
|
1929
1964
|
}
|
|
1930
1965
|
}
|
|
1931
1966
|
function render() {
|
|
1932
|
-
|
|
1967
|
+
sessions = cacheSessions(cache, limit);
|
|
1968
|
+
procs = cacheProcs(cache);
|
|
1933
1969
|
renderHeader();
|
|
1934
1970
|
renderProcesses2();
|
|
1935
1971
|
renderSessions();
|
|
1936
1972
|
renderDetail2();
|
|
1937
1973
|
screen.render();
|
|
1938
1974
|
}
|
|
1975
|
+
function forceRefresh() {
|
|
1976
|
+
cache.sessionsTs = 0;
|
|
1977
|
+
cache.procsTs = 0;
|
|
1978
|
+
cache.details.clear();
|
|
1979
|
+
lastDetailId = "";
|
|
1980
|
+
render();
|
|
1981
|
+
}
|
|
1939
1982
|
screen.key(["q", "C-c"], () => {
|
|
1940
1983
|
screen.destroy();
|
|
1941
1984
|
process.exit(0);
|
|
1942
1985
|
});
|
|
1943
|
-
screen.key(["r"], () =>
|
|
1986
|
+
screen.key(["r"], () => forceRefresh());
|
|
1944
1987
|
screen.key(["tab"], () => {
|
|
1945
1988
|
if (focusedPanel === "sessions") {
|
|
1946
1989
|
focusedPanel = "detail";
|
|
@@ -1977,7 +2020,7 @@ ${rows.join("\n")}`);
|
|
|
1977
2020
|
}
|
|
1978
2021
|
});
|
|
1979
2022
|
sessionList.key(["enter"], () => {
|
|
1980
|
-
renderDetail2();
|
|
2023
|
+
renderDetail2(true);
|
|
1981
2024
|
focusedPanel = "detail";
|
|
1982
2025
|
detailBox.focus();
|
|
1983
2026
|
sessionList.style.border.fg = BORDER_COLOR;
|
|
@@ -1990,82 +2033,6 @@ ${rows.join("\n")}`);
|
|
|
1990
2033
|
const timer = setInterval(render, refreshSec * 1e3);
|
|
1991
2034
|
screen.on("destroy", () => clearInterval(timer));
|
|
1992
2035
|
}
|
|
1993
|
-
var ESC = "\x1B";
|
|
1994
|
-
var CLEAR = `${ESC}[2J${ESC}[H`;
|
|
1995
|
-
var HIDE_CURSOR = `${ESC}[?25l`;
|
|
1996
|
-
var SHOW_CURSOR = `${ESC}[?25h`;
|
|
1997
|
-
function runSimpleDashboard(refreshSec, limit) {
|
|
1998
|
-
process.stdout.write(HIDE_CURSOR);
|
|
1999
|
-
const cleanup = () => {
|
|
2000
|
-
process.stdout.write(SHOW_CURSOR);
|
|
2001
|
-
process.stdout.write(CLEAR);
|
|
2002
|
-
process.exit(0);
|
|
2003
|
-
};
|
|
2004
|
-
process.on("SIGINT", cleanup);
|
|
2005
|
-
process.on("SIGTERM", cleanup);
|
|
2006
|
-
const render = () => {
|
|
2007
|
-
try {
|
|
2008
|
-
const output = buildSimpleScreen(limit);
|
|
2009
|
-
process.stdout.write(CLEAR + output);
|
|
2010
|
-
} catch {
|
|
2011
|
-
}
|
|
2012
|
-
};
|
|
2013
|
-
render();
|
|
2014
|
-
setInterval(render, refreshSec * 1e3);
|
|
2015
|
-
process.stdout.on("resize", render);
|
|
2016
|
-
process.stdin.setRawMode?.(true);
|
|
2017
|
-
process.stdin.resume();
|
|
2018
|
-
process.stdin.on("data", (data) => {
|
|
2019
|
-
const key = data.toString();
|
|
2020
|
-
if (key === "q" || key === "") cleanup();
|
|
2021
|
-
if (key === "r") render();
|
|
2022
|
-
});
|
|
2023
|
-
}
|
|
2024
|
-
function buildSimpleScreen(limit) {
|
|
2025
|
-
const cols = process.stdout.columns || 80;
|
|
2026
|
-
const lines = [];
|
|
2027
|
-
const now = /* @__PURE__ */ new Date();
|
|
2028
|
-
const timeStr = now.toLocaleTimeString("en-GB");
|
|
2029
|
-
lines.push("");
|
|
2030
|
-
lines.push(` ${BOLD}${CYAN}\u250C${"\u2500".repeat(cols - 6)}\u2510${RESET}`);
|
|
2031
|
-
lines.push(` ${BOLD}${CYAN}\u2502${RESET} \u26A1 ${BOLD}Copilot Agent Dashboard${RESET}${" ".repeat(Math.max(0, cols - 37 - timeStr.length))}${DIM}${timeStr}${RESET} ${BOLD}${CYAN}\u2502${RESET}`);
|
|
2032
|
-
lines.push(` ${BOLD}${CYAN}\u2514${"\u2500".repeat(cols - 6)}\u2518${RESET}`);
|
|
2033
|
-
lines.push("");
|
|
2034
|
-
const procs = findAgentProcesses();
|
|
2035
|
-
lines.push(` ${BOLD}${GREEN}\u25CF Active Processes (${procs.length})${RESET}`);
|
|
2036
|
-
lines.push(` ${"\u2500".repeat(Math.min(cols - 4, 70))}`);
|
|
2037
|
-
if (procs.length === 0) {
|
|
2038
|
-
lines.push(` ${DIM}No agent processes running${RESET}`);
|
|
2039
|
-
} else {
|
|
2040
|
-
for (const p of procs) {
|
|
2041
|
-
const agentTag = p.agent === "claude" ? `${YELLOW}[claude]${RESET}` : `${CYAN}[copilot]${RESET}`;
|
|
2042
|
-
const sid = p.sessionId ? p.sessionId.slice(0, 8) + "\u2026" : "\u2014";
|
|
2043
|
-
const cwdShort = p.cwd ? "~/" + p.cwd.split("/").slice(-2).join("/") : "\u2014";
|
|
2044
|
-
lines.push(` ${GREEN}\u2B24${RESET} ${agentTag} PID ${BOLD}${p.pid}${RESET} ${CYAN}${sid}${RESET} ${DIM}${cwdShort}${RESET}`);
|
|
2045
|
-
}
|
|
2046
|
-
}
|
|
2047
|
-
lines.push("");
|
|
2048
|
-
const sessions = listAllSessions(limit);
|
|
2049
|
-
lines.push(` ${BOLD}${CYAN}\u25CF Recent Sessions (${sessions.length})${RESET}`);
|
|
2050
|
-
lines.push(` ${"\u2500".repeat(Math.min(cols - 4, 70))}`);
|
|
2051
|
-
const hdr = [pad("Status", 10), pad("Agent", 8), pad("Premium", 8), pad("Project", 18), pad("Last Activity", 14)];
|
|
2052
|
-
lines.push(` ${DIM}${hdr.join(" ")}${RESET}`);
|
|
2053
|
-
for (const s of sessions) {
|
|
2054
|
-
const icon = s.complete ? `${GREEN}\u2714 done ${RESET}` : `${YELLOW}\u23F8 stop ${RESET}`;
|
|
2055
|
-
const agent = s.agent === "claude" ? `${YELLOW}claude ${RESET}` : `${CYAN}copilot${RESET}`;
|
|
2056
|
-
const prem = pad(String(s.premiumRequests), 8);
|
|
2057
|
-
const proj = pad(s.cwd.split("/").pop() ?? "\u2014", 18);
|
|
2058
|
-
const ago = pad(fmtTimeAgo(s.mtime), 14);
|
|
2059
|
-
lines.push(` ${icon}${agent} ${prem}${proj}${ago}`);
|
|
2060
|
-
}
|
|
2061
|
-
lines.push("");
|
|
2062
|
-
lines.push(` ${DIM}Press ${BOLD}q${RESET}${DIM} to quit, ${BOLD}r${RESET}${DIM} to refresh${RESET}`);
|
|
2063
|
-
return lines.join("\n");
|
|
2064
|
-
}
|
|
2065
|
-
function pad(s, n) {
|
|
2066
|
-
if (s.length >= n) return s.slice(0, n);
|
|
2067
|
-
return s + " ".repeat(n - s.length);
|
|
2068
|
-
}
|
|
2069
2036
|
|
|
2070
2037
|
// src/commands/web.ts
|
|
2071
2038
|
import { Hono } from "hono";
|
|
@@ -3408,9 +3375,922 @@ function formatDur(ms) {
|
|
|
3408
3375
|
return `${h}h ${m}m`;
|
|
3409
3376
|
}
|
|
3410
3377
|
|
|
3378
|
+
// src/commands/template.ts
|
|
3379
|
+
import chalk8 from "chalk";
|
|
3380
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync10, mkdirSync as mkdirSync7 } from "fs";
|
|
3381
|
+
import { join as join12 } from "path";
|
|
3382
|
+
import { homedir as homedir10 } from "os";
|
|
3383
|
+
import { parse as parseYaml3, stringify as stringifyYaml2 } from "yaml";
|
|
3384
|
+
var CONFIG_DIR2 = join12(homedir10(), ".copilot-agent");
|
|
3385
|
+
var TEMPLATES_FILE = join12(CONFIG_DIR2, "templates.yaml");
|
|
3386
|
+
function ensureDir3() {
|
|
3387
|
+
if (!existsSync10(CONFIG_DIR2)) mkdirSync7(CONFIG_DIR2, { recursive: true });
|
|
3388
|
+
}
|
|
3389
|
+
function loadTemplates() {
|
|
3390
|
+
if (!existsSync10(TEMPLATES_FILE)) return [];
|
|
3391
|
+
try {
|
|
3392
|
+
const data = parseYaml3(readFileSync7(TEMPLATES_FILE, "utf-8"));
|
|
3393
|
+
return Array.isArray(data) ? data : [];
|
|
3394
|
+
} catch {
|
|
3395
|
+
return [];
|
|
3396
|
+
}
|
|
3397
|
+
}
|
|
3398
|
+
function saveTemplates(templates) {
|
|
3399
|
+
ensureDir3();
|
|
3400
|
+
writeFileSync5(TEMPLATES_FILE, stringifyYaml2(templates), "utf-8");
|
|
3401
|
+
}
|
|
3402
|
+
function registerTemplateCommand(program2) {
|
|
3403
|
+
const cmd = program2.command("template").description("Manage custom task templates");
|
|
3404
|
+
cmd.command("list").description("Show all custom templates").action(() => {
|
|
3405
|
+
const templates = loadTemplates();
|
|
3406
|
+
console.log(chalk8.bold.cyan("\n \u{1F4CB} Task Templates") + chalk8.dim(` (${templates.length})
|
|
3407
|
+
`));
|
|
3408
|
+
if (templates.length === 0) {
|
|
3409
|
+
console.log(chalk8.dim(" No custom templates"));
|
|
3410
|
+
console.log(chalk8.dim('\n Add one: copilot-agent template add <name> --prompt "..."'));
|
|
3411
|
+
console.log();
|
|
3412
|
+
return;
|
|
3413
|
+
}
|
|
3414
|
+
for (const t of templates) {
|
|
3415
|
+
const prio = t.priority !== void 0 ? chalk8.dim(` [priority: ${t.priority}]`) : "";
|
|
3416
|
+
console.log(` ${chalk8.green("\u25CF")} ${chalk8.bold(t.name)}${prio}`);
|
|
3417
|
+
console.log(chalk8.dim(` ${t.prompt.slice(0, 80)}${t.prompt.length > 80 ? "\u2026" : ""}`));
|
|
3418
|
+
}
|
|
3419
|
+
console.log();
|
|
3420
|
+
});
|
|
3421
|
+
cmd.command("add <name>").description("Add a custom template").requiredOption("-p, --prompt <text>", "Task prompt").option("--priority <n>", "Priority (lower = higher priority)").action((name, opts) => {
|
|
3422
|
+
const templates = loadTemplates();
|
|
3423
|
+
const existing = templates.findIndex((t) => t.name === name);
|
|
3424
|
+
const entry = {
|
|
3425
|
+
name,
|
|
3426
|
+
prompt: opts.prompt,
|
|
3427
|
+
priority: opts.priority ? parseInt(opts.priority, 10) : void 0
|
|
3428
|
+
};
|
|
3429
|
+
if (existing >= 0) {
|
|
3430
|
+
templates[existing] = entry;
|
|
3431
|
+
console.log(chalk8.yellow(` \u2714 Updated template: ${chalk8.bold(name)}`));
|
|
3432
|
+
} else {
|
|
3433
|
+
templates.push(entry);
|
|
3434
|
+
console.log(chalk8.green(` \u2714 Added template: ${chalk8.bold(name)}`));
|
|
3435
|
+
}
|
|
3436
|
+
saveTemplates(templates);
|
|
3437
|
+
});
|
|
3438
|
+
cmd.command("remove <name>").description("Remove a custom template").action((name) => {
|
|
3439
|
+
const templates = loadTemplates();
|
|
3440
|
+
const idx = templates.findIndex((t) => t.name === name);
|
|
3441
|
+
if (idx < 0) {
|
|
3442
|
+
console.log(chalk8.red(` \u2717 Template not found: ${name}`));
|
|
3443
|
+
return;
|
|
3444
|
+
}
|
|
3445
|
+
templates.splice(idx, 1);
|
|
3446
|
+
saveTemplates(templates);
|
|
3447
|
+
console.log(chalk8.green(` \u2714 Removed template: ${chalk8.bold(name)}`));
|
|
3448
|
+
});
|
|
3449
|
+
cmd.command("export").description("Export templates to stdout (YAML)").action(() => {
|
|
3450
|
+
const templates = loadTemplates();
|
|
3451
|
+
if (templates.length === 0) {
|
|
3452
|
+
console.log(chalk8.dim(" No templates to export"));
|
|
3453
|
+
return;
|
|
3454
|
+
}
|
|
3455
|
+
console.log(stringifyYaml2(templates));
|
|
3456
|
+
});
|
|
3457
|
+
cmd.command("import <file>").description("Import templates from a YAML file").action((file) => {
|
|
3458
|
+
try {
|
|
3459
|
+
const data = parseYaml3(readFileSync7(file, "utf-8"));
|
|
3460
|
+
if (!Array.isArray(data)) {
|
|
3461
|
+
console.log(chalk8.red(" \u2717 Invalid format: expected YAML array"));
|
|
3462
|
+
return;
|
|
3463
|
+
}
|
|
3464
|
+
const current = loadTemplates();
|
|
3465
|
+
let added = 0;
|
|
3466
|
+
for (const entry of data) {
|
|
3467
|
+
if (!entry.name || !entry.prompt) continue;
|
|
3468
|
+
const idx = current.findIndex((t) => t.name === entry.name);
|
|
3469
|
+
if (idx >= 0) {
|
|
3470
|
+
current[idx] = entry;
|
|
3471
|
+
} else {
|
|
3472
|
+
current.push(entry);
|
|
3473
|
+
added++;
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
3476
|
+
saveTemplates(current);
|
|
3477
|
+
console.log(chalk8.green(` \u2714 Imported ${added} new templates (${data.length} total processed)`));
|
|
3478
|
+
} catch (err) {
|
|
3479
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3480
|
+
console.log(chalk8.red(` \u2717 ${msg}`));
|
|
3481
|
+
}
|
|
3482
|
+
});
|
|
3483
|
+
}
|
|
3484
|
+
|
|
3485
|
+
// src/commands/log.ts
|
|
3486
|
+
import chalk9 from "chalk";
|
|
3487
|
+
import { writeFileSync as writeFileSync6 } from "fs";
|
|
3488
|
+
function registerLogCommand(program2) {
|
|
3489
|
+
const cmd = program2.command("log").description("Search, browse, and export session history");
|
|
3490
|
+
cmd.command("search <query>").description("Search across all sessions").option("-a, --agent <type>", "Filter: copilot | claude").option("-l, --limit <n>", "Max results", "20").option("--after <date>", "After date (YYYY-MM-DD)").option("--before <date>", "Before date (YYYY-MM-DD)").action((query, opts) => {
|
|
3491
|
+
const limit = parseInt(opts.limit, 10);
|
|
3492
|
+
const sessions = listAllSessions(200);
|
|
3493
|
+
const q = query.toLowerCase();
|
|
3494
|
+
let filtered = sessions;
|
|
3495
|
+
if (opts.agent) filtered = filtered.filter((s) => s.agent === opts.agent);
|
|
3496
|
+
if (opts.after) {
|
|
3497
|
+
const after = new Date(opts.after).getTime();
|
|
3498
|
+
filtered = filtered.filter((s) => s.mtime >= after);
|
|
3499
|
+
}
|
|
3500
|
+
if (opts.before) {
|
|
3501
|
+
const before = new Date(opts.before).getTime();
|
|
3502
|
+
filtered = filtered.filter((s) => s.mtime <= before);
|
|
3503
|
+
}
|
|
3504
|
+
const matches = filtered.filter((s) => {
|
|
3505
|
+
if (s.summary?.toLowerCase().includes(q)) return true;
|
|
3506
|
+
if (s.cwd?.toLowerCase().includes(q)) return true;
|
|
3507
|
+
if (s.lastEvent?.toLowerCase().includes(q)) return true;
|
|
3508
|
+
const report = getAgentSessionReport(s.id, s.agent);
|
|
3509
|
+
if (!report) return false;
|
|
3510
|
+
if (report.gitCommits.some((c) => c.toLowerCase().includes(q))) return true;
|
|
3511
|
+
if (report.filesCreated.some((f) => f.toLowerCase().includes(q))) return true;
|
|
3512
|
+
if (report.filesEdited.some((f) => f.toLowerCase().includes(q))) return true;
|
|
3513
|
+
if (report.taskCompletions.some((t) => t.toLowerCase().includes(q))) return true;
|
|
3514
|
+
return false;
|
|
3515
|
+
}).slice(0, limit);
|
|
3516
|
+
console.log(chalk9.bold.cyan(`
|
|
3517
|
+
\u{1F50D} Search: "${query}"`) + chalk9.dim(` (${matches.length} results)
|
|
3518
|
+
`));
|
|
3519
|
+
if (matches.length === 0) {
|
|
3520
|
+
console.log(chalk9.dim(" No matches found"));
|
|
3521
|
+
return;
|
|
3522
|
+
}
|
|
3523
|
+
for (const s of matches) {
|
|
3524
|
+
const agentTag = s.agent === "claude" ? chalk9.yellow("claude ") : chalk9.cyan("copilot");
|
|
3525
|
+
const icon = s.complete ? chalk9.green("\u2714") : chalk9.yellow("\u23F3");
|
|
3526
|
+
const date = new Date(s.mtime).toLocaleDateString("en-CA");
|
|
3527
|
+
const time = new Date(s.mtime).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
|
|
3528
|
+
const project = s.cwd?.split("/").pop() || "\u2014";
|
|
3529
|
+
const summary = (s.summary || "\u2014").slice(0, 50);
|
|
3530
|
+
console.log(` ${icon} ${agentTag} ${chalk9.dim(date + " " + time)} ${chalk9.bold(project.padEnd(16))} ${summary}`);
|
|
3531
|
+
console.log(chalk9.dim(` ${s.id}`));
|
|
3532
|
+
}
|
|
3533
|
+
console.log();
|
|
3534
|
+
});
|
|
3535
|
+
cmd.command("timeline [session-id]").description("Show chronological timeline of a session").option("-a, --agent <type>", "Agent type").action((sessionId, opts) => {
|
|
3536
|
+
if (!sessionId) {
|
|
3537
|
+
const sessions = listAllSessions(1);
|
|
3538
|
+
if (sessions.length === 0) {
|
|
3539
|
+
console.log(chalk9.dim(" No sessions"));
|
|
3540
|
+
return;
|
|
3541
|
+
}
|
|
3542
|
+
sessionId = sessions[0].id;
|
|
3543
|
+
opts.agent = opts.agent || sessions[0].agent;
|
|
3544
|
+
console.log(chalk9.dim(` Using latest: ${sessionId.slice(0, 12)}\u2026
|
|
3545
|
+
`));
|
|
3546
|
+
}
|
|
3547
|
+
const report = getAgentSessionReport(sessionId, opts.agent);
|
|
3548
|
+
if (!report) {
|
|
3549
|
+
console.log(chalk9.red(` \u2717 Session not found`));
|
|
3550
|
+
return;
|
|
3551
|
+
}
|
|
3552
|
+
const project = report.cwd?.split("/").pop() || "unknown";
|
|
3553
|
+
const agentTag = report.agent === "claude" ? chalk9.yellow("[claude]") : chalk9.cyan("[copilot]");
|
|
3554
|
+
console.log(chalk9.bold.cyan(` \u{1F4C5} Timeline \u2014 ${project}`) + ` ${agentTag}
|
|
3555
|
+
`);
|
|
3556
|
+
const entries = [];
|
|
3557
|
+
if (report.startTime) entries.push(`${chalk9.dim(fmtTime2(report.startTime))} ${chalk9.green("\u25B6")} Session started`);
|
|
3558
|
+
for (const f of report.filesCreated) entries.push(`${chalk9.dim(" ")} ${chalk9.green("+")} Created ${f}`);
|
|
3559
|
+
for (const f of report.filesEdited) entries.push(`${chalk9.dim(" ")} ${chalk9.yellow("~")} Edited ${f}`);
|
|
3560
|
+
for (const c of report.gitCommits) entries.push(`${chalk9.dim(" ")} ${chalk9.cyan("\u25CF")} Commit: ${c.split("\n")[0].slice(0, 60)}`);
|
|
3561
|
+
for (const t of report.taskCompletions) entries.push(`${chalk9.dim(" ")} ${chalk9.green("\u2714")} ${t.split("\n")[0].slice(0, 60)}`);
|
|
3562
|
+
for (const e of report.errors.slice(0, 5)) entries.push(`${chalk9.dim(" ")} ${chalk9.red("\u2717")} ${e.split("\n")[0].slice(0, 60)}`);
|
|
3563
|
+
const statusIcon3 = report.complete ? chalk9.green("\u25A0") : chalk9.yellow("\u23F8");
|
|
3564
|
+
if (report.endTime) entries.push(`${chalk9.dim(fmtTime2(report.endTime))} ${statusIcon3} Session ${report.complete ? "completed" : "stopped"}`);
|
|
3565
|
+
for (const e of entries) console.log(` ${e}`);
|
|
3566
|
+
console.log(chalk9.dim(`
|
|
3567
|
+
Duration: ${fmtDur2(report.durationMs)} | Turns: ${report.assistantTurns} | Premium: \u2B21${report.premiumRequests}
|
|
3568
|
+
`));
|
|
3569
|
+
});
|
|
3570
|
+
cmd.command("export").description("Export session history").option("-f, --format <fmt>", "Format: json | csv", "json").option("-l, --limit <n>", "Max sessions", "50").option("-o, --output <file>", "Output file").action((opts) => {
|
|
3571
|
+
const sessions = listAllSessions(parseInt(opts.limit, 10));
|
|
3572
|
+
const data = sessions.map((s) => {
|
|
3573
|
+
const r = getAgentSessionReport(s.id, s.agent);
|
|
3574
|
+
return {
|
|
3575
|
+
id: s.id,
|
|
3576
|
+
agent: s.agent,
|
|
3577
|
+
project: s.cwd?.split("/").pop() || "",
|
|
3578
|
+
complete: s.complete,
|
|
3579
|
+
premium: s.premiumRequests,
|
|
3580
|
+
tokens: r?.outputTokens || 0,
|
|
3581
|
+
turns: r?.assistantTurns || 0,
|
|
3582
|
+
commits: r?.gitCommits.length || 0,
|
|
3583
|
+
filesCreated: r?.filesCreated.length || 0,
|
|
3584
|
+
filesEdited: r?.filesEdited.length || 0,
|
|
3585
|
+
durationMs: r?.durationMs || 0,
|
|
3586
|
+
summary: s.summary || "",
|
|
3587
|
+
date: new Date(s.mtime).toISOString()
|
|
3588
|
+
};
|
|
3589
|
+
});
|
|
3590
|
+
let output;
|
|
3591
|
+
if (opts.format === "csv") {
|
|
3592
|
+
const headers = Object.keys(data[0] || {}).join(",");
|
|
3593
|
+
const rows = data.map((d) => Object.values(d).map((v) => `"${String(v).replace(/"/g, '""')}"`).join(","));
|
|
3594
|
+
output = [headers, ...rows].join("\n");
|
|
3595
|
+
} else {
|
|
3596
|
+
output = JSON.stringify(data, null, 2);
|
|
3597
|
+
}
|
|
3598
|
+
if (opts.output) {
|
|
3599
|
+
writeFileSync6(opts.output, output, "utf-8");
|
|
3600
|
+
console.log(chalk9.green(` \u2714 Exported ${data.length} sessions to ${opts.output}`));
|
|
3601
|
+
} else {
|
|
3602
|
+
console.log(output);
|
|
3603
|
+
}
|
|
3604
|
+
});
|
|
3605
|
+
}
|
|
3606
|
+
function fmtTime2(iso) {
|
|
3607
|
+
try {
|
|
3608
|
+
return new Date(iso).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
|
|
3609
|
+
} catch {
|
|
3610
|
+
return " ";
|
|
3611
|
+
}
|
|
3612
|
+
}
|
|
3613
|
+
function fmtDur2(ms) {
|
|
3614
|
+
if (ms < 6e4) return `${Math.round(ms / 1e3)}s`;
|
|
3615
|
+
if (ms < 36e5) return `${Math.round(ms / 6e4)}m`;
|
|
3616
|
+
return `${Math.floor(ms / 36e5)}h ${Math.round(ms % 36e5 / 6e4)}m`;
|
|
3617
|
+
}
|
|
3618
|
+
|
|
3619
|
+
// src/commands/multi.ts
|
|
3620
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
3621
|
+
import { join as join13, resolve as resolve7, basename as basename3 } from "path";
|
|
3622
|
+
import { homedir as homedir11 } from "os";
|
|
3623
|
+
import chalk10 from "chalk";
|
|
3624
|
+
import { parse as parseYaml4, stringify as stringifyYaml3 } from "yaml";
|
|
3625
|
+
var CONFIG_DIR3 = join13(homedir11(), ".copilot-agent");
|
|
3626
|
+
var PROJECTS_FILE = join13(CONFIG_DIR3, "multi-projects.txt");
|
|
3627
|
+
var STATUS_FILE = join13(CONFIG_DIR3, "multi-status.yaml");
|
|
3628
|
+
var LOG_DIR = join13(CONFIG_DIR3, "multi-logs");
|
|
3629
|
+
var MAX_CONCURRENCY = 3;
|
|
3630
|
+
function registerMultiCommand(program2) {
|
|
3631
|
+
program2.command("multi <action> [args...]").description("Multi-project orchestration \u2014 add/remove/list/run/status/research").option("-a, --agent <type>", "Agent to use (copilot or claude)", "copilot").option("--parallel", "Run projects in parallel (max 3 concurrent)").option("--cooldown <n>", "Cooldown between projects in seconds", "30").option("-s, --steps <n>", "Max steps per task", "10").option("--max-premium <n>", "Premium budget per project", "50").option("--dry-run", "Preview without executing").action(async (action, args, opts) => {
|
|
3632
|
+
try {
|
|
3633
|
+
await multiCommand(action, args, {
|
|
3634
|
+
agent: resolveAgent(opts.agent),
|
|
3635
|
+
parallel: opts.parallel ?? false,
|
|
3636
|
+
cooldown: parseInt(opts.cooldown, 10),
|
|
3637
|
+
steps: parseInt(opts.steps, 10),
|
|
3638
|
+
maxPremium: parseInt(opts.maxPremium, 10),
|
|
3639
|
+
dryRun: opts.dryRun ?? false
|
|
3640
|
+
});
|
|
3641
|
+
} catch (err) {
|
|
3642
|
+
fail(`Multi error: ${err instanceof Error ? err.message : err}`);
|
|
3643
|
+
process.exit(1);
|
|
3644
|
+
}
|
|
3645
|
+
});
|
|
3646
|
+
}
|
|
3647
|
+
async function multiCommand(action, args, opts) {
|
|
3648
|
+
ensureFiles();
|
|
3649
|
+
switch (action) {
|
|
3650
|
+
case "add":
|
|
3651
|
+
return addProject(args[0]);
|
|
3652
|
+
case "remove":
|
|
3653
|
+
return removeProject(args[0]);
|
|
3654
|
+
case "list":
|
|
3655
|
+
return listProjects();
|
|
3656
|
+
case "status":
|
|
3657
|
+
return showStatus();
|
|
3658
|
+
case "run":
|
|
3659
|
+
case "health":
|
|
3660
|
+
return runAll("health", opts);
|
|
3661
|
+
case "research":
|
|
3662
|
+
return runAll("research", opts);
|
|
3663
|
+
default:
|
|
3664
|
+
fail(`Unknown action: ${action}. Use: add, remove, list, run, status, research`);
|
|
3665
|
+
process.exit(1);
|
|
3666
|
+
}
|
|
3667
|
+
}
|
|
3668
|
+
function ensureFiles() {
|
|
3669
|
+
mkdirSync8(LOG_DIR, { recursive: true });
|
|
3670
|
+
mkdirSync8(CONFIG_DIR3, { recursive: true });
|
|
3671
|
+
if (!existsSync11(PROJECTS_FILE)) writeFileSync7(PROJECTS_FILE, "");
|
|
3672
|
+
}
|
|
3673
|
+
function readProjects() {
|
|
3674
|
+
return readFileSync8(PROJECTS_FILE, "utf-8").split("\n").map((l) => l.trim()).filter(Boolean);
|
|
3675
|
+
}
|
|
3676
|
+
function writeProjects(projects) {
|
|
3677
|
+
writeFileSync7(PROJECTS_FILE, projects.join("\n") + "\n");
|
|
3678
|
+
}
|
|
3679
|
+
function readStatusFile() {
|
|
3680
|
+
if (!existsSync11(STATUS_FILE)) return [];
|
|
3681
|
+
try {
|
|
3682
|
+
const raw = readFileSync8(STATUS_FILE, "utf-8");
|
|
3683
|
+
const parsed = parseYaml4(raw);
|
|
3684
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
3685
|
+
} catch {
|
|
3686
|
+
return [];
|
|
3687
|
+
}
|
|
3688
|
+
}
|
|
3689
|
+
function writeStatusFile(statuses) {
|
|
3690
|
+
writeFileSync7(STATUS_FILE, stringifyYaml3(statuses));
|
|
3691
|
+
}
|
|
3692
|
+
function upsertStatus(entry) {
|
|
3693
|
+
const statuses = readStatusFile();
|
|
3694
|
+
const idx = statuses.findIndex((s) => s.project === entry.project);
|
|
3695
|
+
if (idx >= 0) {
|
|
3696
|
+
statuses[idx] = entry;
|
|
3697
|
+
} else {
|
|
3698
|
+
statuses.push(entry);
|
|
3699
|
+
}
|
|
3700
|
+
writeStatusFile(statuses);
|
|
3701
|
+
}
|
|
3702
|
+
function formatDuration2(ms) {
|
|
3703
|
+
const totalSec = Math.round(ms / 1e3);
|
|
3704
|
+
const min = Math.floor(totalSec / 60);
|
|
3705
|
+
const sec = totalSec % 60;
|
|
3706
|
+
return min > 0 ? `${min}m ${sec}s` : `${sec}s`;
|
|
3707
|
+
}
|
|
3708
|
+
async function addProject(path) {
|
|
3709
|
+
if (!path) {
|
|
3710
|
+
fail("Usage: copilot-agent multi add <path>");
|
|
3711
|
+
process.exit(1);
|
|
3712
|
+
}
|
|
3713
|
+
const resolved = resolve7(path);
|
|
3714
|
+
if (!existsSync11(resolved)) {
|
|
3715
|
+
fail(`Not found: ${resolved}`);
|
|
3716
|
+
process.exit(1);
|
|
3717
|
+
}
|
|
3718
|
+
await withLock("projects-file", async () => {
|
|
3719
|
+
const projects = readProjects();
|
|
3720
|
+
if (projects.includes(resolved)) {
|
|
3721
|
+
warn(`Already registered: ${resolved}`);
|
|
3722
|
+
return;
|
|
3723
|
+
}
|
|
3724
|
+
projects.push(resolved);
|
|
3725
|
+
writeProjects(projects);
|
|
3726
|
+
ok(`Added: ${resolved}`);
|
|
3727
|
+
});
|
|
3728
|
+
}
|
|
3729
|
+
async function removeProject(path) {
|
|
3730
|
+
if (!path) {
|
|
3731
|
+
fail("Usage: copilot-agent multi remove <path>");
|
|
3732
|
+
process.exit(1);
|
|
3733
|
+
}
|
|
3734
|
+
const resolved = resolve7(path);
|
|
3735
|
+
await withLock("projects-file", async () => {
|
|
3736
|
+
const projects = readProjects();
|
|
3737
|
+
const filtered = projects.filter((p) => p !== resolved);
|
|
3738
|
+
if (filtered.length === projects.length) {
|
|
3739
|
+
warn(`Not registered: ${resolved}`);
|
|
3740
|
+
return;
|
|
3741
|
+
}
|
|
3742
|
+
writeProjects(filtered);
|
|
3743
|
+
ok(`Removed: ${resolved}`);
|
|
3744
|
+
});
|
|
3745
|
+
}
|
|
3746
|
+
function listProjects() {
|
|
3747
|
+
const projects = readProjects();
|
|
3748
|
+
if (projects.length === 0) {
|
|
3749
|
+
log("No projects registered. Add: copilot-agent multi add <path>");
|
|
3750
|
+
return;
|
|
3751
|
+
}
|
|
3752
|
+
const statuses = readStatusFile();
|
|
3753
|
+
console.log(chalk10.bold("\nRegistered projects:"));
|
|
3754
|
+
for (let i = 0; i < projects.length; i++) {
|
|
3755
|
+
const p = projects[i];
|
|
3756
|
+
const exists = existsSync11(p);
|
|
3757
|
+
const icon = exists ? chalk10.green("\u2705") : chalk10.red("\u274C");
|
|
3758
|
+
const type = exists ? detectProjectType(p) : "?";
|
|
3759
|
+
const st = statuses.find((s) => s.project === p);
|
|
3760
|
+
const statusTag = st ? chalk10.dim(` [${st.status} \u2014 ${st.agent} \u2014 ${st.duration}]`) : "";
|
|
3761
|
+
console.log(` ${i + 1}. ${icon} ${p} ${chalk10.dim(`(${type})`)}${statusTag}`);
|
|
3762
|
+
}
|
|
3763
|
+
console.log();
|
|
3764
|
+
}
|
|
3765
|
+
function showStatus() {
|
|
3766
|
+
const statuses = readStatusFile();
|
|
3767
|
+
if (statuses.length === 0) {
|
|
3768
|
+
log("No run history. Execute: copilot-agent multi run");
|
|
3769
|
+
return;
|
|
3770
|
+
}
|
|
3771
|
+
console.log(chalk10.bold("\nMulti-project status:"));
|
|
3772
|
+
for (const s of statuses) {
|
|
3773
|
+
const icon = s.status === "success" ? chalk10.green("\u2705") : s.status === "failed" ? chalk10.red("\u274C") : chalk10.yellow("\u{1F504}");
|
|
3774
|
+
console.log(
|
|
3775
|
+
` ${icon} ${chalk10.bold(basename3(s.project))} \u2014 ${s.agent} \u2014 ${s.tasks} tasks \u2014 ${s.duration} \u2014 ${chalk10.dim(s.lastRun)}`
|
|
3776
|
+
);
|
|
3777
|
+
}
|
|
3778
|
+
console.log();
|
|
3779
|
+
}
|
|
3780
|
+
function createSemaphore(max) {
|
|
3781
|
+
let running = 0;
|
|
3782
|
+
const queue = [];
|
|
3783
|
+
async function acquire() {
|
|
3784
|
+
if (running < max) {
|
|
3785
|
+
running++;
|
|
3786
|
+
return;
|
|
3787
|
+
}
|
|
3788
|
+
return new Promise((resolve8) => {
|
|
3789
|
+
queue.push(() => {
|
|
3790
|
+
running++;
|
|
3791
|
+
resolve8();
|
|
3792
|
+
});
|
|
3793
|
+
});
|
|
3794
|
+
}
|
|
3795
|
+
function release() {
|
|
3796
|
+
running--;
|
|
3797
|
+
const next = queue.shift();
|
|
3798
|
+
if (next) next();
|
|
3799
|
+
}
|
|
3800
|
+
return { acquire, release };
|
|
3801
|
+
}
|
|
3802
|
+
async function runAll(mode, opts) {
|
|
3803
|
+
assertAgent(opts.agent);
|
|
3804
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "").slice(0, 15);
|
|
3805
|
+
setLogFile(join13(LOG_DIR, `multi-${mode}-${ts}.log`));
|
|
3806
|
+
const projects = readProjects();
|
|
3807
|
+
if (projects.length === 0) {
|
|
3808
|
+
fail("No projects registered. Add: copilot-agent multi add <path>");
|
|
3809
|
+
process.exit(1);
|
|
3810
|
+
}
|
|
3811
|
+
log(`\u{1F3ED} Multi-project ${mode} \u2014 ${projects.length} projects \u2014 agent: ${opts.agent}${opts.parallel ? " (parallel)" : ""}`);
|
|
3812
|
+
if (opts.dryRun) {
|
|
3813
|
+
for (const p of projects) {
|
|
3814
|
+
const type = existsSync11(p) ? detectProjectType(p) : "unknown";
|
|
3815
|
+
const tasks = getTasksForProject(type);
|
|
3816
|
+
console.log(`
|
|
3817
|
+
${chalk10.bold(basename3(p))} (${type})`);
|
|
3818
|
+
for (const t of tasks.slice(0, 3)) {
|
|
3819
|
+
console.log(` ${chalk10.dim("\u2022")} ${t.title}`);
|
|
3820
|
+
}
|
|
3821
|
+
}
|
|
3822
|
+
log(chalk10.dim("\n(dry-run \u2014 not executing)"));
|
|
3823
|
+
return;
|
|
3824
|
+
}
|
|
3825
|
+
const results = [];
|
|
3826
|
+
if (opts.parallel) {
|
|
3827
|
+
const sem = createSemaphore(MAX_CONCURRENCY);
|
|
3828
|
+
const promises = projects.map(async (project) => {
|
|
3829
|
+
await sem.acquire();
|
|
3830
|
+
try {
|
|
3831
|
+
const res = await runSingleProject(project, mode, opts);
|
|
3832
|
+
results.push(res);
|
|
3833
|
+
} finally {
|
|
3834
|
+
sem.release();
|
|
3835
|
+
}
|
|
3836
|
+
});
|
|
3837
|
+
await Promise.all(promises);
|
|
3838
|
+
} else {
|
|
3839
|
+
for (let i = 0; i < projects.length; i++) {
|
|
3840
|
+
const res = await runSingleProject(projects[i], mode, opts);
|
|
3841
|
+
results.push(res);
|
|
3842
|
+
if (i < projects.length - 1 && !res.skipped) {
|
|
3843
|
+
log(`Cooldown ${opts.cooldown}s\u2026`);
|
|
3844
|
+
await new Promise((r) => setTimeout(r, opts.cooldown * 1e3));
|
|
3845
|
+
}
|
|
3846
|
+
}
|
|
3847
|
+
}
|
|
3848
|
+
const success = results.filter((r) => r.success && !r.skipped).length;
|
|
3849
|
+
const failed = results.filter((r) => !r.success && !r.skipped).length;
|
|
3850
|
+
const skipped = results.filter((r) => r.skipped).length;
|
|
3851
|
+
const total = results.length;
|
|
3852
|
+
console.log(`
|
|
3853
|
+
${"\u2550".repeat(50)}`);
|
|
3854
|
+
log(`\u{1F4CA} Summary: ${success}/${total} succeeded, ${failed} failed, ${skipped} skipped`);
|
|
3855
|
+
for (const r of results) {
|
|
3856
|
+
const icon = r.skipped ? "\u23ED " : r.success ? "\u2705" : "\u274C";
|
|
3857
|
+
console.log(` ${icon} ${r.name}`);
|
|
3858
|
+
}
|
|
3859
|
+
console.log();
|
|
3860
|
+
notify(`Multi-${mode}: ${success}/${total} succeeded`, "copilot-agent");
|
|
3861
|
+
}
|
|
3862
|
+
async function runSingleProject(project, mode, opts) {
|
|
3863
|
+
const name = existsSync11(project) ? detectProjectName(project) : basename3(project);
|
|
3864
|
+
if (!existsSync11(project)) {
|
|
3865
|
+
warn(`Skipping (not found): ${project}`);
|
|
3866
|
+
return { name, success: false, skipped: true };
|
|
3867
|
+
}
|
|
3868
|
+
const type = detectProjectType(project);
|
|
3869
|
+
log(`
|
|
3870
|
+
${"\u2550".repeat(50)}`);
|
|
3871
|
+
log(`${chalk10.bold(name)} (${type}) \u2014 agent: ${opts.agent}`);
|
|
3872
|
+
log(`${"\u2550".repeat(50)}`);
|
|
3873
|
+
const tasks = mode === "research" ? [{ title: "Research", prompt: "Research latest best practices, dependency updates, and architecture improvements. Create a report.", priority: 1 }] : getTasksForProject(type).slice(0, 3);
|
|
3874
|
+
const startTime = Date.now();
|
|
3875
|
+
let projectSuccess = true;
|
|
3876
|
+
upsertStatus({
|
|
3877
|
+
project,
|
|
3878
|
+
lastRun: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3879
|
+
status: "running",
|
|
3880
|
+
agent: opts.agent,
|
|
3881
|
+
tasks: tasks.length,
|
|
3882
|
+
duration: "\u2014"
|
|
3883
|
+
});
|
|
3884
|
+
for (const task of tasks) {
|
|
3885
|
+
try {
|
|
3886
|
+
const result = await withLock(
|
|
3887
|
+
"agent-multi",
|
|
3888
|
+
() => runAgentTask(opts.agent, `Project: ${project}
|
|
3889
|
+
|
|
3890
|
+
${task.prompt}`, opts.steps, project)
|
|
3891
|
+
);
|
|
3892
|
+
ok(`${task.title} \u2014 exit ${result.exitCode}, premium: ${result.premium}`);
|
|
3893
|
+
} catch (err) {
|
|
3894
|
+
fail(`${task.title} failed: ${err}`);
|
|
3895
|
+
projectSuccess = false;
|
|
3896
|
+
}
|
|
3897
|
+
}
|
|
3898
|
+
const duration = formatDuration2(Date.now() - startTime);
|
|
3899
|
+
upsertStatus({
|
|
3900
|
+
project,
|
|
3901
|
+
lastRun: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3902
|
+
status: projectSuccess ? "success" : "failed",
|
|
3903
|
+
agent: opts.agent,
|
|
3904
|
+
tasks: tasks.length,
|
|
3905
|
+
duration
|
|
3906
|
+
});
|
|
3907
|
+
return { name, success: projectSuccess, skipped: false };
|
|
3908
|
+
}
|
|
3909
|
+
|
|
3910
|
+
// src/commands/review.ts
|
|
3911
|
+
import chalk11 from "chalk";
|
|
3912
|
+
import { execSync as execSync6 } from "child_process";
|
|
3913
|
+
var MAX_DIFF_LENGTH = 15e3;
|
|
3914
|
+
var MAX_BUFFER = 1024 * 1024;
|
|
3915
|
+
function getSessionDiff(sessionId) {
|
|
3916
|
+
const report = getAgentSessionReport(sessionId, "copilot") ?? getAgentSessionReport(sessionId, "claude");
|
|
3917
|
+
if (!report || report.gitCommits.length === 0) {
|
|
3918
|
+
return execSync6("git --no-pager diff HEAD~3", {
|
|
3919
|
+
encoding: "utf-8",
|
|
3920
|
+
maxBuffer: MAX_BUFFER
|
|
3921
|
+
});
|
|
3922
|
+
}
|
|
3923
|
+
return execSync6("git --no-pager diff HEAD~" + report.gitCommits.length, {
|
|
3924
|
+
encoding: "utf-8",
|
|
3925
|
+
cwd: report.cwd || process.cwd(),
|
|
3926
|
+
maxBuffer: MAX_BUFFER
|
|
3927
|
+
});
|
|
3928
|
+
}
|
|
3929
|
+
function getWorkingDiff() {
|
|
3930
|
+
const staged = execSync6("git --no-pager diff --cached", {
|
|
3931
|
+
encoding: "utf-8",
|
|
3932
|
+
maxBuffer: MAX_BUFFER
|
|
3933
|
+
});
|
|
3934
|
+
const unstaged = execSync6("git --no-pager diff", {
|
|
3935
|
+
encoding: "utf-8",
|
|
3936
|
+
maxBuffer: MAX_BUFFER
|
|
3937
|
+
});
|
|
3938
|
+
return staged + unstaged;
|
|
3939
|
+
}
|
|
3940
|
+
function getPrDiff(prNumber) {
|
|
3941
|
+
return execSync6(`gh pr diff ${prNumber} --color=never`, {
|
|
3942
|
+
encoding: "utf-8",
|
|
3943
|
+
maxBuffer: MAX_BUFFER
|
|
3944
|
+
});
|
|
3945
|
+
}
|
|
3946
|
+
var focusInstructions = {
|
|
3947
|
+
all: "Review for bugs, security issues, performance problems, and code quality.",
|
|
3948
|
+
security: "Focus exclusively on security vulnerabilities, injection risks, auth issues, and data exposure.",
|
|
3949
|
+
performance: "Focus exclusively on performance issues: N+1 queries, memory leaks, unnecessary allocations, slow algorithms.",
|
|
3950
|
+
bugs: "Focus exclusively on logic bugs, edge cases, null/undefined issues, race conditions.",
|
|
3951
|
+
style: "Focus exclusively on code style, naming, readability, and consistency."
|
|
3952
|
+
};
|
|
3953
|
+
function buildReviewPrompt(diff, focus) {
|
|
3954
|
+
const instruction = focusInstructions[focus] ?? focusInstructions.all;
|
|
3955
|
+
const truncatedDiff = diff.length > MAX_DIFF_LENGTH ? diff.slice(0, MAX_DIFF_LENGTH) + "\n... (truncated)" : diff;
|
|
3956
|
+
return `You are a senior code reviewer. ${instruction}
|
|
3957
|
+
|
|
3958
|
+
Review this diff and provide:
|
|
3959
|
+
1. **Critical Issues** (bugs, security) \u2014 must fix
|
|
3960
|
+
2. **Suggestions** \u2014 should fix for quality
|
|
3961
|
+
3. **Positive Notes** \u2014 good patterns observed
|
|
3962
|
+
4. **Summary** \u2014 one paragraph overall assessment
|
|
3963
|
+
|
|
3964
|
+
Be concise. Only flag real issues, not style nitpicks (unless focus is style).
|
|
3965
|
+
|
|
3966
|
+
\`\`\`diff
|
|
3967
|
+
${truncatedDiff}
|
|
3968
|
+
\`\`\``;
|
|
3969
|
+
}
|
|
3970
|
+
async function runReview(label, diff, agent, focus, steps) {
|
|
3971
|
+
if (!diff.trim()) {
|
|
3972
|
+
fail("No changes found to review.");
|
|
3973
|
+
process.exit(1);
|
|
3974
|
+
}
|
|
3975
|
+
info(`Reviewing: ${chalk11.cyan(label)}`);
|
|
3976
|
+
log(`Focus: ${chalk11.yellow(focus)} | Agent: ${chalk11.yellow(agent)} | Steps: ${steps}`);
|
|
3977
|
+
const prompt = buildReviewPrompt(diff, focus);
|
|
3978
|
+
const result = await runAgentTask(agent, prompt, steps, process.cwd());
|
|
3979
|
+
if (result.exitCode === 0) {
|
|
3980
|
+
ok("Review complete.");
|
|
3981
|
+
} else {
|
|
3982
|
+
fail(`Agent exited with code ${result.exitCode}`);
|
|
3983
|
+
}
|
|
3984
|
+
}
|
|
3985
|
+
function registerReviewCommand(program2) {
|
|
3986
|
+
const cmd = program2.command("review [session-id]").description("AI-powered code review of agent changes or git diffs").option("-a, --agent <type>", "Agent to use: copilot or claude", "copilot").option(
|
|
3987
|
+
"-f, --focus <area>",
|
|
3988
|
+
"Focus area: all, security, performance, bugs, style",
|
|
3989
|
+
"all"
|
|
3990
|
+
).option("-s, --steps <n>", "Max agent steps", "5").action(async (sessionId, opts) => {
|
|
3991
|
+
try {
|
|
3992
|
+
const agent = resolveAgent(opts.agent);
|
|
3993
|
+
const steps = parseInt(opts.steps, 10);
|
|
3994
|
+
let sid = sessionId;
|
|
3995
|
+
if (!sid) {
|
|
3996
|
+
const sessions = listAllSessions(1);
|
|
3997
|
+
if (sessions.length === 0) {
|
|
3998
|
+
fail('No sessions found. Use "review diff" to review working tree changes.');
|
|
3999
|
+
process.exit(1);
|
|
4000
|
+
}
|
|
4001
|
+
sid = sessions[0].id;
|
|
4002
|
+
log(`Using latest session: ${chalk11.dim(sid)}`);
|
|
4003
|
+
}
|
|
4004
|
+
const diff = getSessionDiff(sid);
|
|
4005
|
+
await runReview(`session ${sid.slice(0, 8)}\u2026`, diff, agent, opts.focus, steps);
|
|
4006
|
+
} catch (err) {
|
|
4007
|
+
fail(`Review error: ${err instanceof Error ? err.message : err}`);
|
|
4008
|
+
process.exit(1);
|
|
4009
|
+
}
|
|
4010
|
+
});
|
|
4011
|
+
cmd.command("diff").description("Review current working tree changes").option("-a, --agent <type>", "Agent to use: copilot or claude", "copilot").option(
|
|
4012
|
+
"-f, --focus <area>",
|
|
4013
|
+
"Focus area: all, security, performance, bugs, style",
|
|
4014
|
+
"all"
|
|
4015
|
+
).option("-s, --steps <n>", "Max agent steps", "5").action(async (opts) => {
|
|
4016
|
+
try {
|
|
4017
|
+
const agent = resolveAgent(opts.agent);
|
|
4018
|
+
const steps = parseInt(opts.steps, 10);
|
|
4019
|
+
const diff = getWorkingDiff();
|
|
4020
|
+
await runReview("working tree changes", diff, agent, opts.focus, steps);
|
|
4021
|
+
} catch (err) {
|
|
4022
|
+
fail(`Review error: ${err instanceof Error ? err.message : err}`);
|
|
4023
|
+
process.exit(1);
|
|
4024
|
+
}
|
|
4025
|
+
});
|
|
4026
|
+
cmd.command("pr <number>").description("Review a GitHub Pull Request").option("-a, --agent <type>", "Agent to use: copilot or claude", "copilot").option(
|
|
4027
|
+
"-f, --focus <area>",
|
|
4028
|
+
"Focus area: all, security, performance, bugs, style",
|
|
4029
|
+
"all"
|
|
4030
|
+
).option("-s, --steps <n>", "Max agent steps", "5").action(async (number, opts) => {
|
|
4031
|
+
try {
|
|
4032
|
+
const agent = resolveAgent(opts.agent);
|
|
4033
|
+
const steps = parseInt(opts.steps, 10);
|
|
4034
|
+
const diff = getPrDiff(number);
|
|
4035
|
+
await runReview(`PR #${number}`, diff, agent, opts.focus, steps);
|
|
4036
|
+
} catch (err) {
|
|
4037
|
+
fail(`Review error: ${err instanceof Error ? err.message : err}`);
|
|
4038
|
+
process.exit(1);
|
|
4039
|
+
}
|
|
4040
|
+
});
|
|
4041
|
+
}
|
|
4042
|
+
|
|
4043
|
+
// src/commands/schedule.ts
|
|
4044
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync8, existsSync as existsSync12, mkdirSync as mkdirSync9 } from "fs";
|
|
4045
|
+
import { join as join14 } from "path";
|
|
4046
|
+
import { homedir as homedir12 } from "os";
|
|
4047
|
+
import { parse as parseYaml5, stringify as stringifyYaml4 } from "yaml";
|
|
4048
|
+
var CONFIG_DIR4 = join14(homedir12(), ".copilot-agent");
|
|
4049
|
+
var SCHEDULES_FILE = join14(CONFIG_DIR4, "schedules.yaml");
|
|
4050
|
+
function ensureConfigDir2() {
|
|
4051
|
+
if (!existsSync12(CONFIG_DIR4)) mkdirSync9(CONFIG_DIR4, { recursive: true });
|
|
4052
|
+
}
|
|
4053
|
+
function loadSchedules() {
|
|
4054
|
+
if (!existsSync12(SCHEDULES_FILE)) return [];
|
|
4055
|
+
try {
|
|
4056
|
+
const raw = parseYaml5(readFileSync9(SCHEDULES_FILE, "utf-8"));
|
|
4057
|
+
return Array.isArray(raw) ? raw : [];
|
|
4058
|
+
} catch {
|
|
4059
|
+
return [];
|
|
4060
|
+
}
|
|
4061
|
+
}
|
|
4062
|
+
function saveSchedules(schedules) {
|
|
4063
|
+
ensureConfigDir2();
|
|
4064
|
+
writeFileSync8(SCHEDULES_FILE, stringifyYaml4(schedules), "utf-8");
|
|
4065
|
+
}
|
|
4066
|
+
function matchesCron(cron, date) {
|
|
4067
|
+
const parts = cron.trim().split(/\s+/);
|
|
4068
|
+
if (parts.length !== 5) return false;
|
|
4069
|
+
const [minPart, hourPart, , , dowPart] = parts;
|
|
4070
|
+
const minute = date.getMinutes();
|
|
4071
|
+
const hour = date.getHours();
|
|
4072
|
+
const dow = date.getDay();
|
|
4073
|
+
if (!matchField(minPart, minute)) return false;
|
|
4074
|
+
if (!matchField(hourPart, hour)) return false;
|
|
4075
|
+
if (!matchField(dowPart, dow)) return false;
|
|
4076
|
+
return true;
|
|
4077
|
+
}
|
|
4078
|
+
function matchField(pattern, value) {
|
|
4079
|
+
if (pattern === "*") return true;
|
|
4080
|
+
const stepMatch = pattern.match(/^\*\/(\d+)$/);
|
|
4081
|
+
if (stepMatch) {
|
|
4082
|
+
const step = parseInt(stepMatch[1], 10);
|
|
4083
|
+
return step > 0 && value % step === 0;
|
|
4084
|
+
}
|
|
4085
|
+
const num = parseInt(pattern, 10);
|
|
4086
|
+
if (!isNaN(num)) return value === num;
|
|
4087
|
+
return false;
|
|
4088
|
+
}
|
|
4089
|
+
function nextRunTime(cron) {
|
|
4090
|
+
const now = /* @__PURE__ */ new Date();
|
|
4091
|
+
const limit = 7 * 24 * 60;
|
|
4092
|
+
const candidate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours(), now.getMinutes());
|
|
4093
|
+
for (let i = 1; i <= limit; i++) {
|
|
4094
|
+
candidate.setMinutes(candidate.getMinutes() + 1);
|
|
4095
|
+
if (matchesCron(cron, candidate)) return new Date(candidate);
|
|
4096
|
+
}
|
|
4097
|
+
return null;
|
|
4098
|
+
}
|
|
4099
|
+
function formatTime2(date) {
|
|
4100
|
+
return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", hour12: false });
|
|
4101
|
+
}
|
|
4102
|
+
function formatDateTime(date) {
|
|
4103
|
+
const day = date.toLocaleDateString([], { weekday: "short", month: "short", day: "numeric" });
|
|
4104
|
+
return `${day} ${formatTime2(date)}`;
|
|
4105
|
+
}
|
|
4106
|
+
function timestamp() {
|
|
4107
|
+
return `${DIM}[${formatTime2(/* @__PURE__ */ new Date())}]${RESET}`;
|
|
4108
|
+
}
|
|
4109
|
+
function listCommand() {
|
|
4110
|
+
const schedules = loadSchedules();
|
|
4111
|
+
if (schedules.length === 0) {
|
|
4112
|
+
info("No schedules configured.");
|
|
4113
|
+
info(`Add one with: ${CYAN}copilot-agent schedule add <name> --cron "..." --prompt "..."${RESET}`);
|
|
4114
|
+
return;
|
|
4115
|
+
}
|
|
4116
|
+
log(`
|
|
4117
|
+
${BOLD}Schedules${RESET} ${DIM}(${SCHEDULES_FILE})${RESET}
|
|
4118
|
+
`);
|
|
4119
|
+
for (const s of schedules) {
|
|
4120
|
+
const status = s.enabled ? `${GREEN}\u25CF${RESET}` : `${DIM}\u25CB${RESET}`;
|
|
4121
|
+
const next = s.enabled ? nextRunTime(s.cron) : null;
|
|
4122
|
+
const nextStr = next ? `${DIM}next: ${formatDateTime(next)}${RESET}` : "";
|
|
4123
|
+
log(` ${status} ${BOLD}${s.name}${RESET} ${DIM}${s.cron}${RESET} ${nextStr}`);
|
|
4124
|
+
log(` ${DIM}prompt:${RESET} ${s.prompt.length > 60 ? s.prompt.slice(0, 57) + "..." : s.prompt}`);
|
|
4125
|
+
log(` ${DIM}project:${RESET} ${s.project} ${DIM}agent:${RESET} ${s.agent} ${DIM}steps:${RESET} ${s.maxSteps ?? 30}`);
|
|
4126
|
+
log("");
|
|
4127
|
+
}
|
|
4128
|
+
}
|
|
4129
|
+
function addCommand(name, opts) {
|
|
4130
|
+
const schedules = loadSchedules();
|
|
4131
|
+
if (schedules.some((s) => s.name === name)) {
|
|
4132
|
+
fail(`Schedule "${name}" already exists. Remove it first or use a different name.`);
|
|
4133
|
+
process.exit(1);
|
|
4134
|
+
}
|
|
4135
|
+
if (!opts.cron || opts.cron.trim().split(/\s+/).length !== 5) {
|
|
4136
|
+
fail("Invalid cron expression. Expected 5 fields: minute hour day month weekday");
|
|
4137
|
+
process.exit(1);
|
|
4138
|
+
}
|
|
4139
|
+
const schedule = {
|
|
4140
|
+
name,
|
|
4141
|
+
cron: opts.cron,
|
|
4142
|
+
prompt: opts.prompt,
|
|
4143
|
+
project: opts.project || process.cwd(),
|
|
4144
|
+
agent: resolveAgent(opts.agent),
|
|
4145
|
+
enabled: !opts.disabled,
|
|
4146
|
+
maxSteps: parseInt(opts.steps, 10) || 30
|
|
4147
|
+
};
|
|
4148
|
+
schedules.push(schedule);
|
|
4149
|
+
saveSchedules(schedules);
|
|
4150
|
+
ok(`Added schedule "${name}"`);
|
|
4151
|
+
const next = nextRunTime(schedule.cron);
|
|
4152
|
+
if (next) {
|
|
4153
|
+
info(`Next run: ${formatDateTime(next)}`);
|
|
4154
|
+
}
|
|
4155
|
+
}
|
|
4156
|
+
function removeCommand(name) {
|
|
4157
|
+
const schedules = loadSchedules();
|
|
4158
|
+
const idx = schedules.findIndex((s) => s.name === name);
|
|
4159
|
+
if (idx === -1) {
|
|
4160
|
+
fail(`Schedule "${name}" not found.`);
|
|
4161
|
+
process.exit(1);
|
|
4162
|
+
}
|
|
4163
|
+
schedules.splice(idx, 1);
|
|
4164
|
+
saveSchedules(schedules);
|
|
4165
|
+
ok(`Removed schedule "${name}"`);
|
|
4166
|
+
}
|
|
4167
|
+
function dryRunCommand() {
|
|
4168
|
+
const schedules = loadSchedules().filter((s) => s.enabled);
|
|
4169
|
+
if (schedules.length === 0) {
|
|
4170
|
+
info("No enabled schedules.");
|
|
4171
|
+
return;
|
|
4172
|
+
}
|
|
4173
|
+
log(`
|
|
4174
|
+
${BOLD}Dry run \u2014 what would run next${RESET}
|
|
4175
|
+
`);
|
|
4176
|
+
for (const s of schedules) {
|
|
4177
|
+
const next = nextRunTime(s.cron);
|
|
4178
|
+
if (next) {
|
|
4179
|
+
log(` ${CYAN}${s.name}${RESET} \u2192 ${formatDateTime(next)}`);
|
|
4180
|
+
log(` ${DIM}${s.agent}${RESET} in ${s.project}`);
|
|
4181
|
+
log(` ${DIM}prompt:${RESET} ${s.prompt.length > 70 ? s.prompt.slice(0, 67) + "..." : s.prompt}`);
|
|
4182
|
+
log("");
|
|
4183
|
+
}
|
|
4184
|
+
}
|
|
4185
|
+
}
|
|
4186
|
+
async function runDaemon() {
|
|
4187
|
+
const schedules = loadSchedules();
|
|
4188
|
+
const enabled = schedules.filter((s) => s.enabled);
|
|
4189
|
+
if (enabled.length === 0) {
|
|
4190
|
+
fail("No enabled schedules. Add one first.");
|
|
4191
|
+
process.exit(1);
|
|
4192
|
+
}
|
|
4193
|
+
for (const s of enabled) {
|
|
4194
|
+
assertAgent(s.agent);
|
|
4195
|
+
}
|
|
4196
|
+
log(`
|
|
4197
|
+
${BOLD}${CYAN}Scheduler daemon started${RESET}`);
|
|
4198
|
+
info(`${enabled.length} schedule(s) active. Press Ctrl+C to stop.
|
|
4199
|
+
`);
|
|
4200
|
+
const lastRun = /* @__PURE__ */ new Map();
|
|
4201
|
+
const showStatus2 = () => {
|
|
4202
|
+
const now = /* @__PURE__ */ new Date();
|
|
4203
|
+
let soonestName = "";
|
|
4204
|
+
let soonestTime = null;
|
|
4205
|
+
for (const s of enabled) {
|
|
4206
|
+
const next = nextRunTime(s.cron);
|
|
4207
|
+
if (next && (!soonestTime || next < soonestTime)) {
|
|
4208
|
+
soonestTime = next;
|
|
4209
|
+
soonestName = s.name;
|
|
4210
|
+
}
|
|
4211
|
+
}
|
|
4212
|
+
const nextStr = soonestTime ? `next: ${CYAN}${soonestName}${RESET} at ${formatTime2(soonestTime)}` : "no upcoming runs";
|
|
4213
|
+
log(`${timestamp()} ${DIM}Scheduler running \u2014 ${enabled.length} schedules, ${nextStr}${RESET}`);
|
|
4214
|
+
};
|
|
4215
|
+
showStatus2();
|
|
4216
|
+
const check = async () => {
|
|
4217
|
+
const now = /* @__PURE__ */ new Date();
|
|
4218
|
+
const minuteKey = `${now.getFullYear()}-${now.getMonth()}-${now.getDate()}-${now.getHours()}-${now.getMinutes()}`;
|
|
4219
|
+
for (const s of enabled) {
|
|
4220
|
+
if (!matchesCron(s.cron, now)) continue;
|
|
4221
|
+
const runKey = `${s.name}:${minuteKey}`;
|
|
4222
|
+
if (lastRun.has(runKey)) continue;
|
|
4223
|
+
lastRun.set(runKey, Date.now());
|
|
4224
|
+
const cutoff = Date.now() - 2 * 60 * 1e3;
|
|
4225
|
+
for (const [k, v] of lastRun) {
|
|
4226
|
+
if (v < cutoff) lastRun.delete(k);
|
|
4227
|
+
}
|
|
4228
|
+
log(`
|
|
4229
|
+
${timestamp()} ${BOLD}${YELLOW}\u25B6 Running: ${s.name}${RESET}`);
|
|
4230
|
+
log(` ${DIM}cron:${RESET} ${s.cron} ${DIM}agent:${RESET} ${s.agent} ${DIM}project:${RESET} ${s.project}`);
|
|
4231
|
+
log(` ${DIM}prompt:${RESET} ${s.prompt}`);
|
|
4232
|
+
try {
|
|
4233
|
+
const result = await runAgentTask(
|
|
4234
|
+
s.agent,
|
|
4235
|
+
s.prompt,
|
|
4236
|
+
s.maxSteps ?? 30,
|
|
4237
|
+
s.project
|
|
4238
|
+
);
|
|
4239
|
+
if (result.exitCode === 0) {
|
|
4240
|
+
ok(`${timestamp()} \u2713 ${s.name} completed (session: ${result.sessionId?.slice(0, 8) ?? "n/a"})`);
|
|
4241
|
+
} else {
|
|
4242
|
+
warn(`${timestamp()} ${s.name} exited with code ${result.exitCode}`);
|
|
4243
|
+
}
|
|
4244
|
+
} catch (err) {
|
|
4245
|
+
fail(`${timestamp()} ${s.name} failed: ${err instanceof Error ? err.message : err}`);
|
|
4246
|
+
}
|
|
4247
|
+
showStatus2();
|
|
4248
|
+
}
|
|
4249
|
+
};
|
|
4250
|
+
await check();
|
|
4251
|
+
const interval = setInterval(() => {
|
|
4252
|
+
check().catch((err) => {
|
|
4253
|
+
fail(`Scheduler error: ${err instanceof Error ? err.message : err}`);
|
|
4254
|
+
});
|
|
4255
|
+
}, 6e4);
|
|
4256
|
+
const shutdown = () => {
|
|
4257
|
+
log(`
|
|
4258
|
+
${timestamp()} ${DIM}Scheduler stopped.${RESET}`);
|
|
4259
|
+
clearInterval(interval);
|
|
4260
|
+
process.exit(0);
|
|
4261
|
+
};
|
|
4262
|
+
process.on("SIGINT", shutdown);
|
|
4263
|
+
process.on("SIGTERM", shutdown);
|
|
4264
|
+
await new Promise(() => {
|
|
4265
|
+
});
|
|
4266
|
+
}
|
|
4267
|
+
function registerScheduleCommand(program2) {
|
|
4268
|
+
const cmd = program2.command("schedule").description("Cron-like recurring task scheduler");
|
|
4269
|
+
cmd.command("list").description("Show all configured schedules").action(() => {
|
|
4270
|
+
listCommand();
|
|
4271
|
+
});
|
|
4272
|
+
cmd.command("add <name>").description("Add a recurring schedule").requiredOption("--cron <expression>", 'Cron expression (e.g. "0 */6 * * *")').requiredOption("--prompt <text>", "Agent prompt to execute").option("--project <path>", "Project directory", process.cwd()).option("-a, --agent <type>", "Agent: copilot or claude").option("-s, --steps <n>", "Max autopilot steps", "30").option("--disabled", "Add as disabled").action((name, opts) => {
|
|
4273
|
+
addCommand(name, opts);
|
|
4274
|
+
});
|
|
4275
|
+
cmd.command("remove <name>").description("Remove a schedule").action((name) => {
|
|
4276
|
+
removeCommand(name);
|
|
4277
|
+
});
|
|
4278
|
+
cmd.command("run").description("Start the scheduler daemon (foreground)").action(async () => {
|
|
4279
|
+
try {
|
|
4280
|
+
await runDaemon();
|
|
4281
|
+
} catch (err) {
|
|
4282
|
+
fail(`Scheduler error: ${err instanceof Error ? err.message : err}`);
|
|
4283
|
+
process.exit(1);
|
|
4284
|
+
}
|
|
4285
|
+
});
|
|
4286
|
+
cmd.command("dry-run").description("Show what would run next for each schedule").action(() => {
|
|
4287
|
+
dryRunCommand();
|
|
4288
|
+
});
|
|
4289
|
+
}
|
|
4290
|
+
|
|
3411
4291
|
// src/index.ts
|
|
3412
4292
|
var program = new Command();
|
|
3413
|
-
program.name("copilot-agent").version("0.
|
|
4293
|
+
program.name("copilot-agent").version("1.0.0").description("Autonomous AI agent manager \u2014 auto-resume, task discovery, overnight runs. Supports GitHub Copilot CLI + Claude Code.");
|
|
3414
4294
|
registerStatusCommand(program);
|
|
3415
4295
|
registerWatchCommand(program);
|
|
3416
4296
|
registerRunCommand(program);
|
|
@@ -3426,5 +4306,10 @@ registerQuotaCommand(program);
|
|
|
3426
4306
|
registerCompactCommand(program);
|
|
3427
4307
|
registerHooksCommand(program);
|
|
3428
4308
|
registerPrCommand(program);
|
|
4309
|
+
registerLogCommand(program);
|
|
4310
|
+
registerTemplateCommand(program);
|
|
4311
|
+
registerMultiCommand(program);
|
|
4312
|
+
registerReviewCommand(program);
|
|
4313
|
+
registerScheduleCommand(program);
|
|
3429
4314
|
program.parse();
|
|
3430
4315
|
//# sourceMappingURL=index.js.map
|