@rubytech/create-maxy-code 0.1.368 → 0.1.369
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/cron-heartbeat-registration.test.js +100 -0
- package/dist/cron-registration.js +72 -0
- package/dist/index.js +50 -3
- package/dist/uninstall.js +31 -0
- package/package.json +1 -1
- package/payload/platform/plugins/email/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/email/PLUGIN.md +5 -1
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-delete.test.d.ts +2 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-delete.test.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-delete.test.js +151 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-delete.test.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/index.js +29 -0
- package/payload/platform/plugins/email/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts +23 -0
- package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/imap.js +66 -0
- package/payload/platform/plugins/email/mcp/dist/lib/imap.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-delete.d.ts +21 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-delete.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-delete.js +51 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-delete.js.map +1 -0
- package/payload/platform/plugins/email/references/email-reference.md +10 -0
- package/payload/platform/plugins/email/skills/email-composition/SKILL.md +21 -4
- package/payload/platform/plugins/scheduling/PLUGIN.md +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.d.ts +5 -5
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +5 -5
- package/payload/platform/plugins/workflows/PLUGIN.md +2 -2
- package/payload/platform/plugins/workflows/skills/workflow-manager/SKILL.md +1 -1
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +1 -0
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
- package/payload/platform/templates/specialists/agents/personal-assistant.md +1 -1
- package/payload/platform/templates/specialists/agents/project-manager.md +1 -1
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Acceptance gate for the restored scheduling-watcher heartbeat cron.
|
|
2
|
+
//
|
|
3
|
+
// The installer registers exactly ONE install-level cron line that ticks
|
|
4
|
+
// check-due-events.js once a minute. These invariants protect the two
|
|
5
|
+
// regressions that the original block caused (and that its narrowed
|
|
6
|
+
// replacement must not reintroduce), plus the marker contract the admin
|
|
7
|
+
// `system-status` check reads back.
|
|
8
|
+
//
|
|
9
|
+
// (a) The block carries a single heartbeat entry — no email-fetch /
|
|
10
|
+
// email-auto-respond line. Those dispatchers act without an operator
|
|
11
|
+
// booking and stay unscheduled by the installer.
|
|
12
|
+
// (b) The entry never `mkdir`s an account log dir. The legacy line ran
|
|
13
|
+
// `mkdir -p data/accounts/<id>/logs` every 60s, tripping
|
|
14
|
+
// setup-account/seed-neo4j's stub-account-dirs guard on reinstall.
|
|
15
|
+
// Logs land under <persistDir>/logs, outside the seed-guard zone.
|
|
16
|
+
// (c) NEO4J_URI is pinned to bolt://localhost:<brand port>. Cron inherits
|
|
17
|
+
// no shell env, so an unpinned URI would default to 7687 and leak
|
|
18
|
+
// across co-resident brand installs.
|
|
19
|
+
// (d) The BEGIN/END markers equal `# BEGIN <PRODUCT_UPPER> CRONS` — the
|
|
20
|
+
// exact strings admin `system-status` greps to report registration.
|
|
21
|
+
// (e) Re-running the merge is idempotent: a re-install replaces the block,
|
|
22
|
+
// never stacks a second copy.
|
|
23
|
+
import test from "node:test";
|
|
24
|
+
import assert from "node:assert/strict";
|
|
25
|
+
import { cronBlockMarkers, buildHeartbeatCronBlock, stripCronBlock, mergeHeartbeatBlock, } from "../cron-registration.js";
|
|
26
|
+
const OPTS = {
|
|
27
|
+
productName: "maxy-code",
|
|
28
|
+
platformRoot: "/home/me/maxy-code/platform",
|
|
29
|
+
neo4jPort: 17687,
|
|
30
|
+
nodeBin: "/usr/bin/node",
|
|
31
|
+
logDir: "/home/me/.maxy-code/logs",
|
|
32
|
+
};
|
|
33
|
+
test("markers are # BEGIN/END <PRODUCT_UPPER> CRONS (admin system-status contract)", () => {
|
|
34
|
+
const { begin, end } = cronBlockMarkers("maxy-code");
|
|
35
|
+
assert.equal(begin, "# BEGIN MAXY-CODE CRONS");
|
|
36
|
+
assert.equal(end, "# END MAXY-CODE CRONS");
|
|
37
|
+
});
|
|
38
|
+
test("block has exactly one heartbeat entry, no email lines", () => {
|
|
39
|
+
const block = buildHeartbeatCronBlock(OPTS);
|
|
40
|
+
const entries = block.split("\n").filter((l) => l.trim() && !l.trim().startsWith("#"));
|
|
41
|
+
assert.equal(entries.length, 1, "expected exactly one cron entry");
|
|
42
|
+
assert.match(entries[0], / # heartbeat$/, "entry must be tagged # heartbeat");
|
|
43
|
+
assert.doesNotMatch(block, /email-fetch/, "email-fetch must not be scheduled");
|
|
44
|
+
assert.doesNotMatch(block, /email-auto-respond/, "email-auto-respond must not be scheduled");
|
|
45
|
+
});
|
|
46
|
+
test("heartbeat entry never mkdirs an account log dir (Task 039 guard)", () => {
|
|
47
|
+
const block = buildHeartbeatCronBlock(OPTS);
|
|
48
|
+
assert.doesNotMatch(block, /mkdir/, "no mkdir in the cron line");
|
|
49
|
+
assert.doesNotMatch(block, /data\/accounts/, "must not touch the seed-guard account dir");
|
|
50
|
+
assert.match(block, />> \/home\/me\/\.maxy-code\/logs\/check-due-events\.log 2>&1/, "log must redirect to <persistDir>/logs");
|
|
51
|
+
});
|
|
52
|
+
test("NEO4J_URI is pinned to the brand bolt port, never defaulting 7687 (Task 571 guard)", () => {
|
|
53
|
+
const block = buildHeartbeatCronBlock(OPTS);
|
|
54
|
+
assert.match(block, /NEO4J_URI=bolt:\/\/localhost:17687 /, "NEO4J_URI must pin the brand port");
|
|
55
|
+
assert.doesNotMatch(block, /7687(?!.)/, "must not reference the default 7687 port");
|
|
56
|
+
});
|
|
57
|
+
test("entry runs check-due-events.js with PLATFORM_ROOT and no ACCOUNT_ID", () => {
|
|
58
|
+
const block = buildHeartbeatCronBlock(OPTS);
|
|
59
|
+
assert.match(block, /PLATFORM_ROOT=\/home\/me\/maxy-code\/platform /);
|
|
60
|
+
assert.match(block, /\/plugins\/scheduling\/mcp\/dist\/scripts\/check-due-events\.js/);
|
|
61
|
+
assert.doesNotMatch(block, /ACCOUNT_ID=/, "watcher scans all accounts itself; no per-account scoping");
|
|
62
|
+
});
|
|
63
|
+
test("entry fires every minute", () => {
|
|
64
|
+
const block = buildHeartbeatCronBlock(OPTS);
|
|
65
|
+
const entry = block.split("\n").find((l) => l.includes("check-due-events"));
|
|
66
|
+
assert.match(entry, /^\* \* \* \* \* /, "must be a once-a-minute schedule");
|
|
67
|
+
});
|
|
68
|
+
test("stripCronBlock removes a present block and counts its entries", () => {
|
|
69
|
+
const tab = `MAILTO=""\n${buildHeartbeatCronBlock(OPTS)}\n`;
|
|
70
|
+
const { cleaned, removedEntries, matched } = stripCronBlock(tab, "maxy-code");
|
|
71
|
+
assert.equal(matched, true);
|
|
72
|
+
assert.equal(removedEntries, 1);
|
|
73
|
+
assert.equal(cleaned, 'MAILTO=""');
|
|
74
|
+
});
|
|
75
|
+
test("stripCronBlock is a no-op signal when no block is present", () => {
|
|
76
|
+
const tab = `0 3 * * * /usr/bin/backup.sh\n`;
|
|
77
|
+
const { cleaned, removedEntries, matched } = stripCronBlock(tab, "maxy-code");
|
|
78
|
+
assert.equal(matched, false);
|
|
79
|
+
assert.equal(removedEntries, 0);
|
|
80
|
+
assert.equal(cleaned, "0 3 * * * /usr/bin/backup.sh");
|
|
81
|
+
});
|
|
82
|
+
test("stripCronBlock escapes the brand name (hyphen/dot safe)", () => {
|
|
83
|
+
const block = buildHeartbeatCronBlock({ ...OPTS, productName: "real.agent-code" });
|
|
84
|
+
const { matched, removedEntries } = stripCronBlock(block, "real.agent-code");
|
|
85
|
+
assert.equal(matched, true);
|
|
86
|
+
assert.equal(removedEntries, 1);
|
|
87
|
+
});
|
|
88
|
+
test("mergeHeartbeatBlock appends to an empty crontab", () => {
|
|
89
|
+
const block = buildHeartbeatCronBlock(OPTS);
|
|
90
|
+
const merged = mergeHeartbeatBlock("", block, "maxy-code");
|
|
91
|
+
assert.equal(merged, `${block}\n`);
|
|
92
|
+
});
|
|
93
|
+
test("mergeHeartbeatBlock replaces an existing block — idempotent re-install", () => {
|
|
94
|
+
const block = buildHeartbeatCronBlock(OPTS);
|
|
95
|
+
const once = mergeHeartbeatBlock(`0 3 * * * /usr/bin/backup.sh\n`, block, "maxy-code");
|
|
96
|
+
const twice = mergeHeartbeatBlock(once, block, "maxy-code");
|
|
97
|
+
assert.equal(once, twice, "re-applying the merge must not stack a second block");
|
|
98
|
+
const blockCount = (twice.match(/# BEGIN MAXY-CODE CRONS/g) || []).length;
|
|
99
|
+
assert.equal(blockCount, 1, "exactly one brand block after re-install");
|
|
100
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Scheduling-watcher heartbeat cron — pure string builders.
|
|
2
|
+
//
|
|
3
|
+
// The installer registers ONE install-level cron line that ticks
|
|
4
|
+
// `scheduling/mcp/dist/scripts/check-due-events.js` once a minute. The watcher
|
|
5
|
+
// scans every account itself (listLocalAccountIds → data/accounts/*/account.json)
|
|
6
|
+
// and only dispatches operator-created bookings whose time has come, so a
|
|
7
|
+
// per-minute tick creates no activity of its own.
|
|
8
|
+
//
|
|
9
|
+
// Narrowed from the block removed in Task 039 (commit 08e64c1f7), which also
|
|
10
|
+
// reintroduces neither regression that removal fixed:
|
|
11
|
+
// - No `mkdir` of an account log dir. Logs go to <persistDir>/logs, outside
|
|
12
|
+
// the seed-guard zone, so reinstall after `rm -rf ~/<brand>` never trips
|
|
13
|
+
// the stub-account-dirs guard.
|
|
14
|
+
// - NEO4J_URI pinned to the brand bolt port. Cron inherits no shell env, so
|
|
15
|
+
// an unpinned URI would default to 7687 and leak across co-resident brands
|
|
16
|
+
// (Task 571).
|
|
17
|
+
//
|
|
18
|
+
// The email dispatchers (email-fetch, email-auto-respond) act without an
|
|
19
|
+
// operator booking and stay unscheduled by the installer — they are NOT part
|
|
20
|
+
// of this block.
|
|
21
|
+
//
|
|
22
|
+
// I/O (read/write the user crontab) lives in the installer/uninstaller; this
|
|
23
|
+
// module is pure so the line shape and merge semantics are unit-testable.
|
|
24
|
+
/**
|
|
25
|
+
* BEGIN/END marker pair, byte-identical to the strings admin `system-status`
|
|
26
|
+
* greps (`# BEGIN ${productName.toUpperCase()} CRONS`).
|
|
27
|
+
*/
|
|
28
|
+
export function cronBlockMarkers(productName) {
|
|
29
|
+
const upper = productName.toUpperCase();
|
|
30
|
+
return { begin: `# BEGIN ${upper} CRONS`, end: `# END ${upper} CRONS` };
|
|
31
|
+
}
|
|
32
|
+
/** The three-line cron block: BEGIN marker, the single heartbeat entry, END marker. */
|
|
33
|
+
export function buildHeartbeatCronBlock(opts) {
|
|
34
|
+
const { begin, end } = cronBlockMarkers(opts.productName);
|
|
35
|
+
const script = `${opts.platformRoot}/plugins/scheduling/mcp/dist/scripts/check-due-events.js`;
|
|
36
|
+
const log = `${opts.logDir}/check-due-events.log`;
|
|
37
|
+
const entry = `* * * * * PLATFORM_ROOT=${opts.platformRoot} ` +
|
|
38
|
+
`NEO4J_URI=bolt://localhost:${opts.neo4jPort} ` +
|
|
39
|
+
`${opts.nodeBin} ${script} >> ${log} 2>&1 # heartbeat`;
|
|
40
|
+
return [begin, entry, end].join("\n");
|
|
41
|
+
}
|
|
42
|
+
/** Escape a brand name for safe embedding in a RegExp (hyphens, dots). */
|
|
43
|
+
function escapeRegExp(s) {
|
|
44
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Strip every brand cron block from a crontab string.
|
|
48
|
+
* `matched` is true when at least one block was present; `removedEntries`
|
|
49
|
+
* counts the non-comment, non-blank lines inside the removed block(s).
|
|
50
|
+
*/
|
|
51
|
+
export function stripCronBlock(crontab, productName) {
|
|
52
|
+
const { begin, end } = cronBlockMarkers(productName);
|
|
53
|
+
const pattern = new RegExp(`${escapeRegExp(begin)}[\\s\\S]*?${escapeRegExp(end)}\\n?`, "g");
|
|
54
|
+
const blocks = crontab.match(pattern) ?? [];
|
|
55
|
+
let removedEntries = 0;
|
|
56
|
+
for (const block of blocks) {
|
|
57
|
+
removedEntries += block
|
|
58
|
+
.split("\n")
|
|
59
|
+
.filter((line) => line.trim() && !line.trim().startsWith("#")).length;
|
|
60
|
+
}
|
|
61
|
+
const cleaned = crontab.replace(pattern, "").replace(/\n{3,}/g, "\n\n").trimEnd();
|
|
62
|
+
return { cleaned, removedEntries, matched: blocks.length > 0 };
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Produce the next crontab: strip any existing brand block, then append the
|
|
66
|
+
* fresh block. Re-running with the same block is idempotent — a re-install
|
|
67
|
+
* replaces the block, never stacks a second copy.
|
|
68
|
+
*/
|
|
69
|
+
export function mergeHeartbeatBlock(existing, block, productName) {
|
|
70
|
+
const { cleaned } = stripCronBlock(existing, productName);
|
|
71
|
+
return cleaned ? `${cleaned}\n${block}\n` : `${block}\n`;
|
|
72
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { seedBypassPermissionsSettings, assertBypassPermissionsSeed } from "./pe
|
|
|
6
6
|
import { resolve, join, dirname } from "node:path";
|
|
7
7
|
import { randomBytes } from "node:crypto";
|
|
8
8
|
import { resolveInstallPortFromFs, buildMaxyUnitFile, buildClaudeSessionManagerUnitFile, buildClaudePtysSliceUnitFile, buildCloudflaredSliceUnitFile } from "./port-resolution.js";
|
|
9
|
+
import { buildHeartbeatCronBlock, mergeHeartbeatBlock } from "./cron-registration.js";
|
|
9
10
|
import { validateTierFlag, validateEntitlementBase64, applyTierToAccountConfig, entitlementPath } from "./tier-flag.js";
|
|
10
11
|
import { parseOsRelease, isUbuntuLike as isUbuntuLikePure, parseAptCacheCandidate, decideAptResolution, } from "./apt-resolve.js";
|
|
11
12
|
import { findPeerBrandOnDefaultNeo4jPort } from "./peer-brand-detect.js";
|
|
@@ -2981,9 +2982,10 @@ function registerSpecialistAgents() {
|
|
|
2981
2982
|
// written by setup-account.sh during setupAccount(). One reader, one shape, one
|
|
2982
2983
|
// source of truth.
|
|
2983
2984
|
//
|
|
2984
|
-
//
|
|
2985
|
-
//
|
|
2986
|
-
//
|
|
2985
|
+
// `resolveInstallAccountId` has two consumers: `installService` (stamps the
|
|
2986
|
+
// unit env) and the post-install banner. The restored heartbeat cron
|
|
2987
|
+
// (`installCrons` below) is NOT a third consumer — the scheduling watcher
|
|
2988
|
+
// scans every account itself, so the cron line carries no ACCOUNT_ID.
|
|
2987
2989
|
// ---------------------------------------------------------------------------
|
|
2988
2990
|
function resolveInstallAccountId() {
|
|
2989
2991
|
// Managed-service model: prefer the single `role:"house"` account; fall back
|
|
@@ -3015,6 +3017,47 @@ function resolveInstallAccountId() {
|
|
|
3015
3017
|
catch { /* directory unreadable */ }
|
|
3016
3018
|
return "";
|
|
3017
3019
|
}
|
|
3020
|
+
// ---------------------------------------------------------------------------
|
|
3021
|
+
// Cron registration — scheduling-watcher heartbeat
|
|
3022
|
+
//
|
|
3023
|
+
// Registers ONE install-level cron line that ticks check-due-events.js every
|
|
3024
|
+
// minute (see cron-registration.ts for the line shape and the two regression
|
|
3025
|
+
// guards). Idempotent across re-installs via the BEGIN/END markers. Linux-only
|
|
3026
|
+
// — darwin runs under launchd and registers no cron. The email dispatchers are
|
|
3027
|
+
// deliberately NOT registered here.
|
|
3028
|
+
//
|
|
3029
|
+
// The log dir <persistDir>/logs is created earlier in installServiceLinux
|
|
3030
|
+
// (alongside the .env write); the cron line never mkdir's it.
|
|
3031
|
+
// ---------------------------------------------------------------------------
|
|
3032
|
+
function installCrons() {
|
|
3033
|
+
if (!isLinux())
|
|
3034
|
+
return;
|
|
3035
|
+
const nodeBin = spawnSync("which", ["node"], { encoding: "utf-8" }).stdout.trim() || "/usr/bin/node";
|
|
3036
|
+
const platformRoot = join(INSTALL_DIR, "platform");
|
|
3037
|
+
const logDir = join(resolve(process.env.HOME ?? "/root", BRAND.configDir), "logs");
|
|
3038
|
+
const block = buildHeartbeatCronBlock({
|
|
3039
|
+
productName: BRAND.productName,
|
|
3040
|
+
platformRoot,
|
|
3041
|
+
neo4jPort: NEO4J_PORT,
|
|
3042
|
+
nodeBin,
|
|
3043
|
+
logDir,
|
|
3044
|
+
});
|
|
3045
|
+
const existing = spawnSync("crontab", ["-l"], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
3046
|
+
const currentCrontab = existing.status === 0 ? existing.stdout : "";
|
|
3047
|
+
const newCrontab = mergeHeartbeatBlock(currentCrontab, block, BRAND.productName);
|
|
3048
|
+
const write = spawnSync("crontab", ["-"], {
|
|
3049
|
+
input: newCrontab,
|
|
3050
|
+
encoding: "utf-8",
|
|
3051
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
3052
|
+
});
|
|
3053
|
+
if (write.status === 0) {
|
|
3054
|
+
console.log(" Cron jobs: registered (heartbeat)");
|
|
3055
|
+
}
|
|
3056
|
+
else {
|
|
3057
|
+
console.error(` Cron jobs: failed to register — ${(write.stderr || "").trim()}`);
|
|
3058
|
+
logFile(` crontab write failed: ${write.stderr}`);
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3018
3061
|
// retired the ttyd/tmux/xterm admin terminal stack. Upgrades run
|
|
3019
3062
|
// via the action runner — `systemd-run --user` transient units spawned by
|
|
3020
3063
|
// POST /api/admin/actions/upgrade — whose lifetime is independent of
|
|
@@ -3846,6 +3889,10 @@ WantedBy=multi-user.target
|
|
|
3846
3889
|
if (!webServerUp) {
|
|
3847
3890
|
console.log(` Server may still be starting. Check http://${DEVICE_HOSTNAME}.local:${PORT} in a moment.`);
|
|
3848
3891
|
}
|
|
3892
|
+
// Register the scheduling-watcher heartbeat cron — depends on the web server
|
|
3893
|
+
// being up (the dispatched MCP tools talk to the running platform), independent
|
|
3894
|
+
// of CDP. <persistDir>/logs was created above.
|
|
3895
|
+
installCrons();
|
|
3849
3896
|
// Validate CDP: the programmatic Playwright MCP server connects to Chromium
|
|
3850
3897
|
// via --cdp-endpoint http://127.0.0.1:9222. In virtual (VNC) mode, Chromium is
|
|
3851
3898
|
// started by vnc.sh (ExecStartPre) before the web server — a failed probe here
|
package/dist/uninstall.js
CHANGED
|
@@ -4,6 +4,7 @@ import { resolve, join, dirname } from "node:path";
|
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { createInterface } from "node:readline";
|
|
6
6
|
import { removeBrandStanza, hasAnyBrandStanza } from "./samba-provision.js";
|
|
7
|
+
import { stripCronBlock } from "./cron-registration.js";
|
|
7
8
|
import { resolveDedicatedNeo4jTeardown } from "./neo4j-teardown.js";
|
|
8
9
|
import { KNOWN_BRAND_HOSTNAMES } from "./known-brands.js";
|
|
9
10
|
const HOME = homedir();
|
|
@@ -94,6 +95,35 @@ function commandExists(cmd) {
|
|
|
94
95
|
}
|
|
95
96
|
}
|
|
96
97
|
// ---------------------------------------------------------------------------
|
|
98
|
+
// Cron block removal
|
|
99
|
+
//
|
|
100
|
+
// Strips the `# BEGIN <PRODUCT> CRONS` … `# END` block (the installer's
|
|
101
|
+
// scheduling-watcher heartbeat) from the user crontab. Idempotent: a no-op
|
|
102
|
+
// when the block is absent, leaving a clean crontab byte-identical. When the
|
|
103
|
+
// block was the only content, the crontab is dropped entirely with `crontab -r`
|
|
104
|
+
// so no empty tab is left behind. Linux-only — darwin registers no cron.
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
function removeCronBlock() {
|
|
107
|
+
if (!isLinux())
|
|
108
|
+
return;
|
|
109
|
+
if (!commandExists("crontab"))
|
|
110
|
+
return;
|
|
111
|
+
const existing = spawnSync("crontab", ["-l"], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
112
|
+
const currentCrontab = existing.status === 0 ? existing.stdout : "";
|
|
113
|
+
const { cleaned, removedEntries, matched } = stripCronBlock(currentCrontab, BRAND.productName);
|
|
114
|
+
if (!matched) {
|
|
115
|
+
console.log(" Cron block: none present");
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (cleaned.trim() === "") {
|
|
119
|
+
spawnSync("crontab", ["-r"], { stdio: ["pipe", "pipe", "pipe"] });
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
spawnSync("crontab", ["-"], { input: `${cleaned}\n`, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
123
|
+
}
|
|
124
|
+
console.log(` Cron block: removed ${removedEntries} entries`);
|
|
125
|
+
}
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
97
127
|
// Detection
|
|
98
128
|
// ---------------------------------------------------------------------------
|
|
99
129
|
export function isMaxyInstalled() {
|
|
@@ -949,6 +979,7 @@ export async function runUninstall(options) {
|
|
|
949
979
|
const failures = [];
|
|
950
980
|
const steps = [
|
|
951
981
|
{ name: "Stop services", fn: stopServices },
|
|
982
|
+
{ name: "Remove cron block", fn: removeCronBlock },
|
|
952
983
|
{ name: "Delete Cloudflare tunnel", fn: deleteCloudflareTunnel },
|
|
953
984
|
...(options.exportPath
|
|
954
985
|
? [{ name: "Export data", fn: () => exportData(options.exportPath) }]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "email",
|
|
3
|
-
"description": "Email — dedicated email account. Retrieval: email-graph-query for stored email-thread history as :ConversationArchive {source:'email'} with :Section chunks (recall, search, 'threads about X', 'what did Y send?'); email-read / email-search for live IMAP inbox previews (with best-effort per-archive section hints from the graph); email-fetch-body for the COMPLETE decoded body of one message (by uid or message_id) when a preview is not enough. Ingestion (operator-initiated, human-in-the-loop): email-fetch lists new messages, email-ingest writes the operator-approved subset via the conversation-archive pipeline. Sending: email-send (new), email-reply (threaded — resolves the original via IMAP Message-ID search), email-draft (compose to the mailbox Drafts folder, unsent, for operator review; pass messageId for a threaded draft-reply), email-draft-edit (revise a stored draft, located by its Drafts UID or, when Gmail has renumbered it, its draft Message-ID), email-draft-send (dispatch a stored draft by Drafts UID, then remove it). Config: email-provider-info (pre-password host + app-password lookup from an address alone), email-setup, email-status, email-otp-extract.",
|
|
3
|
+
"description": "Email — dedicated email account. Retrieval: email-graph-query for stored email-thread history as :ConversationArchive {source:'email'} with :Section chunks (recall, search, 'threads about X', 'what did Y send?'); email-read / email-search for live IMAP inbox previews (with best-effort per-archive section hints from the graph); email-fetch-body for the COMPLETE decoded body of one message (by uid or message_id) when a preview is not enough. Ingestion (operator-initiated, human-in-the-loop): email-fetch lists new messages, email-ingest writes the operator-approved subset via the conversation-archive pipeline. Sending: email-send (new), email-reply (threaded — resolves the original via IMAP Message-ID search), email-draft (compose to the mailbox Drafts folder, unsent, for operator review; pass messageId for a threaded draft-reply), email-draft-edit (revise a stored draft, located by its Drafts UID or, when Gmail has renumbered it, its draft Message-ID), email-draft-send (dispatch a stored draft by Drafts UID, then remove it). Deleting: email-delete (move named UIDs from email-read/email-search to the mailbox Trash — recoverable, never an expunge). Config: email-provider-info (pre-password host + app-password lookup from an address alone), email-setup, email-status, email-otp-extract.",
|
|
4
4
|
"version": "0.1.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Rubytech LLC"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: email
|
|
3
|
-
description: "Email — dedicated email account. Retrieval: email-graph-query for stored email-thread history as :ConversationArchive {source:'email'} with :Section chunks (recall, search, 'threads about X', 'what did Y send?'); email-read / email-search for live IMAP inbox previews (with best-effort per-archive section hints from the graph); email-fetch-body for the COMPLETE decoded body of one message (by uid or message_id) when a preview is not enough. Ingestion (operator-initiated, human-in-the-loop): email-fetch lists new messages, email-ingest writes the operator-approved subset via the conversation-archive pipeline. Sending: email-send (new), email-reply (threaded — resolves the original via IMAP Message-ID search), email-draft (compose to the mailbox Drafts folder, unsent, for operator review; pass messageId for a threaded draft-reply), email-draft-edit (revise a stored draft, located by its Drafts UID or, when Gmail has renumbered it, its draft Message-ID), email-draft-send (dispatch a stored draft by Drafts UID, then remove it). Config: email-provider-info (pre-password host + app-password lookup from an address alone), email-setup, email-status, email-otp-extract."
|
|
3
|
+
description: "Email — dedicated email account. Retrieval: email-graph-query for stored email-thread history as :ConversationArchive {source:'email'} with :Section chunks (recall, search, 'threads about X', 'what did Y send?'); email-read / email-search for live IMAP inbox previews (with best-effort per-archive section hints from the graph); email-fetch-body for the COMPLETE decoded body of one message (by uid or message_id) when a preview is not enough. Ingestion (operator-initiated, human-in-the-loop): email-fetch lists new messages, email-ingest writes the operator-approved subset via the conversation-archive pipeline. Sending: email-send (new), email-reply (threaded — resolves the original via IMAP Message-ID search), email-draft (compose to the mailbox Drafts folder, unsent, for operator review; pass messageId for a threaded draft-reply), email-draft-edit (revise a stored draft, located by its Drafts UID or, when Gmail has renumbered it, its draft Message-ID), email-draft-send (dispatch a stored draft by Drafts UID, then remove it). Deleting: email-delete (move named UIDs from email-read/email-search to the mailbox Trash — recoverable, never an expunge). Config: email-provider-info (pre-password host + app-password lookup from an address alone), email-setup, email-status, email-otp-extract."
|
|
4
4
|
tools:
|
|
5
5
|
- name: email-provider-info
|
|
6
6
|
publicAllowlist: false
|
|
@@ -32,6 +32,9 @@ tools:
|
|
|
32
32
|
- name: email-search
|
|
33
33
|
publicAllowlist: false
|
|
34
34
|
adminAllowlist: false
|
|
35
|
+
- name: email-delete
|
|
36
|
+
publicAllowlist: false
|
|
37
|
+
adminAllowlist: false
|
|
35
38
|
- name: email-graph-query
|
|
36
39
|
publicAllowlist: false
|
|
37
40
|
adminAllowlist: false
|
|
@@ -78,6 +81,7 @@ Manages the agent's own dedicated email account — IMAP for reading, SMTP for s
|
|
|
78
81
|
- **Setup:** `email-setup` — collect credentials in plain chat and connect IMAP/SMTP. Hosts are auto-inferred for known providers; supports alias addresses (`agentAddress`).
|
|
79
82
|
- **Read inbox:** `email-read` — metadata only (UID, sender, subject, date), plus a best-effort `Archive`/`Archive-Section` hint when the IMAP message's `receivedAt` falls inside a stored `:ConversationArchive {source:'email'}` section, plus an `Attachments (N): filename (mime, size); …` footer when the message carries enclosures. Supports pagination (`before_uid`), folders (`inbox`/`sent`), filtering by sender/date/subject. Metadata is derived from IMAP `bodyStructure` — no bytes downloaded.
|
|
80
83
|
- **Search inbox:** `email-search` — live IMAP search by sender, subject, body keyword, date range. Same per-archive hint and attachment footer as `email-read`.
|
|
84
|
+
- **Delete (move to Trash):** `email-delete` — move the messages named by `uids` (from a prior `email-read`/`email-search`, scoped to a source `folder` of `inbox` or `sent`) into the mailbox's Trash folder. **Recoverable, never an expunge** — the message leaves the inbox and permanent removal is left to the operator's own mail client or the provider's Trash auto-purge; this tool never permanently deletes. Trash is resolved per provider: the `\Trash` special-use folder if advertised, else the first existing of `Trash`, `[Gmail]/Trash`, `Deleted Items`, `Deleted Messages`. `Junk` is never treated as Trash. A mailbox with no resolvable Trash yields a typed error and moves nothing — there is no silent expunge fallback. A `uid` that is already gone is counted not-moved without failing the rest; the result reports `Moved N of M message(s) to <Trash>`. It deletes only the UIDs you name, never by search criteria.
|
|
81
85
|
- **Read one whole message:** `email-fetch-body` — the COMPLETE decoded body of a single message addressed by `uid` (with optional `folder`) or `message_id` (resolved across inbox and sent). No 500-char preview cap. Downloads the full RFC822 source and decodes it with `mailparser`, so every MIME shape — flat text, single-part HTML, `multipart/alternative`, and nested `multipart/related`/`multipart/mixed` (Outlook's inline-image house style) — yields a body; the preview tools above intentionally stay preview-only. Use this when the operator wants to read or act on a message's actual content.
|
|
82
86
|
- **Send:** `email-send` — new outbound email. Pass `cc`/`bcc` (each an array of email addresses) to copy or blind-copy recipients — CC appears in the message header, BCC recipients receive the message without appearing in any header. Pass `attachments` (absolute file paths under the account directory, ≤25 MB each) to attach files as SMTP attachments; an invalid path fails the send with no email dispatched. The success message names the CC recipients and reports the BCC count without naming them.
|
|
83
87
|
- **Reply:** `email-reply` — threaded reply to an existing email by `messageId`. Resolves the original envelope via IMAP `SEARCH HEADER Message-ID` (INBOX then `\Sent`), so threading works for any message still present on the server even if the graph never ingested it. Supports `replyAll`, `cc`/`bcc`, and `attachments` (same path rules as `email-send`). Explicit `cc`/`bcc` are added in addition to whatever `replyAll` places in `To`, passed through as given (no overlap de-duplication against `To`).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-delete.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/email-delete.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
// Mock imapflow so moveToTrash's internal createClient() yields our spy client.
|
|
3
|
+
// `calls` records the wire sequence (lock, search, messageMove) so the move
|
|
4
|
+
// target, the not-moved accounting, and the resolve-before-lock contract can be
|
|
5
|
+
// asserted directly. `folders` is the client.list() result; `existingUids` is
|
|
6
|
+
// which UIDs the source folder actually holds (the rest are already gone).
|
|
7
|
+
const state = {
|
|
8
|
+
folders: [],
|
|
9
|
+
existingUids: [],
|
|
10
|
+
calls: { locked: null, searched: null, moves: [], released: false },
|
|
11
|
+
};
|
|
12
|
+
const mockClient = {
|
|
13
|
+
connect: vi.fn(async () => { }),
|
|
14
|
+
list: vi.fn(async () => state.folders),
|
|
15
|
+
getMailboxLock: vi.fn(async (path) => {
|
|
16
|
+
state.calls.locked = path;
|
|
17
|
+
return { release: () => { state.calls.released = true; } };
|
|
18
|
+
}),
|
|
19
|
+
search: vi.fn(async (query) => {
|
|
20
|
+
state.calls.searched = query;
|
|
21
|
+
const requested = String(query.uid).split(",").map(Number);
|
|
22
|
+
return requested.filter((u) => state.existingUids.includes(u));
|
|
23
|
+
}),
|
|
24
|
+
messageMove: vi.fn(async (range, dest, opts) => {
|
|
25
|
+
state.calls.moves.push({ range, dest, opts });
|
|
26
|
+
}),
|
|
27
|
+
logout: vi.fn(async () => { }),
|
|
28
|
+
close: vi.fn(() => { }),
|
|
29
|
+
};
|
|
30
|
+
vi.mock("imapflow", () => ({
|
|
31
|
+
ImapFlow: function ImapFlow() {
|
|
32
|
+
return mockClient;
|
|
33
|
+
},
|
|
34
|
+
}));
|
|
35
|
+
// Mock credentials so the tool resolves an account without touching Neo4j. The
|
|
36
|
+
// spy records that loadCredentials was called by accountId (the by-address read
|
|
37
|
+
// happens inside loadCredentials → readPassword), and supplies the config/password
|
|
38
|
+
// moveToTrash receives — the tool itself takes no password input. vi.hoisted keeps
|
|
39
|
+
// the spy and its config available to the (hoisted) vi.mock factory without a TDZ.
|
|
40
|
+
const { loadCredentials, credConfig } = vi.hoisted(() => {
|
|
41
|
+
const credConfig = {
|
|
42
|
+
email: "owner@x.com",
|
|
43
|
+
agentAddress: "owner@x.com",
|
|
44
|
+
imapHost: "imap.x.com",
|
|
45
|
+
imapPort: 993,
|
|
46
|
+
imapSecurity: "tls",
|
|
47
|
+
smtpHost: "smtp.x.com",
|
|
48
|
+
smtpPort: 587,
|
|
49
|
+
smtpSecurity: "starttls",
|
|
50
|
+
};
|
|
51
|
+
const loadCredentials = vi.fn(async (_accountId) => ({
|
|
52
|
+
credentials: { config: credConfig, password: "resolved-pw" },
|
|
53
|
+
}));
|
|
54
|
+
return { loadCredentials, credConfig };
|
|
55
|
+
});
|
|
56
|
+
vi.mock("../lib/credentials.js", () => ({ loadCredentials }));
|
|
57
|
+
import { moveToTrash, resolveTrashFolder } from "../lib/imap.js";
|
|
58
|
+
import { emailDelete, deleteLine } from "../tools/email-delete.js";
|
|
59
|
+
const config = {
|
|
60
|
+
email: "a@x.com",
|
|
61
|
+
agentAddress: "a@x.com",
|
|
62
|
+
imapHost: "imap.x.com",
|
|
63
|
+
imapPort: 993,
|
|
64
|
+
imapSecurity: "tls",
|
|
65
|
+
smtpHost: "smtp.x.com",
|
|
66
|
+
smtpPort: 587,
|
|
67
|
+
smtpSecurity: "starttls",
|
|
68
|
+
};
|
|
69
|
+
function reset(folders, existingUids) {
|
|
70
|
+
state.folders = folders;
|
|
71
|
+
state.existingUids = existingUids;
|
|
72
|
+
state.calls = { locked: null, searched: null, moves: [], released: false };
|
|
73
|
+
}
|
|
74
|
+
beforeEach(() => {
|
|
75
|
+
reset([], []);
|
|
76
|
+
});
|
|
77
|
+
describe("resolveTrashFolder", () => {
|
|
78
|
+
it("prefers the \\Trash special-use folder over a name match", async () => {
|
|
79
|
+
const folders = [{ path: "INBOX" }, { path: "Trash" }, { path: "Bin", specialUse: "\\Trash" }];
|
|
80
|
+
await expect(resolveTrashFolder({ list: async () => folders })).resolves.toBe("Bin");
|
|
81
|
+
});
|
|
82
|
+
it("falls back to a known Deleted-folder name when no special-use exists", async () => {
|
|
83
|
+
const folders = [{ path: "INBOX" }, { path: "Deleted Items" }];
|
|
84
|
+
await expect(resolveTrashFolder({ list: async () => folders })).resolves.toBe("Deleted Items");
|
|
85
|
+
});
|
|
86
|
+
it("throws a typed no-trash-folder error and never treats Junk as Trash", async () => {
|
|
87
|
+
const folders = [{ path: "INBOX" }, { path: "Junk" }];
|
|
88
|
+
await expect(resolveTrashFolder({ list: async () => folders })).rejects.toMatchObject({ code: "no-trash-folder" });
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
describe("moveToTrash", () => {
|
|
92
|
+
it("moves every requested UID that exists, targets the resolved Trash, and reports moved=requested", async () => {
|
|
93
|
+
reset([{ path: "INBOX" }, { path: "Trash", specialUse: "\\Trash" }], [10, 11, 12]);
|
|
94
|
+
const res = await moveToTrash(config, "pw", { folder: "inbox", uids: [10, 11, 12] });
|
|
95
|
+
expect(res).toEqual({ moved: 3, requested: 3, trash: "Trash" });
|
|
96
|
+
expect(state.calls.locked).toBe("INBOX");
|
|
97
|
+
expect(state.calls.moves).toHaveLength(1);
|
|
98
|
+
expect(state.calls.moves[0].range).toEqual([10, 11, 12]);
|
|
99
|
+
expect(state.calls.moves[0].dest).toBe("Trash");
|
|
100
|
+
expect(state.calls.moves[0].opts.uid).toBe(true);
|
|
101
|
+
expect(state.calls.released).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
it("counts a vanished UID as not-moved and still moves the rest", async () => {
|
|
104
|
+
reset([{ path: "INBOX" }, { path: "Trash", specialUse: "\\Trash" }], [10, 12]); // 11 is gone
|
|
105
|
+
const res = await moveToTrash(config, "pw", { folder: "inbox", uids: [10, 11, 12] });
|
|
106
|
+
expect(res).toEqual({ moved: 2, requested: 3, trash: "Trash" });
|
|
107
|
+
expect(state.calls.moves[0].range).toEqual([10, 12]);
|
|
108
|
+
});
|
|
109
|
+
it("with no Trash folder errors, moves nothing, and never locks the source", async () => {
|
|
110
|
+
reset([{ path: "INBOX" }, { path: "Junk" }], [10]);
|
|
111
|
+
await expect(moveToTrash(config, "pw", { folder: "inbox", uids: [10] })).rejects.toMatchObject({ code: "no-trash-folder" });
|
|
112
|
+
expect(state.calls.moves).toHaveLength(0);
|
|
113
|
+
expect(state.calls.locked).toBeNull();
|
|
114
|
+
});
|
|
115
|
+
it("makes no messageMove call when none of the requested UIDs still exist", async () => {
|
|
116
|
+
reset([{ path: "INBOX" }, { path: "Trash", specialUse: "\\Trash" }], []);
|
|
117
|
+
const res = await moveToTrash(config, "pw", { folder: "inbox", uids: [10, 11] });
|
|
118
|
+
expect(res).toEqual({ moved: 0, requested: 2, trash: "Trash" });
|
|
119
|
+
expect(state.calls.moves).toHaveLength(0);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
describe("emailDelete tool", () => {
|
|
123
|
+
it("resolves the credential by accountId (no password input) and moves via the resolved Trash", async () => {
|
|
124
|
+
reset([{ path: "INBOX" }, { path: "Trash", specialUse: "\\Trash" }], [10, 11]);
|
|
125
|
+
loadCredentials.mockClear();
|
|
126
|
+
const text = await emailDelete({ accountId: "acct-1", uids: [10, 11], folder: "inbox" });
|
|
127
|
+
expect(loadCredentials).toHaveBeenCalledWith("acct-1");
|
|
128
|
+
// The owner address came from the resolved config, not from any tool input.
|
|
129
|
+
expect(text).toBe("Moved 2 of 2 message(s) to Trash.");
|
|
130
|
+
expect(state.calls.moves[0].dest).toBe("Trash");
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
describe("deleteLine", () => {
|
|
134
|
+
it("carries the mailbox, counts, trash, and ok — never a password", () => {
|
|
135
|
+
const ok = deleteLine("owner@x.com", "inbox", 3, 2, "Trash", true);
|
|
136
|
+
expect(ok).toContain("op=delete");
|
|
137
|
+
expect(ok).toContain("email=owner@x.com");
|
|
138
|
+
expect(ok).toContain("folder=inbox");
|
|
139
|
+
expect(ok).toContain("requested=3");
|
|
140
|
+
expect(ok).toContain("moved=2");
|
|
141
|
+
expect(ok).toContain("trash=Trash");
|
|
142
|
+
expect(ok).toContain("ok=true");
|
|
143
|
+
expect(ok).not.toContain("resolved-pw");
|
|
144
|
+
});
|
|
145
|
+
it("records the typed reason and ok=false when no Trash resolves", () => {
|
|
146
|
+
const fail = deleteLine("owner@x.com", "inbox", 1, 0, null, false, "no-trash-folder");
|
|
147
|
+
expect(fail).toContain("ok=false");
|
|
148
|
+
expect(fail).toContain("reason=no-trash-folder");
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
//# sourceMappingURL=email-delete.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-delete.test.js","sourceRoot":"","sources":["../../src/__tests__/email-delete.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -16,6 +16,7 @@ import { emailDraft } from "./tools/email-draft.js";
|
|
|
16
16
|
import { emailDraftEdit } from "./tools/email-draft-edit.js";
|
|
17
17
|
import { emailDraftSend } from "./tools/email-draft-send.js";
|
|
18
18
|
import { emailSearch } from "./tools/email-search.js";
|
|
19
|
+
import { emailDelete } from "./tools/email-delete.js";
|
|
19
20
|
import { emailOtpExtract } from "./tools/email-otp-extract.js";
|
|
20
21
|
import { emailStatus } from "./tools/email-status.js";
|
|
21
22
|
import { emailReply } from "./tools/email-reply.js";
|
|
@@ -378,6 +379,34 @@ eagerTool(server, "email-search", "Search messages by sender, subject, body, or
|
|
|
378
379
|
}
|
|
379
380
|
});
|
|
380
381
|
// ===================================================================
|
|
382
|
+
// email-delete — move operator-named messages to the mailbox Trash
|
|
383
|
+
// ===================================================================
|
|
384
|
+
eagerTool(server, "email-delete", "Delete one or more messages by moving them to the mailbox's Trash folder. This is RECOVERABLE: the message leaves the inbox, and permanent removal is left to the operator's own mail client or the provider's Trash auto-purge — this tool never permanently deletes and never expunges. Target the exact messages by passing the uid values you already got from email-read or email-search, scoped to the folder they came from (inbox or sent). It never deletes by search criteria — only the UIDs you name. A uid that is already gone is counted not-moved without failing the rest. If the mailbox has no resolvable Trash folder, nothing is moved and the call errors.", {
|
|
385
|
+
uids: z
|
|
386
|
+
.array(z.coerce.number().int().positive())
|
|
387
|
+
.min(1)
|
|
388
|
+
.describe("The uid values of the messages to delete, taken from a prior email-read or email-search result. At least one."),
|
|
389
|
+
folder: z.enum(["inbox", "sent"]).optional().describe("The folder the messages are in (default: inbox)."),
|
|
390
|
+
}, async (params) => {
|
|
391
|
+
if (!accountId)
|
|
392
|
+
return refuseNoAccount("email-delete");
|
|
393
|
+
try {
|
|
394
|
+
const text = await emailDelete({ ...params, accountId });
|
|
395
|
+
return { content: [{ type: "text", text }] };
|
|
396
|
+
}
|
|
397
|
+
catch (err) {
|
|
398
|
+
return {
|
|
399
|
+
content: [
|
|
400
|
+
{
|
|
401
|
+
type: "text",
|
|
402
|
+
text: `Failed to delete email: ${err instanceof Error ? err.message : String(err)}`,
|
|
403
|
+
},
|
|
404
|
+
],
|
|
405
|
+
isError: true,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
// ===================================================================
|
|
381
410
|
// email-otp-extract — poll for and extract OTP verification codes
|
|
382
411
|
// ===================================================================
|
|
383
412
|
eagerTool(server, "email-otp-extract", "Poll the agent's email inbox for a verification code (OTP) from a specific sender. Searches recent messages first, then polls at intervals until a matching email arrives or the timeout is reached. Used by other skills during service authentication.", {
|