@askexenow/exe-os 0.9.294 → 0.9.296
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/deploy/compose/cloudflared/config.yml.example +14 -9
- package/deploy/compose/docker-compose.yml +86 -8
- package/deploy/compose/sso-edge/default.conf.template +87 -0
- package/deploy/compose/sso-edge/entrypoint.sh +23 -0
- package/deploy/compose/sso-edge/sso-redirect.conf +63 -0
- package/deploy/stack-manifests/v0.9.json +2 -2
- package/dist/active-agent-AFX2FODG.js +28 -0
- package/dist/active-agent-E2IJA7YX.js +27 -0
- package/dist/agentic-ontology-A2YUZK5O.js +25 -0
- package/dist/assets/com.askexe.exed.plist +4 -1
- package/dist/backfill-metadata-OC7EOD5U.js +600 -0
- package/dist/behaviors-H5ZOVHDH.js +46 -0
- package/dist/bin/agentic-ontology-backfill.js +5 -5
- package/dist/bin/agentic-reflection-backfill.js +6 -6
- package/dist/bin/agentic-semantic-label.js +5 -5
- package/dist/bin/backfill-conversations.js +6 -6
- package/dist/bin/backfill-responses.js +6 -6
- package/dist/bin/backfill-vectors.js +8 -8
- package/dist/bin/bulk-sync-postgres.js +7 -7
- package/dist/bin/cc-doctor.js +4 -4
- package/dist/bin/cleanup-stale-review-tasks.js +11 -11
- package/dist/bin/cli.js +16 -16
- package/dist/bin/deferred-daemon-restart.js +1 -1
- package/dist/bin/exe-agent-config.js +2 -2
- package/dist/bin/exe-agent.js +4 -4
- package/dist/bin/exe-assign.js +8 -8
- package/dist/bin/exe-boot.js +21 -18
- package/dist/bin/exe-call.js +4 -4
- package/dist/bin/exe-cloud.js +7 -7
- package/dist/bin/exe-dispatch.js +11 -11
- package/dist/bin/exe-doctor.js +3 -2
- package/dist/bin/exe-export-behaviors.js +7 -7
- package/dist/bin/exe-forget.js +6 -6
- package/dist/bin/exe-gateway.js +7 -7
- package/dist/bin/exe-healthcheck.js +6 -4
- package/dist/bin/exe-heartbeat.js +11 -11
- package/dist/bin/exe-kill.js +14 -14
- package/dist/bin/exe-launch-agent.js +18 -18
- package/dist/bin/exe-new-employee.js +6 -6
- package/dist/bin/exe-pending-messages.js +12 -12
- package/dist/bin/exe-pending-notifications.js +11 -11
- package/dist/bin/exe-pending-reviews.js +11 -11
- package/dist/bin/exe-rename.js +4 -4
- package/dist/bin/exe-review.js +13 -13
- package/dist/bin/exe-search.js +5 -5
- package/dist/bin/exe-session-cleanup.js +16 -16
- package/dist/bin/exe-settings.js +39 -9
- package/dist/bin/exe-start-codex.js +11 -11
- package/dist/bin/exe-start-opencode.js +8 -8
- package/dist/bin/exe-status.js +12 -12
- package/dist/bin/exe-team.js +3 -3
- package/dist/bin/git-sweep.js +12 -12
- package/dist/bin/graph-backfill.js +4 -4
- package/dist/bin/graph-export.js +5 -5
- package/dist/bin/import-history.js +7 -7
- package/dist/bin/install-launchd.js +13 -6
- package/dist/bin/install.js +26 -14
- package/dist/bin/intercom-check.js +4 -4
- package/dist/bin/mcp-sessions.js +2 -2
- package/dist/bin/orchestration-metrics.js +4 -4
- package/dist/bin/postgres-agentic-reflection-backfill.js +2 -2
- package/dist/bin/postgres-agentic-semantic-backfill.js +1 -1
- package/dist/bin/scan-tasks.js +11 -11
- package/dist/bin/setup.js +1 -1
- package/dist/bin/shard-migrate.js +4 -4
- package/dist/bin/stack-update.js +2 -2
- package/dist/bin/vps-health-gate.js +1 -1
- package/dist/capability-cards-4USI7CUW.js +89 -0
- package/dist/capacity-monitor-WLCBTEYR.js +51 -0
- package/dist/catchup-brief-ZR3NX6LZ.js +175 -0
- package/dist/chunk-22TVSRQQ.js +226 -0
- package/dist/chunk-2E43UXRH.js +395 -0
- package/dist/chunk-2PIGT6UJ.js +460 -0
- package/dist/chunk-3XTMW2MZ.js +535 -0
- package/dist/chunk-465PQFTH.js +262 -0
- package/dist/chunk-5CCXU2AW.js +129 -0
- package/dist/chunk-5D6MPWR7.js +1094 -0
- package/dist/chunk-5Q4MR6SL.js +123 -0
- package/dist/chunk-6327RBWR.js +345 -0
- package/dist/chunk-6MZZREZY.js +199 -0
- package/dist/chunk-7DI2Q4O5.js +1186 -0
- package/dist/chunk-7PW5VNIY.js +122 -0
- package/dist/chunk-7T7Y56HW.js +43 -0
- package/dist/chunk-7UHCWCLT.js +128 -0
- package/dist/chunk-A2ZUMF6L.js +1350 -0
- package/dist/chunk-AKV44JEH.js +185 -0
- package/dist/chunk-ANHWGX5N.js +735 -0
- package/dist/chunk-BQ3P4TKD.js +97 -0
- package/dist/chunk-BUZMT3KZ.js +604 -0
- package/dist/chunk-C2SBESBO.js +210 -0
- package/dist/chunk-CLSXZUZW.js +51 -0
- package/dist/chunk-CONHLVAR.js +1079 -0
- package/dist/chunk-D3WTZPFX.js +456 -0
- package/dist/chunk-DE6SOIYL.js +197 -0
- package/dist/chunk-EIVNMA3Q.js +284 -0
- package/dist/chunk-EJIF4FNT.js +12 -0
- package/dist/chunk-FDFOW564.js +171 -0
- package/dist/chunk-GZUBJ5EC.js +127 -0
- package/dist/chunk-HGZITN22.js +105 -0
- package/dist/chunk-HSRKDU6X.js +362 -0
- package/dist/chunk-IIEN2PHV.js +85 -0
- package/dist/chunk-JQ56VLMM.js +567 -0
- package/dist/chunk-JVHHXRFY.js +280 -0
- package/dist/chunk-JXCXGZ3S.js +55 -0
- package/dist/chunk-K5ZO532Q.js +4388 -0
- package/dist/chunk-K6CAAMXF.js +97 -0
- package/dist/chunk-KA26YTNU.js +81 -0
- package/dist/chunk-KMUW5C3R.js +381 -0
- package/dist/chunk-KOO3J5PV.js +20 -0
- package/dist/chunk-LSV7OFIH.js +290 -0
- package/dist/chunk-LSVFDVNY.js +1158 -0
- package/dist/chunk-LXDQTW32.js +230 -0
- package/dist/chunk-MEP7OUVZ.js +181 -0
- package/dist/chunk-MN2B2LKS.js +240 -0
- package/dist/chunk-N2EAYPYQ.js +1352 -0
- package/dist/chunk-N7I2A667.js +70 -0
- package/dist/chunk-NLZHVIOP.js +630 -0
- package/dist/chunk-NUH5TRZL.js +227 -0
- package/dist/chunk-OAHEIH3G.js +167 -0
- package/dist/chunk-OBHRQGCK.js +58 -0
- package/dist/chunk-ODFA7B2V.js +54 -0
- package/dist/chunk-OSNUP45F.js +731 -0
- package/dist/chunk-OTPRHBTO.js +33 -0
- package/dist/chunk-P6MUA4QU.js +157 -0
- package/dist/chunk-PGIOFKSK.js +2093 -0
- package/dist/chunk-PSE7VHWK.js +50 -0
- package/dist/chunk-QIFUVZFW.js +331 -0
- package/dist/chunk-RDPXKTVK.js +221 -0
- package/dist/chunk-RKYTYJGB.js +76 -0
- package/dist/chunk-RXLR6EFM.js +348 -0
- package/dist/chunk-SDB67PQJ.js +159 -0
- package/dist/chunk-SF2T7MP3.js +402 -0
- package/dist/chunk-SLU3FRFQ.js +2133 -0
- package/dist/chunk-SNDZJ5IV.js +214 -0
- package/dist/chunk-STEEAABW.js +448 -0
- package/dist/chunk-TUTWNHIQ.js +244 -0
- package/dist/chunk-UDP35QBR.js +30 -0
- package/dist/chunk-UKFHNJBI.js +85 -0
- package/dist/chunk-VC2DTK2X.js +382 -0
- package/dist/chunk-VRRAE5JX.js +836 -0
- package/dist/chunk-VVJTBQPR.js +38 -0
- package/dist/chunk-W3EQ362K.js +581 -0
- package/dist/chunk-WHIXIFHC.js +2242 -0
- package/dist/chunk-WRNGJJNR.js +377 -0
- package/dist/chunk-WUKHLCBE.js +3313 -0
- package/dist/chunk-WVPLHGDG.js +150 -0
- package/dist/chunk-XJZBSTL5.js +204 -0
- package/dist/chunk-Y3PMNUM5.js +304 -0
- package/dist/chunk-YHVS4QOV.js +14597 -0
- package/dist/chunk-YJ2OYAOC.js +668 -0
- package/dist/chunk-YYAD2GXX.js +128 -0
- package/dist/chunk-ZQML7EWE.js +333 -0
- package/dist/co-activation-XJLH46OX.js +74 -0
- package/dist/co-occurrence-GNN2X526.js +95 -0
- package/dist/code-context-index-OCPRLFG5.js +30 -0
- package/dist/core-memory-J4W2IYOF.js +110 -0
- package/dist/crdt-sync-QCBTSHIH.js +33 -0
- package/dist/crm-webhook-EM442VUW.js +10 -0
- package/dist/cto-delegation-gate-MLJMVHBK.js +280 -0
- package/dist/daemon-orchestration-2VNLZVTW.js +139 -0
- package/dist/db-backup-VUGFTPJ4.js +43 -0
- package/dist/doc-graph-extractor-PNRSFPSS.js +133 -0
- package/dist/dreaming-SK5VEQRF.js +34 -0
- package/dist/entity-boost-TQWWJUC2.js +375 -0
- package/dist/exe-drift-N34UPO7S.js +70 -0
- package/dist/exe-export-KACBKGVV.js +77 -0
- package/dist/exe-import-GXGDWACG.js +80 -0
- package/dist/exe-key-XPDOZBWW.js +673 -0
- package/dist/exe-snapshot-32GQKGQ5.js +338 -0
- package/dist/fast-db-init-F3TDD5VV.js +7 -0
- package/dist/gateway/index.js +8 -8
- package/dist/git-staleness-J45WNYRF.js +112 -0
- package/dist/git-task-sweep-BTGVQPFB.js +42 -0
- package/dist/global-procedures-6JCQWU4D.js +22 -0
- package/dist/graph-auto-extract-3ZQNXTPC.js +183 -0
- package/dist/hooks/bug-report-worker.js +13 -13
- package/dist/hooks/codex-stop-task-finalizer.js +13 -13
- package/dist/hooks/commit-complete.js +13 -13
- package/dist/hooks/error-recall.js +6 -6
- package/dist/hooks/exe-heartbeat-hook.js +3 -3
- package/dist/hooks/ingest-worker.js +3 -3
- package/dist/hooks/ingest.js +6 -6
- package/dist/hooks/instructions-loaded.js +4 -4
- package/dist/hooks/manifest.json +20 -20
- package/dist/hooks/notification.js +4 -4
- package/dist/hooks/post-compact.js +12 -12
- package/dist/hooks/post-tool-combined.js +6 -6
- package/dist/hooks/pre-compact.js +16 -16
- package/dist/hooks/pre-tool-use.js +16 -16
- package/dist/hooks/prompt-submit.js +24 -24
- package/dist/hooks/session-end.js +21 -21
- package/dist/hooks/session-start.js +12 -12
- package/dist/hooks/stop.js +19 -19
- package/dist/hooks/subagent-stop.js +12 -12
- package/dist/hooks/summary-worker.js +19 -19
- package/dist/index.js +19 -19
- package/dist/installer-5VPFY7SB.js +298 -0
- package/dist/installer-OENFPMA2.js +344 -0
- package/dist/installer-OIX4QOG5.js +40 -0
- package/dist/lib/cloud-sync.js +7 -7
- package/dist/lib/consolidation.js +6 -5
- package/dist/lib/database.js +2 -2
- package/dist/lib/db-daemon-client.js +2 -2
- package/dist/lib/db.js +2 -2
- package/dist/lib/embed-worker.js +1 -0
- package/dist/lib/embedder.js +7 -3
- package/dist/lib/employee-templates.js +4 -4
- package/dist/lib/employees.js +2 -2
- package/dist/lib/exe-daemon-client.js +2 -2
- package/dist/lib/exe-daemon.js +160 -79
- package/dist/lib/hybrid-search.js +5 -5
- package/dist/lib/identity.js +2 -2
- package/dist/lib/messaging.js +11 -11
- package/dist/lib/reminders.js +3 -3
- package/dist/lib/schedules.js +5 -5
- package/dist/lib/session-registry.js +4 -4
- package/dist/lib/skill-learning.js +6 -6
- package/dist/lib/store.js +4 -4
- package/dist/lib/task-router.js +3 -3
- package/dist/lib/tasks.js +12 -12
- package/dist/lib/tmux-routing.js +12 -10
- package/dist/lib/tmux-transport.js +1 -1
- package/dist/lib/token-spend.js +3 -3
- package/dist/lib/transport.js +2 -2
- package/dist/mcp/register-tools.js +62 -61
- package/dist/mcp/server.js +63 -62
- package/dist/mcp/tools/complete-reminder.js +4 -4
- package/dist/mcp/tools/create-reminder.js +4 -4
- package/dist/mcp/tools/create-task.js +14 -14
- package/dist/mcp/tools/deactivate-behavior.js +7 -7
- package/dist/mcp/tools/list-reminders.js +4 -4
- package/dist/mcp/tools/list-tasks.js +14 -14
- package/dist/mcp/tools/send-message.js +13 -13
- package/dist/mcp/tools/update-task.js +13 -13
- package/dist/mcp-http-config-PQTOLCTP.js +29 -0
- package/dist/memory-cards-4RVDZIY2.js +180 -0
- package/dist/memory-graph-extractor-L6YC7G4M.js +22 -0
- package/dist/memory-poisoning-defense-4YVJYH4G.js +224 -0
- package/dist/memory-queue-client-MVAUOZNJ.js +16 -0
- package/dist/memory-reflection-SHHDQNOH.js +244 -0
- package/dist/message-queue-client-DCKZT6X2.js +92 -0
- package/dist/notifications-JFR3G42W.js +47 -0
- package/dist/orchestration-events-MGCGPTDN.js +27 -0
- package/dist/orchestrator-DAFL2YZB.js +35 -0
- package/dist/pipeline-router-WWSZVPCH.js +15 -0
- package/dist/plan-limits-C7XCSDZC.js +28 -0
- package/dist/project-boot-N3NTBVLE.js +299 -0
- package/dist/projection-worker-MTPAPCWX.js +1084 -0
- package/dist/prospective-memory-BTIVUJSB.js +232 -0
- package/dist/reranker-UA6WVESJ.js +19 -0
- package/dist/retrieval-health-7XNZJEBF.js +12 -0
- package/dist/review-polling-4ALGMXC3.js +126 -0
- package/dist/runtime/index.js +13 -13
- package/dist/self-query-router-MROFQLQB.js +192 -0
- package/dist/session-events-CK44XOU4.js +38 -0
- package/dist/session-kill-telemetry-MT6ITDOG.js +31 -0
- package/dist/session-scope-3XDBWV65.js +88 -0
- package/dist/setup-wizard-X6DOD7MC.js +12 -0
- package/dist/skill-refinement-G2CCY3GM.js +159 -0
- package/dist/stack-update-JF7F56AS.js +84 -0
- package/dist/steward-gate-YF2CYXE7.js +15 -0
- package/dist/task-enforcement-YN6HK7NE.js +506 -0
- package/dist/task-scope-CVK6ISCZ.js +37 -0
- package/dist/tasks-crud-NTNET4JE.js +79 -0
- package/dist/tasks-notify-4LJVFPCV.js +40 -0
- package/dist/tasks-review-3V4WOIRG.js +49 -0
- package/dist/telemetry-upload-5PNUKGTM.js +741 -0
- package/dist/token-budget-E46G7ZAQ.js +86 -0
- package/dist/tool-capability-index-JDSMKJER.js +10 -0
- package/dist/tool-telemetry-J3NLS3LJ.js +17 -0
- package/dist/tui/App.js +18 -18
- package/dist/tui-data-6DOMUUCM.js +260 -0
- package/dist/wiki-acl-5UK37LKF.js +111 -0
- package/dist/worker-gate-FM7AEC7G.js +21 -0
- package/dist/workflow-engine-2EDUHUIY.js +28 -0
- package/dist/worktree-7YKKJIYR.js +28 -0
- package/dist/worktree-sweep-C3ELFGDN.js +21 -0
- package/package.json +1 -1
- package/release-notes.json +88 -88
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getGitRoot,
|
|
3
|
+
isWorktreeDirty
|
|
4
|
+
} from "./chunk-JVHHXRFY.js";
|
|
5
|
+
|
|
6
|
+
// src/lib/worktree-sweep.ts
|
|
7
|
+
import { execFileSync } from "child_process";
|
|
8
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
|
9
|
+
import path from "path";
|
|
10
|
+
var GIT_TIMEOUT_MS = 1e4;
|
|
11
|
+
var DIRTY_ORPHAN_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
12
|
+
function isWorktreeUnpushed(wtPath, branch) {
|
|
13
|
+
if (!branch) return false;
|
|
14
|
+
try {
|
|
15
|
+
const remote = execFileSync("git", ["-C", wtPath, "rev-parse", "--abbrev-ref", `${branch}@{upstream}`], { encoding: "utf-8", timeout: GIT_TIMEOUT_MS, stdio: ["pipe", "pipe", "ignore"] }).trim();
|
|
16
|
+
if (!remote) return true;
|
|
17
|
+
const ahead = execFileSync("git", ["-C", wtPath, "rev-list", `${remote}..${branch}`, "--count"], { encoding: "utf-8", timeout: GIT_TIMEOUT_MS, stdio: ["pipe", "pipe", "ignore"] }).trim();
|
|
18
|
+
return parseInt(ahead, 10) > 0;
|
|
19
|
+
} catch {
|
|
20
|
+
try {
|
|
21
|
+
let ahead = "";
|
|
22
|
+
try {
|
|
23
|
+
ahead = execFileSync("git", ["-C", wtPath, "rev-list", `main..${branch}`, "--count"], { encoding: "utf-8", timeout: GIT_TIMEOUT_MS, stdio: ["pipe", "pipe", "ignore"] }).trim();
|
|
24
|
+
} catch {
|
|
25
|
+
ahead = execFileSync("git", ["-C", wtPath, "rev-list", `master..${branch}`, "--count"], { encoding: "utf-8", timeout: GIT_TIMEOUT_MS, stdio: ["pipe", "pipe", "ignore"] }).trim();
|
|
26
|
+
}
|
|
27
|
+
return parseInt(ahead, 10) > 0;
|
|
28
|
+
} catch {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
var REPO_ALIASES = {
|
|
34
|
+
"exe-crm": ["openclaw", "exe-crm"],
|
|
35
|
+
"exe-wiki": ["ink", "exe-wiki"]
|
|
36
|
+
};
|
|
37
|
+
function discoverRepoPaths() {
|
|
38
|
+
const roots = [];
|
|
39
|
+
let exeOsRoot = null;
|
|
40
|
+
let dir = path.dirname(new URL(import.meta.url).pathname);
|
|
41
|
+
for (let i = 0; i < 5; i++) {
|
|
42
|
+
const pkgPath = path.join(dir, "package.json");
|
|
43
|
+
if (existsSync(pkgPath)) {
|
|
44
|
+
try {
|
|
45
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
46
|
+
if (pkg.name === "@askexenow/exe-os") {
|
|
47
|
+
exeOsRoot = dir;
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
dir = path.dirname(dir);
|
|
54
|
+
}
|
|
55
|
+
if (!exeOsRoot) {
|
|
56
|
+
exeOsRoot = process.cwd();
|
|
57
|
+
}
|
|
58
|
+
roots.push(exeOsRoot);
|
|
59
|
+
const parentDir = path.dirname(exeOsRoot);
|
|
60
|
+
try {
|
|
61
|
+
const siblings = readdirSync(parentDir);
|
|
62
|
+
for (const sibling of siblings) {
|
|
63
|
+
if (sibling.startsWith(".")) continue;
|
|
64
|
+
const siblingPath = path.join(parentDir, sibling);
|
|
65
|
+
if (siblingPath === exeOsRoot) continue;
|
|
66
|
+
const root = getGitRoot(siblingPath);
|
|
67
|
+
if (root && !roots.includes(root)) {
|
|
68
|
+
roots.push(root);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
}
|
|
73
|
+
for (const aliases of Object.values(REPO_ALIASES)) {
|
|
74
|
+
for (const alias of aliases) {
|
|
75
|
+
const aliasPath = path.join(parentDir, alias);
|
|
76
|
+
if (existsSync(aliasPath)) {
|
|
77
|
+
const root = getGitRoot(aliasPath);
|
|
78
|
+
if (root && !roots.includes(root)) {
|
|
79
|
+
roots.push(root);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return roots;
|
|
85
|
+
}
|
|
86
|
+
function listWorktrees(repoRoot) {
|
|
87
|
+
try {
|
|
88
|
+
const output = execFileSync("git", ["worktree", "list", "--porcelain"], {
|
|
89
|
+
cwd: repoRoot,
|
|
90
|
+
encoding: "utf8",
|
|
91
|
+
timeout: GIT_TIMEOUT_MS,
|
|
92
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
93
|
+
});
|
|
94
|
+
const worktrees = [];
|
|
95
|
+
let currentPath = "";
|
|
96
|
+
for (const line of output.split("\n")) {
|
|
97
|
+
if (line.startsWith("worktree ")) {
|
|
98
|
+
currentPath = line.slice("worktree ".length);
|
|
99
|
+
} else if (line.startsWith("branch ") && currentPath) {
|
|
100
|
+
const branch = line.slice("branch refs/heads/".length);
|
|
101
|
+
worktrees.push({ path: currentPath, branch });
|
|
102
|
+
currentPath = "";
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return worktrees;
|
|
106
|
+
} catch {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function detectMainBranch(repoRoot) {
|
|
111
|
+
for (const candidate of ["main", "master"]) {
|
|
112
|
+
try {
|
|
113
|
+
execFileSync("git", ["rev-parse", "--verify", `refs/heads/${candidate}`], {
|
|
114
|
+
cwd: repoRoot,
|
|
115
|
+
encoding: "utf8",
|
|
116
|
+
timeout: GIT_TIMEOUT_MS,
|
|
117
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
118
|
+
});
|
|
119
|
+
return candidate;
|
|
120
|
+
} catch {
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
function isBranchMerged(repoRoot, branch, into) {
|
|
126
|
+
try {
|
|
127
|
+
const merged = execFileSync("git", ["branch", "--merged", into], {
|
|
128
|
+
cwd: repoRoot,
|
|
129
|
+
encoding: "utf8",
|
|
130
|
+
timeout: GIT_TIMEOUT_MS,
|
|
131
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
132
|
+
});
|
|
133
|
+
return merged.split("\n").map((l) => l.replace(/^[*+]?\s+/, "").trim()).includes(branch);
|
|
134
|
+
} catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function hasLiveTmuxSession(agentName) {
|
|
139
|
+
try {
|
|
140
|
+
const sessions = execFileSync("tmux", ["list-sessions", "-F", "#S"], {
|
|
141
|
+
encoding: "utf8",
|
|
142
|
+
timeout: 5e3,
|
|
143
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
144
|
+
}).trim();
|
|
145
|
+
if (!sessions) return false;
|
|
146
|
+
for (const session of sessions.split("\n")) {
|
|
147
|
+
const agentPart = session.split("-")[0];
|
|
148
|
+
if (!agentPart) continue;
|
|
149
|
+
const baseName = agentPart.replace(/\d+$/, "");
|
|
150
|
+
if (baseName === agentName || agentPart === agentName) return true;
|
|
151
|
+
}
|
|
152
|
+
return false;
|
|
153
|
+
} catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function removeWorktree(repoRoot, wtPath) {
|
|
158
|
+
try {
|
|
159
|
+
execFileSync("git", ["worktree", "remove", wtPath, "--force"], {
|
|
160
|
+
cwd: repoRoot,
|
|
161
|
+
encoding: "utf8",
|
|
162
|
+
timeout: GIT_TIMEOUT_MS,
|
|
163
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
164
|
+
});
|
|
165
|
+
return true;
|
|
166
|
+
} catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function deleteBranch(repoRoot, branch) {
|
|
171
|
+
try {
|
|
172
|
+
execFileSync("git", ["branch", "-D", branch], {
|
|
173
|
+
cwd: repoRoot,
|
|
174
|
+
encoding: "utf8",
|
|
175
|
+
timeout: GIT_TIMEOUT_MS,
|
|
176
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
177
|
+
});
|
|
178
|
+
return true;
|
|
179
|
+
} catch {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function deleteRemoteBranch(repoRoot, branch) {
|
|
184
|
+
try {
|
|
185
|
+
execFileSync("git", ["push", "origin", "--delete", branch], {
|
|
186
|
+
cwd: repoRoot,
|
|
187
|
+
encoding: "utf8",
|
|
188
|
+
timeout: 15e3,
|
|
189
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
190
|
+
});
|
|
191
|
+
return true;
|
|
192
|
+
} catch {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function pruneWorktreeRegistry(repoRoot) {
|
|
197
|
+
try {
|
|
198
|
+
execFileSync("git", ["worktree", "prune"], {
|
|
199
|
+
cwd: repoRoot,
|
|
200
|
+
encoding: "utf8",
|
|
201
|
+
timeout: GIT_TIMEOUT_MS,
|
|
202
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
203
|
+
});
|
|
204
|
+
} catch {
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function clearStashes(repoRoot) {
|
|
208
|
+
try {
|
|
209
|
+
const stashList = execFileSync("git", ["stash", "list"], {
|
|
210
|
+
cwd: repoRoot,
|
|
211
|
+
encoding: "utf8",
|
|
212
|
+
timeout: GIT_TIMEOUT_MS,
|
|
213
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
214
|
+
}).trim();
|
|
215
|
+
if (!stashList) return 0;
|
|
216
|
+
const count = stashList.split("\n").length;
|
|
217
|
+
execFileSync("git", ["stash", "clear"], {
|
|
218
|
+
cwd: repoRoot,
|
|
219
|
+
encoding: "utf8",
|
|
220
|
+
timeout: GIT_TIMEOUT_MS,
|
|
221
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
222
|
+
});
|
|
223
|
+
return count;
|
|
224
|
+
} catch {
|
|
225
|
+
return 0;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function parseAgentName(wtPath) {
|
|
229
|
+
const dirName = path.basename(wtPath);
|
|
230
|
+
if (!dirName) return null;
|
|
231
|
+
return dirName.replace(/\d+$/, "") || dirName;
|
|
232
|
+
}
|
|
233
|
+
function getDirMtimeMs(dirPath) {
|
|
234
|
+
try {
|
|
235
|
+
return statSync(dirPath).mtimeMs;
|
|
236
|
+
} catch {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function archiveDirtyWorktree(wtPath, agentName) {
|
|
241
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
242
|
+
const quarantineBranch = `quarantine/${agentName}-${timestamp}`;
|
|
243
|
+
try {
|
|
244
|
+
execFileSync("git", ["-C", wtPath, "checkout", "-b", quarantineBranch], {
|
|
245
|
+
encoding: "utf8",
|
|
246
|
+
timeout: GIT_TIMEOUT_MS,
|
|
247
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
248
|
+
});
|
|
249
|
+
execFileSync("git", ["-C", wtPath, "add", "-A"], {
|
|
250
|
+
encoding: "utf8",
|
|
251
|
+
timeout: GIT_TIMEOUT_MS,
|
|
252
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
253
|
+
});
|
|
254
|
+
execFileSync("git", [
|
|
255
|
+
"-C",
|
|
256
|
+
wtPath,
|
|
257
|
+
"commit",
|
|
258
|
+
"-m",
|
|
259
|
+
`[quarantine] Auto-archived dirty orphan worktree
|
|
260
|
+
|
|
261
|
+
Agent: ${agentName}
|
|
262
|
+
Original worktree: ${wtPath}
|
|
263
|
+
Archived by worktree-sweep after ${DIRTY_ORPHAN_MAX_AGE_MS / 864e5}-day dirty orphan threshold.`
|
|
264
|
+
], {
|
|
265
|
+
encoding: "utf8",
|
|
266
|
+
timeout: GIT_TIMEOUT_MS,
|
|
267
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
268
|
+
env: {
|
|
269
|
+
...process.env,
|
|
270
|
+
GIT_AUTHOR_NAME: "exe-os daemon",
|
|
271
|
+
GIT_AUTHOR_EMAIL: "daemon@exe-os.local",
|
|
272
|
+
GIT_COMMITTER_NAME: "exe-os daemon",
|
|
273
|
+
GIT_COMMITTER_EMAIL: "daemon@exe-os.local"
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
process.stderr.write(`[worktree-sweep] Archived dirty changes to branch ${quarantineBranch}
|
|
277
|
+
`);
|
|
278
|
+
return true;
|
|
279
|
+
} catch {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
async function sweepWorktrees(options = {}) {
|
|
284
|
+
const dryRun = options.dryRun ?? false;
|
|
285
|
+
const repoPaths = options.repoPaths ?? discoverRepoPaths();
|
|
286
|
+
const result = {
|
|
287
|
+
reposScanned: 0,
|
|
288
|
+
removed: [],
|
|
289
|
+
branchesDeleted: [],
|
|
290
|
+
stashesCleared: 0,
|
|
291
|
+
skipped: [],
|
|
292
|
+
errors: []
|
|
293
|
+
};
|
|
294
|
+
for (const repoPath of repoPaths) {
|
|
295
|
+
const repoRoot = getGitRoot(repoPath);
|
|
296
|
+
if (!repoRoot) continue;
|
|
297
|
+
result.reposScanned++;
|
|
298
|
+
const repoName = path.basename(repoRoot);
|
|
299
|
+
const mainBranch = detectMainBranch(repoRoot);
|
|
300
|
+
if (!dryRun) {
|
|
301
|
+
pruneWorktreeRegistry(repoRoot);
|
|
302
|
+
}
|
|
303
|
+
const worktrees = listWorktrees(repoRoot);
|
|
304
|
+
for (const wt of worktrees) {
|
|
305
|
+
if (!wt.path.includes("/.worktrees/")) continue;
|
|
306
|
+
const agentName = parseAgentName(wt.path);
|
|
307
|
+
const wtName = path.basename(wt.path);
|
|
308
|
+
if (agentName && hasLiveTmuxSession(agentName)) {
|
|
309
|
+
result.skipped.push({
|
|
310
|
+
repo: repoName,
|
|
311
|
+
worktree: wtName,
|
|
312
|
+
branch: wt.branch,
|
|
313
|
+
reason: "live tmux session"
|
|
314
|
+
});
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if (existsSync(wt.path) && isWorktreeDirty(wt.path)) {
|
|
318
|
+
const mtimeMs = getDirMtimeMs(wt.path);
|
|
319
|
+
const ageMs = mtimeMs !== null ? Date.now() - mtimeMs : 0;
|
|
320
|
+
if (ageMs >= DIRTY_ORPHAN_MAX_AGE_MS && agentName && !dryRun) {
|
|
321
|
+
const ageD = Math.round(ageMs / 864e5);
|
|
322
|
+
process.stderr.write(
|
|
323
|
+
`[worktree-sweep] Dirty orphan aged out (${ageD}d), archiving: ${repoName}/.worktrees/${wtName}
|
|
324
|
+
`
|
|
325
|
+
);
|
|
326
|
+
const archived = archiveDirtyWorktree(wt.path, agentName);
|
|
327
|
+
if (archived) {
|
|
328
|
+
} else {
|
|
329
|
+
result.skipped.push({
|
|
330
|
+
repo: repoName,
|
|
331
|
+
worktree: wtName,
|
|
332
|
+
branch: wt.branch,
|
|
333
|
+
reason: `uncommitted changes (archive failed after ${ageD}d)`
|
|
334
|
+
});
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
} else if (ageMs >= DIRTY_ORPHAN_MAX_AGE_MS && dryRun) {
|
|
338
|
+
const ageD = Math.round(ageMs / 864e5);
|
|
339
|
+
result.removed.push({ repo: repoName, worktree: wtName, branch: wt.branch });
|
|
340
|
+
process.stderr.write(
|
|
341
|
+
`[worktree-sweep] [DRY-RUN] Would archive+remove dirty orphan (${ageD}d): ${repoName}/.worktrees/${wtName}
|
|
342
|
+
`
|
|
343
|
+
);
|
|
344
|
+
continue;
|
|
345
|
+
} else {
|
|
346
|
+
result.skipped.push({
|
|
347
|
+
repo: repoName,
|
|
348
|
+
worktree: wtName,
|
|
349
|
+
branch: wt.branch,
|
|
350
|
+
reason: "uncommitted changes"
|
|
351
|
+
});
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (existsSync(wt.path) && isWorktreeUnpushed(wt.path, wt.branch)) {
|
|
356
|
+
result.skipped.push({
|
|
357
|
+
repo: repoName,
|
|
358
|
+
worktree: wtName,
|
|
359
|
+
branch: wt.branch,
|
|
360
|
+
reason: "unpushed commits \u2014 push first or use --force"
|
|
361
|
+
});
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
const isMerged = mainBranch ? isBranchMerged(repoRoot, wt.branch, mainBranch) : false;
|
|
365
|
+
if (dryRun) {
|
|
366
|
+
result.removed.push({ repo: repoName, worktree: wtName, branch: wt.branch });
|
|
367
|
+
if (isMerged || !mainBranch) {
|
|
368
|
+
result.branchesDeleted.push({ repo: repoName, branch: wt.branch, remote: true });
|
|
369
|
+
} else {
|
|
370
|
+
result.branchesDeleted.push({ repo: repoName, branch: wt.branch, remote: false });
|
|
371
|
+
}
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
const removed = removeWorktree(repoRoot, wt.path);
|
|
375
|
+
if (!removed) {
|
|
376
|
+
result.errors.push(`${repoName}: failed to remove worktree ${wtName}`);
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
result.removed.push({ repo: repoName, worktree: wtName, branch: wt.branch });
|
|
380
|
+
process.stderr.write(`[worktree-sweep] Removed ${repoName}/.worktrees/${wtName} (branch: ${wt.branch})
|
|
381
|
+
`);
|
|
382
|
+
const localDeleted = deleteBranch(repoRoot, wt.branch);
|
|
383
|
+
let remoteDeleted = false;
|
|
384
|
+
if (localDeleted) {
|
|
385
|
+
remoteDeleted = deleteRemoteBranch(repoRoot, wt.branch);
|
|
386
|
+
}
|
|
387
|
+
if (localDeleted) {
|
|
388
|
+
result.branchesDeleted.push({ repo: repoName, branch: wt.branch, remote: remoteDeleted });
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (!dryRun) {
|
|
392
|
+
const cleared = clearStashes(repoRoot);
|
|
393
|
+
if (cleared > 0) {
|
|
394
|
+
result.stashesCleared += cleared;
|
|
395
|
+
process.stderr.write(`[worktree-sweep] Cleared ${cleared} stash(es) in ${repoName}
|
|
396
|
+
`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
if (!dryRun) {
|
|
400
|
+
pruneWorktreeRegistry(repoRoot);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return result;
|
|
404
|
+
}
|
|
405
|
+
function cleanupAgentWorktree(repoRoot, agentName) {
|
|
406
|
+
const worktreesDir = path.join(repoRoot, ".worktrees");
|
|
407
|
+
if (!existsSync(worktreesDir)) return "";
|
|
408
|
+
const notes = [];
|
|
409
|
+
try {
|
|
410
|
+
const entries = readdirSync(worktreesDir);
|
|
411
|
+
for (const entry of entries) {
|
|
412
|
+
const baseName = entry.replace(/\d+$/, "") || entry;
|
|
413
|
+
const agentBase = agentName.replace(/\d+$/, "") || agentName;
|
|
414
|
+
if (baseName !== agentBase) continue;
|
|
415
|
+
const wtPath = path.join(worktreesDir, entry);
|
|
416
|
+
const branch = getWorktreeBranch(repoRoot, wtPath);
|
|
417
|
+
if (isWorktreeDirty(wtPath)) {
|
|
418
|
+
notes.push(`Worktree .worktrees/${entry} has uncommitted changes \u2014 preserved`);
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
if (isWorktreeUnpushed(wtPath, branch)) {
|
|
422
|
+
notes.push(`Worktree .worktrees/${entry} has unpushed commits \u2014 preserved`);
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
if (removeWorktree(repoRoot, wtPath)) {
|
|
426
|
+
notes.push(`Removed worktree .worktrees/${entry}`);
|
|
427
|
+
if (branch && deleteBranch(repoRoot, branch)) {
|
|
428
|
+
notes.push(`Deleted branch ${branch}`);
|
|
429
|
+
if (deleteRemoteBranch(repoRoot, branch)) {
|
|
430
|
+
notes.push(`Deleted remote branch ${branch}`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
} catch {
|
|
436
|
+
}
|
|
437
|
+
pruneWorktreeRegistry(repoRoot);
|
|
438
|
+
return notes.join("\n");
|
|
439
|
+
}
|
|
440
|
+
function getWorktreeBranch(repoRoot, wtPath) {
|
|
441
|
+
try {
|
|
442
|
+
const worktrees = listWorktrees(repoRoot);
|
|
443
|
+
for (const wt of worktrees) {
|
|
444
|
+
if (wt.path === wtPath) return wt.branch;
|
|
445
|
+
}
|
|
446
|
+
return null;
|
|
447
|
+
} catch {
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export {
|
|
453
|
+
discoverRepoPaths,
|
|
454
|
+
sweepWorktrees,
|
|
455
|
+
cleanupAgentWorktree
|
|
456
|
+
};
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import {
|
|
2
|
+
initStore
|
|
3
|
+
} from "./chunk-VRRAE5JX.js";
|
|
4
|
+
import {
|
|
5
|
+
getClient,
|
|
6
|
+
isInitialized
|
|
7
|
+
} from "./chunk-WUKHLCBE.js";
|
|
8
|
+
|
|
9
|
+
// src/lib/schedules.ts
|
|
10
|
+
import crypto from "crypto";
|
|
11
|
+
import { execSync } from "child_process";
|
|
12
|
+
var CRON_FIELD = /^[\d*/,\-]+$/;
|
|
13
|
+
function isValidCron(cron) {
|
|
14
|
+
const fields = cron.trim().split(/\s+/);
|
|
15
|
+
if (fields.length !== 5) return false;
|
|
16
|
+
return fields.every((f) => CRON_FIELD.test(f));
|
|
17
|
+
}
|
|
18
|
+
var SAFE_ID = /^[a-zA-Z0-9_\-]+$/;
|
|
19
|
+
function isValidScheduleId(id) {
|
|
20
|
+
return SAFE_ID.test(id) && id.length <= 128;
|
|
21
|
+
}
|
|
22
|
+
async function ensureDb() {
|
|
23
|
+
if (!isInitialized()) {
|
|
24
|
+
await initStore();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function parseHumanCron(input) {
|
|
28
|
+
const s = input.toLowerCase().trim();
|
|
29
|
+
if (/^[\d*\/,-]+\s+[\d*\/,-]+\s+[\d*\/,-]+\s+[\d*\/,-]+\s+[\d*\/,-]+$/.test(s)) {
|
|
30
|
+
return s;
|
|
31
|
+
}
|
|
32
|
+
const dayMap = {
|
|
33
|
+
sunday: "0",
|
|
34
|
+
sun: "0",
|
|
35
|
+
monday: "1",
|
|
36
|
+
mon: "1",
|
|
37
|
+
tuesday: "2",
|
|
38
|
+
tue: "2",
|
|
39
|
+
wednesday: "3",
|
|
40
|
+
wed: "3",
|
|
41
|
+
thursday: "4",
|
|
42
|
+
thu: "4",
|
|
43
|
+
friday: "5",
|
|
44
|
+
fri: "5",
|
|
45
|
+
saturday: "6",
|
|
46
|
+
sat: "6"
|
|
47
|
+
};
|
|
48
|
+
const everyMatch = s.match(/every\s+(\d+)\s*([mh])/);
|
|
49
|
+
if (everyMatch) {
|
|
50
|
+
const n = everyMatch[1];
|
|
51
|
+
const unit = everyMatch[2];
|
|
52
|
+
if (unit === "m") return `*/${n} * * * *`;
|
|
53
|
+
if (unit === "h") return `0 */${n} * * *`;
|
|
54
|
+
}
|
|
55
|
+
let hour = -1;
|
|
56
|
+
let minute = 0;
|
|
57
|
+
if (/midnight/.test(s)) {
|
|
58
|
+
hour = 0;
|
|
59
|
+
} else if (/noon/.test(s)) {
|
|
60
|
+
hour = 12;
|
|
61
|
+
} else {
|
|
62
|
+
const timeMatch = s.match(/(\d{1,2})(?::(\d{2}))?\s*(am|pm)?/);
|
|
63
|
+
if (timeMatch) {
|
|
64
|
+
hour = parseInt(timeMatch[1], 10);
|
|
65
|
+
minute = timeMatch[2] ? parseInt(timeMatch[2], 10) : 0;
|
|
66
|
+
if (timeMatch[3] === "pm" && hour < 12) hour += 12;
|
|
67
|
+
if (timeMatch[3] === "am" && hour === 12) hour = 0;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (hour === -1) hour = 0;
|
|
71
|
+
let dow = "*";
|
|
72
|
+
if (/weekday|weekdays/.test(s)) {
|
|
73
|
+
dow = "1-5";
|
|
74
|
+
} else if (/weekend|weekends/.test(s)) {
|
|
75
|
+
dow = "0,6";
|
|
76
|
+
} else {
|
|
77
|
+
for (const [name, val] of Object.entries(dayMap)) {
|
|
78
|
+
if (s.includes(name)) {
|
|
79
|
+
dow = val;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return `${minute} ${hour} * * ${dow}`;
|
|
85
|
+
}
|
|
86
|
+
async function createSchedule(input) {
|
|
87
|
+
if (!isValidCron(input.cron)) {
|
|
88
|
+
throw new Error(`Invalid cron expression: ${input.cron}. Must be 5 fields with only digits, *, /, -, comma.`);
|
|
89
|
+
}
|
|
90
|
+
await ensureDb();
|
|
91
|
+
const client = getClient();
|
|
92
|
+
const id = crypto.randomUUID().slice(0, 8);
|
|
93
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
94
|
+
const prompt = input.prompt ?? input.description;
|
|
95
|
+
await client.execute({
|
|
96
|
+
sql: `INSERT INTO schedules (id, cron, description, job_type, prompt, assigned_to, project_name, active, use_crontab, created_at)
|
|
97
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?, ?)`,
|
|
98
|
+
args: [
|
|
99
|
+
id,
|
|
100
|
+
input.cron,
|
|
101
|
+
input.description,
|
|
102
|
+
input.jobType ?? "report",
|
|
103
|
+
prompt,
|
|
104
|
+
input.assignedTo ?? null,
|
|
105
|
+
input.projectName ?? null,
|
|
106
|
+
input.useCrontab ? 1 : 0,
|
|
107
|
+
now
|
|
108
|
+
]
|
|
109
|
+
});
|
|
110
|
+
if (input.useCrontab) {
|
|
111
|
+
addToCrontab(id, input.cron, prompt, input.projectName);
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
id,
|
|
115
|
+
cron: input.cron,
|
|
116
|
+
description: input.description,
|
|
117
|
+
jobType: input.jobType ?? "report",
|
|
118
|
+
prompt,
|
|
119
|
+
assignedTo: input.assignedTo,
|
|
120
|
+
projectName: input.projectName,
|
|
121
|
+
active: true,
|
|
122
|
+
useCrontab: input.useCrontab ?? false,
|
|
123
|
+
createdAt: now
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
async function listSchedules(activeOnly = true) {
|
|
127
|
+
await ensureDb();
|
|
128
|
+
const client = getClient();
|
|
129
|
+
const sql = activeOnly ? "SELECT * FROM schedules WHERE active = 1 ORDER BY created_at ASC" : "SELECT * FROM schedules ORDER BY created_at ASC";
|
|
130
|
+
const result = await client.execute({ sql, args: [] });
|
|
131
|
+
return result.rows.map((row) => ({
|
|
132
|
+
id: String(row.id),
|
|
133
|
+
cron: String(row.cron),
|
|
134
|
+
description: String(row.description),
|
|
135
|
+
jobType: String(row.job_type),
|
|
136
|
+
prompt: row.prompt ? String(row.prompt) : void 0,
|
|
137
|
+
assignedTo: row.assigned_to ? String(row.assigned_to) : void 0,
|
|
138
|
+
projectName: row.project_name ? String(row.project_name) : void 0,
|
|
139
|
+
active: Number(row.active) === 1,
|
|
140
|
+
useCrontab: Number(row.use_crontab) === 1,
|
|
141
|
+
createdAt: String(row.created_at)
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
144
|
+
async function deleteSchedule(id) {
|
|
145
|
+
await ensureDb();
|
|
146
|
+
const client = getClient();
|
|
147
|
+
const existing = await client.execute({
|
|
148
|
+
sql: "SELECT use_crontab FROM schedules WHERE id = ?",
|
|
149
|
+
args: [id]
|
|
150
|
+
});
|
|
151
|
+
if (existing.rows.length === 0) return false;
|
|
152
|
+
const usesCrontab = Number(existing.rows[0].use_crontab) === 1;
|
|
153
|
+
await client.execute({
|
|
154
|
+
sql: "DELETE FROM schedules WHERE id = ?",
|
|
155
|
+
args: [id]
|
|
156
|
+
});
|
|
157
|
+
if (usesCrontab) {
|
|
158
|
+
removeFromCrontab(id);
|
|
159
|
+
}
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
function addToCrontab(id, cron, prompt, projectDir) {
|
|
163
|
+
if (!isValidCron(cron)) {
|
|
164
|
+
throw new Error(`Invalid cron expression: ${cron}`);
|
|
165
|
+
}
|
|
166
|
+
if (!isValidScheduleId(id)) {
|
|
167
|
+
throw new Error(`Invalid schedule ID: ${id}`);
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
const cwd = projectDir ? `cd ${JSON.stringify(projectDir)} && ` : "";
|
|
171
|
+
const escapedPrompt = prompt.replace(/"/g, '\\"');
|
|
172
|
+
const entry = `${cron} ${cwd}claude -p --dangerously-skip-permissions "${escapedPrompt}" # exe-schedule:${id}`;
|
|
173
|
+
execSync(
|
|
174
|
+
`(crontab -l 2>/dev/null; echo ${JSON.stringify(entry)}) | crontab -`,
|
|
175
|
+
{ timeout: 5e3, stdio: "ignore" }
|
|
176
|
+
);
|
|
177
|
+
} catch (err) {
|
|
178
|
+
if (err instanceof Error && err.message.startsWith("Invalid")) throw err;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function removeFromCrontab(id) {
|
|
182
|
+
if (!isValidScheduleId(id)) return;
|
|
183
|
+
try {
|
|
184
|
+
execSync(
|
|
185
|
+
`crontab -l 2>/dev/null | grep -v "exe-schedule:${id}" | crontab -`,
|
|
186
|
+
{ timeout: 5e3, stdio: "ignore" }
|
|
187
|
+
);
|
|
188
|
+
} catch {
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export {
|
|
193
|
+
parseHumanCron,
|
|
194
|
+
createSchedule,
|
|
195
|
+
listSchedules,
|
|
196
|
+
deleteSchedule
|
|
197
|
+
};
|