@agenticmail/claudecode 0.1.12 → 0.1.14

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 CHANGED
@@ -16,7 +16,13 @@ Agent { subagent_type: "agenticmail-fola", prompt: "draft a reply to my last ema
16
16
 
17
17
  This package is to Claude Code what `@agenticmail/openclaw` is to OpenClaw: an integration package that wires AgenticMail into the host AI runtime. It mirrors that package's layout 1:1, so if you know one, you know the other.
18
18
 
19
- ## ✨ What's new in 0.1.11
19
+ ## ✨ What's new in 0.1.14
20
+
21
+ - **Workers run for hours** — dropped the 30-min hard timeout. Per-worker log file at `~/.agenticmail/worker-logs/<id>.log` capturing every SDK tool call + result + assistant chunk as a one-liner. Heartbeats POSTed to the API every 30 s so `check_activity` sees real progress. Per-worker scratch cwd at `~/.agenticmail/worker-cwds/<id>/` prevents parallel agents from clobbering each other's output. Tail via the new MCP tool `tail_worker`.
22
+ - **Autonomous-mode awareness via Stop hook** — the mail hook now registers on the **Stop** Claude Code event too. Long headless runs (no user prompts) finally see teammate replies: when bridge mail is unread at a turn boundary, the hook returns `{decision: 'block', reason: '...'}`, forcing Claude to continue with the new-mail summary in context. This is the schema-correct supported way to inject context at Stop, unlike the 0.8.22 PreToolUse attempt.
23
+ - **Hook bin resolved with absolute path** — previously the hook was registered as the bare bin name `agenticmail-mail-hook`, which produced `command not found` errors when the npm global bin dir wasn't on `$PATH`. Now resolved via `import.meta.url` at install time and registered as `node "/abs/.../mail-hook.js"`. Old bare-name installs auto-heal on the next `agenticmail claudecode` run.
24
+
25
+ ## ✨ Earlier — 0.1.11
20
26
 
21
27
  - **Selective wake** — when the sender sets `wake: ["alice", "bob"]` on `send_email` / `reply_email`, the dispatcher gives a Claude turn only to listed agents. CC'd-but-not-listed agents still receive the mail in their inbox but stay asleep. Single biggest token saver on multi-agent threads.
22
28
  - **Thread-close markers** — `[FINAL]`, `[DONE]`, `[CLOSED]`, `[WRAP]` in a subject. The dispatcher stops waking workers on any further reply to that thread. Closes the "no native done signal" gap from the 5-agent stress test.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  removeUserPromptSubmitHook
3
- } from "./chunk-3ZIHOOA4.js";
3
+ } from "./chunk-DKTAW2N5.js";
4
4
  import {
5
5
  removeMcpServer,
6
6
  stopDispatcher
@@ -1,8 +1,12 @@
1
1
  // src/claude-hooks-config.ts
2
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync } from "fs";
3
3
  import { dirname } from "path";
4
- var AGENTICMAIL_HOOK_MARKER = "agenticmail-mail-hook";
5
- var HOOK_EVENTS = ["UserPromptSubmit", "PreToolUse"];
4
+ function isAgenticMailHookCommand(command) {
5
+ if (typeof command !== "string") return false;
6
+ return command.includes("agenticmail-mail-hook") || command.includes("mail-hook.js");
7
+ }
8
+ var HOOK_EVENTS_TO_REGISTER = ["UserPromptSubmit", "Stop"];
9
+ var HOOK_EVENTS_TO_REMOVE = ["UserPromptSubmit", "Stop", "PreToolUse"];
6
10
  function readSettings(path) {
7
11
  if (!existsSync(path)) return {};
8
12
  const raw = readFileSync(path, "utf-8");
@@ -29,15 +33,30 @@ function upsertMailHook(path, command) {
29
33
  const settings = readSettings(path);
30
34
  if (!settings.hooks) settings.hooks = {};
31
35
  let changed = false;
32
- for (const event of HOOK_EVENTS) {
36
+ for (const event of HOOK_EVENTS_TO_REGISTER) {
33
37
  if (upsertOneEvent(settings.hooks, event, command)) changed = true;
34
38
  }
39
+ for (const event of HOOK_EVENTS_TO_REMOVE) {
40
+ if (HOOK_EVENTS_TO_REGISTER.includes(event)) continue;
41
+ if (removeOneEvent(settings.hooks, event)) changed = true;
42
+ }
35
43
  if (changed) writeSettings(path, settings);
36
44
  return changed;
37
45
  }
46
+ function removeOneEvent(hooks, event) {
47
+ const list = hooks[event] ?? [];
48
+ if (list.length === 0) return false;
49
+ const filtered = list.filter(
50
+ (rule) => !rule.hooks?.some((h) => isAgenticMailHookCommand(h.command))
51
+ );
52
+ if (filtered.length === list.length) return false;
53
+ if (filtered.length === 0) delete hooks[event];
54
+ else hooks[event] = filtered;
55
+ return true;
56
+ }
38
57
  function upsertOneEvent(hooks, event, command) {
39
58
  const list = hooks[event] ?? [];
40
- const isOurs = (rule) => rule.hooks?.some((h) => typeof h.command === "string" && h.command.includes(AGENTICMAIL_HOOK_MARKER)) ?? false;
59
+ const isOurs = (rule) => rule.hooks?.some((h) => isAgenticMailHookCommand(h.command)) ?? false;
41
60
  const desired = {
42
61
  matcher: "",
43
62
  // empty = match every fire of this event
@@ -61,19 +80,8 @@ function removeMailHook(path) {
61
80
  const settings = readSettings(path);
62
81
  if (!settings.hooks) return false;
63
82
  let changed = false;
64
- for (const event of HOOK_EVENTS) {
65
- const list = settings.hooks[event] ?? [];
66
- if (list.length === 0) continue;
67
- const filtered = list.filter(
68
- (rule) => !rule.hooks?.some((h) => typeof h.command === "string" && h.command.includes(AGENTICMAIL_HOOK_MARKER))
69
- );
70
- if (filtered.length === list.length) continue;
71
- if (filtered.length === 0) {
72
- delete settings.hooks[event];
73
- } else {
74
- settings.hooks[event] = filtered;
75
- }
76
- changed = true;
83
+ for (const event of HOOK_EVENTS_TO_REMOVE) {
84
+ if (removeOneEvent(settings.hooks, event)) changed = true;
77
85
  }
78
86
  if (settings.hooks && Object.keys(settings.hooks).length === 0) {
79
87
  delete settings.hooks;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  upsertUserPromptSubmitHook
3
- } from "./chunk-3ZIHOOA4.js";
3
+ } from "./chunk-DKTAW2N5.js";
4
4
  import {
5
5
  startDispatcher,
6
6
  upsertMcpServer
@@ -111,7 +111,7 @@ async function install(opts = {}) {
111
111
  const pruned = pruneStaleSubagentFiles(cfg.agentsDir, cfg, liveNames);
112
112
  let hookChanged = false;
113
113
  try {
114
- hookChanged = upsertUserPromptSubmitHook(cfg.claudeSettingsPath, "agenticmail-mail-hook");
114
+ hookChanged = upsertUserPromptSubmitHook(cfg.claudeSettingsPath, resolveMailHookCommand());
115
115
  } catch {
116
116
  }
117
117
  const dispatcherStatus = await startDispatcherForInstall(cfg);
@@ -129,6 +129,12 @@ function resolveDispatcherBinPath() {
129
129
  const dir = thisFile.slice(0, thisFile.lastIndexOf("/"));
130
130
  return `${dir}/dispatcher-bin.js`;
131
131
  }
132
+ function resolveMailHookCommand() {
133
+ const thisFile = fileURLToPath(import.meta.url);
134
+ const dir = thisFile.slice(0, thisFile.lastIndexOf("/"));
135
+ const hookPath = `${dir}/mail-hook.js`;
136
+ return `node "${hookPath}"`;
137
+ }
132
138
  async function startDispatcherForInstall(cfg) {
133
139
  const binPath = resolveDispatcherBinPath();
134
140
  return startDispatcher({
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  uninstall
3
- } from "./chunk-6SCQVJBL.js";
3
+ } from "./chunk-2GPBHK2M.js";
4
4
  import {
5
5
  install
6
- } from "./chunk-DI3DIMUQ.js";
6
+ } from "./chunk-GAD64LKZ.js";
7
7
  import {
8
8
  status
9
9
  } from "./chunk-O4H76K3B.js";
@@ -36,6 +36,9 @@ function loadPersonaForAgent(opts) {
36
36
  }
37
37
 
38
38
  // src/dispatcher.ts
39
+ import { mkdirSync, createWriteStream, rmSync } from "fs";
40
+ import { join as join2 } from "path";
41
+ import { homedir } from "os";
39
42
  function extractSubject(event) {
40
43
  if (typeof event.subject === "string") return event.subject;
41
44
  if (event.message && typeof event.message.subject === "string") return event.message.subject;
@@ -101,7 +104,7 @@ function threadIdFromSubject(subject) {
101
104
  }
102
105
  var DEFAULT_MAX_WAKES_PER_THREAD = 10;
103
106
  var DEFAULT_WAKE_WINDOW_MS = 24 * 60 * 60 * 1e3;
104
- async function runWorker(query, persona, userPrompt, agent, mcpServerName, mcpCommand, mcpArgs, mcpEnv, log, abortSignal) {
107
+ async function runWorker(query, persona, userPrompt, agent, mcpServerName, mcpCommand, mcpArgs, mcpEnv, log, abortSignal, observer, cwd) {
105
108
  const opts = {
106
109
  systemPrompt: persona,
107
110
  mcpServers: {
@@ -133,6 +136,7 @@ async function runWorker(query, persona, userPrompt, agent, mcpServerName, mcpCo
133
136
  permissionMode: "bypassPermissions",
134
137
  abortController: abortSignal ? wrapSignal(abortSignal) : void 0
135
138
  };
139
+ if (cwd) opts.cwd = cwd;
136
140
  const collectedText = [];
137
141
  try {
138
142
  for await (const msg of query({ prompt: userPrompt, options: opts })) {
@@ -140,11 +144,32 @@ async function runWorker(query, persona, userPrompt, agent, mcpServerName, mcpCo
140
144
  if (m.type === "assistant" && Array.isArray(m.message && m.message.content)) {
141
145
  for (const block of m.message.content) {
142
146
  const b = block;
143
- if (b.type === "text" && typeof b.text === "string") collectedText.push(b.text);
147
+ if (b.type === "text" && typeof b.text === "string") {
148
+ collectedText.push(b.text);
149
+ if (observer) observer.onMessage("assistant", b.text.slice(0, 240).replace(/\s+/g, " ").trim());
150
+ } else if (b.type === "tool_use" && typeof b.name === "string") {
151
+ const inputSummary = (() => {
152
+ try {
153
+ return JSON.stringify(b.input).slice(0, 200);
154
+ } catch {
155
+ return "(uninspectable input)";
156
+ }
157
+ })();
158
+ if (observer) observer.onMessage("tool_use", `${b.name} ${inputSummary}`);
159
+ }
160
+ }
161
+ } else if (m.type === "user" && Array.isArray(m.message && m.message.content)) {
162
+ for (const block of m.message.content) {
163
+ const b = block;
164
+ if (b.type === "tool_result") {
165
+ const bodyStr = typeof b.content === "string" ? b.content : Array.isArray(b.content) ? b.content.map((c) => c.text ?? "").join(" ") : "";
166
+ if (observer) observer.onMessage("tool_result", bodyStr.slice(0, 240).replace(/\s+/g, " ").trim());
167
+ }
144
168
  }
145
169
  }
146
170
  if (m.type === "result" && typeof m.result === "string") {
147
171
  collectedText.push(m.result);
172
+ if (observer) observer.onMessage("result", m.result.slice(0, 240).replace(/\s+/g, " ").trim());
148
173
  }
149
174
  }
150
175
  const text = collectedText.join("\n").trim();
@@ -153,6 +178,7 @@ async function runWorker(query, persona, userPrompt, agent, mcpServerName, mcpCo
153
178
  } catch (err) {
154
179
  const msg = err instanceof Error ? err.message : String(err);
155
180
  log("error", `[dispatcher] worker for "${agent.name}" failed: ${msg}`);
181
+ if (observer) observer.onMessage("error", msg);
156
182
  return { ok: false, error: msg };
157
183
  }
158
184
  }
@@ -699,6 +725,50 @@ var Dispatcher = class {
699
725
  kind: ctx.kind,
700
726
  trigger: { uid: ctx.uid, taskId: ctx.taskId, subject: ctx.subject, from: ctx.from }
701
727
  });
728
+ const logsDir = join2(homedir(), ".agenticmail", "worker-logs");
729
+ try {
730
+ mkdirSync(logsDir, { recursive: true });
731
+ } catch {
732
+ }
733
+ const logPath = join2(logsDir, `${sanitizeId(workerId)}.log`);
734
+ let logStream = null;
735
+ try {
736
+ logStream = createWriteStream(logPath, { flags: "a" });
737
+ } catch {
738
+ }
739
+ const writeLog = (line) => {
740
+ try {
741
+ logStream?.write(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${line}
742
+ `);
743
+ } catch {
744
+ }
745
+ };
746
+ writeLog(`worker_started agent=${account.name} kind=${ctx.kind}${ctx.uid ? " uid=" + ctx.uid : ""}${ctx.taskId ? " task=" + ctx.taskId : ""}`);
747
+ const cwdDir = join2(homedir(), ".agenticmail", "worker-cwds", sanitizeId(workerId));
748
+ try {
749
+ mkdirSync(cwdDir, { recursive: true });
750
+ } catch {
751
+ }
752
+ let turnCount = 0;
753
+ let lastTool = "";
754
+ const observer = {
755
+ onMessage: (tag, summary) => {
756
+ writeLog(`${tag} ${summary}`);
757
+ if (tag === "tool_use") {
758
+ lastTool = summary.split(" ")[0];
759
+ turnCount++;
760
+ }
761
+ }
762
+ };
763
+ const heartbeatHandle = setInterval(() => {
764
+ this.postActivity("/dispatcher/worker-heartbeat", {
765
+ workerId,
766
+ agentName: account.name,
767
+ lastTool: lastTool || void 0,
768
+ turnCount
769
+ });
770
+ }, 3e4);
771
+ heartbeatHandle.unref?.();
702
772
  try {
703
773
  const { body } = loadPersonaForAgent({
704
774
  agent: account,
@@ -717,16 +787,30 @@ var Dispatcher = class {
717
787
  this.cfg.mcpCommand,
718
788
  this.cfg.mcpArgs,
719
789
  mcpEnv,
720
- this.log
790
+ this.log,
791
+ void 0,
792
+ observer,
793
+ cwdDir
721
794
  );
722
795
  } finally {
796
+ clearInterval(heartbeatHandle);
723
797
  this.releaseSlot();
724
798
  const ok = workerResult?.ok === true;
725
799
  const preview = workerResult?.ok ? workerResult.text : workerResult ? workerResult.error : "worker did not start";
800
+ writeLog(`worker_finished ok=${ok} chars=${preview.length}`);
801
+ try {
802
+ logStream?.end();
803
+ } catch {
804
+ }
805
+ try {
806
+ rmSync(cwdDir, { recursive: true, force: true });
807
+ } catch {
808
+ }
726
809
  this.postActivity("/dispatcher/worker-finished", {
727
810
  workerId,
728
811
  agentName: account.name,
729
812
  ok,
813
+ turnCount,
730
814
  resultPreview: typeof preview === "string" ? preview.slice(0, 240) : void 0
731
815
  });
732
816
  }
@@ -789,6 +873,9 @@ var Dispatcher = class {
789
873
  function sleep(ms) {
790
874
  return new Promise((resolve) => setTimeout(resolve, ms));
791
875
  }
876
+ function sanitizeId(id) {
877
+ return id.replace(/[^a-zA-Z0-9._-]/g, "_");
878
+ }
792
879
  function defaultLog(level, msg) {
793
880
  const stream = level === "error" ? process.stderr : process.stdout;
794
881
  stream.write(`[${(/* @__PURE__ */ new Date()).toISOString()}] [${level}] ${msg}
package/dist/cli.js CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  uninstall
4
- } from "./chunk-6SCQVJBL.js";
4
+ } from "./chunk-2GPBHK2M.js";
5
5
  import {
6
6
  install
7
- } from "./chunk-DI3DIMUQ.js";
8
- import "./chunk-3ZIHOOA4.js";
7
+ } from "./chunk-GAD64LKZ.js";
8
+ import "./chunk-DKTAW2N5.js";
9
9
  import {
10
10
  status
11
11
  } from "./chunk-O4H76K3B.js";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  Dispatcher
4
- } from "./chunk-YWUZKKQ5.js";
4
+ } from "./chunk-RNKJRBEF.js";
5
5
  import "./chunk-SBP7MJP2.js";
6
6
 
7
7
  // src/dispatcher-bin.ts
@@ -110,6 +110,20 @@ interface QueryFn {
110
110
  options?: Record<string, unknown>;
111
111
  }): AsyncIterable<unknown>;
112
112
  }
113
+ /**
114
+ * Per-worker observation channel. `runWorker` calls `onMessage` for every
115
+ * SDK message — assistant text, tool calls, tool results, result frames.
116
+ * The caller (spawnWorker) wires this to:
117
+ * - a per-worker log file at `~/.agenticmail/worker-logs/<id>.log`
118
+ * - a heartbeat ticker that POSTs progress to /dispatcher/worker-heartbeat
119
+ *
120
+ * Kept generic so tests don't need to mock disk + network to verify the
121
+ * observation path.
122
+ */
123
+ interface WorkerObserver {
124
+ /** Called once per SDK message. Tag is a short event name. */
125
+ onMessage(tag: string, summary: string): void;
126
+ }
113
127
  /**
114
128
  * The dispatcher itself. Construct once, call .start() to begin watching,
115
129
  * .stop() to tear down. Returns when stop() has finished cleaning up.
@@ -224,4 +238,4 @@ declare class Dispatcher {
224
238
  private releaseSlot;
225
239
  }
226
240
 
227
- export { Dispatcher, type DispatcherOptions, type QueryFn };
241
+ export { Dispatcher, type DispatcherOptions, type QueryFn, type WorkerObserver };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  Dispatcher
3
- } from "./chunk-YWUZKKQ5.js";
3
+ } from "./chunk-RNKJRBEF.js";
4
4
  import "./chunk-SBP7MJP2.js";
5
5
  export {
6
6
  Dispatcher
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  createIntegrationRoutes
3
- } from "./chunk-5H46YKCX.js";
4
- import "./chunk-6SCQVJBL.js";
5
- import "./chunk-DI3DIMUQ.js";
6
- import "./chunk-3ZIHOOA4.js";
3
+ } from "./chunk-RGLMLG7Y.js";
4
+ import "./chunk-2GPBHK2M.js";
5
+ import "./chunk-GAD64LKZ.js";
6
+ import "./chunk-DKTAW2N5.js";
7
7
  import "./chunk-O4H76K3B.js";
8
8
  import "./chunk-US5FT2UB.js";
9
9
  import "./chunk-SBP7MJP2.js";
package/dist/index.js CHANGED
@@ -1,17 +1,17 @@
1
1
  import {
2
2
  Dispatcher,
3
3
  loadPersonaForAgent
4
- } from "./chunk-YWUZKKQ5.js";
4
+ } from "./chunk-RNKJRBEF.js";
5
5
  import {
6
6
  createIntegrationRoutes
7
- } from "./chunk-5H46YKCX.js";
7
+ } from "./chunk-RGLMLG7Y.js";
8
8
  import {
9
9
  uninstall
10
- } from "./chunk-6SCQVJBL.js";
10
+ } from "./chunk-2GPBHK2M.js";
11
11
  import {
12
12
  install
13
- } from "./chunk-DI3DIMUQ.js";
14
- import "./chunk-3ZIHOOA4.js";
13
+ } from "./chunk-GAD64LKZ.js";
14
+ import "./chunk-DKTAW2N5.js";
15
15
  import {
16
16
  status
17
17
  } from "./chunk-O4H76K3B.js";
package/dist/install.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  install,
3
3
  selectExposableAgents
4
- } from "./chunk-DI3DIMUQ.js";
5
- import "./chunk-3ZIHOOA4.js";
4
+ } from "./chunk-GAD64LKZ.js";
5
+ import "./chunk-DKTAW2N5.js";
6
6
  import "./chunk-US5FT2UB.js";
7
7
  import "./chunk-SBP7MJP2.js";
8
8
  export {
package/dist/mail-hook.js CHANGED
@@ -9,7 +9,7 @@ var CONFIG_PATH = join(AGENTICMAIL_DIR, "config.json");
9
9
  var CURSOR_PATH = join(AGENTICMAIL_DIR, "claudecode-hook-cursor.json");
10
10
  var HOOK_VERSION = "1";
11
11
  var HTTP_TIMEOUT_MS = 2e3;
12
- var PRE_TOOL_USE_THROTTLE_MS = 3e4;
12
+ var STOP_THROTTLE_MS = 15e3;
13
13
  async function readStdinJson() {
14
14
  if (process.stdin.isTTY) return null;
15
15
  return new Promise((resolve) => {
@@ -73,7 +73,7 @@ async function main() {
73
73
  }
74
74
  }
75
75
  const now = Date.now();
76
- if (eventName === "PreToolUse" && now - lastCheckedMs < PRE_TOOL_USE_THROTTLE_MS) {
76
+ if (eventName === "Stop" && now - lastCheckedMs < STOP_THROTTLE_MS) {
77
77
  return;
78
78
  }
79
79
  let messages = [];
@@ -94,7 +94,7 @@ async function main() {
94
94
  return Number.isFinite(t) && t > cursorMs;
95
95
  });
96
96
  if (newOnes.length === 0) {
97
- if (eventName === "PreToolUse") {
97
+ if (eventName === "Stop") {
98
98
  try {
99
99
  if (!existsSync(dirname(CURSOR_PATH))) mkdirSync(dirname(CURSOR_PATH), { recursive: true });
100
100
  writeFileSync(
@@ -135,12 +135,19 @@ async function main() {
135
135
  );
136
136
  } catch {
137
137
  }
138
- process.stdout.write(JSON.stringify({
139
- hookSpecificOutput: {
140
- hookEventName: eventName,
141
- additionalContext: lines.join("\n")
142
- }
143
- }));
138
+ if (eventName === "Stop") {
139
+ process.stdout.write(JSON.stringify({
140
+ decision: "block",
141
+ reason: lines.join("\n")
142
+ }));
143
+ } else {
144
+ process.stdout.write(JSON.stringify({
145
+ hookSpecificOutput: {
146
+ hookEventName: eventName,
147
+ additionalContext: lines.join("\n")
148
+ }
149
+ }));
150
+ }
144
151
  }
145
152
  main().catch(() => {
146
153
  process.exit(0);
package/dist/uninstall.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  uninstall
3
- } from "./chunk-6SCQVJBL.js";
4
- import "./chunk-3ZIHOOA4.js";
3
+ } from "./chunk-2GPBHK2M.js";
4
+ import "./chunk-DKTAW2N5.js";
5
5
  import "./chunk-US5FT2UB.js";
6
6
  import "./chunk-SBP7MJP2.js";
7
7
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agenticmail/claudecode",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "Claude Code integration for AgenticMail — surfaces every AgenticMail agent as a native Claude Code subagent so any Claude Code session can delegate to them with the Agent tool",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",