agenthub-multiagent-mcp 1.50.0 → 1.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/brain/backend.d.ts.map +1 -1
- package/dist/brain/backend.js +11 -2
- package/dist/brain/backend.js.map +1 -1
- package/dist/brain/memvidBrain.d.ts +24 -0
- package/dist/brain/memvidBrain.d.ts.map +1 -1
- package/dist/brain/memvidBrain.js +350 -93
- package/dist/brain/memvidBrain.js.map +1 -1
- package/dist/brain/memvidCompaction.test.d.ts +10 -0
- package/dist/brain/memvidCompaction.test.d.ts.map +1 -0
- package/dist/brain/memvidCompaction.test.js +71 -0
- package/dist/brain/memvidCompaction.test.js.map +1 -0
- package/dist/client.d.ts +30 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +122 -6
- package/dist/client.js.map +1 -1
- package/dist/client.unit.test.js +50 -0
- package/dist/client.unit.test.js.map +1 -1
- package/dist/daemon.js +76 -1
- package/dist/daemon.js.map +1 -1
- package/dist/employee.d.ts +0 -5
- package/dist/employee.d.ts.map +1 -1
- package/dist/employee.js +14 -1
- package/dist/employee.js.map +1 -1
- package/dist/streamingRuntimeLib.d.ts +132 -0
- package/dist/streamingRuntimeLib.d.ts.map +1 -0
- package/dist/streamingRuntimeLib.js +155 -0
- package/dist/streamingRuntimeLib.js.map +1 -0
- package/dist/streamingRuntimeLib.test.d.ts +2 -0
- package/dist/streamingRuntimeLib.test.d.ts.map +1 -0
- package/dist/streamingRuntimeLib.test.js +147 -0
- package/dist/streamingRuntimeLib.test.js.map +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +132 -4
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/tools.test.js +3 -2
- package/dist/tools/tools.test.js.map +1 -1
- package/dist/tools/zohoAttachments.test.d.ts +10 -0
- package/dist/tools/zohoAttachments.test.d.ts.map +1 -0
- package/dist/tools/zohoAttachments.test.js +204 -0
- package/dist/tools/zohoAttachments.test.js.map +1 -0
- package/native/agenthub-memvid/agenthub-memvid.linux-x64-gnu.node +0 -0
- package/native/agenthub-memvid/agenthub-memvid.win32-x64-msvc.node +0 -0
- package/native/agenthub-memvid/index.d.ts +58 -0
- package/native/agenthub-memvid/index.js +319 -0
- package/package.json +5 -3
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for the employee streaming runtime (add-employee-streaming-runtime).
|
|
3
|
+
*
|
|
4
|
+
* Extracted from streamingRuntime.ts so the auth assert, inbound-message decision
|
|
5
|
+
* (self-filter / non-actionable / dedup), turn rendering, and context-recycle
|
|
6
|
+
* decision are unit-testable without an SDK subprocess or a live WebSocket.
|
|
7
|
+
*
|
|
8
|
+
* Verified premise (VPS spike 2026-06-26, org brain #2209): the Claude Agent SDK
|
|
9
|
+
* streaming mode runs on the subscription (CLAUDE_CODE_OAUTH_TOKEN, no
|
|
10
|
+
* ANTHROPIC_API_KEY) and processes a mid-run yielded message in the same session.
|
|
11
|
+
*/
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// Kill switch + auth (§2.1 / §D3)
|
|
14
|
+
// =============================================================================
|
|
15
|
+
/** §2.1 — the streaming runtime is OFF unless explicitly enabled. */
|
|
16
|
+
export function streamingEnabled(env = process.env) {
|
|
17
|
+
return /^(1|true|yes|on)$/i.test((env.AGENTHUB_EMPLOYEE_STREAMING_ENABLED || "").trim());
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* §D3 — assert subscription auth and fail CLOSED against metered billing. The
|
|
21
|
+
* SDK subprocess inherits the env; if ANTHROPIC_API_KEY were set it would take
|
|
22
|
+
* precedence over the subscription token, so we refuse to start.
|
|
23
|
+
*/
|
|
24
|
+
export function assertSubscriptionAuth(env = process.env) {
|
|
25
|
+
if (env.ANTHROPIC_API_KEY) {
|
|
26
|
+
throw new Error("ANTHROPIC_API_KEY is set — refusing to start the streaming runtime " +
|
|
27
|
+
"(subscription-only; an API key would route to metered billing). Unset it.");
|
|
28
|
+
}
|
|
29
|
+
const oauthToken = env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
30
|
+
if (!oauthToken) {
|
|
31
|
+
throw new Error("CLAUDE_CODE_OAUTH_TOKEN is required for the streaming runtime (subscription auth via `claude setup-token`).");
|
|
32
|
+
}
|
|
33
|
+
return { oauthToken };
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Decide whether a raw WS event becomes a session turn. Mirrors the one-shot
|
|
37
|
+
* daemon's at-most-once guards: ignore self-messages, never auto-act on
|
|
38
|
+
* `response`/`status` (loop-safety), and dedup against handled ids. The caller
|
|
39
|
+
* ACKs regardless and records handled ids.
|
|
40
|
+
*/
|
|
41
|
+
export function decideInbound(envelope, agentId, handled) {
|
|
42
|
+
if (!envelope || typeof envelope !== "object")
|
|
43
|
+
return { yield: false, reason: "not-a-message" };
|
|
44
|
+
const env = envelope;
|
|
45
|
+
if (env.type !== "message" || !env.message)
|
|
46
|
+
return { yield: false, reason: "not-a-message" };
|
|
47
|
+
const m = env.message;
|
|
48
|
+
if (!m.id)
|
|
49
|
+
return { yield: false, reason: "no-id" };
|
|
50
|
+
if (m.from_agent === agentId)
|
|
51
|
+
return { yield: false, reason: "self", message: m };
|
|
52
|
+
if (m.type === "response" || m.type === "status")
|
|
53
|
+
return { yield: false, reason: "non-actionable", message: m };
|
|
54
|
+
if (handled.has(m.id))
|
|
55
|
+
return { yield: false, reason: "duplicate", message: m };
|
|
56
|
+
return { yield: true, message: m };
|
|
57
|
+
}
|
|
58
|
+
/** Render an AgentHub message as the text content of a session turn. */
|
|
59
|
+
export function buildTurnContent(m) {
|
|
60
|
+
const priority = m.priority && m.priority !== "normal" ? ` | Priority: ${m.priority}` : "";
|
|
61
|
+
return (`You received a message on AgentHub.\n` +
|
|
62
|
+
`From: ${m.from_agent}\n` +
|
|
63
|
+
`Subject: ${m.subject ?? "(none)"}\n` +
|
|
64
|
+
`Type: ${m.type ?? "message"}${priority}\n\n` +
|
|
65
|
+
`${m.body}\n\n` +
|
|
66
|
+
`Reply with mcp__agenthub__reply(message_id="${m.id}", body="…"). ` +
|
|
67
|
+
`If this is outside your charter, escalate via send_message instead of guessing.`);
|
|
68
|
+
}
|
|
69
|
+
export function toUserTurn(content, sessionId) {
|
|
70
|
+
return { type: "user", message: { role: "user", content }, parent_tool_use_id: null, session_id: sessionId };
|
|
71
|
+
}
|
|
72
|
+
export function defaultRecycleOptions(env = process.env) {
|
|
73
|
+
const n = (v, d) => {
|
|
74
|
+
const x = Number.parseInt((v || "").trim(), 10);
|
|
75
|
+
return Number.isFinite(x) && x > 0 ? x : d;
|
|
76
|
+
};
|
|
77
|
+
return {
|
|
78
|
+
maxTurns: n(env.AGENTHUB_EMPLOYEE_STREAM_MAX_TURNS, 25),
|
|
79
|
+
tokenBudget: n(env.AGENTHUB_EMPLOYEE_STREAM_TOKEN_BUDGET, 200_000),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* §D4 — recycle the kept-alive session once it has run enough turns or
|
|
84
|
+
* accumulated enough input-token context, to bound Max-cap consumption. The
|
|
85
|
+
* caller starts a fresh session (carrying a short summary) on `true`.
|
|
86
|
+
*/
|
|
87
|
+
export function shouldRecycle(state, opts) {
|
|
88
|
+
return state.turns >= opts.maxTurns || state.estInputTokens >= opts.tokenBudget;
|
|
89
|
+
}
|
|
90
|
+
export function newRuntimeStats() {
|
|
91
|
+
return { turns: 0, estInputTokens: 0, estOutputTokens: 0, sessionRecycles: 0, reconnects: 0, dedupHits: 0 };
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Fold one SDK `result` message's usage into the cumulative stats (mutates + returns).
|
|
95
|
+
* estInputTokens counts fresh + cache-read + cache-creation input (matching the
|
|
96
|
+
* recycle accounting); estOutputTokens is the generated output.
|
|
97
|
+
*/
|
|
98
|
+
export function recordResultUsage(stats, usage) {
|
|
99
|
+
const u = usage ?? {};
|
|
100
|
+
stats.turns += 1;
|
|
101
|
+
stats.estInputTokens +=
|
|
102
|
+
(u.input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0);
|
|
103
|
+
stats.estOutputTokens += u.output_tokens ?? 0;
|
|
104
|
+
return stats;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* A `/usage`-style structured one-liner for the runtime log — a single greppable
|
|
108
|
+
* line (log-exporter / Loki friendly). Native Prometheus is deliberately deferred
|
|
109
|
+
* (§4.2): the one-shot daemon exposes no metrics series to mirror, so a scrapable
|
|
110
|
+
* log line is the sensible surface for a default-off feature.
|
|
111
|
+
*/
|
|
112
|
+
export function formatUsageLine(agentId, stats, uptimeSec) {
|
|
113
|
+
const uptime = Math.max(0, Math.floor(uptimeSec));
|
|
114
|
+
return (`[usage] employee=${agentId} uptime_s=${uptime} turns=${stats.turns} ` +
|
|
115
|
+
`est_input_tokens=${stats.estInputTokens} est_output_tokens=${stats.estOutputTokens} ` +
|
|
116
|
+
`session_recycles=${stats.sessionRecycles} reconnects=${stats.reconnects} dedup_hits=${stats.dedupHits}`);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Env for the in-session MCP the streaming runtime launches. Critically sets
|
|
120
|
+
* `AGENTHUB_HEADLESS=true`: without it, a failed startup reconnect routes the
|
|
121
|
+
* model to `agent_register`, which polls a browser-confirm flow for 10 min
|
|
122
|
+
* (3000ms × 200) that nobody can complete in a headless SDK subprocess — the
|
|
123
|
+
* observed ~7-min stall (§3). Headless routes `agent_register` to the fast,
|
|
124
|
+
* non-interactive register instead. A user API key (when the daemon has one) is
|
|
125
|
+
* passed through so any such fallback register is coherent and user-owned rather
|
|
126
|
+
* than a browser round-trip.
|
|
127
|
+
*/
|
|
128
|
+
export function inSessionMcpEnv(opts) {
|
|
129
|
+
const env = {
|
|
130
|
+
AGENTHUB_URL: opts.apiUrl,
|
|
131
|
+
AGENTHUB_API_KEY: opts.apiKey,
|
|
132
|
+
AGENTHUB_AGENT_ID: opts.agentId,
|
|
133
|
+
AGENTHUB_DISABLE_WEBSOCKET: "true",
|
|
134
|
+
AGENTHUB_HEADLESS: "true",
|
|
135
|
+
};
|
|
136
|
+
if (opts.userApiKey)
|
|
137
|
+
env.AGENTHUB_USER_API_KEY = opts.userApiKey;
|
|
138
|
+
return env;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Fail-fast identity guard the runtime previously lacked. The daemon establishes
|
|
142
|
+
* the worker connect token in the workspace's `.agenthub/state` before handing
|
|
143
|
+
* off (and reconnect is a pure re-attach server-side, so the in-session MCP
|
|
144
|
+
* safely shares it). If that state is absent the hand-off is broken; refuse to
|
|
145
|
+
* start sessions (throw → `main()` exits non-zero → the daemon restarts and
|
|
146
|
+
* re-heals identity) rather than letting the in-session MCP hang on registration.
|
|
147
|
+
*/
|
|
148
|
+
export function assertWorkerIdentityReady(stateExists, agentId) {
|
|
149
|
+
if (!stateExists) {
|
|
150
|
+
throw new Error(`no worker connect-token state for "${agentId}" in the workspace — refusing to ` +
|
|
151
|
+
`start streaming sessions. The daemon establishes .agenthub/state before handing ` +
|
|
152
|
+
`off; a missing token would hang the in-session MCP. Exiting for restart + re-heal.`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=streamingRuntimeLib.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"streamingRuntimeLib.js","sourceRoot":"","sources":["../src/streamingRuntimeLib.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"streamingRuntimeLib.test.d.ts","sourceRoot":"","sources":["../src/streamingRuntimeLib.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { streamingEnabled, assertSubscriptionAuth, decideInbound, buildTurnContent, toUserTurn, shouldRecycle, defaultRecycleOptions, newRuntimeStats, recordResultUsage, formatUsageLine, inSessionMcpEnv, assertWorkerIdentityReady, } from "./streamingRuntimeLib.js";
|
|
3
|
+
// add-employee-streaming-runtime §2 — pure-helper tests.
|
|
4
|
+
describe("streamingEnabled (kill switch §2.1)", () => {
|
|
5
|
+
it("off by default / falsey", () => {
|
|
6
|
+
for (const v of [undefined, "", "false", "0", "no", "off"]) {
|
|
7
|
+
expect(streamingEnabled(v === undefined ? {} : { AGENTHUB_EMPLOYEE_STREAMING_ENABLED: v })).toBe(false);
|
|
8
|
+
}
|
|
9
|
+
});
|
|
10
|
+
it("on for explicit truthy", () => {
|
|
11
|
+
for (const v of ["true", "1", "yes", "on", " TRUE "]) {
|
|
12
|
+
expect(streamingEnabled({ AGENTHUB_EMPLOYEE_STREAMING_ENABLED: v })).toBe(true);
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
describe("assertSubscriptionAuth (§D3 fail-closed)", () => {
|
|
17
|
+
it("throws when ANTHROPIC_API_KEY is set (refuse metered billing)", () => {
|
|
18
|
+
expect(() => assertSubscriptionAuth({ ANTHROPIC_API_KEY: "sk-x", CLAUDE_CODE_OAUTH_TOKEN: "t" })).toThrow(/ANTHROPIC_API_KEY/);
|
|
19
|
+
});
|
|
20
|
+
it("throws when no oauth token", () => {
|
|
21
|
+
expect(() => assertSubscriptionAuth({})).toThrow(/CLAUDE_CODE_OAUTH_TOKEN/);
|
|
22
|
+
});
|
|
23
|
+
it("returns the token on subscription-only env", () => {
|
|
24
|
+
expect(assertSubscriptionAuth({ CLAUDE_CODE_OAUTH_TOKEN: "tok" })).toEqual({ oauthToken: "tok" });
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
describe("decideInbound (§2.5 loop-safety)", () => {
|
|
28
|
+
const handled = (ids = []) => ({ has: (id) => ids.includes(id) });
|
|
29
|
+
const msg = (over) => ({ type: "message", message: { id: "m1", from_agent: "alice", type: "task", body: "do it", ...over } });
|
|
30
|
+
it("yields an actionable inbound from another agent", () => {
|
|
31
|
+
const d = decideInbound(msg({}), "me", handled());
|
|
32
|
+
expect(d.yield).toBe(true);
|
|
33
|
+
expect(d.message?.id).toBe("m1");
|
|
34
|
+
});
|
|
35
|
+
it("skips self messages", () => {
|
|
36
|
+
expect(decideInbound(msg({ from_agent: "me" }), "me", handled()).yield).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
it("skips response/status (non-actionable)", () => {
|
|
39
|
+
expect(decideInbound(msg({ type: "response" }), "me", handled()).reason).toBe("non-actionable");
|
|
40
|
+
expect(decideInbound(msg({ type: "status" }), "me", handled()).reason).toBe("non-actionable");
|
|
41
|
+
});
|
|
42
|
+
it("skips duplicates", () => {
|
|
43
|
+
expect(decideInbound(msg({}), "me", handled(["m1"])).reason).toBe("duplicate");
|
|
44
|
+
});
|
|
45
|
+
it("skips non-message envelopes", () => {
|
|
46
|
+
expect(decideInbound({ type: "ping" }, "me", handled()).yield).toBe(false);
|
|
47
|
+
expect(decideInbound(null, "me", handled()).yield).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
describe("buildTurnContent + toUserTurn", () => {
|
|
51
|
+
it("renders sender, body, and reply hint", () => {
|
|
52
|
+
const c = buildTurnContent({ id: "m1", from_agent: "alice", type: "task", subject: "Hi", body: "ping" });
|
|
53
|
+
expect(c).toContain("From: alice");
|
|
54
|
+
expect(c).toContain("ping");
|
|
55
|
+
expect(c).toContain('mcp__agenthub__reply(message_id="m1"');
|
|
56
|
+
});
|
|
57
|
+
it("wraps in the proven SDK user-turn shape", () => {
|
|
58
|
+
const t = toUserTurn("hello", "sess");
|
|
59
|
+
expect(t).toEqual({ type: "user", message: { role: "user", content: "hello" }, parent_tool_use_id: null, session_id: "sess" });
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
describe("shouldRecycle (§D4 context bounding)", () => {
|
|
63
|
+
const opts = { maxTurns: 10, tokenBudget: 100 };
|
|
64
|
+
it("recycles at the turn bound", () => {
|
|
65
|
+
expect(shouldRecycle({ turns: 10, estInputTokens: 0 }, opts)).toBe(true);
|
|
66
|
+
expect(shouldRecycle({ turns: 9, estInputTokens: 0 }, opts)).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
it("recycles at the token bound", () => {
|
|
69
|
+
expect(shouldRecycle({ turns: 0, estInputTokens: 100 }, opts)).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
it("default options read env with sane fallbacks", () => {
|
|
72
|
+
expect(defaultRecycleOptions({})).toEqual({ maxTurns: 25, tokenBudget: 200_000 });
|
|
73
|
+
expect(defaultRecycleOptions({ AGENTHUB_EMPLOYEE_STREAM_MAX_TURNS: "5" }).maxTurns).toBe(5);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
describe("RuntimeStats (§4.1 per-employee observability)", () => {
|
|
77
|
+
it("newRuntimeStats starts every counter at zero", () => {
|
|
78
|
+
expect(newRuntimeStats()).toEqual({
|
|
79
|
+
turns: 0,
|
|
80
|
+
estInputTokens: 0,
|
|
81
|
+
estOutputTokens: 0,
|
|
82
|
+
sessionRecycles: 0,
|
|
83
|
+
reconnects: 0,
|
|
84
|
+
dedupHits: 0,
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
it("recordResultUsage folds fresh + cache-read + cache-creation input and output across turns", () => {
|
|
88
|
+
const s = newRuntimeStats();
|
|
89
|
+
recordResultUsage(s, {
|
|
90
|
+
input_tokens: 100,
|
|
91
|
+
cache_read_input_tokens: 50,
|
|
92
|
+
cache_creation_input_tokens: 10,
|
|
93
|
+
output_tokens: 20,
|
|
94
|
+
});
|
|
95
|
+
recordResultUsage(s, { input_tokens: 5, output_tokens: 3 });
|
|
96
|
+
expect(s.turns).toBe(2);
|
|
97
|
+
expect(s.estInputTokens).toBe(165); // 160 + 5
|
|
98
|
+
expect(s.estOutputTokens).toBe(23); // 20 + 3
|
|
99
|
+
});
|
|
100
|
+
it("recordResultUsage tolerates missing/undefined usage (still counts the turn)", () => {
|
|
101
|
+
const s = newRuntimeStats();
|
|
102
|
+
recordResultUsage(s, undefined);
|
|
103
|
+
recordResultUsage(s, {});
|
|
104
|
+
expect(s.turns).toBe(2);
|
|
105
|
+
expect(s.estInputTokens).toBe(0);
|
|
106
|
+
expect(s.estOutputTokens).toBe(0);
|
|
107
|
+
});
|
|
108
|
+
it("formatUsageLine renders one greppable line with every counter", () => {
|
|
109
|
+
const s = { turns: 4, estInputTokens: 1000, estOutputTokens: 200, sessionRecycles: 1, reconnects: 2, dedupHits: 3 };
|
|
110
|
+
const line = formatUsageLine("eng-ic-test", s, 65.9);
|
|
111
|
+
expect(line).toBe("[usage] employee=eng-ic-test uptime_s=65 turns=4 est_input_tokens=1000 est_output_tokens=200 " +
|
|
112
|
+
"session_recycles=1 reconnects=2 dedup_hits=3");
|
|
113
|
+
expect(line).not.toContain("\n");
|
|
114
|
+
});
|
|
115
|
+
it("formatUsageLine floors negative/fractional uptime to a non-negative integer", () => {
|
|
116
|
+
expect(formatUsageLine("x", newRuntimeStats(), -5)).toContain("uptime_s=0");
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
describe("inSessionMcpEnv (§3 connect-token — no browser-poll stall)", () => {
|
|
120
|
+
const base = { apiUrl: "https://h", apiKey: "ah_x", agentId: "eng-ic-test" };
|
|
121
|
+
it("always sets AGENTHUB_HEADLESS=true so agent_register can't enter the 10-min browser poll", () => {
|
|
122
|
+
expect(inSessionMcpEnv(base).AGENTHUB_HEADLESS).toBe("true");
|
|
123
|
+
});
|
|
124
|
+
it("carries the core connect-token env (url/key/agent) and disables the in-session WS", () => {
|
|
125
|
+
const env = inSessionMcpEnv(base);
|
|
126
|
+
expect(env.AGENTHUB_URL).toBe("https://h");
|
|
127
|
+
expect(env.AGENTHUB_API_KEY).toBe("ah_x");
|
|
128
|
+
expect(env.AGENTHUB_AGENT_ID).toBe("eng-ic-test");
|
|
129
|
+
expect(env.AGENTHUB_DISABLE_WEBSOCKET).toBe("true");
|
|
130
|
+
});
|
|
131
|
+
it("passes a user API key through only when present (coherent fallback register)", () => {
|
|
132
|
+
expect(inSessionMcpEnv(base).AGENTHUB_USER_API_KEY).toBeUndefined();
|
|
133
|
+
expect(inSessionMcpEnv({ ...base, userApiKey: "ah_user" }).AGENTHUB_USER_API_KEY).toBe("ah_user");
|
|
134
|
+
});
|
|
135
|
+
it("does not pass an empty user API key", () => {
|
|
136
|
+
expect(inSessionMcpEnv({ ...base, userApiKey: "" }).AGENTHUB_USER_API_KEY).toBeUndefined();
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
describe("assertWorkerIdentityReady (§3 fail-fast identity guard)", () => {
|
|
140
|
+
it("throws when the workspace connect-token state is missing", () => {
|
|
141
|
+
expect(() => assertWorkerIdentityReady(false, "eng-ic-test")).toThrow(/eng-ic-test/);
|
|
142
|
+
});
|
|
143
|
+
it("passes silently when the state file exists", () => {
|
|
144
|
+
expect(() => assertWorkerIdentityReady(true, "eng-ic-test")).not.toThrow();
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
//# sourceMappingURL=streamingRuntimeLib.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"streamingRuntimeLib.test.js","sourceRoot":"","sources":["../src/streamingRuntimeLib.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,SAAS,EAAE,OAAO,EAAqD,MAAM,cAAc,CAAC;AACrG,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,oCAAoC,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,SAAS,EAAE,OAAO,EAAqD,MAAM,cAAc,CAAC;AACrG,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,oCAAoC,CAAC;AA4E/D,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,QAAQ,EAAE,OAAO,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,iBAAiB,EAAE,MAAM,MAAM,CAAC;IAChC,iBAAiB,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B,aAAa,EAAE,MAAM,MAAM,CAAC;IAC5B,aAAa,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,cAAc,EAAE,MAAM,WAAW,CAAC;CACnC;AAED,UAAU,WAAW;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAwJD,wBAAgB,aAAa,IAAI,IAAI,EAAE,CA20FtC;AA2BD,wBAAsB,cAAc,CAClC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,MAAM,EAAE,SAAS,EACjB,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,OAAO,CAAC,CAqoFlB"}
|
package/dist/tools/index.js
CHANGED
|
@@ -11,6 +11,41 @@ import { registerOpenSpecVizTools, handleOpenSpecVizToolCall } from "./openspec-
|
|
|
11
11
|
import { installPackFromArchive } from "../skillPackInstaller.js";
|
|
12
12
|
import { getServerTime, useTimeInjection } from "../lib/server-time.js";
|
|
13
13
|
import { ENG_IC_CHARTER } from "../employee.js";
|
|
14
|
+
import { promises as fsp } from "node:fs";
|
|
15
|
+
import * as os from "node:os";
|
|
16
|
+
import * as path from "node:path";
|
|
17
|
+
// add-zoho-attachments — downloads land on local disk and the agent receives a
|
|
18
|
+
// PATH. Inline content stays opt-in and small; a 5 MB catalogue as text costs a
|
|
19
|
+
// context window and buys nothing a file reader can't do better.
|
|
20
|
+
const INLINE_ATTACHMENT_MAX_BYTES = 1024 * 1024;
|
|
21
|
+
/** Root for downloaded attachments. Overridable for tests and for operators. */
|
|
22
|
+
function attachmentDownloadRoot() {
|
|
23
|
+
return (process.env.AGENTHUB_ATTACHMENT_DIR ||
|
|
24
|
+
path.join(os.homedir(), ".agenthub", "zoho-attachments"));
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Sanitize an untrusted path segment into a single safe directory/file name.
|
|
28
|
+
* Upstream supplies both the message id and the filename, so neither may be
|
|
29
|
+
* allowed to traverse out of the download root.
|
|
30
|
+
*/
|
|
31
|
+
function safeSegment(raw, fallback) {
|
|
32
|
+
const base = path.basename((raw || "").replace(/\\/g, "/")).replace(/[\x00-\x1f]/g, "");
|
|
33
|
+
const cleaned = base.replace(/^\.+$/, "").trim();
|
|
34
|
+
return cleaned || fallback;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Write attachment bytes under <root>/<message-id>/<filename> and return the
|
|
38
|
+
* absolute path. Grouping by message keeps a thread's documents together and
|
|
39
|
+
* avoids collisions between identically named files on different messages.
|
|
40
|
+
*/
|
|
41
|
+
async function writeAttachmentToDisk(messageId, filename, bytes) {
|
|
42
|
+
const root = attachmentDownloadRoot();
|
|
43
|
+
const dir = path.join(root, safeSegment(messageId, "message"));
|
|
44
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
45
|
+
const filePath = path.join(dir, safeSegment(filename, "attachment"));
|
|
46
|
+
await fsp.writeFile(filePath, bytes);
|
|
47
|
+
return filePath;
|
|
48
|
+
}
|
|
14
49
|
/**
|
|
15
50
|
* Background sync: pushes local context to server after every save.
|
|
16
51
|
* Non-blocking — logs errors but never throws.
|
|
@@ -738,7 +773,7 @@ export function registerTools() {
|
|
|
738
773
|
},
|
|
739
774
|
{
|
|
740
775
|
name: "slack_upload_file",
|
|
741
|
-
description: "Upload a file you produced (PDF, CSV, docx, deck, log, etc.) to a Slack channel or a person's DM (
|
|
776
|
+
description: "Upload a file you produced (PDF, CSV, docx, deck, log, etc.) to a Slack channel or a person's DM. Employees use a trusted path; other agents (e.g. a Claude Code session) use a governed path — the target must be on the deployment's file-delivery allowlist and the kill switch enabled, else the call is rejected (target_not_allowed / file_delivery_disabled). Generate the file locally first, then call this with the target and the file's local path. target is a Slack channel id (C0123ABC), a #channel-name (the bot must be a member), or a user id (U0123ABC) to deliver it as a DM. For delivering into your current task's thread, use deliver_file instead.",
|
|
742
777
|
inputSchema: {
|
|
743
778
|
type: "object",
|
|
744
779
|
properties: {
|
|
@@ -2824,17 +2859,78 @@ export function registerTools() {
|
|
|
2824
2859
|
},
|
|
2825
2860
|
{
|
|
2826
2861
|
name: "zoho_search",
|
|
2827
|
-
description: "Search messages in one of your registered mailboxes
|
|
2862
|
+
description: "Search messages in one of your registered mailboxes. `query` accepts a plain term " +
|
|
2863
|
+
"(normalized to Zoho's `entire:` form for you) OR Zoho's typed search syntax, which is " +
|
|
2864
|
+
"passed through unchanged. Typed forms: `entire:<term>` (anywhere in the mail), " +
|
|
2865
|
+
"`content:<term>` (body only), `sender:<addr-or-word>`, `to:<addr>`, `cc:<addr>`, " +
|
|
2866
|
+
"`subject:<term>`. Combine conditions with `::` (e.g. `entire:payroll::has:attachment`) " +
|
|
2867
|
+
"and alternate with `:or:` (e.g. `sender:a@x.com::or:to:a@x.com`). " +
|
|
2868
|
+
"Searches the last 30 days unless you pass `since`. (requires agent_register first)",
|
|
2828
2869
|
inputSchema: {
|
|
2829
2870
|
type: "object",
|
|
2830
2871
|
properties: {
|
|
2831
2872
|
mailbox: { type: "string", description: "Your mailbox address" },
|
|
2832
|
-
query: {
|
|
2873
|
+
query: {
|
|
2874
|
+
type: "string",
|
|
2875
|
+
description: "A plain term (e.g. 'Baccara') or a typed Zoho search key (e.g. 'sender:eknath.a@baccara-geva.com')",
|
|
2876
|
+
},
|
|
2833
2877
|
limit: { type: "number", description: "Max results (default 20)" },
|
|
2878
|
+
since: {
|
|
2879
|
+
type: "string",
|
|
2880
|
+
description: "Oldest message to search, as RFC3339 (e.g. '2026-01-01T00:00:00Z') or unix millis. " +
|
|
2881
|
+
"Defaults to 30 days ago — widen this to search further back.",
|
|
2882
|
+
},
|
|
2834
2883
|
},
|
|
2835
2884
|
required: ["mailbox", "query"],
|
|
2836
2885
|
},
|
|
2837
2886
|
},
|
|
2887
|
+
{
|
|
2888
|
+
name: "zoho_list_attachments",
|
|
2889
|
+
description: "List the attachments on a message in one of your registered mailboxes, without downloading " +
|
|
2890
|
+
"them. Returns attachment_id, filename, mime_type (inferred from the extension — Zoho sends " +
|
|
2891
|
+
"no content type) and size_bytes. Inline parts (signatures, logos) are excluded unless you " +
|
|
2892
|
+
"pass include_inline. Use this to see WHAT is attached, then zoho_download_attachment to " +
|
|
2893
|
+
"fetch one. (requires agent_register first)",
|
|
2894
|
+
inputSchema: {
|
|
2895
|
+
type: "object",
|
|
2896
|
+
properties: {
|
|
2897
|
+
mailbox: { type: "string", description: "Your mailbox address (must be registered by you)" },
|
|
2898
|
+
message_id: { type: "string", description: "Zoho message id" },
|
|
2899
|
+
folder: { type: "string", description: "Zoho folder id the message is in" },
|
|
2900
|
+
include_inline: {
|
|
2901
|
+
type: "boolean",
|
|
2902
|
+
description: "Also return inline content-id parts (signatures, logos). Default false.",
|
|
2903
|
+
},
|
|
2904
|
+
},
|
|
2905
|
+
required: ["mailbox", "message_id"],
|
|
2906
|
+
},
|
|
2907
|
+
},
|
|
2908
|
+
{
|
|
2909
|
+
name: "zoho_download_attachment",
|
|
2910
|
+
description: "Download one attachment to a local file and return its PATH — hand that path to a PDF/doc " +
|
|
2911
|
+
"reader rather than reading the bytes into context. Returns { file_path, filename, mime_type, " +
|
|
2912
|
+
"size_bytes }. Content is NOT returned inline unless you pass inline:true, which is capped at " +
|
|
2913
|
+
"1 MB and intended only for small text files. Attachments over the server's size cap fail with " +
|
|
2914
|
+
"an error naming the actual size and the cap — never a truncated file. " +
|
|
2915
|
+
"(requires agent_register first)",
|
|
2916
|
+
inputSchema: {
|
|
2917
|
+
type: "object",
|
|
2918
|
+
properties: {
|
|
2919
|
+
mailbox: { type: "string", description: "Your mailbox address (must be registered by you)" },
|
|
2920
|
+
message_id: { type: "string", description: "Zoho message id" },
|
|
2921
|
+
attachment_id: {
|
|
2922
|
+
type: "string",
|
|
2923
|
+
description: "Attachment id from zoho_list_attachments or zoho_read_message",
|
|
2924
|
+
},
|
|
2925
|
+
folder: { type: "string", description: "Zoho folder id the message is in" },
|
|
2926
|
+
inline: {
|
|
2927
|
+
type: "boolean",
|
|
2928
|
+
description: "Also return the content as text in the response (max 1 MB). Default false — prefer file_path.",
|
|
2929
|
+
},
|
|
2930
|
+
},
|
|
2931
|
+
required: ["mailbox", "message_id", "attachment_id"],
|
|
2932
|
+
},
|
|
2933
|
+
},
|
|
2838
2934
|
{
|
|
2839
2935
|
name: "zoho_read_message",
|
|
2840
2936
|
description: "Read one message (headers + body) from one of your registered mailboxes. (requires agent_register first)",
|
|
@@ -5246,9 +5342,41 @@ export async function handleToolCall(name, args, client, context) {
|
|
|
5246
5342
|
case "zoho_search": {
|
|
5247
5343
|
if (!agentId)
|
|
5248
5344
|
throw new Error("Not registered. Call agent_register first.");
|
|
5249
|
-
const res = await client.zohoSearchMessages(args.mailbox, args.query, args.limit);
|
|
5345
|
+
const res = await client.zohoSearchMessages(args.mailbox, args.query, args.limit, args.since);
|
|
5250
5346
|
return { content: [{ type: "text", text: JSON.stringify(res, null, 2) }] };
|
|
5251
5347
|
}
|
|
5348
|
+
case "zoho_list_attachments": {
|
|
5349
|
+
if (!agentId)
|
|
5350
|
+
throw new Error("Not registered. Call agent_register first.");
|
|
5351
|
+
const res = await client.zohoListAttachments(args.mailbox, args.message_id, args.folder, args.include_inline === true);
|
|
5352
|
+
return { content: [{ type: "text", text: JSON.stringify(res, null, 2) }] };
|
|
5353
|
+
}
|
|
5354
|
+
case "zoho_download_attachment": {
|
|
5355
|
+
if (!agentId)
|
|
5356
|
+
throw new Error("Not registered. Call agent_register first.");
|
|
5357
|
+
const messageId = args.message_id;
|
|
5358
|
+
const { filename, mimeType, bytes } = await client.zohoDownloadAttachment(args.mailbox, messageId, args.attachment_id, args.folder);
|
|
5359
|
+
const filePath = await writeAttachmentToDisk(messageId, filename, bytes);
|
|
5360
|
+
const out = {
|
|
5361
|
+
file_path: filePath,
|
|
5362
|
+
filename: path.basename(filePath),
|
|
5363
|
+
mime_type: mimeType,
|
|
5364
|
+
size_bytes: bytes.length,
|
|
5365
|
+
};
|
|
5366
|
+
// Inline content is opt-in and capped: a multi-MB document as text blows
|
|
5367
|
+
// out the context window for no benefit when a path will do.
|
|
5368
|
+
if (args.inline === true) {
|
|
5369
|
+
if (bytes.length <= INLINE_ATTACHMENT_MAX_BYTES) {
|
|
5370
|
+
out.content = bytes.toString("utf8");
|
|
5371
|
+
}
|
|
5372
|
+
else {
|
|
5373
|
+
out.inline_omitted =
|
|
5374
|
+
`Attachment is ${bytes.length} bytes, over the ${INLINE_ATTACHMENT_MAX_BYTES} byte ` +
|
|
5375
|
+
`inline limit. It was written to file_path — read that instead.`;
|
|
5376
|
+
}
|
|
5377
|
+
}
|
|
5378
|
+
return { content: [{ type: "text", text: JSON.stringify(out, null, 2) }] };
|
|
5379
|
+
}
|
|
5252
5380
|
case "zoho_read_message": {
|
|
5253
5381
|
if (!agentId)
|
|
5254
5382
|
throw new Error("Not registered. Call agent_register first.");
|