@turmind/halo 1.4.6 → 1.5.0-alpha
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 +0 -1
- package/admin-out/404.html +1 -1
- package/admin-out/_next/static/chunks/160.c52d3d6cf7bc8286.js +1 -0
- package/admin-out/_next/static/chunks/220.e250bbbf65d24094.js +1 -0
- package/admin-out/_next/static/chunks/362.a0c58d3bf9b1300b.js +1 -0
- package/admin-out/_next/static/chunks/449.c4b881b9ebdc556e.js +1 -0
- package/admin-out/_next/static/chunks/613.fb09e55e0a61d3fe.js +1 -0
- package/admin-out/_next/static/chunks/73.05abce37d312e880.js +1 -0
- package/admin-out/_next/static/chunks/803-7128009ee0e1c10e.js +1 -0
- package/admin-out/_next/static/chunks/839.a2729fd33013d763.js +1 -0
- package/admin-out/_next/static/chunks/868.bbb91afd48c9eb58.js +1 -0
- package/admin-out/_next/static/chunks/892.a35118e09d8d8efd.js +1 -0
- package/admin-out/_next/static/chunks/989-7420ee650083df43.js +1 -0
- package/admin-out/_next/static/chunks/app/page-c33392db67853747.js +1 -0
- package/admin-out/_next/static/chunks/webpack-1a264efd965db7ab.js +1 -0
- package/admin-out/_next/static/css/7997e44c08e34b2b.css +3 -0
- package/admin-out/index.html +1 -1
- package/admin-out/index.txt +5 -5
- package/bundled-docs/design/agent.md +8 -7
- package/bundled-docs/design/architecture.md +9 -5
- package/bundled-docs/design/canvas-extensions.md +181 -0
- package/bundled-docs/design/cron.md +1 -1
- package/bundled-docs/design/feishu.md +3 -1
- package/bundled-docs/design/observability.md +2 -0
- package/bundled-docs/design/prompt-system.md +6 -10
- package/bundled-docs/design/relay.md +19 -15
- package/bundled-docs/design/session.md +15 -12
- package/bundled-docs/design/slack.md +2 -2
- package/bundled-docs/design/storage.md +4 -2
- package/bundled-docs/design/telegram.md +4 -2
- package/bundled-docs/design/wechat.md +7 -5
- package/bundled-docs/design/wecom.md +2 -2
- package/bundled-docs/design/ws.md +25 -18
- package/bundled-docs/dev/add-model-provider.md +21 -18
- package/bundled-docs/dev/api.md +25 -24
- package/bundled-docs/dev/env.md +2 -2
- package/bundled-docs/dev/tools.md +17 -82
- package/bundled-docs/guide/channels/wechat.md +1 -1
- package/bundled-docs/guide/cli.md +0 -1
- package/bundled-docs/guide/delegation-and-access.md +1 -2
- package/bundled-docs/guide/workspace.md +11 -0
- package/bundled-docs/requirements/chat.md +5 -4
- package/bundled-docs/requirements/command.md +1 -12
- package/bundled-docs/requirements/editor.md +9 -0
- package/bundled-docs/requirements/settings.md +9 -0
- package/dist/index.js +1898 -1037
- package/package.json +2 -2
- package/templates/agents/deep-executor/agent.yaml +0 -3
- package/templates/agents/default/AGENT.md +0 -17
- package/templates/agents/default/agent.yaml +0 -1
- package/templates/agents/executor/agent.yaml +1 -1
- package/templates/models/aws-bedrock-claude-invoke.yaml +31 -0
- package/templates/models/aws-bedrock-mantle.yaml +42 -7
- package/templates/models/aws-bedrock-openai.yaml +53 -17
- package/templates/prompts/root/DELEGATION.md +58 -0
- package/templates/skills/extension/SKILL.md +70 -0
- package/templates/skills/extension/templates/ext.sh +112 -0
- package/templates/skills/halo/SKILL.md +3 -0
- package/templates/skills/send-file/SKILL.md +1 -1
- package/admin-out/_next/static/chunks/220.1ed70c874686122f.js +0 -1
- package/admin-out/_next/static/chunks/23-092ac3901411582e.js +0 -1
- package/admin-out/_next/static/chunks/362.5970ef15ec3d7b15.js +0 -1
- package/admin-out/_next/static/chunks/449.a403f7ad18054e3f.js +0 -1
- package/admin-out/_next/static/chunks/613.f314fc12e05a566c.js +0 -1
- package/admin-out/_next/static/chunks/73.a4617a551a8e176c.js +0 -1
- package/admin-out/_next/static/chunks/767.16a0b33b6f5e7965.js +0 -1
- package/admin-out/_next/static/chunks/839.e381287756453ffe.js +0 -1
- package/admin-out/_next/static/chunks/868.cc7fd84c9f2fd711.js +0 -1
- package/admin-out/_next/static/chunks/892.92b2ed455683b49a.js +0 -1
- package/admin-out/_next/static/chunks/989-6a0ec35b8ae2cb39.js +0 -1
- package/admin-out/_next/static/chunks/app/page-bf9c0b21ae431c92.js +0 -1
- package/admin-out/_next/static/chunks/webpack-ca926228f8cba1ff.js +0 -1
- package/admin-out/_next/static/css/c0ea31cd787f7aa7.css +0 -3
- package/bundled-docs/design/goal-mode.md +0 -144
- /package/admin-out/_next/static/{qYSILjqBjfgTggO0T8E7T → Wn72NPclwda8BiGd0ChhY}/_buildManifest.js +0 -0
- /package/admin-out/_next/static/{qYSILjqBjfgTggO0T8E7T → Wn72NPclwda8BiGd0ChhY}/_ssgManifest.js +0 -0
package/dist/index.js
CHANGED
|
@@ -1048,6 +1048,9 @@ var init_config = __esm({
|
|
|
1048
1048
|
ENV_PATTERN2 = /<<([A-Z_][A-Z0-9_]*)>>/g;
|
|
1049
1049
|
_wsSettingsCache = /* @__PURE__ */ new Map();
|
|
1050
1050
|
config = {
|
|
1051
|
+
// Goal entry points are retired, not the runtime. Internal/global-only, no UI:
|
|
1052
|
+
// set general.goal_mode_enabled in ~/.halo/secrets/settings.yaml, then restart.
|
|
1053
|
+
goalModeEnabled: settingsBool("general.goal_mode_enabled", false),
|
|
1051
1054
|
server: {
|
|
1052
1055
|
port: systemInt("HALO_PORT", "server.port", 9527),
|
|
1053
1056
|
/** scrypt hash stored in `secrets/config.yaml`, set by `halo setup`.
|
|
@@ -1142,10 +1145,12 @@ var init_config = __esm({
|
|
|
1142
1145
|
timeout: {
|
|
1143
1146
|
shellExec: systemInt("HALO_SHELL_TIMEOUT", "timeout.shell_exec", 12e4),
|
|
1144
1147
|
webFetch: systemInt("HALO_WEB_FETCH_TIMEOUT", "timeout.web_fetch", 1e4),
|
|
1145
|
-
// Per model-call
|
|
1146
|
-
//
|
|
1147
|
-
//
|
|
1148
|
-
//
|
|
1148
|
+
// Per model-call cap. A bare fetch has no default timeout, so a half-open
|
|
1149
|
+
// connection (server accepted but never sends bytes / sends no RST) hangs
|
|
1150
|
+
// the await indefinitely. Bounds each callModel; on expiry the request
|
|
1151
|
+
// aborts and the agent loop's retry takes over. Streaming providers reset
|
|
1152
|
+
// the timer on every chunk, so for them it is an idle timeout — a long but
|
|
1153
|
+
// live stream is never cut off.
|
|
1149
1154
|
modelRequest: systemInt("HALO_MODEL_TIMEOUT", "timeout.model_request", 30 * 6e4),
|
|
1150
1155
|
sessionGrace: systemInt("HALO_SESSION_GRACE", "timeout.session_grace", 5 * 6e4),
|
|
1151
1156
|
terminalGrace: systemInt("HALO_TERMINAL_GRACE", "timeout.terminal_grace", 5 * 6e4)
|
|
@@ -1402,7 +1407,7 @@ function classifyKind(msg, errName, httpStatus) {
|
|
|
1402
1407
|
if (errName === "ThrottlingException" || httpStatus === 429 || msg.includes("throttl") || msg.includes("rate limit") || msg.includes("ThrottlingException") || msg.includes("ServiceUnavailableException") || msg.includes("API error 429")) {
|
|
1403
1408
|
return "throttle";
|
|
1404
1409
|
}
|
|
1405
|
-
if (errName === "InternalServerException" || errName === "ModelTimeoutException" || errName === "ServiceUnavailableException" || httpStatus === 500 || httpStatus === 502 || httpStatus === 503 || httpStatus === 504 || httpStatus === 529 || httpStatus === 408) {
|
|
1410
|
+
if (errName === "InternalServerException" || errName === "ModelTimeoutException" || errName === "ModelStreamErrorException" || errName === "ServiceUnavailableException" || httpStatus === 500 || httpStatus === 502 || httpStatus === 503 || httpStatus === 504 || httpStatus === 529 || httpStatus === 408) {
|
|
1406
1411
|
return "server_error";
|
|
1407
1412
|
}
|
|
1408
1413
|
if (msg === "fetch failed" || msg === "Model request timed out" || msg.includes("http2 request did not get a response") || msg.includes("UND_ERR_HEADERS_TIMEOUT") || msg.includes("HeadersTimeoutError") || msg.includes("socket hang up") || msg.includes("ECONNRESET") || msg.includes("ECONNREFUSED") || msg.includes("ETIMEDOUT") || msg.includes("EAI_AGAIN")) {
|
|
@@ -1616,7 +1621,7 @@ function toolResultImages(content) {
|
|
|
1616
1621
|
}
|
|
1617
1622
|
return images.length > 0 ? [{ type: "text", text: "[Images from the tool results above]" }, ...images] : [];
|
|
1618
1623
|
}
|
|
1619
|
-
var TOOL_ERROR_MARKER, TOOL_WARN_MARKER, MODEL_TIMEOUT_ERROR, AgentLoop;
|
|
1624
|
+
var TOOL_ERROR_MARKER, TOOL_WARN_MARKER, MODEL_TIMEOUT_ERROR, INTERRUPTED_REPLY_MARKER, AgentLoop;
|
|
1620
1625
|
var init_agent_loop = __esm({
|
|
1621
1626
|
"../server/dist/agents/agent-loop.js"() {
|
|
1622
1627
|
"use strict";
|
|
@@ -1624,6 +1629,7 @@ var init_agent_loop = __esm({
|
|
|
1624
1629
|
TOOL_ERROR_MARKER = "__TOOL_ERROR__";
|
|
1625
1630
|
TOOL_WARN_MARKER = "__TOOL_WARN__";
|
|
1626
1631
|
MODEL_TIMEOUT_ERROR = "Model request timed out";
|
|
1632
|
+
INTERRUPTED_REPLY_MARKER = "\n\n[reply interrupted by the user here \u2014 the text above is what was shown before the cut]";
|
|
1627
1633
|
AgentLoop = class {
|
|
1628
1634
|
tools;
|
|
1629
1635
|
messages = [];
|
|
@@ -1658,8 +1664,42 @@ var init_agent_loop = __esm({
|
|
|
1658
1664
|
timer.unref?.();
|
|
1659
1665
|
const callSignal = options?.cancelSignal ? AbortSignal.any([options.cancelSignal, timeoutController.signal]) : timeoutController.signal;
|
|
1660
1666
|
let result;
|
|
1667
|
+
let streamedText = "";
|
|
1661
1668
|
try {
|
|
1662
|
-
|
|
1669
|
+
const deltas = [];
|
|
1670
|
+
let wake = null;
|
|
1671
|
+
let settled = false;
|
|
1672
|
+
const notify = () => {
|
|
1673
|
+
if (wake) {
|
|
1674
|
+
wake();
|
|
1675
|
+
wake = null;
|
|
1676
|
+
}
|
|
1677
|
+
};
|
|
1678
|
+
const call = this.callModel(callSignal, (d) => {
|
|
1679
|
+
deltas.push(d);
|
|
1680
|
+
timer.refresh();
|
|
1681
|
+
notify();
|
|
1682
|
+
});
|
|
1683
|
+
call.then(() => {
|
|
1684
|
+
settled = true;
|
|
1685
|
+
notify();
|
|
1686
|
+
}, () => {
|
|
1687
|
+
settled = true;
|
|
1688
|
+
notify();
|
|
1689
|
+
});
|
|
1690
|
+
while (!settled) {
|
|
1691
|
+
if (deltas.length === 0)
|
|
1692
|
+
await new Promise((r) => {
|
|
1693
|
+
wake = r;
|
|
1694
|
+
});
|
|
1695
|
+
while (deltas.length > 0) {
|
|
1696
|
+
const d = deltas.shift();
|
|
1697
|
+
if (d.type === "text_delta")
|
|
1698
|
+
streamedText += d.text;
|
|
1699
|
+
yield { type: d.type, text: d.text };
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
result = await call;
|
|
1663
1703
|
} catch (err) {
|
|
1664
1704
|
if (timeoutController.signal.aborted && !options?.cancelSignal?.aborted) {
|
|
1665
1705
|
throw new Error(MODEL_TIMEOUT_ERROR);
|
|
@@ -1667,9 +1707,12 @@ var init_agent_loop = __esm({
|
|
|
1667
1707
|
throw err;
|
|
1668
1708
|
} finally {
|
|
1669
1709
|
clearTimeout(timer);
|
|
1710
|
+
if (result === void 0 && options?.cancelSignal?.aborted && streamedText.length > 0) {
|
|
1711
|
+
this.messages.push({ role: "assistant", content: [{ type: "text", text: streamedText + INTERRUPTED_REPLY_MARKER }] });
|
|
1712
|
+
}
|
|
1670
1713
|
}
|
|
1671
1714
|
if (result.stopReason === "refusal") {
|
|
1672
|
-
yield { type: "usage", usage: result.usage, durationMs: result.durationMs };
|
|
1715
|
+
yield { type: "usage", usage: result.usage, durationMs: result.durationMs, ttftMs: result.ttftMs };
|
|
1673
1716
|
yield { type: "stop", stopReason: "refusal", stopDetails: result.stopDetails };
|
|
1674
1717
|
return;
|
|
1675
1718
|
}
|
|
@@ -1688,7 +1731,8 @@ var init_agent_loop = __esm({
|
|
|
1688
1731
|
yield {
|
|
1689
1732
|
type: "usage",
|
|
1690
1733
|
usage: result.usage,
|
|
1691
|
-
durationMs: result.durationMs
|
|
1734
|
+
durationMs: result.durationMs,
|
|
1735
|
+
ttftMs: result.ttftMs
|
|
1692
1736
|
};
|
|
1693
1737
|
if (result.stopReason !== "tool_use") {
|
|
1694
1738
|
yield { type: "stop", stopReason: result.stopReason };
|
|
@@ -2457,6 +2501,7 @@ function ensureHaloHome(haloHome2) {
|
|
|
2457
2501
|
const globalDir2 = path8.join(haloHome2, "global");
|
|
2458
2502
|
const secretsDir = path8.join(haloHome2, "secrets");
|
|
2459
2503
|
fs7.mkdirSync(globalDir2, { recursive: true });
|
|
2504
|
+
fs7.mkdirSync(path8.join(globalDir2, "extensions"), { recursive: true });
|
|
2460
2505
|
fs7.mkdirSync(secretsDir, { recursive: true, mode: 448 });
|
|
2461
2506
|
try {
|
|
2462
2507
|
fs7.chmodSync(secretsDir, 448);
|
|
@@ -2573,7 +2618,7 @@ var init_init = __esm({
|
|
|
2573
2618
|
path8.resolve(__dirname, "..", "bundled-docs"),
|
|
2574
2619
|
path8.resolve(__dirname, "..", "..", "..", ".halo", "docs")
|
|
2575
2620
|
];
|
|
2576
|
-
TEMPLATE_VERSION =
|
|
2621
|
+
TEMPLATE_VERSION = 74;
|
|
2577
2622
|
VERSION_FILE = ".template-version";
|
|
2578
2623
|
SKIP_NAMES = /* @__PURE__ */ new Set([".DS_Store", "schema.sql", "__pycache__", ".pytest_cache"]);
|
|
2579
2624
|
BUILTIN_AGENT_IDS = /* @__PURE__ */ new Set([
|
|
@@ -2617,7 +2662,12 @@ var init_init = __esm({
|
|
|
2617
2662
|
// root-prompt injection. Loaded on demand via `activate_skill('halo')`
|
|
2618
2663
|
// when the user asks anything about Halo itself (config dirs, MD scopes,
|
|
2619
2664
|
// doc lookup table). user-invocable: false — no `/halo` command.
|
|
2620
|
-
"halo"
|
|
2665
|
+
"halo",
|
|
2666
|
+
// Canvas preview extensions: `/extension install|list|remove` manages
|
|
2667
|
+
// `~/.halo/global/extensions/<id>/` via templates/ext.sh (download from
|
|
2668
|
+
// halo-hub, unpack, pre-check, atomic replace). The server's root-dir
|
|
2669
|
+
// watcher picks the change up — no endpoint call from the skill.
|
|
2670
|
+
"extension"
|
|
2621
2671
|
]);
|
|
2622
2672
|
BUNDLED_DOCS = [
|
|
2623
2673
|
"guide/getting-started.md",
|
|
@@ -2773,7 +2823,7 @@ function buildSessionTools(sm, sessionId) {
|
|
|
2773
2823
|
type: "object",
|
|
2774
2824
|
properties: {
|
|
2775
2825
|
agent_id: { type: "string", description: "The agent ID \u2014 must be one of the agents listed in your system prompt. Do not invent ids." },
|
|
2776
|
-
message: { type: "string", description: "
|
|
2826
|
+
message: { type: "string", description: "The brief \u2014 all the worker will know. State: the goal and what done looks like (where the deliverable lives, which checks pass); decisions the user already made in this conversation (choices, rejected options, constraints \u2014 quote them when wording matters); what you already know (paths, line numbers, snippets, the pattern to copy \u2014 findings, not pointers); boundaries (files not to touch, whether commit / push / build / deploy is allowed, in-flight work to leave alone); the exact verification commands. One task per session." },
|
|
2777
2827
|
system_prompt_context: { type: "string", description: "Optional background material prepended to the opening message BEFORE your task text \u2014 despite the name, it is NOT injected into the system prompt. Put here what frames the whole task: relevant file paths, prior findings, user-stated constraints, role framing. Keep one-shot task instructions in `message` itself so it stays a clean task statement. Note the cost: this text lives in the child's conversation history and is re-sent on every one of its model calls until compaction \u2014 include what the child genuinely needs throughout, not bulk material it could read from files instead." },
|
|
2778
2828
|
working_dir: { type: "string", description: "Optional focus directory for the sub-agent (workspace-relative or absolute; must be inside the workspace). The platform bakes the directory-scoped INSTRUCTIONS.md found along the path from the workspace root down to this directory into the sub-agent's system prompt (present every turn), and tags its prompt with this focus. Does NOT change where tools run (shell/file tools still operate from the project root). Omit for project-root scope." },
|
|
2779
2829
|
title: { type: "string", description: "Optional session title shown in the admin sidebar. When omitted, auto-generated from the task message." }
|
|
@@ -3019,7 +3069,7 @@ ${skillInfo}` : ""
|
|
|
3019
3069
|
function buildContinueTaskTool(sm, sessionId) {
|
|
3020
3070
|
return {
|
|
3021
3071
|
name: "continue_task",
|
|
3022
|
-
description: "Call this when your turn was started by an interruption (a user or parent message arrived while you were working) and the task you were doing is NOT finished yet. After your current reply ends, the session automatically resumes the interrupted task. Do NOT call it if the task is done, if the user told you to stop, or in a normal (uninterrupted) turn \u2014 it does nothing there. The flag lasts one turn: if you get interrupted again before resuming, call it again.",
|
|
3072
|
+
description: "Call this when your turn was started by an interruption (a user or parent message arrived while you were working) and the task you were doing is NOT finished yet. After your current reply ends, the session automatically resumes the interrupted task. Do NOT call it if the task is done, if the user told you to stop, or in a normal (uninterrupted) turn \u2014 it does nothing there. The flag lasts one turn: if you get interrupted again before resuming, call it again. Write your reply to the interrupting message FIRST, then call this.",
|
|
3023
3073
|
inputSchema: { type: "object", properties: {} },
|
|
3024
3074
|
callback: async () => {
|
|
3025
3075
|
switch (sm.requestSelfKick(sessionId)) {
|
|
@@ -4003,6 +4053,7 @@ var init_i18n = __esm({
|
|
|
4003
4053
|
"agent.delete_usage": { zh: "\u7528\u6CD5\uFF1A/agent delete <\u540D\u79F0\u6216\u7F16\u53F7>", en: "Usage: /agent delete <name or number>" },
|
|
4004
4054
|
"agent.delete_done": { zh: '\u{1F5D1} \u5DF2\u5220\u9664 agent "{name}"\uFF08{scope}\uFF09', en: '\u{1F5D1} Deleted agent "{name}" ({scope})' },
|
|
4005
4055
|
// ── /goal verbs (goal mode) ──
|
|
4056
|
+
"goal.disabled": { zh: "Goal \u6A21\u5F0F\u5DF2\u4E0B\u7EBF\u3002", en: "Goal mode is disabled." },
|
|
4006
4057
|
"verb.goal.create": { zh: "\u5728\u5F53\u524D\u4F1A\u8BDD\u4E0A\u542F\u52A8 goal \u610F\u56FE\u5BF9\u8BDD", en: "Start goal intake on the current session" },
|
|
4007
4058
|
"verb.goal.status": { zh: "\u67E5\u770B\u5F53\u524D goal \u7684\u8F6E\u6B21 / \u4E0A\u9650 / \u72B6\u6001", en: "Show the current goal's round / caps / state" },
|
|
4008
4059
|
"verb.goal.pause": { zh: "\u6682\u505C goal\uFF08\u505C\u6B62 worker \u548C goal \u4F1A\u8BDD\uFF09", en: "Pause the goal (stops worker + goal session)" },
|
|
@@ -4052,8 +4103,23 @@ var init_registry = __esm({
|
|
|
4052
4103
|
registerDescriptor(desc4) {
|
|
4053
4104
|
this.descriptors.set(desc4.name, desc4);
|
|
4054
4105
|
}
|
|
4055
|
-
listDescriptors() {
|
|
4056
|
-
return Array.from(this.descriptors.values()).filter((d) => !d.hidden);
|
|
4106
|
+
listDescriptors(opts) {
|
|
4107
|
+
return Array.from(this.descriptors.values()).filter((d) => opts?.includeHidden || !d.hidden);
|
|
4108
|
+
}
|
|
4109
|
+
/** Startup integrity checks include hidden commands: visibility is not routing. */
|
|
4110
|
+
assertDispatchCommands(commands) {
|
|
4111
|
+
const builtins = this.listDescriptors({ includeHidden: true }).filter((d) => d.source === "builtin");
|
|
4112
|
+
const declared = new Set(builtins.map((d) => d.slashName));
|
|
4113
|
+
const dispatched = new Set(commands);
|
|
4114
|
+
const declaredServer = new Set(builtins.filter((d) => d.type === "server").map((d) => d.slashName));
|
|
4115
|
+
const missingDispatch = [...declaredServer].filter((n) => !dispatched.has(n));
|
|
4116
|
+
const orphanDispatch = [...dispatched].filter((n) => !declared.has(n));
|
|
4117
|
+
if (missingDispatch.length > 0) {
|
|
4118
|
+
throw new Error(`[Server] Command descriptors without a dispatch case: ${missingDispatch.join(", ")}. Either add a case in channels/shared/commands.ts dispatchCommand or change the descriptor type to 'client'.`);
|
|
4119
|
+
}
|
|
4120
|
+
if (orphanDispatch.length > 0) {
|
|
4121
|
+
throw new Error(`[Server] Dispatch cases without a descriptor: ${orphanDispatch.join(", ")}. Add a registerDescriptor entry in commands/index.ts or remove the dispatch case.`);
|
|
4122
|
+
}
|
|
4057
4123
|
}
|
|
4058
4124
|
};
|
|
4059
4125
|
}
|
|
@@ -4068,6 +4134,7 @@ var init_commands = __esm({
|
|
|
4068
4134
|
"../server/dist/commands/index.js"() {
|
|
4069
4135
|
"use strict";
|
|
4070
4136
|
init_registry();
|
|
4137
|
+
init_config();
|
|
4071
4138
|
commandRegistry = new CommandRegistry();
|
|
4072
4139
|
commandRegistry.registerDescriptor({ name: "help", slashName: "/help", description: "Show available commands", type: "server", source: "builtin" });
|
|
4073
4140
|
commandRegistry.registerDescriptor({ name: "session", slashName: "/session", description: "Manage sessions", type: "server", argHint: "<verb>", source: "builtin", verbs: [
|
|
@@ -4096,7 +4163,7 @@ var init_commands = __esm({
|
|
|
4096
4163
|
{ name: "create", desc: "Create a new agent" },
|
|
4097
4164
|
{ name: "update", desc: "Modify an existing agent" }
|
|
4098
4165
|
] });
|
|
4099
|
-
commandRegistry.registerDescriptor({ name: "goal", slashName: "/goal", description: "Run a goal loop (judge + dispatch until done)", type: "server", argHint: "<verb>", source: "builtin", verbs: [
|
|
4166
|
+
commandRegistry.registerDescriptor({ name: "goal", slashName: "/goal", description: "Run a goal loop (judge + dispatch until done)", type: "server", argHint: "<verb>", source: "builtin", hidden: !config.goalModeEnabled, verbs: [
|
|
4100
4167
|
{ name: "create", builtin: true, desc: "Start goal intake on the current session" },
|
|
4101
4168
|
{ name: "status", builtin: true, desc: "Show the current goal's round / caps / state" },
|
|
4102
4169
|
{ name: "pause", builtin: true, desc: "Pause the goal (stops worker + goal session)" },
|
|
@@ -4173,7 +4240,7 @@ async function getCommandSkillInfo(slashCommand, workspaceRoot) {
|
|
|
4173
4240
|
}
|
|
4174
4241
|
async function scanSkillDescriptors(workspaceRoot) {
|
|
4175
4242
|
const entries = await scanSkillEntries(workspaceRoot);
|
|
4176
|
-
const builtinSlashes = new Set(commandRegistry.listDescriptors().filter((d) => d.source === "builtin").map((d) => d.slashName));
|
|
4243
|
+
const builtinSlashes = new Set(commandRegistry.listDescriptors({ includeHidden: true }).filter((d) => d.source === "builtin").map((d) => d.slashName));
|
|
4177
4244
|
const claimed = new Set(builtinSlashes);
|
|
4178
4245
|
const listed = [];
|
|
4179
4246
|
for (const entry of entries) {
|
|
@@ -4409,6 +4476,9 @@ function wsEvoArchivedApplyZip(workspacePath, applyId) {
|
|
|
4409
4476
|
function wsEvoHistoryDir(workspacePath, applyId) {
|
|
4410
4477
|
return path15.join(wsEvoDir(workspacePath), "history", `apply-${applyId}`);
|
|
4411
4478
|
}
|
|
4479
|
+
function globalExtensionsDir() {
|
|
4480
|
+
return path15.join(globalDir(), "extensions");
|
|
4481
|
+
}
|
|
4412
4482
|
function globalLogsDir() {
|
|
4413
4483
|
return path15.join(globalDir(), "logs");
|
|
4414
4484
|
}
|
|
@@ -5132,7 +5202,7 @@ async function dispatchCommand(ctx, command, arg, opts) {
|
|
|
5132
5202
|
// Goal mode (docs/plans/loop-mode.md): all verbs are builtin (no backing
|
|
5133
5203
|
// skill) — create/status/pause/resume/clear in SUBCOMMAND_ROUTES.
|
|
5134
5204
|
case "/goal":
|
|
5135
|
-
return routeObjectOrSkill(ctx, command, arg);
|
|
5205
|
+
return config.goalModeEnabled ? routeObjectOrSkill(ctx, command, arg) : { text: t("goal.disabled", ctx.lang) };
|
|
5136
5206
|
default:
|
|
5137
5207
|
return routeObjectOrSkill(ctx, command, arg);
|
|
5138
5208
|
}
|
|
@@ -5453,6 +5523,7 @@ var init_commands2 = __esm({
|
|
|
5453
5523
|
init_i18n();
|
|
5454
5524
|
init_skill_command();
|
|
5455
5525
|
init_commands();
|
|
5526
|
+
init_config();
|
|
5456
5527
|
init_enqueue();
|
|
5457
5528
|
init_goal_mode();
|
|
5458
5529
|
ALIASES_FILE = path17.join(os.homedir(), ".halo", "global", "aliases.yaml");
|
|
@@ -5571,7 +5642,7 @@ ${report}`;
|
|
|
5571
5642
|
const body = report.length > cap2 ? report.slice(0, cap2) + `
|
|
5572
5643
|
|
|
5573
5644
|
[Report truncated: ${report.length} chars total. Use relay_read("${host.workspaceRoot}", "${session.id}") for the full text.]` : report;
|
|
5574
|
-
const header = `[Relay report \xB7 workspace ${host.workspaceRoot} \xB7 session ${session.id}]`;
|
|
5645
|
+
const header = `[Relay report \xB7 workspace ${host.workspaceRoot} \xB7 session ${session.id} \xB7 status: ${session.turnError ? "aborted" : "completed"}]`;
|
|
5575
5646
|
const text6 = `${header}
|
|
5576
5647
|
|
|
5577
5648
|
${body}`;
|
|
@@ -5603,7 +5674,7 @@ async function deliverRelayInterim(host, sessionId, body) {
|
|
|
5603
5674
|
console.error(`[Relay] caller workspace ${to.workspace} unreachable: ${err instanceof Error ? err.message : String(err)}`);
|
|
5604
5675
|
return;
|
|
5605
5676
|
}
|
|
5606
|
-
const header = `[Relay interim report \xB7 workspace ${host.workspaceRoot} \xB7 session ${sessionId}
|
|
5677
|
+
const header = `[Relay interim report \xB7 workspace ${host.workspaceRoot} \xB7 session ${sessionId} \xB7 status: still running] This is an interim reply \u2014 the session is still working; its final [Relay report] follows when done. Do not treat this as the result.`;
|
|
5607
5678
|
const text6 = `${header}
|
|
5608
5679
|
|
|
5609
5680
|
${body}`;
|
|
@@ -5670,7 +5741,7 @@ function buildRelayTools(host, callerSessionId) {
|
|
|
5670
5741
|
async function dispatch6(params, hard) {
|
|
5671
5742
|
const registry2 = getRelayRegistry();
|
|
5672
5743
|
if (!registry2)
|
|
5673
|
-
return jsonErr2(
|
|
5744
|
+
return jsonErr2(RELAY_UNAVAILABLE);
|
|
5674
5745
|
const resolved = resolveTarget(registry2, params.workspace);
|
|
5675
5746
|
if (typeof resolved === "string")
|
|
5676
5747
|
return resolved;
|
|
@@ -5705,7 +5776,7 @@ ${params.message}`;
|
|
|
5705
5776
|
try {
|
|
5706
5777
|
const registry2 = getRelayRegistry();
|
|
5707
5778
|
if (!registry2)
|
|
5708
|
-
return jsonErr2(
|
|
5779
|
+
return jsonErr2(RELAY_UNAVAILABLE);
|
|
5709
5780
|
const resolved = resolveTarget(registry2, params.workspace);
|
|
5710
5781
|
if (typeof resolved === "string")
|
|
5711
5782
|
return resolved;
|
|
@@ -5732,7 +5803,7 @@ ${params.message}`;
|
|
|
5732
5803
|
try {
|
|
5733
5804
|
const registry2 = getRelayRegistry();
|
|
5734
5805
|
if (!registry2)
|
|
5735
|
-
return jsonErr2(
|
|
5806
|
+
return jsonErr2(RELAY_UNAVAILABLE);
|
|
5736
5807
|
const resolved = resolveTarget(registry2, params.workspace);
|
|
5737
5808
|
if (typeof resolved === "string")
|
|
5738
5809
|
return resolved;
|
|
@@ -5757,7 +5828,7 @@ ${params.message}`;
|
|
|
5757
5828
|
try {
|
|
5758
5829
|
const registry2 = getRelayRegistry();
|
|
5759
5830
|
if (!registry2)
|
|
5760
|
-
return jsonErr2(
|
|
5831
|
+
return jsonErr2(RELAY_UNAVAILABLE);
|
|
5761
5832
|
const resolved = resolveTarget(registry2, params.workspace ?? host.workspaceRoot);
|
|
5762
5833
|
if (typeof resolved === "string")
|
|
5763
5834
|
return resolved;
|
|
@@ -5779,7 +5850,7 @@ ${params.message}`;
|
|
|
5779
5850
|
};
|
|
5780
5851
|
return [relaySend, relayInterrupt, relayStop, relayRead, relayList];
|
|
5781
5852
|
}
|
|
5782
|
-
var RELAY_CHANNEL_PREFIX, _registry, WORKSPACE_SESSION_PROPS;
|
|
5853
|
+
var RELAY_CHANNEL_PREFIX, _registry, RELAY_UNAVAILABLE, WORKSPACE_SESSION_PROPS;
|
|
5783
5854
|
var init_relay = __esm({
|
|
5784
5855
|
"../server/dist/agents/relay.js"() {
|
|
5785
5856
|
"use strict";
|
|
@@ -5788,6 +5859,7 @@ var init_relay = __esm({
|
|
|
5788
5859
|
init_commands2();
|
|
5789
5860
|
RELAY_CHANNEL_PREFIX = "[channel: relay | from: ";
|
|
5790
5861
|
_registry = null;
|
|
5862
|
+
RELAY_UNAVAILABLE = "relay is not available in the CLI / TUI \u2014 it only runs inside `halo server`. This is permanent for this runtime, not a temporary outage: do not retry. Tell the user to send this request from the admin UI or an IM / Web channel connected to the server, or to open the target workspace directly with `halo tui -w <workspace path>`.";
|
|
5791
5863
|
WORKSPACE_SESSION_PROPS = {
|
|
5792
5864
|
workspace: { type: "string", description: "Absolute path of the target workspace on this server." },
|
|
5793
5865
|
session_id: { type: "string", description: "Session id inside that workspace." }
|
|
@@ -5982,27 +6054,27 @@ var init_workspace = __esm({
|
|
|
5982
6054
|
constructor(projectRoot) {
|
|
5983
6055
|
this.projectRoot = projectRoot;
|
|
5984
6056
|
}
|
|
5985
|
-
async readFile(
|
|
5986
|
-
const fullPath = this.validatePath(
|
|
6057
|
+
async readFile(path74) {
|
|
6058
|
+
const fullPath = this.validatePath(path74);
|
|
5987
6059
|
return readFile(fullPath, "utf-8");
|
|
5988
6060
|
}
|
|
5989
|
-
async writeFile(
|
|
5990
|
-
const fullPath = this.validatePath(
|
|
6061
|
+
async writeFile(path74, content) {
|
|
6062
|
+
const fullPath = this.validatePath(path74);
|
|
5991
6063
|
const dir = resolve(fullPath, "..");
|
|
5992
6064
|
await mkdir(dir, { recursive: true });
|
|
5993
6065
|
await writeFile(fullPath, content, "utf-8");
|
|
5994
6066
|
}
|
|
5995
|
-
validatePath(
|
|
5996
|
-
const fullPath = isAbsolute(
|
|
6067
|
+
validatePath(path74) {
|
|
6068
|
+
const fullPath = isAbsolute(path74) ? resolve(path74) : resolve(this.projectRoot, path74);
|
|
5997
6069
|
const root = resolve(this.projectRoot);
|
|
5998
6070
|
if (fullPath !== root && !fullPath.startsWith(root + sep)) {
|
|
5999
|
-
throw new WorkspaceError(`Path "${
|
|
6071
|
+
throw new WorkspaceError(`Path "${path74}" is outside the workspace directory "${this.projectRoot}"`);
|
|
6000
6072
|
}
|
|
6001
6073
|
try {
|
|
6002
6074
|
const real = realpathSync(fullPath);
|
|
6003
6075
|
const realRoot = realpathSync(root);
|
|
6004
6076
|
if (real !== realRoot && !real.startsWith(realRoot + sep)) {
|
|
6005
|
-
throw new WorkspaceError(`Path "${
|
|
6077
|
+
throw new WorkspaceError(`Path "${path74}" is outside the workspace directory "${this.projectRoot}"`);
|
|
6006
6078
|
}
|
|
6007
6079
|
} catch (err) {
|
|
6008
6080
|
if (err instanceof WorkspaceError)
|
|
@@ -6192,12 +6264,12 @@ var init_git_manager = __esm({
|
|
|
6192
6264
|
const code = parts[0];
|
|
6193
6265
|
const status = code[0];
|
|
6194
6266
|
const isRenameOrCopy = status === "R" || status === "C";
|
|
6195
|
-
const
|
|
6196
|
-
if (!
|
|
6267
|
+
const path74 = isRenameOrCopy ? parts[2] : parts[1];
|
|
6268
|
+
if (!path74 || seen.has(path74))
|
|
6197
6269
|
continue;
|
|
6198
|
-
seen.add(
|
|
6270
|
+
seen.add(path74);
|
|
6199
6271
|
files.push({
|
|
6200
|
-
path:
|
|
6272
|
+
path: path74,
|
|
6201
6273
|
status,
|
|
6202
6274
|
from: isRenameOrCopy ? parts[1] : void 0
|
|
6203
6275
|
});
|
|
@@ -6345,9 +6417,9 @@ var init_git_manager = __esm({
|
|
|
6345
6417
|
* empty for an added file or the root commit) and `modified` is the version
|
|
6346
6418
|
* at `commit` (empty for a deletion). The working-tree/staged path above is
|
|
6347
6419
|
* left entirely untouched (commit === undefined preserves existing behavior). */
|
|
6348
|
-
async getFileDiff(
|
|
6420
|
+
async getFileDiff(path74, staged, from, commit) {
|
|
6349
6421
|
if (commit) {
|
|
6350
|
-
const parentPath = from ||
|
|
6422
|
+
const parentPath = from || path74;
|
|
6351
6423
|
let original2 = "";
|
|
6352
6424
|
try {
|
|
6353
6425
|
original2 = await this.git.show([`${commit}^:${parentPath}`]);
|
|
@@ -6356,13 +6428,13 @@ var init_git_manager = __esm({
|
|
|
6356
6428
|
}
|
|
6357
6429
|
let modified2 = "";
|
|
6358
6430
|
try {
|
|
6359
|
-
modified2 = await this.git.show([`${commit}:${
|
|
6431
|
+
modified2 = await this.git.show([`${commit}:${path74}`]);
|
|
6360
6432
|
} catch {
|
|
6361
6433
|
modified2 = "";
|
|
6362
6434
|
}
|
|
6363
6435
|
return { original: original2, modified: modified2 };
|
|
6364
6436
|
}
|
|
6365
|
-
const headPath = from ||
|
|
6437
|
+
const headPath = from || path74;
|
|
6366
6438
|
let original = "";
|
|
6367
6439
|
try {
|
|
6368
6440
|
original = await this.git.show([`HEAD:${headPath}`]);
|
|
@@ -6372,13 +6444,13 @@ var init_git_manager = __esm({
|
|
|
6372
6444
|
let modified = "";
|
|
6373
6445
|
if (staged) {
|
|
6374
6446
|
try {
|
|
6375
|
-
modified = await this.git.show([`:${
|
|
6447
|
+
modified = await this.git.show([`:${path74}`]);
|
|
6376
6448
|
} catch {
|
|
6377
6449
|
modified = "";
|
|
6378
6450
|
}
|
|
6379
6451
|
} else {
|
|
6380
6452
|
try {
|
|
6381
|
-
modified = await this.workspace.readFile(
|
|
6453
|
+
modified = await this.workspace.readFile(path74);
|
|
6382
6454
|
} catch {
|
|
6383
6455
|
modified = "";
|
|
6384
6456
|
}
|
|
@@ -6736,10 +6808,24 @@ function applyEvent(state, event) {
|
|
|
6736
6808
|
initSubSessionLog(state, taskId, event.agentId ?? "", agentName, "");
|
|
6737
6809
|
}
|
|
6738
6810
|
switch (event.type) {
|
|
6811
|
+
// Streaming chunks land in the turn blocks as they arrive; the whole
|
|
6812
|
+
// `thinking` / `stream` that follows is stamped `streamed` and skipped so
|
|
6813
|
+
// the text isn't appended twice. Deltas don't trigger a save — the usage /
|
|
6814
|
+
// tool events that follow do.
|
|
6815
|
+
case "thinking_delta":
|
|
6816
|
+
appendThinking(getTarget(state, taskId), event.text ?? "");
|
|
6817
|
+
break;
|
|
6818
|
+
case "stream_delta":
|
|
6819
|
+
appendStream(getTarget(state, taskId), event.text ?? "", agentName);
|
|
6820
|
+
break;
|
|
6739
6821
|
case "thinking":
|
|
6822
|
+
if (event.streamed)
|
|
6823
|
+
break;
|
|
6740
6824
|
appendThinking(getTarget(state, taskId), event.text ?? "");
|
|
6741
6825
|
break;
|
|
6742
6826
|
case "stream":
|
|
6827
|
+
if (event.streamed)
|
|
6828
|
+
break;
|
|
6743
6829
|
appendStream(getTarget(state, taskId), event.text ?? "", agentName);
|
|
6744
6830
|
break;
|
|
6745
6831
|
case "agent_start":
|
|
@@ -6826,9 +6912,8 @@ function applyEvent(state, event) {
|
|
|
6826
6912
|
if (taskId) {
|
|
6827
6913
|
const sub = state.subSessionLogs.get(taskId);
|
|
6828
6914
|
if (sub) {
|
|
6829
|
-
|
|
6830
|
-
|
|
6831
|
-
sub.messageLog.push(buildUsageMsg(event, agentName, taskId, turnId));
|
|
6915
|
+
sub.messageLog.push(buildUsageMsg(event, agentName, taskId, sub.currentTurnId));
|
|
6916
|
+
sub.currentTurnId = randomUUID();
|
|
6832
6917
|
result.subSessionSave = taskId;
|
|
6833
6918
|
}
|
|
6834
6919
|
}
|
|
@@ -7147,15 +7232,15 @@ var init_session_ui_store = __esm({
|
|
|
7147
7232
|
persistUIState(rootId, state, archive) {
|
|
7148
7233
|
try {
|
|
7149
7234
|
const projectPath = this.uiStateProjectPaths.get(rootId) ?? this.host.workspaceRoot;
|
|
7150
|
-
const
|
|
7151
|
-
if (
|
|
7235
|
+
const snapshot2 = createSaveSnapshot(state);
|
|
7236
|
+
if (snapshot2.length === 0)
|
|
7152
7237
|
return;
|
|
7153
7238
|
const inMem = this.host.getSession(rootId);
|
|
7154
7239
|
const row = inMem ? null : this.db.select().from(agentSessions).where(eq6(agentSessions.id, rootId)).get();
|
|
7155
7240
|
this.host.persistSessionFile({
|
|
7156
7241
|
sessionId: rootId,
|
|
7157
7242
|
projectPath,
|
|
7158
|
-
messages:
|
|
7243
|
+
messages: snapshot2,
|
|
7159
7244
|
contextTokens: state.contextTokens,
|
|
7160
7245
|
outputTokens: state.outputTokens,
|
|
7161
7246
|
agentId: inMem?.agentId ?? row?.agentId,
|
|
@@ -7676,8 +7761,236 @@ var init_session_query_store = __esm({
|
|
|
7676
7761
|
}
|
|
7677
7762
|
});
|
|
7678
7763
|
|
|
7764
|
+
// ../server/dist/agents/sse.js
|
|
7765
|
+
async function* readSseJson(body) {
|
|
7766
|
+
const reader = body.getReader();
|
|
7767
|
+
const decoder = new TextDecoder();
|
|
7768
|
+
let buffer = "";
|
|
7769
|
+
try {
|
|
7770
|
+
while (true) {
|
|
7771
|
+
const { done, value } = await reader.read();
|
|
7772
|
+
if (done)
|
|
7773
|
+
break;
|
|
7774
|
+
buffer = (buffer + decoder.decode(value, { stream: true })).replace(/\r\n/g, "\n");
|
|
7775
|
+
let sep2;
|
|
7776
|
+
while ((sep2 = buffer.indexOf("\n\n")) !== -1) {
|
|
7777
|
+
const payload2 = frameData(buffer.slice(0, sep2));
|
|
7778
|
+
buffer = buffer.slice(sep2 + 2);
|
|
7779
|
+
if (payload2 !== void 0)
|
|
7780
|
+
yield JSON.parse(payload2);
|
|
7781
|
+
}
|
|
7782
|
+
}
|
|
7783
|
+
const payload = frameData((buffer + decoder.decode()).replace(/\r\n/g, "\n"));
|
|
7784
|
+
if (payload !== void 0)
|
|
7785
|
+
yield JSON.parse(payload);
|
|
7786
|
+
} finally {
|
|
7787
|
+
reader.releaseLock();
|
|
7788
|
+
}
|
|
7789
|
+
}
|
|
7790
|
+
function frameData(frame) {
|
|
7791
|
+
const data = [];
|
|
7792
|
+
for (const line of frame.split("\n")) {
|
|
7793
|
+
if (!line.startsWith("data:"))
|
|
7794
|
+
continue;
|
|
7795
|
+
data.push(line.slice(line[5] === " " ? 6 : 5));
|
|
7796
|
+
}
|
|
7797
|
+
if (data.length === 0)
|
|
7798
|
+
return void 0;
|
|
7799
|
+
const payload = data.join("\n");
|
|
7800
|
+
return payload === "[DONE]" ? void 0 : payload;
|
|
7801
|
+
}
|
|
7802
|
+
var init_sse = __esm({
|
|
7803
|
+
"../server/dist/agents/sse.js"() {
|
|
7804
|
+
"use strict";
|
|
7805
|
+
}
|
|
7806
|
+
});
|
|
7807
|
+
|
|
7808
|
+
// ../server/dist/agents/anthropic-stream.js
|
|
7809
|
+
async function fetchAnthropicStream(opts) {
|
|
7810
|
+
const { tag } = opts;
|
|
7811
|
+
const startTime = Date.now();
|
|
7812
|
+
const res = await fetch(opts.url, {
|
|
7813
|
+
method: "POST",
|
|
7814
|
+
signal: opts.signal,
|
|
7815
|
+
headers: { "content-type": "application/json", ...opts.headers },
|
|
7816
|
+
body: JSON.stringify({ ...opts.body, stream: true })
|
|
7817
|
+
});
|
|
7818
|
+
if (!res.ok) {
|
|
7819
|
+
const raw = await res.text();
|
|
7820
|
+
let err;
|
|
7821
|
+
try {
|
|
7822
|
+
err = JSON.parse(raw);
|
|
7823
|
+
} catch {
|
|
7824
|
+
throw new Error(`[${tag}] non-JSON response (status=${res.status}): ${raw.slice(0, 200)}`);
|
|
7825
|
+
}
|
|
7826
|
+
throw new Error(`[${tag}] ${res.status} ${err.error?.type ?? "?"}: ${err.error?.message ?? raw.slice(0, 200)}`);
|
|
7827
|
+
}
|
|
7828
|
+
if (!res.body)
|
|
7829
|
+
throw new Error(`[${tag}] empty response body`);
|
|
7830
|
+
const acc = new AnthropicStreamAccumulator(startTime, opts.onDelta);
|
|
7831
|
+
for await (const ev of readSseJson(res.body)) {
|
|
7832
|
+
if (ev.type === "error") {
|
|
7833
|
+
throw new Error(`[${tag}] ${SSE_ERROR_STATUS[ev.error?.type ?? ""] ?? "?"} ${ev.error?.type ?? "?"}: ${ev.error?.message ?? ""}`);
|
|
7834
|
+
}
|
|
7835
|
+
acc.push(ev);
|
|
7836
|
+
}
|
|
7837
|
+
if (opts.signal?.aborted)
|
|
7838
|
+
throw new DOMException("Model call aborted", "AbortError");
|
|
7839
|
+
return acc.finish();
|
|
7840
|
+
}
|
|
7841
|
+
var AnthropicStreamAccumulator, SSE_ERROR_STATUS;
|
|
7842
|
+
var init_anthropic_stream = __esm({
|
|
7843
|
+
"../server/dist/agents/anthropic-stream.js"() {
|
|
7844
|
+
"use strict";
|
|
7845
|
+
init_sse();
|
|
7846
|
+
AnthropicStreamAccumulator = class {
|
|
7847
|
+
startTime;
|
|
7848
|
+
onDelta;
|
|
7849
|
+
blocks = /* @__PURE__ */ new Map();
|
|
7850
|
+
/** Block indices in arrival order — `assistantBlocks` must keep wire order. */
|
|
7851
|
+
order = [];
|
|
7852
|
+
stopReason;
|
|
7853
|
+
stopDetails;
|
|
7854
|
+
inputTokens = 0;
|
|
7855
|
+
outputTokens = 0;
|
|
7856
|
+
cacheReadTokens = 0;
|
|
7857
|
+
cacheWriteTokens = 0;
|
|
7858
|
+
invocationMetrics;
|
|
7859
|
+
firstDeltaAt;
|
|
7860
|
+
constructor(startTime, onDelta) {
|
|
7861
|
+
this.startTime = startTime;
|
|
7862
|
+
this.onDelta = onDelta;
|
|
7863
|
+
}
|
|
7864
|
+
push(event) {
|
|
7865
|
+
switch (event.type) {
|
|
7866
|
+
case "message_start": {
|
|
7867
|
+
const u = event.message?.usage;
|
|
7868
|
+
this.inputTokens = u?.input_tokens ?? 0;
|
|
7869
|
+
this.cacheReadTokens = u?.cache_read_input_tokens ?? 0;
|
|
7870
|
+
this.cacheWriteTokens = u?.cache_creation_input_tokens ?? 0;
|
|
7871
|
+
break;
|
|
7872
|
+
}
|
|
7873
|
+
case "content_block_start": {
|
|
7874
|
+
const cb = event.content_block;
|
|
7875
|
+
const index = event.index ?? this.order.length;
|
|
7876
|
+
if (!cb)
|
|
7877
|
+
break;
|
|
7878
|
+
if (cb.type === "text")
|
|
7879
|
+
this.open(index, { type: "text", text: "" });
|
|
7880
|
+
else if (cb.type === "thinking")
|
|
7881
|
+
this.open(index, { type: "thinking", thinking: "" });
|
|
7882
|
+
else if (cb.type === "tool_use")
|
|
7883
|
+
this.open(index, { type: "tool_use", id: cb.id ?? "", name: cb.name ?? "", partialJson: "" });
|
|
7884
|
+
break;
|
|
7885
|
+
}
|
|
7886
|
+
case "content_block_delta": {
|
|
7887
|
+
const d = event.delta;
|
|
7888
|
+
if (!d)
|
|
7889
|
+
break;
|
|
7890
|
+
if (this.firstDeltaAt === void 0)
|
|
7891
|
+
this.firstDeltaAt = Date.now();
|
|
7892
|
+
const block = this.blocks.get(event.index ?? -1);
|
|
7893
|
+
if (d.type === "text_delta" && block?.type === "text") {
|
|
7894
|
+
block.text += d.text ?? "";
|
|
7895
|
+
if (d.text)
|
|
7896
|
+
this.onDelta?.({ type: "text_delta", text: d.text });
|
|
7897
|
+
} else if (d.type === "thinking_delta" && block?.type === "thinking") {
|
|
7898
|
+
block.thinking += d.thinking ?? "";
|
|
7899
|
+
if (d.thinking)
|
|
7900
|
+
this.onDelta?.({ type: "thinking_delta", text: d.thinking });
|
|
7901
|
+
} else if (d.type === "input_json_delta" && block?.type === "tool_use") {
|
|
7902
|
+
block.partialJson += d.partial_json ?? "";
|
|
7903
|
+
}
|
|
7904
|
+
break;
|
|
7905
|
+
}
|
|
7906
|
+
case "message_delta": {
|
|
7907
|
+
if (event.delta?.stop_reason)
|
|
7908
|
+
this.stopReason = event.delta.stop_reason;
|
|
7909
|
+
if (event.delta?.stop_details) {
|
|
7910
|
+
this.stopDetails = {
|
|
7911
|
+
category: event.delta.stop_details.category ?? null,
|
|
7912
|
+
explanation: event.delta.stop_details.explanation ?? null
|
|
7913
|
+
};
|
|
7914
|
+
}
|
|
7915
|
+
const u = event.usage;
|
|
7916
|
+
if (typeof u?.output_tokens === "number")
|
|
7917
|
+
this.outputTokens = u.output_tokens;
|
|
7918
|
+
if (typeof u?.input_tokens === "number")
|
|
7919
|
+
this.inputTokens = u.input_tokens;
|
|
7920
|
+
if (typeof u?.cache_read_input_tokens === "number")
|
|
7921
|
+
this.cacheReadTokens = u.cache_read_input_tokens;
|
|
7922
|
+
if (typeof u?.cache_creation_input_tokens === "number")
|
|
7923
|
+
this.cacheWriteTokens = u.cache_creation_input_tokens;
|
|
7924
|
+
break;
|
|
7925
|
+
}
|
|
7926
|
+
case "message_stop":
|
|
7927
|
+
this.invocationMetrics = event["amazon-bedrock-invocationMetrics"];
|
|
7928
|
+
break;
|
|
7929
|
+
}
|
|
7930
|
+
}
|
|
7931
|
+
/** Build the loop-facing result. Call once, after the stream has ended. */
|
|
7932
|
+
finish() {
|
|
7933
|
+
let text6 = "";
|
|
7934
|
+
let thinking = "";
|
|
7935
|
+
const toolCalls = [];
|
|
7936
|
+
const assistantBlocks = [];
|
|
7937
|
+
for (const index of this.order) {
|
|
7938
|
+
const block = this.blocks.get(index);
|
|
7939
|
+
if (block.type === "text") {
|
|
7940
|
+
if (!block.text)
|
|
7941
|
+
continue;
|
|
7942
|
+
text6 += block.text;
|
|
7943
|
+
assistantBlocks.push({ type: "text", text: block.text });
|
|
7944
|
+
} else if (block.type === "thinking") {
|
|
7945
|
+
thinking += block.thinking;
|
|
7946
|
+
} else {
|
|
7947
|
+
const input = JSON.parse(block.partialJson || "{}");
|
|
7948
|
+
toolCalls.push({ id: block.id, name: block.name, input });
|
|
7949
|
+
assistantBlocks.push({ type: "tool_use", id: block.id, name: block.name, input });
|
|
7950
|
+
}
|
|
7951
|
+
}
|
|
7952
|
+
const inputTokens = this.inputTokens || (this.invocationMetrics?.inputTokenCount ?? 0);
|
|
7953
|
+
const outputTokens = this.outputTokens || (this.invocationMetrics?.outputTokenCount ?? 0);
|
|
7954
|
+
return {
|
|
7955
|
+
assistantBlocks,
|
|
7956
|
+
stopReason: this.stopReason ?? "end_turn",
|
|
7957
|
+
...this.stopDetails ? { stopDetails: this.stopDetails } : {},
|
|
7958
|
+
text: text6,
|
|
7959
|
+
thinking,
|
|
7960
|
+
toolCalls,
|
|
7961
|
+
usage: {
|
|
7962
|
+
inputTokens,
|
|
7963
|
+
outputTokens,
|
|
7964
|
+
totalTokens: inputTokens + outputTokens,
|
|
7965
|
+
...this.cacheReadTokens ? { cacheReadInputTokens: this.cacheReadTokens } : {},
|
|
7966
|
+
...this.cacheWriteTokens ? { cacheWriteInputTokens: this.cacheWriteTokens } : {}
|
|
7967
|
+
},
|
|
7968
|
+
durationMs: Date.now() - this.startTime,
|
|
7969
|
+
...this.firstDeltaAt !== void 0 ? { ttftMs: this.firstDeltaAt - this.startTime } : {}
|
|
7970
|
+
};
|
|
7971
|
+
}
|
|
7972
|
+
open(index, block) {
|
|
7973
|
+
this.blocks.set(index, block);
|
|
7974
|
+
this.order.push(index);
|
|
7975
|
+
}
|
|
7976
|
+
};
|
|
7977
|
+
SSE_ERROR_STATUS = {
|
|
7978
|
+
invalid_request_error: 400,
|
|
7979
|
+
authentication_error: 401,
|
|
7980
|
+
billing_error: 402,
|
|
7981
|
+
permission_error: 403,
|
|
7982
|
+
not_found_error: 404,
|
|
7983
|
+
request_too_large: 413,
|
|
7984
|
+
rate_limit_error: 429,
|
|
7985
|
+
api_error: 500,
|
|
7986
|
+
timeout_error: 504,
|
|
7987
|
+
overloaded_error: 529
|
|
7988
|
+
};
|
|
7989
|
+
}
|
|
7990
|
+
});
|
|
7991
|
+
|
|
7679
7992
|
// ../server/dist/agents/bedrock-agent.js
|
|
7680
|
-
import { BedrockRuntimeClient,
|
|
7993
|
+
import { BedrockRuntimeClient, InvokeModelWithResponseStreamCommand } from "@aws-sdk/client-bedrock-runtime";
|
|
7681
7994
|
function effortToBudget(effort, maxTokens) {
|
|
7682
7995
|
const table = {
|
|
7683
7996
|
low: 2048,
|
|
@@ -7702,6 +8015,7 @@ var init_bedrock_agent = __esm({
|
|
|
7702
8015
|
"use strict";
|
|
7703
8016
|
init_config();
|
|
7704
8017
|
init_agent_loop();
|
|
8018
|
+
init_anthropic_stream();
|
|
7705
8019
|
BedrockAgent = class extends AgentLoop {
|
|
7706
8020
|
client;
|
|
7707
8021
|
config;
|
|
@@ -7712,10 +8026,10 @@ var init_bedrock_agent = __esm({
|
|
|
7712
8026
|
const region = extractRegionFromEndpoint(config2.endpoint);
|
|
7713
8027
|
this.client = new BedrockRuntimeClient({ region, endpoint: config2.endpoint, ...creds });
|
|
7714
8028
|
}
|
|
7715
|
-
async callModel(signal) {
|
|
8029
|
+
async callModel(signal, onDelta) {
|
|
7716
8030
|
const body = this.buildRequestBody();
|
|
7717
8031
|
const startTime = Date.now();
|
|
7718
|
-
const command = new
|
|
8032
|
+
const command = new InvokeModelWithResponseStreamCommand({
|
|
7719
8033
|
modelId: this.config.modelId,
|
|
7720
8034
|
contentType: "application/json",
|
|
7721
8035
|
accept: "application/json",
|
|
@@ -7724,45 +8038,15 @@ var init_bedrock_agent = __esm({
|
|
|
7724
8038
|
const response = await this.client.send(command, {
|
|
7725
8039
|
abortSignal: signal
|
|
7726
8040
|
});
|
|
7727
|
-
const
|
|
7728
|
-
const
|
|
7729
|
-
const
|
|
7730
|
-
|
|
7731
|
-
|
|
7732
|
-
const toolCalls = [];
|
|
7733
|
-
const assistantBlocks = [];
|
|
7734
|
-
for (const block of msg.content) {
|
|
7735
|
-
if (block.type === "text" && block.text) {
|
|
7736
|
-
text6 += block.text;
|
|
7737
|
-
assistantBlocks.push({ type: "text", text: block.text });
|
|
7738
|
-
} else if (block.type === "thinking" && block.thinking) {
|
|
7739
|
-
thinking += block.thinking;
|
|
7740
|
-
} else if (block.type === "tool_use") {
|
|
7741
|
-
toolCalls.push({ id: block.id, name: block.name, input: block.input ?? {} });
|
|
7742
|
-
assistantBlocks.push({ type: "tool_use", id: block.id, name: block.name, input: block.input ?? {} });
|
|
7743
|
-
}
|
|
8041
|
+
const acc = new AnthropicStreamAccumulator(startTime, onDelta);
|
|
8042
|
+
const decoder = new TextDecoder();
|
|
8043
|
+
for await (const chunk of response.body ?? []) {
|
|
8044
|
+
if (chunk.chunk?.bytes)
|
|
8045
|
+
acc.push(JSON.parse(decoder.decode(chunk.chunk.bytes)));
|
|
7744
8046
|
}
|
|
7745
|
-
|
|
7746
|
-
|
|
7747
|
-
|
|
7748
|
-
const cacheReadTokens = u?.cache_read_input_tokens ?? 0;
|
|
7749
|
-
const cacheWriteTokens = u?.cache_creation_input_tokens ?? 0;
|
|
7750
|
-
return {
|
|
7751
|
-
assistantBlocks,
|
|
7752
|
-
stopReason: msg.stop_reason ?? "end_turn",
|
|
7753
|
-
...msg.stop_details ? { stopDetails: { category: msg.stop_details.category ?? null, explanation: msg.stop_details.explanation ?? null } } : {},
|
|
7754
|
-
text: text6,
|
|
7755
|
-
thinking,
|
|
7756
|
-
toolCalls,
|
|
7757
|
-
usage: {
|
|
7758
|
-
inputTokens,
|
|
7759
|
-
outputTokens,
|
|
7760
|
-
totalTokens: inputTokens + outputTokens,
|
|
7761
|
-
...cacheReadTokens ? { cacheReadInputTokens: cacheReadTokens } : {},
|
|
7762
|
-
...cacheWriteTokens ? { cacheWriteInputTokens: cacheWriteTokens } : {}
|
|
7763
|
-
},
|
|
7764
|
-
durationMs
|
|
7765
|
-
};
|
|
8047
|
+
if (signal?.aborted)
|
|
8048
|
+
throw new DOMException("Model call aborted", "AbortError");
|
|
8049
|
+
return acc.finish();
|
|
7766
8050
|
}
|
|
7767
8051
|
/** Build Anthropic Messages API request body */
|
|
7768
8052
|
buildRequestBody() {
|
|
@@ -7828,6 +8112,107 @@ var init_bedrock_agent = __esm({
|
|
|
7828
8112
|
}
|
|
7829
8113
|
});
|
|
7830
8114
|
|
|
8115
|
+
// ../server/dist/agents/openai-chat-stream.js
|
|
8116
|
+
async function fetchChatCompletionStream(opts) {
|
|
8117
|
+
const { tag } = opts;
|
|
8118
|
+
const startTime = Date.now();
|
|
8119
|
+
const res = await fetch(opts.url, {
|
|
8120
|
+
method: "POST",
|
|
8121
|
+
signal: opts.signal,
|
|
8122
|
+
headers: { "Content-Type": "application/json", ...opts.headers },
|
|
8123
|
+
body: JSON.stringify({ ...opts.body, stream: true, stream_options: { include_usage: true } })
|
|
8124
|
+
});
|
|
8125
|
+
if (!res.ok) {
|
|
8126
|
+
const errText2 = await res.text().catch(() => "");
|
|
8127
|
+
throw new Error(`[${tag}] API error ${res.status}: ${errText2}`);
|
|
8128
|
+
}
|
|
8129
|
+
if (!res.body)
|
|
8130
|
+
throw new Error(`[${tag}] empty response body`);
|
|
8131
|
+
const acc = new ChatCompletionAccumulator(startTime, opts.onDelta);
|
|
8132
|
+
for await (const chunk of readSseJson(res.body)) {
|
|
8133
|
+
if (chunk.error) {
|
|
8134
|
+
throw new Error(`[${tag}] API error in stream: ${JSON.stringify(chunk.error).slice(0, 300)}`);
|
|
8135
|
+
}
|
|
8136
|
+
acc.push(chunk);
|
|
8137
|
+
}
|
|
8138
|
+
if (opts.signal?.aborted)
|
|
8139
|
+
throw new DOMException("Model call aborted", "AbortError");
|
|
8140
|
+
return acc.finish();
|
|
8141
|
+
}
|
|
8142
|
+
var ChatCompletionAccumulator;
|
|
8143
|
+
var init_openai_chat_stream = __esm({
|
|
8144
|
+
"../server/dist/agents/openai-chat-stream.js"() {
|
|
8145
|
+
"use strict";
|
|
8146
|
+
init_sse();
|
|
8147
|
+
ChatCompletionAccumulator = class {
|
|
8148
|
+
startTime;
|
|
8149
|
+
onDelta;
|
|
8150
|
+
content = "";
|
|
8151
|
+
reasoning = "";
|
|
8152
|
+
toolCalls = [];
|
|
8153
|
+
finishReason;
|
|
8154
|
+
usage;
|
|
8155
|
+
firstDeltaAt;
|
|
8156
|
+
constructor(startTime, onDelta) {
|
|
8157
|
+
this.startTime = startTime;
|
|
8158
|
+
this.onDelta = onDelta;
|
|
8159
|
+
}
|
|
8160
|
+
push(chunk) {
|
|
8161
|
+
if (chunk.usage)
|
|
8162
|
+
this.usage = chunk.usage;
|
|
8163
|
+
const choice = chunk.choices?.[0];
|
|
8164
|
+
if (!choice || typeof choice.index === "number" && choice.index !== 0)
|
|
8165
|
+
return;
|
|
8166
|
+
if (choice.finish_reason)
|
|
8167
|
+
this.finishReason = choice.finish_reason;
|
|
8168
|
+
const d = choice.delta;
|
|
8169
|
+
if (!d)
|
|
8170
|
+
return;
|
|
8171
|
+
if (typeof d.content === "string" && d.content !== "") {
|
|
8172
|
+
this.markFirstDelta();
|
|
8173
|
+
this.content += d.content;
|
|
8174
|
+
this.onDelta?.({ type: "text_delta", text: d.content });
|
|
8175
|
+
}
|
|
8176
|
+
const reasoning = d.reasoning_content ?? d.reasoning;
|
|
8177
|
+
if (typeof reasoning === "string" && reasoning !== "") {
|
|
8178
|
+
this.markFirstDelta();
|
|
8179
|
+
this.reasoning += reasoning;
|
|
8180
|
+
this.onDelta?.({ type: "thinking_delta", text: reasoning });
|
|
8181
|
+
}
|
|
8182
|
+
for (const tc of d.tool_calls ?? []) {
|
|
8183
|
+
this.markFirstDelta();
|
|
8184
|
+
const slot = tc.index ?? (tc.id ? this.toolCalls.length : Math.max(this.toolCalls.length - 1, 0));
|
|
8185
|
+
let call = this.toolCalls[slot];
|
|
8186
|
+
if (!call) {
|
|
8187
|
+
call = { id: tc.id ?? "", type: "function", function: { name: tc.function?.name ?? "", arguments: "" } };
|
|
8188
|
+
this.toolCalls[slot] = call;
|
|
8189
|
+
}
|
|
8190
|
+
if (tc.function?.name && !call.function.name)
|
|
8191
|
+
call.function.name = tc.function.name;
|
|
8192
|
+
call.function.arguments += tc.function?.arguments ?? "";
|
|
8193
|
+
}
|
|
8194
|
+
}
|
|
8195
|
+
/** Build the agent-facing result. Call once, after the stream has ended. */
|
|
8196
|
+
finish() {
|
|
8197
|
+
return {
|
|
8198
|
+
message: {
|
|
8199
|
+
...this.content ? { content: this.content } : {},
|
|
8200
|
+
...this.reasoning ? { reasoning_content: this.reasoning } : {},
|
|
8201
|
+
...this.toolCalls.length > 0 ? { tool_calls: this.toolCalls.filter(Boolean) } : {}
|
|
8202
|
+
},
|
|
8203
|
+
finishReason: this.finishReason,
|
|
8204
|
+
usage: this.usage,
|
|
8205
|
+
...this.firstDeltaAt !== void 0 ? { ttftMs: this.firstDeltaAt - this.startTime } : {}
|
|
8206
|
+
};
|
|
8207
|
+
}
|
|
8208
|
+
markFirstDelta() {
|
|
8209
|
+
if (this.firstDeltaAt === void 0)
|
|
8210
|
+
this.firstDeltaAt = Date.now();
|
|
8211
|
+
}
|
|
8212
|
+
};
|
|
8213
|
+
}
|
|
8214
|
+
});
|
|
8215
|
+
|
|
7831
8216
|
// ../server/dist/agents/deepseek-agent.js
|
|
7832
8217
|
function safeParse(json) {
|
|
7833
8218
|
try {
|
|
@@ -7842,13 +8227,14 @@ var init_deepseek_agent = __esm({
|
|
|
7842
8227
|
"use strict";
|
|
7843
8228
|
init_config();
|
|
7844
8229
|
init_agent_loop();
|
|
8230
|
+
init_openai_chat_stream();
|
|
7845
8231
|
DeepSeekAgent = class extends AgentLoop {
|
|
7846
8232
|
config;
|
|
7847
8233
|
constructor(config2) {
|
|
7848
8234
|
super(config2.tools);
|
|
7849
8235
|
this.config = config2;
|
|
7850
8236
|
}
|
|
7851
|
-
async callModel(signal) {
|
|
8237
|
+
async callModel(signal, onDelta) {
|
|
7852
8238
|
const url = this.config.endpoint.replace(/\/+$/, "") + "/chat/completions";
|
|
7853
8239
|
const startTime = Date.now();
|
|
7854
8240
|
const messages2 = this.buildMessages();
|
|
@@ -7856,7 +8242,6 @@ var init_deepseek_agent = __esm({
|
|
|
7856
8242
|
const body = {
|
|
7857
8243
|
model: this.config.modelId,
|
|
7858
8244
|
messages: messages2,
|
|
7859
|
-
stream: false,
|
|
7860
8245
|
max_completion_tokens: this.config.maxTokens ?? resolveMaxOutputTokens(this.config.modelId),
|
|
7861
8246
|
...tools.length > 0 ? { tools } : {}
|
|
7862
8247
|
};
|
|
@@ -7865,24 +8250,14 @@ var init_deepseek_agent = __esm({
|
|
|
7865
8250
|
} else {
|
|
7866
8251
|
body.thinking = { type: "enabled" };
|
|
7867
8252
|
}
|
|
7868
|
-
const
|
|
7869
|
-
|
|
7870
|
-
headers: {
|
|
7871
|
-
|
|
7872
|
-
|
|
7873
|
-
|
|
7874
|
-
|
|
7875
|
-
signal
|
|
8253
|
+
const { message: msg, finishReason, usage: rawUsage, ttftMs } = await fetchChatCompletionStream({
|
|
8254
|
+
url,
|
|
8255
|
+
headers: { "Authorization": `Bearer ${this.config.apiKey}` },
|
|
8256
|
+
body,
|
|
8257
|
+
signal,
|
|
8258
|
+
onDelta,
|
|
8259
|
+
tag: "DeepSeekAgent"
|
|
7876
8260
|
});
|
|
7877
|
-
if (!response.ok) {
|
|
7878
|
-
const errText2 = await response.text().catch(() => "");
|
|
7879
|
-
throw new Error(`[DeepSeekAgent] API error ${response.status}: ${errText2}`);
|
|
7880
|
-
}
|
|
7881
|
-
const data = await response.json();
|
|
7882
|
-
const choices = data.choices;
|
|
7883
|
-
const choice = choices?.[0];
|
|
7884
|
-
const msg = choice?.message;
|
|
7885
|
-
const finishReason = choice?.finish_reason;
|
|
7886
8261
|
let text6 = "";
|
|
7887
8262
|
let thinking = "";
|
|
7888
8263
|
const toolCalls = [];
|
|
@@ -7914,7 +8289,7 @@ var init_deepseek_agent = __esm({
|
|
|
7914
8289
|
assistantBlocks.push({ type: "text", text: text6 });
|
|
7915
8290
|
}
|
|
7916
8291
|
const stopReason = finishReason === "tool_calls" ? "tool_use" : finishReason === "length" ? "max_tokens" : "end_turn";
|
|
7917
|
-
const usage =
|
|
8292
|
+
const usage = rawUsage;
|
|
7918
8293
|
const inputTokens = usage?.prompt_tokens ?? 0;
|
|
7919
8294
|
const outputTokens = usage?.completion_tokens ?? 0;
|
|
7920
8295
|
const cacheHitTokens = usage?.prompt_cache_hit_tokens ?? 0;
|
|
@@ -7930,7 +8305,8 @@ var init_deepseek_agent = __esm({
|
|
|
7930
8305
|
totalTokens: inputTokens - cacheHitTokens + outputTokens,
|
|
7931
8306
|
...cacheHitTokens ? { cacheReadInputTokens: cacheHitTokens } : {}
|
|
7932
8307
|
},
|
|
7933
|
-
durationMs: Date.now() - startTime
|
|
8308
|
+
durationMs: Date.now() - startTime,
|
|
8309
|
+
ttftMs
|
|
7934
8310
|
};
|
|
7935
8311
|
}
|
|
7936
8312
|
buildMessages() {
|
|
@@ -8047,13 +8423,14 @@ var init_kimi_agent = __esm({
|
|
|
8047
8423
|
"use strict";
|
|
8048
8424
|
init_config();
|
|
8049
8425
|
init_agent_loop();
|
|
8426
|
+
init_openai_chat_stream();
|
|
8050
8427
|
KimiAgent = class extends AgentLoop {
|
|
8051
8428
|
config;
|
|
8052
8429
|
constructor(config2) {
|
|
8053
8430
|
super(config2.tools);
|
|
8054
8431
|
this.config = config2;
|
|
8055
8432
|
}
|
|
8056
|
-
async callModel(signal) {
|
|
8433
|
+
async callModel(signal, onDelta) {
|
|
8057
8434
|
const url = this.config.endpoint.replace(/\/+$/, "") + "/chat/completions";
|
|
8058
8435
|
const startTime = Date.now();
|
|
8059
8436
|
const messages2 = this.buildMessages();
|
|
@@ -8061,7 +8438,6 @@ var init_kimi_agent = __esm({
|
|
|
8061
8438
|
const body = {
|
|
8062
8439
|
model: this.config.modelId,
|
|
8063
8440
|
messages: messages2,
|
|
8064
|
-
stream: false,
|
|
8065
8441
|
max_completion_tokens: this.config.maxTokens ?? resolveMaxOutputTokens(this.config.modelId),
|
|
8066
8442
|
...tools.length > 0 ? { tools } : {},
|
|
8067
8443
|
prompt_cache_key: this.config.cacheKey ?? void 0
|
|
@@ -8075,24 +8451,14 @@ var init_kimi_agent = __esm({
|
|
|
8075
8451
|
body.thinking = { type: "disabled" };
|
|
8076
8452
|
}
|
|
8077
8453
|
}
|
|
8078
|
-
const
|
|
8079
|
-
|
|
8080
|
-
headers: {
|
|
8081
|
-
|
|
8082
|
-
|
|
8083
|
-
|
|
8084
|
-
|
|
8085
|
-
signal
|
|
8454
|
+
const { message: msg, finishReason, usage: rawUsage, ttftMs } = await fetchChatCompletionStream({
|
|
8455
|
+
url,
|
|
8456
|
+
headers: { "Authorization": `Bearer ${this.config.apiKey}` },
|
|
8457
|
+
body,
|
|
8458
|
+
signal,
|
|
8459
|
+
onDelta,
|
|
8460
|
+
tag: "KimiAgent"
|
|
8086
8461
|
});
|
|
8087
|
-
if (!response.ok) {
|
|
8088
|
-
const errText2 = await response.text().catch(() => "");
|
|
8089
|
-
throw new Error(`[KimiAgent] API error ${response.status}: ${errText2}`);
|
|
8090
|
-
}
|
|
8091
|
-
const data = await response.json();
|
|
8092
|
-
const choices = data.choices;
|
|
8093
|
-
const choice = choices?.[0];
|
|
8094
|
-
const msg = choice?.message;
|
|
8095
|
-
const finishReason = choice?.finish_reason;
|
|
8096
8462
|
let text6 = "";
|
|
8097
8463
|
let thinking = "";
|
|
8098
8464
|
const toolCalls = [];
|
|
@@ -8124,7 +8490,7 @@ var init_kimi_agent = __esm({
|
|
|
8124
8490
|
assistantBlocks.push({ type: "text", text: text6 });
|
|
8125
8491
|
}
|
|
8126
8492
|
const stopReason = finishReason === "tool_calls" ? "tool_use" : finishReason === "length" ? "max_tokens" : "end_turn";
|
|
8127
|
-
const usage =
|
|
8493
|
+
const usage = rawUsage;
|
|
8128
8494
|
const inputTokens = usage?.prompt_tokens ?? 0;
|
|
8129
8495
|
const outputTokens = usage?.completion_tokens ?? 0;
|
|
8130
8496
|
const cachedTokens = usage?.cached_tokens ?? 0;
|
|
@@ -8140,7 +8506,8 @@ var init_kimi_agent = __esm({
|
|
|
8140
8506
|
totalTokens: inputTokens - cachedTokens + outputTokens,
|
|
8141
8507
|
...cachedTokens ? { cacheReadInputTokens: cachedTokens } : {}
|
|
8142
8508
|
},
|
|
8143
|
-
durationMs: Date.now() - startTime
|
|
8509
|
+
durationMs: Date.now() - startTime,
|
|
8510
|
+
ttftMs
|
|
8144
8511
|
};
|
|
8145
8512
|
}
|
|
8146
8513
|
buildMessages() {
|
|
@@ -8260,72 +8627,25 @@ var init_minimax_agent = __esm({
|
|
|
8260
8627
|
"use strict";
|
|
8261
8628
|
init_config();
|
|
8262
8629
|
init_agent_loop();
|
|
8630
|
+
init_anthropic_stream();
|
|
8263
8631
|
MiniMaxAgent = class extends AgentLoop {
|
|
8264
8632
|
config;
|
|
8265
8633
|
constructor(config2) {
|
|
8266
8634
|
super(config2.tools);
|
|
8267
8635
|
this.config = config2;
|
|
8268
8636
|
}
|
|
8269
|
-
async callModel(signal) {
|
|
8270
|
-
|
|
8271
|
-
|
|
8272
|
-
const startTime = Date.now();
|
|
8273
|
-
const res = await fetch(url, {
|
|
8274
|
-
method: "POST",
|
|
8275
|
-
signal,
|
|
8637
|
+
async callModel(signal, onDelta) {
|
|
8638
|
+
return fetchAnthropicStream({
|
|
8639
|
+
url: `${this.config.endpoint.replace(/\/$/, "")}/v1/messages`,
|
|
8276
8640
|
headers: {
|
|
8277
8641
|
"x-api-key": this.config.apiKey,
|
|
8278
|
-
"anthropic-version": "2023-06-01"
|
|
8279
|
-
"content-type": "application/json"
|
|
8642
|
+
"anthropic-version": "2023-06-01"
|
|
8280
8643
|
},
|
|
8281
|
-
body:
|
|
8644
|
+
body: this.buildRequestBody(),
|
|
8645
|
+
signal,
|
|
8646
|
+
onDelta,
|
|
8647
|
+
tag: "minimax"
|
|
8282
8648
|
});
|
|
8283
|
-
const durationMs = Date.now() - startTime;
|
|
8284
|
-
const raw = await res.text();
|
|
8285
|
-
let msg;
|
|
8286
|
-
try {
|
|
8287
|
-
msg = JSON.parse(raw);
|
|
8288
|
-
} catch {
|
|
8289
|
-
throw new Error(`[minimax] non-JSON response (status=${res.status}): ${raw.slice(0, 200)}`);
|
|
8290
|
-
}
|
|
8291
|
-
if (!res.ok || msg.error) {
|
|
8292
|
-
throw new Error(`[minimax] ${res.status} ${msg.error?.type ?? "?"}: ${msg.error?.message ?? raw.slice(0, 200)}`);
|
|
8293
|
-
}
|
|
8294
|
-
let text6 = "";
|
|
8295
|
-
let thinking = "";
|
|
8296
|
-
const toolCalls = [];
|
|
8297
|
-
const assistantBlocks = [];
|
|
8298
|
-
for (const block of msg.content ?? []) {
|
|
8299
|
-
if (block.type === "text" && block.text) {
|
|
8300
|
-
text6 += block.text;
|
|
8301
|
-
assistantBlocks.push({ type: "text", text: block.text });
|
|
8302
|
-
} else if (block.type === "thinking" && block.thinking) {
|
|
8303
|
-
thinking += block.thinking;
|
|
8304
|
-
} else if (block.type === "tool_use") {
|
|
8305
|
-
toolCalls.push({ id: block.id, name: block.name, input: block.input ?? {} });
|
|
8306
|
-
assistantBlocks.push({ type: "tool_use", id: block.id, name: block.name, input: block.input ?? {} });
|
|
8307
|
-
}
|
|
8308
|
-
}
|
|
8309
|
-
const u = msg.usage;
|
|
8310
|
-
const inputTokens = u?.input_tokens ?? 0;
|
|
8311
|
-
const outputTokens = u?.output_tokens ?? 0;
|
|
8312
|
-
const cacheReadTokens = u?.cache_read_input_tokens ?? 0;
|
|
8313
|
-
const cacheWriteTokens = u?.cache_creation_input_tokens ?? 0;
|
|
8314
|
-
return {
|
|
8315
|
-
assistantBlocks,
|
|
8316
|
-
stopReason: msg.stop_reason ?? "end_turn",
|
|
8317
|
-
text: text6,
|
|
8318
|
-
thinking,
|
|
8319
|
-
toolCalls,
|
|
8320
|
-
usage: {
|
|
8321
|
-
inputTokens,
|
|
8322
|
-
outputTokens,
|
|
8323
|
-
totalTokens: inputTokens + outputTokens,
|
|
8324
|
-
...cacheReadTokens ? { cacheReadInputTokens: cacheReadTokens } : {},
|
|
8325
|
-
...cacheWriteTokens ? { cacheWriteInputTokens: cacheWriteTokens } : {}
|
|
8326
|
-
},
|
|
8327
|
-
durationMs
|
|
8328
|
-
};
|
|
8329
8649
|
}
|
|
8330
8650
|
buildRequestBody() {
|
|
8331
8651
|
const caching = this.config.promptCaching;
|
|
@@ -8399,71 +8719,22 @@ var init_qwen_agent = __esm({
|
|
|
8399
8719
|
"use strict";
|
|
8400
8720
|
init_config();
|
|
8401
8721
|
init_agent_loop();
|
|
8722
|
+
init_anthropic_stream();
|
|
8402
8723
|
QwenAgent = class extends AgentLoop {
|
|
8403
8724
|
config;
|
|
8404
8725
|
constructor(config2) {
|
|
8405
8726
|
super(config2.tools);
|
|
8406
8727
|
this.config = config2;
|
|
8407
8728
|
}
|
|
8408
|
-
async callModel(signal) {
|
|
8409
|
-
|
|
8410
|
-
|
|
8411
|
-
|
|
8412
|
-
|
|
8413
|
-
method: "POST",
|
|
8729
|
+
async callModel(signal, onDelta) {
|
|
8730
|
+
return fetchAnthropicStream({
|
|
8731
|
+
url: `${this.config.endpoint.replace(/\/$/, "")}/v1/messages`,
|
|
8732
|
+
headers: { "x-api-key": this.config.apiKey },
|
|
8733
|
+
body: this.buildRequestBody(),
|
|
8414
8734
|
signal,
|
|
8415
|
-
|
|
8416
|
-
|
|
8417
|
-
"content-type": "application/json"
|
|
8418
|
-
},
|
|
8419
|
-
body: JSON.stringify(body)
|
|
8735
|
+
onDelta,
|
|
8736
|
+
tag: "qwen"
|
|
8420
8737
|
});
|
|
8421
|
-
const durationMs = Date.now() - startTime;
|
|
8422
|
-
const raw = await res.text();
|
|
8423
|
-
let msg;
|
|
8424
|
-
try {
|
|
8425
|
-
msg = JSON.parse(raw);
|
|
8426
|
-
} catch {
|
|
8427
|
-
throw new Error(`[qwen] non-JSON response (status=${res.status}): ${raw.slice(0, 200)}`);
|
|
8428
|
-
}
|
|
8429
|
-
if (!res.ok || msg.error) {
|
|
8430
|
-
throw new Error(`[qwen] ${res.status} ${msg.error?.type ?? "?"}: ${msg.error?.message ?? raw.slice(0, 200)}`);
|
|
8431
|
-
}
|
|
8432
|
-
let text6 = "";
|
|
8433
|
-
let thinking = "";
|
|
8434
|
-
const toolCalls = [];
|
|
8435
|
-
const assistantBlocks = [];
|
|
8436
|
-
for (const block of msg.content ?? []) {
|
|
8437
|
-
if (block.type === "text" && block.text) {
|
|
8438
|
-
text6 += block.text;
|
|
8439
|
-
assistantBlocks.push({ type: "text", text: block.text });
|
|
8440
|
-
} else if (block.type === "thinking" && block.thinking) {
|
|
8441
|
-
thinking += block.thinking;
|
|
8442
|
-
} else if (block.type === "tool_use") {
|
|
8443
|
-
toolCalls.push({ id: block.id, name: block.name, input: block.input ?? {} });
|
|
8444
|
-
assistantBlocks.push({ type: "tool_use", id: block.id, name: block.name, input: block.input ?? {} });
|
|
8445
|
-
}
|
|
8446
|
-
}
|
|
8447
|
-
const u = msg.usage;
|
|
8448
|
-
const inputTokens = u?.input_tokens ?? 0;
|
|
8449
|
-
const outputTokens = u?.output_tokens ?? 0;
|
|
8450
|
-
const cacheReadTokens = u?.cache_read_input_tokens ?? 0;
|
|
8451
|
-
const cacheWriteTokens = u?.cache_creation_input_tokens ?? 0;
|
|
8452
|
-
return {
|
|
8453
|
-
assistantBlocks,
|
|
8454
|
-
stopReason: msg.stop_reason ?? "end_turn",
|
|
8455
|
-
text: text6,
|
|
8456
|
-
thinking,
|
|
8457
|
-
toolCalls,
|
|
8458
|
-
usage: {
|
|
8459
|
-
inputTokens,
|
|
8460
|
-
outputTokens,
|
|
8461
|
-
totalTokens: inputTokens + outputTokens,
|
|
8462
|
-
...cacheReadTokens ? { cacheReadInputTokens: cacheReadTokens } : {},
|
|
8463
|
-
...cacheWriteTokens ? { cacheWriteInputTokens: cacheWriteTokens } : {}
|
|
8464
|
-
},
|
|
8465
|
-
durationMs
|
|
8466
|
-
};
|
|
8467
8738
|
}
|
|
8468
8739
|
buildRequestBody() {
|
|
8469
8740
|
const cacheControl = this.config.promptCaching ? { type: "ephemeral" } : null;
|
|
@@ -8527,13 +8798,14 @@ var init_hunyuan_agent = __esm({
|
|
|
8527
8798
|
"use strict";
|
|
8528
8799
|
init_config();
|
|
8529
8800
|
init_agent_loop();
|
|
8801
|
+
init_openai_chat_stream();
|
|
8530
8802
|
HunyuanAgent = class extends AgentLoop {
|
|
8531
8803
|
config;
|
|
8532
8804
|
constructor(config2) {
|
|
8533
8805
|
super(config2.tools);
|
|
8534
8806
|
this.config = config2;
|
|
8535
8807
|
}
|
|
8536
|
-
async callModel(signal) {
|
|
8808
|
+
async callModel(signal, onDelta) {
|
|
8537
8809
|
const url = this.config.endpoint.replace(/\/+$/, "") + "/chat/completions";
|
|
8538
8810
|
const startTime = Date.now();
|
|
8539
8811
|
const messages2 = this.buildMessages();
|
|
@@ -8541,31 +8813,20 @@ var init_hunyuan_agent = __esm({
|
|
|
8541
8813
|
const body = {
|
|
8542
8814
|
model: this.config.modelId,
|
|
8543
8815
|
messages: messages2,
|
|
8544
|
-
stream: false,
|
|
8545
8816
|
max_tokens: this.config.maxTokens ?? resolveMaxOutputTokens(this.config.modelId),
|
|
8546
8817
|
...tools.length > 0 ? { tools } : {}
|
|
8547
8818
|
};
|
|
8548
8819
|
if (this.config.thinking?.enabled && this.config.thinking.effort) {
|
|
8549
8820
|
body.reasoning_effort = this.config.thinking.effort;
|
|
8550
8821
|
}
|
|
8551
|
-
const
|
|
8552
|
-
|
|
8553
|
-
headers: {
|
|
8554
|
-
|
|
8555
|
-
|
|
8556
|
-
|
|
8557
|
-
|
|
8558
|
-
signal
|
|
8822
|
+
const { message: msg, finishReason, usage, ttftMs } = await fetchChatCompletionStream({
|
|
8823
|
+
url,
|
|
8824
|
+
headers: { "Authorization": `Bearer ${this.config.apiKey}` },
|
|
8825
|
+
body,
|
|
8826
|
+
signal,
|
|
8827
|
+
onDelta,
|
|
8828
|
+
tag: "HunyuanAgent"
|
|
8559
8829
|
});
|
|
8560
|
-
if (!response.ok) {
|
|
8561
|
-
const errText2 = await response.text().catch(() => "");
|
|
8562
|
-
throw new Error(`[HunyuanAgent] API error ${response.status}: ${errText2}`);
|
|
8563
|
-
}
|
|
8564
|
-
const data = await response.json();
|
|
8565
|
-
const choices = data.choices;
|
|
8566
|
-
const choice = choices?.[0];
|
|
8567
|
-
const msg = choice?.message;
|
|
8568
|
-
const finishReason = choice?.finish_reason;
|
|
8569
8830
|
let text6 = "";
|
|
8570
8831
|
let thinking = "";
|
|
8571
8832
|
const toolCalls = [];
|
|
@@ -8597,7 +8858,6 @@ var init_hunyuan_agent = __esm({
|
|
|
8597
8858
|
assistantBlocks.push({ type: "text", text: text6 });
|
|
8598
8859
|
}
|
|
8599
8860
|
const stopReason = finishReason === "tool_calls" ? "tool_use" : finishReason === "length" ? "max_tokens" : "end_turn";
|
|
8600
|
-
const usage = data.usage;
|
|
8601
8861
|
const promptTokens = usage?.prompt_tokens ?? 0;
|
|
8602
8862
|
const completionTokens = usage?.completion_tokens ?? 0;
|
|
8603
8863
|
const promptDetails = usage?.prompt_tokens_details;
|
|
@@ -8614,7 +8874,8 @@ var init_hunyuan_agent = __esm({
|
|
|
8614
8874
|
totalTokens: promptTokens - cachedTokens + completionTokens,
|
|
8615
8875
|
...cachedTokens ? { cacheReadInputTokens: cachedTokens } : {}
|
|
8616
8876
|
},
|
|
8617
|
-
durationMs: Date.now() - startTime
|
|
8877
|
+
durationMs: Date.now() - startTime,
|
|
8878
|
+
ttftMs
|
|
8618
8879
|
};
|
|
8619
8880
|
}
|
|
8620
8881
|
buildMessages() {
|
|
@@ -8708,13 +8969,14 @@ var init_doubao_agent = __esm({
|
|
|
8708
8969
|
"use strict";
|
|
8709
8970
|
init_config();
|
|
8710
8971
|
init_agent_loop();
|
|
8972
|
+
init_openai_chat_stream();
|
|
8711
8973
|
DoubaoAgent = class extends AgentLoop {
|
|
8712
8974
|
config;
|
|
8713
8975
|
constructor(config2) {
|
|
8714
8976
|
super(config2.tools);
|
|
8715
8977
|
this.config = config2;
|
|
8716
8978
|
}
|
|
8717
|
-
async callModel(signal) {
|
|
8979
|
+
async callModel(signal, onDelta) {
|
|
8718
8980
|
const url = this.config.endpoint.replace(/\/+$/, "") + "/chat/completions";
|
|
8719
8981
|
const startTime = Date.now();
|
|
8720
8982
|
const messages2 = this.buildMessages();
|
|
@@ -8722,31 +8984,20 @@ var init_doubao_agent = __esm({
|
|
|
8722
8984
|
const body = {
|
|
8723
8985
|
model: this.config.modelId,
|
|
8724
8986
|
messages: messages2,
|
|
8725
|
-
stream: false,
|
|
8726
8987
|
max_tokens: this.config.maxTokens ?? resolveMaxOutputTokens(this.config.modelId),
|
|
8727
8988
|
...tools.length > 0 ? { tools } : {}
|
|
8728
8989
|
};
|
|
8729
8990
|
if (this.config.thinking) {
|
|
8730
8991
|
body.thinking = { type: this.config.thinking.enabled ? "enabled" : "disabled" };
|
|
8731
8992
|
}
|
|
8732
|
-
const
|
|
8733
|
-
|
|
8734
|
-
headers: {
|
|
8735
|
-
|
|
8736
|
-
|
|
8737
|
-
|
|
8738
|
-
|
|
8739
|
-
signal
|
|
8993
|
+
const { message: msg, finishReason, usage, ttftMs } = await fetchChatCompletionStream({
|
|
8994
|
+
url,
|
|
8995
|
+
headers: { "Authorization": `Bearer ${this.config.apiKey}` },
|
|
8996
|
+
body,
|
|
8997
|
+
signal,
|
|
8998
|
+
onDelta,
|
|
8999
|
+
tag: "DoubaoAgent"
|
|
8740
9000
|
});
|
|
8741
|
-
if (!response.ok) {
|
|
8742
|
-
const errText2 = await response.text().catch(() => "");
|
|
8743
|
-
throw new Error(`[DoubaoAgent] API error ${response.status}: ${errText2}`);
|
|
8744
|
-
}
|
|
8745
|
-
const data = await response.json();
|
|
8746
|
-
const choices = data.choices;
|
|
8747
|
-
const choice = choices?.[0];
|
|
8748
|
-
const msg = choice?.message;
|
|
8749
|
-
const finishReason = choice?.finish_reason;
|
|
8750
9001
|
let text6 = "";
|
|
8751
9002
|
let thinking = "";
|
|
8752
9003
|
const toolCalls = [];
|
|
@@ -8778,7 +9029,6 @@ var init_doubao_agent = __esm({
|
|
|
8778
9029
|
assistantBlocks.push({ type: "text", text: text6 });
|
|
8779
9030
|
}
|
|
8780
9031
|
const stopReason = finishReason === "tool_calls" ? "tool_use" : finishReason === "length" ? "max_tokens" : "end_turn";
|
|
8781
|
-
const usage = data.usage;
|
|
8782
9032
|
const promptTokens = usage?.prompt_tokens ?? 0;
|
|
8783
9033
|
const completionTokens = usage?.completion_tokens ?? 0;
|
|
8784
9034
|
const promptDetails = usage?.prompt_tokens_details;
|
|
@@ -8795,7 +9045,8 @@ var init_doubao_agent = __esm({
|
|
|
8795
9045
|
totalTokens: promptTokens - cachedTokens + completionTokens,
|
|
8796
9046
|
...cachedTokens ? { cacheReadInputTokens: cachedTokens } : {}
|
|
8797
9047
|
},
|
|
8798
|
-
durationMs: Date.now() - startTime
|
|
9048
|
+
durationMs: Date.now() - startTime,
|
|
9049
|
+
ttftMs
|
|
8799
9050
|
};
|
|
8800
9051
|
}
|
|
8801
9052
|
buildMessages() {
|
|
@@ -8889,6 +9140,7 @@ var init_zhipu_agent = __esm({
|
|
|
8889
9140
|
"use strict";
|
|
8890
9141
|
init_config();
|
|
8891
9142
|
init_agent_loop();
|
|
9143
|
+
init_openai_chat_stream();
|
|
8892
9144
|
EFFORTS = /* @__PURE__ */ new Set(["low", "high", "max"]);
|
|
8893
9145
|
ZhipuAgent = class extends AgentLoop {
|
|
8894
9146
|
config;
|
|
@@ -8896,7 +9148,7 @@ var init_zhipu_agent = __esm({
|
|
|
8896
9148
|
super(config2.tools);
|
|
8897
9149
|
this.config = config2;
|
|
8898
9150
|
}
|
|
8899
|
-
async callModel(signal) {
|
|
9151
|
+
async callModel(signal, onDelta) {
|
|
8900
9152
|
const url = this.config.endpoint.replace(/\/+$/, "") + "/chat/completions";
|
|
8901
9153
|
const startTime = Date.now();
|
|
8902
9154
|
const messages2 = this.buildMessages();
|
|
@@ -8904,7 +9156,6 @@ var init_zhipu_agent = __esm({
|
|
|
8904
9156
|
const body = {
|
|
8905
9157
|
model: this.config.modelId,
|
|
8906
9158
|
messages: messages2,
|
|
8907
|
-
stream: false,
|
|
8908
9159
|
max_tokens: this.config.maxTokens ?? resolveMaxOutputTokens(this.config.modelId),
|
|
8909
9160
|
...tools.length > 0 ? { tools } : {}
|
|
8910
9161
|
};
|
|
@@ -8920,24 +9171,14 @@ var init_zhipu_agent = __esm({
|
|
|
8920
9171
|
} else {
|
|
8921
9172
|
body.thinking = { type: "disabled" };
|
|
8922
9173
|
}
|
|
8923
|
-
const
|
|
8924
|
-
|
|
8925
|
-
headers: {
|
|
8926
|
-
|
|
8927
|
-
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
signal
|
|
9174
|
+
const { message: msg, finishReason, usage, ttftMs } = await fetchChatCompletionStream({
|
|
9175
|
+
url,
|
|
9176
|
+
headers: { "Authorization": `Bearer ${this.config.apiKey}` },
|
|
9177
|
+
body,
|
|
9178
|
+
signal,
|
|
9179
|
+
onDelta,
|
|
9180
|
+
tag: "ZhipuAgent"
|
|
8931
9181
|
});
|
|
8932
|
-
if (!response.ok) {
|
|
8933
|
-
const errText2 = await response.text().catch(() => "");
|
|
8934
|
-
throw new Error(`[ZhipuAgent] API error ${response.status}: ${errText2}`);
|
|
8935
|
-
}
|
|
8936
|
-
const data = await response.json();
|
|
8937
|
-
const choices = data.choices;
|
|
8938
|
-
const choice = choices?.[0];
|
|
8939
|
-
const msg = choice?.message;
|
|
8940
|
-
const finishReason = choice?.finish_reason;
|
|
8941
9182
|
let text6 = "";
|
|
8942
9183
|
let thinking = "";
|
|
8943
9184
|
const toolCalls = [];
|
|
@@ -8969,7 +9210,6 @@ var init_zhipu_agent = __esm({
|
|
|
8969
9210
|
assistantBlocks.push({ type: "text", text: text6 });
|
|
8970
9211
|
}
|
|
8971
9212
|
const stopReason = finishReason === "tool_calls" ? "tool_use" : finishReason === "length" ? "max_tokens" : "end_turn";
|
|
8972
|
-
const usage = data.usage;
|
|
8973
9213
|
const promptTokens = usage?.prompt_tokens ?? 0;
|
|
8974
9214
|
const outputTokens = usage?.completion_tokens ?? 0;
|
|
8975
9215
|
const details = usage?.prompt_tokens_details;
|
|
@@ -8987,7 +9227,8 @@ var init_zhipu_agent = __esm({
|
|
|
8987
9227
|
totalTokens: inputTokens + outputTokens,
|
|
8988
9228
|
...cachedTokens ? { cacheReadInputTokens: cachedTokens } : {}
|
|
8989
9229
|
},
|
|
8990
|
-
durationMs: Date.now() - startTime
|
|
9230
|
+
durationMs: Date.now() - startTime,
|
|
9231
|
+
ttftMs
|
|
8991
9232
|
};
|
|
8992
9233
|
}
|
|
8993
9234
|
buildMessages() {
|
|
@@ -9100,13 +9341,14 @@ var init_openai_agent = __esm({
|
|
|
9100
9341
|
"use strict";
|
|
9101
9342
|
init_config();
|
|
9102
9343
|
init_agent_loop();
|
|
9344
|
+
init_openai_chat_stream();
|
|
9103
9345
|
OpenAIAgent = class extends AgentLoop {
|
|
9104
9346
|
config;
|
|
9105
9347
|
constructor(config2) {
|
|
9106
9348
|
super(config2.tools);
|
|
9107
9349
|
this.config = config2;
|
|
9108
9350
|
}
|
|
9109
|
-
async callModel(signal) {
|
|
9351
|
+
async callModel(signal, onDelta) {
|
|
9110
9352
|
const url = this.config.endpoint.replace(/\/+$/, "") + "/chat/completions";
|
|
9111
9353
|
const startTime = Date.now();
|
|
9112
9354
|
const messages2 = this.buildMessages();
|
|
@@ -9114,37 +9356,26 @@ var init_openai_agent = __esm({
|
|
|
9114
9356
|
const body = {
|
|
9115
9357
|
model: this.config.modelId,
|
|
9116
9358
|
messages: messages2,
|
|
9117
|
-
stream: false,
|
|
9118
9359
|
max_tokens: this.config.maxTokens ?? resolveMaxOutputTokens(this.config.modelId),
|
|
9119
9360
|
...tools.length > 0 ? { tools } : {}
|
|
9120
9361
|
};
|
|
9121
9362
|
if (this.config.thinking?.enabled && this.config.thinking.effort) {
|
|
9122
9363
|
body.reasoning_effort = this.config.thinking.effort;
|
|
9123
9364
|
}
|
|
9124
|
-
const
|
|
9125
|
-
|
|
9126
|
-
headers: {
|
|
9127
|
-
|
|
9128
|
-
|
|
9129
|
-
|
|
9130
|
-
|
|
9131
|
-
signal
|
|
9365
|
+
const { message: msg, finishReason, usage, ttftMs } = await fetchChatCompletionStream({
|
|
9366
|
+
url,
|
|
9367
|
+
headers: { "Authorization": `Bearer ${this.config.apiKey}` },
|
|
9368
|
+
body,
|
|
9369
|
+
signal,
|
|
9370
|
+
onDelta,
|
|
9371
|
+
tag: "OpenAIAgent"
|
|
9132
9372
|
});
|
|
9133
|
-
if (!response.ok) {
|
|
9134
|
-
const errText2 = await response.text().catch(() => "");
|
|
9135
|
-
throw new Error(`[OpenAIAgent] API error ${response.status}: ${errText2}`);
|
|
9136
|
-
}
|
|
9137
|
-
const data = await response.json();
|
|
9138
|
-
const choices = data.choices;
|
|
9139
|
-
const choice = choices?.[0];
|
|
9140
|
-
const msg = choice?.message;
|
|
9141
|
-
const finishReason = choice?.finish_reason;
|
|
9142
9373
|
let text6 = "";
|
|
9143
9374
|
let thinking = "";
|
|
9144
9375
|
const toolCalls = [];
|
|
9145
9376
|
const assistantBlocks = [];
|
|
9146
9377
|
if (msg) {
|
|
9147
|
-
const reasoning = msg.reasoning_content
|
|
9378
|
+
const reasoning = msg.reasoning_content;
|
|
9148
9379
|
if (reasoning && typeof reasoning === "string") {
|
|
9149
9380
|
thinking = reasoning;
|
|
9150
9381
|
}
|
|
@@ -9171,7 +9402,6 @@ var init_openai_agent = __esm({
|
|
|
9171
9402
|
assistantBlocks.push({ type: "text", text: text6 });
|
|
9172
9403
|
}
|
|
9173
9404
|
const stopReason = finishReason === "tool_calls" ? "tool_use" : finishReason === "length" ? "max_tokens" : "end_turn";
|
|
9174
|
-
const usage = data.usage;
|
|
9175
9405
|
const promptTokens = usage?.prompt_tokens ?? 0;
|
|
9176
9406
|
const completionTokens = usage?.completion_tokens ?? 0;
|
|
9177
9407
|
const promptDetails = usage?.prompt_tokens_details;
|
|
@@ -9188,7 +9418,8 @@ var init_openai_agent = __esm({
|
|
|
9188
9418
|
totalTokens: promptTokens - cachedTokens + completionTokens,
|
|
9189
9419
|
...cachedTokens ? { cacheReadInputTokens: cachedTokens } : {}
|
|
9190
9420
|
},
|
|
9191
|
-
durationMs: Date.now() - startTime
|
|
9421
|
+
durationMs: Date.now() - startTime,
|
|
9422
|
+
ttftMs
|
|
9192
9423
|
};
|
|
9193
9424
|
}
|
|
9194
9425
|
buildMessages() {
|
|
@@ -9282,6 +9513,46 @@ var init_openai_agent = __esm({
|
|
|
9282
9513
|
import { SignatureV4 } from "@smithy/signature-v4";
|
|
9283
9514
|
import { Sha256 } from "@aws-crypto/sha256-js";
|
|
9284
9515
|
import { defaultProvider } from "@aws-sdk/credential-provider-node";
|
|
9516
|
+
async function readResponsesStream(res, onDelta, startTime, signal) {
|
|
9517
|
+
if (!res.body)
|
|
9518
|
+
throw new Error("[MantleAgent] empty response body");
|
|
9519
|
+
let final;
|
|
9520
|
+
let firstDeltaAt;
|
|
9521
|
+
for await (const ev of readSseJson(res.body)) {
|
|
9522
|
+
switch (ev.type) {
|
|
9523
|
+
case "response.output_text.delta":
|
|
9524
|
+
firstDeltaAt ??= Date.now();
|
|
9525
|
+
if (ev.delta)
|
|
9526
|
+
onDelta?.({ type: "text_delta", text: ev.delta });
|
|
9527
|
+
break;
|
|
9528
|
+
case "response.reasoning_summary_text.delta":
|
|
9529
|
+
firstDeltaAt ??= Date.now();
|
|
9530
|
+
if (ev.delta)
|
|
9531
|
+
onDelta?.({ type: "thinking_delta", text: ev.delta });
|
|
9532
|
+
break;
|
|
9533
|
+
case "response.function_call_arguments.delta":
|
|
9534
|
+
firstDeltaAt ??= Date.now();
|
|
9535
|
+
break;
|
|
9536
|
+
case "response.completed":
|
|
9537
|
+
case "response.incomplete":
|
|
9538
|
+
final = ev.response;
|
|
9539
|
+
break;
|
|
9540
|
+
// No HTTP status exists mid-stream (the 200 already went out), so
|
|
9541
|
+
// classifyModelError falls back to its keyword checks on these messages.
|
|
9542
|
+
case "response.failed": {
|
|
9543
|
+
const failed = ev.response;
|
|
9544
|
+
throw new Error(`[MantleAgent] API error in stream: ${JSON.stringify(failed?.error ?? ev.response).slice(0, 300)}`);
|
|
9545
|
+
}
|
|
9546
|
+
case "error":
|
|
9547
|
+
throw new Error(`[MantleAgent] API error in stream: ${ev.code ?? "?"}: ${ev.message ?? ""}`);
|
|
9548
|
+
}
|
|
9549
|
+
}
|
|
9550
|
+
if (signal?.aborted)
|
|
9551
|
+
throw new DOMException("Model call aborted", "AbortError");
|
|
9552
|
+
if (!final)
|
|
9553
|
+
throw new Error("MantleEmptyResponse: stream ended without response.completed; transient, retrying");
|
|
9554
|
+
return { data: final, ttftMs: firstDeltaAt !== void 0 ? firstDeltaAt - startTime : void 0 };
|
|
9555
|
+
}
|
|
9285
9556
|
function extractRegionFromHost(hostname) {
|
|
9286
9557
|
const m = hostname.match(/^bedrock-(?:mantle|runtime)\.([a-z0-9-]+)\./);
|
|
9287
9558
|
return m?.[1] ?? "us-east-1";
|
|
@@ -9299,13 +9570,14 @@ var init_mantle_agent = __esm({
|
|
|
9299
9570
|
"use strict";
|
|
9300
9571
|
init_config();
|
|
9301
9572
|
init_agent_loop();
|
|
9573
|
+
init_sse();
|
|
9302
9574
|
MantleAgent = class extends AgentLoop {
|
|
9303
9575
|
config;
|
|
9304
9576
|
constructor(config2) {
|
|
9305
9577
|
super(config2.tools);
|
|
9306
9578
|
this.config = config2;
|
|
9307
9579
|
}
|
|
9308
|
-
async callModel(signal) {
|
|
9580
|
+
async callModel(signal, onDelta) {
|
|
9309
9581
|
const url = this.config.endpoint.replace(/\/+$/, "") + "/responses";
|
|
9310
9582
|
const startTime = Date.now();
|
|
9311
9583
|
const input = this.buildInput();
|
|
@@ -9313,7 +9585,7 @@ var init_mantle_agent = __esm({
|
|
|
9313
9585
|
const body = {
|
|
9314
9586
|
model: this.config.modelId,
|
|
9315
9587
|
input,
|
|
9316
|
-
stream:
|
|
9588
|
+
stream: true,
|
|
9317
9589
|
max_output_tokens: this.config.maxTokens ?? resolveMaxOutputTokens(this.config.modelId),
|
|
9318
9590
|
text: { verbosity: this.config.verbosity ?? "low" },
|
|
9319
9591
|
...tools.length > 0 ? { tools } : {}
|
|
@@ -9334,7 +9606,7 @@ var init_mantle_agent = __esm({
|
|
|
9334
9606
|
const errText2 = await response.text().catch(() => "");
|
|
9335
9607
|
throw new Error(`[MantleAgent] API error ${response.status}: ${errText2}`);
|
|
9336
9608
|
}
|
|
9337
|
-
const data = await response
|
|
9609
|
+
const { data, ttftMs } = await readResponsesStream(response, onDelta, startTime, signal);
|
|
9338
9610
|
const output = data.output ?? [];
|
|
9339
9611
|
let text6 = "";
|
|
9340
9612
|
let thinking = "";
|
|
@@ -9396,7 +9668,8 @@ var init_mantle_agent = __esm({
|
|
|
9396
9668
|
totalTokens: inputTokens - cachedTokens + outputTokens,
|
|
9397
9669
|
...cachedTokens ? { cacheReadInputTokens: cachedTokens } : {}
|
|
9398
9670
|
},
|
|
9399
|
-
durationMs: Date.now() - startTime
|
|
9671
|
+
durationMs: Date.now() - startTime,
|
|
9672
|
+
ttftMs
|
|
9400
9673
|
};
|
|
9401
9674
|
}
|
|
9402
9675
|
/** Build the Responses API `input` array from system prompt + message log. */
|
|
@@ -9523,73 +9796,25 @@ var init_anthropic_agent = __esm({
|
|
|
9523
9796
|
"use strict";
|
|
9524
9797
|
init_config();
|
|
9525
9798
|
init_agent_loop();
|
|
9799
|
+
init_anthropic_stream();
|
|
9526
9800
|
AnthropicAgent = class extends AgentLoop {
|
|
9527
9801
|
config;
|
|
9528
9802
|
constructor(config2) {
|
|
9529
9803
|
super(config2.tools);
|
|
9530
9804
|
this.config = config2;
|
|
9531
9805
|
}
|
|
9532
|
-
async callModel(signal) {
|
|
9533
|
-
|
|
9534
|
-
|
|
9535
|
-
const startTime = Date.now();
|
|
9536
|
-
const res = await fetch(url, {
|
|
9537
|
-
method: "POST",
|
|
9538
|
-
signal,
|
|
9806
|
+
async callModel(signal, onDelta) {
|
|
9807
|
+
return fetchAnthropicStream({
|
|
9808
|
+
url: `${this.config.endpoint.replace(/\/$/, "")}/v1/messages`,
|
|
9539
9809
|
headers: {
|
|
9540
9810
|
"x-api-key": this.config.apiKey,
|
|
9541
|
-
"anthropic-version": "2023-06-01"
|
|
9542
|
-
"content-type": "application/json"
|
|
9811
|
+
"anthropic-version": "2023-06-01"
|
|
9543
9812
|
},
|
|
9544
|
-
body:
|
|
9813
|
+
body: this.buildRequestBody(),
|
|
9814
|
+
signal,
|
|
9815
|
+
onDelta,
|
|
9816
|
+
tag: "anthropic"
|
|
9545
9817
|
});
|
|
9546
|
-
const durationMs = Date.now() - startTime;
|
|
9547
|
-
const raw = await res.text();
|
|
9548
|
-
let msg;
|
|
9549
|
-
try {
|
|
9550
|
-
msg = JSON.parse(raw);
|
|
9551
|
-
} catch {
|
|
9552
|
-
throw new Error(`[anthropic] non-JSON response (status=${res.status}): ${raw.slice(0, 200)}`);
|
|
9553
|
-
}
|
|
9554
|
-
if (!res.ok || msg.error) {
|
|
9555
|
-
throw new Error(`[anthropic] ${res.status} ${msg.error?.type ?? "?"}: ${msg.error?.message ?? raw.slice(0, 200)}`);
|
|
9556
|
-
}
|
|
9557
|
-
let text6 = "";
|
|
9558
|
-
let thinking = "";
|
|
9559
|
-
const toolCalls = [];
|
|
9560
|
-
const assistantBlocks = [];
|
|
9561
|
-
for (const block of msg.content ?? []) {
|
|
9562
|
-
if (block.type === "text" && block.text) {
|
|
9563
|
-
text6 += block.text;
|
|
9564
|
-
assistantBlocks.push({ type: "text", text: block.text });
|
|
9565
|
-
} else if (block.type === "thinking" && block.thinking) {
|
|
9566
|
-
thinking += block.thinking;
|
|
9567
|
-
} else if (block.type === "tool_use") {
|
|
9568
|
-
toolCalls.push({ id: block.id, name: block.name, input: block.input ?? {} });
|
|
9569
|
-
assistantBlocks.push({ type: "tool_use", id: block.id, name: block.name, input: block.input ?? {} });
|
|
9570
|
-
}
|
|
9571
|
-
}
|
|
9572
|
-
const u = msg.usage;
|
|
9573
|
-
const inputTokens = u?.input_tokens ?? 0;
|
|
9574
|
-
const outputTokens = u?.output_tokens ?? 0;
|
|
9575
|
-
const cacheReadTokens = u?.cache_read_input_tokens ?? 0;
|
|
9576
|
-
const cacheWriteTokens = u?.cache_creation_input_tokens ?? 0;
|
|
9577
|
-
return {
|
|
9578
|
-
assistantBlocks,
|
|
9579
|
-
stopReason: msg.stop_reason ?? "end_turn",
|
|
9580
|
-
...msg.stop_details ? { stopDetails: { category: msg.stop_details.category ?? null, explanation: msg.stop_details.explanation ?? null } } : {},
|
|
9581
|
-
text: text6,
|
|
9582
|
-
thinking,
|
|
9583
|
-
toolCalls,
|
|
9584
|
-
usage: {
|
|
9585
|
-
inputTokens,
|
|
9586
|
-
outputTokens,
|
|
9587
|
-
totalTokens: inputTokens + outputTokens,
|
|
9588
|
-
...cacheReadTokens ? { cacheReadInputTokens: cacheReadTokens } : {},
|
|
9589
|
-
...cacheWriteTokens ? { cacheWriteInputTokens: cacheWriteTokens } : {}
|
|
9590
|
-
},
|
|
9591
|
-
durationMs
|
|
9592
|
-
};
|
|
9593
9818
|
}
|
|
9594
9819
|
buildRequestBody() {
|
|
9595
9820
|
const caching = this.config.promptCaching;
|
|
@@ -11404,55 +11629,6 @@ var init_workspace_tools = __esm({
|
|
|
11404
11629
|
}
|
|
11405
11630
|
});
|
|
11406
11631
|
|
|
11407
|
-
// ../server/dist/tools/draft-tool.js
|
|
11408
|
-
function createDraftTool() {
|
|
11409
|
-
let drafts = 0;
|
|
11410
|
-
const tool = {
|
|
11411
|
-
name: "draft",
|
|
11412
|
-
description: "Submit a draft answer for self-review. Returns a checklist that critiques the draft so you can improve it before sending the final answer.",
|
|
11413
|
-
inputSchema: {
|
|
11414
|
-
type: "object",
|
|
11415
|
-
properties: {
|
|
11416
|
-
content: {
|
|
11417
|
-
type: "string",
|
|
11418
|
-
description: "The complete draft answer. Required and must be non-empty."
|
|
11419
|
-
}
|
|
11420
|
-
},
|
|
11421
|
-
required: ["content"]
|
|
11422
|
-
},
|
|
11423
|
-
callback: (input) => {
|
|
11424
|
-
const content = input?.content;
|
|
11425
|
-
if (typeof content !== "string" || content.trim().length === 0) {
|
|
11426
|
-
return `${TOOL_ERROR_MARKER}
|
|
11427
|
-
Error: draft requires non-empty \`content\` \u2014 put your full draft answer there.`;
|
|
11428
|
-
}
|
|
11429
|
-
drafts++;
|
|
11430
|
-
if (drafts > MAX_DRAFTS) {
|
|
11431
|
-
return `You have drafted ${MAX_DRAFTS} times this turn \u2014 that is the limit. Write your final answer now, directly, without calling draft again.`;
|
|
11432
|
-
}
|
|
11433
|
-
return REVIEW_CHECKLIST;
|
|
11434
|
-
}
|
|
11435
|
-
};
|
|
11436
|
-
return { tool, reset: () => {
|
|
11437
|
-
drafts = 0;
|
|
11438
|
-
} };
|
|
11439
|
-
}
|
|
11440
|
-
var MAX_DRAFTS, REVIEW_CHECKLIST;
|
|
11441
|
-
var init_draft_tool = __esm({
|
|
11442
|
-
"../server/dist/tools/draft-tool.js"() {
|
|
11443
|
-
"use strict";
|
|
11444
|
-
init_agent_loop();
|
|
11445
|
-
MAX_DRAFTS = 3;
|
|
11446
|
-
REVIEW_CHECKLIST = `You are now a hostile reviewer whose job is to find everything wrong with the draft above. Do not defend it \u2014 attack it.
|
|
11447
|
-
|
|
11448
|
-
1. Facts: list every factual claim in the draft, each tagged [verified-with-tool] / [from-memory] / [guess]. For anything not [verified-with-tool], verify it NOW with file_read / grep / search, or mark it "unconfirmed" in the answer. Do not assert from memory.
|
|
11449
|
-
2. Delivery: is it friendly? Does it answer the question simply and directly, or is it dancing around? If it uses a rhetorical question, is that question actually necessary?
|
|
11450
|
-
3. Gaps: is anything missing? If it can be looked up, look it up; if it can't, say "unknown" \u2014 don't paper over it.
|
|
11451
|
-
|
|
11452
|
-
If the draft clears all three, write the final answer now. If not, fix it (gather more via tools, or call draft again to rewrite).`;
|
|
11453
|
-
}
|
|
11454
|
-
});
|
|
11455
|
-
|
|
11456
11632
|
// ../server/dist/prompts/system-prompts.js
|
|
11457
11633
|
import fs23 from "node:fs/promises";
|
|
11458
11634
|
import path27 from "node:path";
|
|
@@ -11553,7 +11729,6 @@ var init_session_agent_builder = __esm({
|
|
|
11553
11729
|
"use strict";
|
|
11554
11730
|
init_model_runtime();
|
|
11555
11731
|
init_workspace_tools();
|
|
11556
|
-
init_draft_tool();
|
|
11557
11732
|
init_system_prompts();
|
|
11558
11733
|
init_md_loader();
|
|
11559
11734
|
init_config();
|
|
@@ -11576,7 +11751,7 @@ var init_session_agent_builder = __esm({
|
|
|
11576
11751
|
const yamlConfig = await loadAgentYaml(agentId, this.host.workspaceRoot);
|
|
11577
11752
|
const isRoot = !parentId;
|
|
11578
11753
|
const { modelId, endpoint, providerId } = this.validateAgentModelConfig(agentId, yamlConfig);
|
|
11579
|
-
const { workspaceTools, sessionTools, allowedNamespaces
|
|
11754
|
+
const { workspaceTools, sessionTools, allowedNamespaces } = this.resolveBaseToolSet({
|
|
11580
11755
|
agentId,
|
|
11581
11756
|
sessionId,
|
|
11582
11757
|
modelId,
|
|
@@ -11615,7 +11790,7 @@ var init_session_agent_builder = __esm({
|
|
|
11615
11790
|
systemPrompts,
|
|
11616
11791
|
allToolNames
|
|
11617
11792
|
});
|
|
11618
|
-
return { agent, yamlConfig, contextConfig, modelId, systemPrompt, thinkingEffort, meta
|
|
11793
|
+
return { agent, yamlConfig, contextConfig, modelId, systemPrompt, thinkingEffort, meta };
|
|
11619
11794
|
}
|
|
11620
11795
|
/** Throw with a clear message if agent.yaml is missing the model triple.
|
|
11621
11796
|
* Includes the resolved yaml path and distinguishes "file unreadable"
|
|
@@ -11659,16 +11834,10 @@ var init_session_agent_builder = __esm({
|
|
|
11659
11834
|
const delegates = canDelegate(yamlConfig);
|
|
11660
11835
|
const sessionTools = delegates ? this.host.createSessionTools(sessionId) : agentId === GOAL_AGENT_ID ? this.host.createGoalTools(sessionId) : [];
|
|
11661
11836
|
sessionTools.push(this.host.createContinueTaskTool(sessionId));
|
|
11662
|
-
let draftReset = null;
|
|
11663
|
-
if (nameSet.has("draft")) {
|
|
11664
|
-
const { tool, reset } = createDraftTool();
|
|
11665
|
-
sessionTools.push(tool);
|
|
11666
|
-
draftReset = reset;
|
|
11667
|
-
}
|
|
11668
11837
|
if (nameSet.has("relay_send") && accessLevel === null) {
|
|
11669
11838
|
sessionTools.push(...this.host.createRelayTools(sessionId));
|
|
11670
11839
|
}
|
|
11671
|
-
return { workspaceTools, sessionTools, allowedNamespaces
|
|
11840
|
+
return { workspaceTools, sessionTools, allowedNamespaces };
|
|
11672
11841
|
}
|
|
11673
11842
|
/**
|
|
11674
11843
|
* Compose the agent's system prompt from the MD layer cake +
|
|
@@ -12141,7 +12310,7 @@ async function initObservability() {
|
|
|
12141
12310
|
return true;
|
|
12142
12311
|
}
|
|
12143
12312
|
const { registerSdk: registerSdk2 } = await Promise.resolve().then(() => (init_otel_sdk(), otel_sdk_exports));
|
|
12144
|
-
providers = registerSdk2(process.env.OTEL_SERVICE_NAME, "1.
|
|
12313
|
+
providers = registerSdk2(process.env.OTEL_SERVICE_NAME, "1.5.0-alpha");
|
|
12145
12314
|
console.log(`[Observability] OTLP export \u2192 ${process.env.OTEL_EXPORTER_OTLP_ENDPOINT} (service.name=${process.env.OTEL_SERVICE_NAME}, capture_content=${config.observability.captureContent})`);
|
|
12146
12315
|
return true;
|
|
12147
12316
|
}
|
|
@@ -12182,6 +12351,14 @@ function instruments() {
|
|
|
12182
12351
|
return {
|
|
12183
12352
|
tokenUsage: meter.createHistogram("gen_ai.client.token.usage", { unit: "{token}", description: "Tokens per model call, split by gen_ai.token.type" }),
|
|
12184
12353
|
operationDuration: meter.createHistogram("gen_ai.client.operation.duration", { unit: "s", description: "Model call latency" }),
|
|
12354
|
+
// semconv's only TTFT metric (named `server.` though it's measured client-side
|
|
12355
|
+
// here: first streamed delta, see ModelCallResult.ttftMs). Buckets are the
|
|
12356
|
+
// spec's recommended set — the SDK default is ms-scale, useless for seconds.
|
|
12357
|
+
timeToFirstToken: meter.createHistogram("gen_ai.server.time_to_first_token", {
|
|
12358
|
+
unit: "s",
|
|
12359
|
+
description: "Time to first streamed token per model call",
|
|
12360
|
+
advice: { explicitBucketBoundaries: [1e-3, 5e-3, 0.01, 0.02, 0.04, 0.06, 0.08, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10] }
|
|
12361
|
+
}),
|
|
12185
12362
|
toolDuration: meter.createHistogram("halo.tool.duration", { unit: "s", description: "Tool execution latency" }),
|
|
12186
12363
|
turnDuration: meter.createHistogram("halo.turn.duration", { unit: "s", description: "Agent turn latency (invoke_agent)" }),
|
|
12187
12364
|
modelRetries: meter.createCounter("halo.model.retries", { description: "Model-call retries by kind" })
|
|
@@ -12310,7 +12487,8 @@ function recordModelCall(session, turn, event) {
|
|
|
12310
12487
|
"gen_ai.usage.input_tokens": event.usage?.inputTokens ?? 0,
|
|
12311
12488
|
"gen_ai.usage.output_tokens": event.usage?.outputTokens ?? 0,
|
|
12312
12489
|
"gen_ai.response.finish_reasons": [turn.cycleToolCalls.length > 0 ? "tool_use" : "end_turn"],
|
|
12313
|
-
"session.id": session.id
|
|
12490
|
+
"session.id": session.id,
|
|
12491
|
+
...event.ttftMs != null ? { "halo.ttft_ms": event.ttftMs } : {}
|
|
12314
12492
|
}
|
|
12315
12493
|
}, turn.ctx);
|
|
12316
12494
|
if (captureContent()) {
|
|
@@ -12334,6 +12512,8 @@ function recordModelCall(session, turn, event) {
|
|
|
12334
12512
|
m.tokenUsage.record(event.usage?.inputTokens ?? 0, { ...modelAttr, "gen_ai.token.type": "input" });
|
|
12335
12513
|
m.tokenUsage.record(event.usage?.outputTokens ?? 0, { ...modelAttr, "gen_ai.token.type": "output" });
|
|
12336
12514
|
m.operationDuration.record(durationMs / 1e3, { ...modelAttr, "gen_ai.operation.name": "chat" });
|
|
12515
|
+
if (event.ttftMs != null)
|
|
12516
|
+
m.timeToFirstToken.record(event.ttftMs / 1e3, modelAttr);
|
|
12337
12517
|
}
|
|
12338
12518
|
function recordToolCall(session, turn, event) {
|
|
12339
12519
|
const id = event.toolUseId ?? "";
|
|
@@ -12428,6 +12608,15 @@ function simpleHash(str) {
|
|
|
12428
12608
|
function sleep(ms) {
|
|
12429
12609
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
12430
12610
|
}
|
|
12611
|
+
function formatAge(ms) {
|
|
12612
|
+
const s = Math.max(0, Math.floor(ms / 1e3));
|
|
12613
|
+
if (s < 60)
|
|
12614
|
+
return `${s}s`;
|
|
12615
|
+
const m = Math.floor(s / 60);
|
|
12616
|
+
if (m < 60)
|
|
12617
|
+
return `${m}m`;
|
|
12618
|
+
return `${Math.floor(m / 60)}h${String(m % 60).padStart(2, "0")}m`;
|
|
12619
|
+
}
|
|
12431
12620
|
function abortReason(reason) {
|
|
12432
12621
|
return new DOMException(reason, "AbortError");
|
|
12433
12622
|
}
|
|
@@ -12484,8 +12673,8 @@ function stripImageMarkers(s) {
|
|
|
12484
12673
|
}
|
|
12485
12674
|
function generateSessionId() {
|
|
12486
12675
|
const ts = Date.now().toString(36);
|
|
12487
|
-
const
|
|
12488
|
-
return `sid_${ts}_${
|
|
12676
|
+
const rand2 = Math.random().toString(36).slice(2, 8);
|
|
12677
|
+
return `sid_${ts}_${rand2}`;
|
|
12489
12678
|
}
|
|
12490
12679
|
var CONTINUE_TASK_KICK, SessionManager;
|
|
12491
12680
|
var init_session_manager = __esm({
|
|
@@ -12760,7 +12949,7 @@ var init_session_manager = __esm({
|
|
|
12760
12949
|
}
|
|
12761
12950
|
// ── Session lifecycle ───────────────────────────────────────────────
|
|
12762
12951
|
/** Create default AgentSession fields */
|
|
12763
|
-
createAgentSession(id, parentId, agentId, agentName, agent, description, contextConfig, modelId, systemPrompt, thinkingEffort = "off", workingDir = null, accessLevel = null, meta, imageOverride
|
|
12952
|
+
createAgentSession(id, parentId, agentId, agentName, agent, description, contextConfig, modelId, systemPrompt, thinkingEffort = "off", workingDir = null, accessLevel = null, meta, imageOverride) {
|
|
12764
12953
|
return {
|
|
12765
12954
|
id,
|
|
12766
12955
|
parentId,
|
|
@@ -12768,9 +12957,9 @@ var init_session_manager = __esm({
|
|
|
12768
12957
|
agentName,
|
|
12769
12958
|
agent,
|
|
12770
12959
|
description,
|
|
12771
|
-
draftReset,
|
|
12772
12960
|
output: "",
|
|
12773
12961
|
lastActivityAt: null,
|
|
12962
|
+
streamedThisCall: false,
|
|
12774
12963
|
finalOutput: "",
|
|
12775
12964
|
turnError: null,
|
|
12776
12965
|
promise: null,
|
|
@@ -12834,14 +13023,14 @@ var init_session_manager = __esm({
|
|
|
12834
13023
|
throw new Error(`Session ${sessionId} is archived and cannot be resumed`);
|
|
12835
13024
|
const restoredAccessLevel = meta.accessLevel === "readonly" ? "readonly" : meta.accessLevel === "workspace" ? "workspace" : null;
|
|
12836
13025
|
const restoredWorkingDir = meta.workingDir ? path30.resolve(this.workspaceRoot, meta.workingDir) : void 0;
|
|
12837
|
-
const { agent, yamlConfig: resumedYaml, contextConfig, modelId, systemPrompt, thinkingEffort, meta: agentMeta
|
|
13026
|
+
const { agent, yamlConfig: resumedYaml, contextConfig, modelId, systemPrompt, thinkingEffort, meta: agentMeta } = await this.buildAgentInstance(meta.agentId, sessionId, meta.parentId, restoredWorkingDir, restoredAccessLevel);
|
|
12838
13027
|
const savedMessages = this.loadAgentState(sessionId, meta.agentId);
|
|
12839
13028
|
if (savedMessages.length > 0) {
|
|
12840
13029
|
agent.messages = savedMessages;
|
|
12841
13030
|
}
|
|
12842
13031
|
const resumedImageOverride = resumedYaml?.model?.image;
|
|
12843
13032
|
const resumedAgentName = meta.agentName ?? resumedYaml?.name ?? meta.agentId;
|
|
12844
|
-
const session = this.createAgentSession(sessionId, meta.parentId, meta.agentId, resumedAgentName, agent, meta.description ?? "", contextConfig, modelId, systemPrompt, thinkingEffort, restoredWorkingDir ?? null, restoredAccessLevel, agentMeta, resumedImageOverride
|
|
13033
|
+
const session = this.createAgentSession(sessionId, meta.parentId, meta.agentId, resumedAgentName, agent, meta.description ?? "", contextConfig, modelId, systemPrompt, thinkingEffort, restoredWorkingDir ?? null, restoredAccessLevel, agentMeta, resumedImageOverride);
|
|
12845
13034
|
this.sessions.set(sessionId, session);
|
|
12846
13035
|
const rootId = this.findRootSessionId(sessionId);
|
|
12847
13036
|
const state = this.uiStore.ensureUIState(rootId);
|
|
@@ -12889,10 +13078,12 @@ var init_session_manager = __esm({
|
|
|
12889
13078
|
* @param explicitId - Optional explicit session ID (for unifying frontend sessionId with agent sessionId)
|
|
12890
13079
|
*/
|
|
12891
13080
|
async createSession(agentId, parentId, description, agentName, explicitId, workingDir, accessLevel = null, title) {
|
|
13081
|
+
if (agentId === GOAL_AGENT_ID && !config.goalModeEnabled)
|
|
13082
|
+
throw new Error("Goal mode is disabled.");
|
|
12892
13083
|
const segment = generateSessionId();
|
|
12893
13084
|
const sessionId = explicitId ?? (parentId ? `${parentId}>${segment}` : segment);
|
|
12894
13085
|
const now = Date.now();
|
|
12895
|
-
const { agent, yamlConfig: createdYaml, contextConfig, modelId, systemPrompt, thinkingEffort, meta
|
|
13086
|
+
const { agent, yamlConfig: createdYaml, contextConfig, modelId, systemPrompt, thinkingEffort, meta } = await this.buildAgentInstance(agentId, sessionId, parentId, workingDir ?? void 0, accessLevel);
|
|
12896
13087
|
const resolvedAgentName = agentName ?? createdYaml?.name ?? agentId;
|
|
12897
13088
|
this.db.insert(agentSessions).values({
|
|
12898
13089
|
id: sessionId,
|
|
@@ -12915,7 +13106,7 @@ var init_session_manager = __esm({
|
|
|
12915
13106
|
}
|
|
12916
13107
|
}
|
|
12917
13108
|
const createdImageOverride = createdYaml?.model?.image;
|
|
12918
|
-
const session = this.createAgentSession(sessionId, parentId, agentId, resolvedAgentName, agent, description, contextConfig, modelId, systemPrompt, thinkingEffort, workingDir ?? null, accessLevel, meta, createdImageOverride
|
|
13109
|
+
const session = this.createAgentSession(sessionId, parentId, agentId, resolvedAgentName, agent, description, contextConfig, modelId, systemPrompt, thinkingEffort, workingDir ?? null, accessLevel, meta, createdImageOverride);
|
|
12919
13110
|
this.sessions.set(sessionId, session);
|
|
12920
13111
|
this.emitEvent(sessionId, { type: "context", agentId, agentName: resolvedAgentName, systemPrompt, taskId: parentId ? sessionId : void 0 });
|
|
12921
13112
|
console.debug(`[SessionManager] Created session ${sessionId} for agent "${agentId}" (parent: ${parentId ?? "none"}, workingDir: ${workingDir ?? "project root"})`);
|
|
@@ -13157,16 +13348,28 @@ var init_session_manager = __esm({
|
|
|
13157
13348
|
const agentId = session.agentId;
|
|
13158
13349
|
const taskId = session.parentId ? session.id : void 0;
|
|
13159
13350
|
switch (event.type) {
|
|
13351
|
+
// Streaming chunks: UI-only. They never touch output / finalOutput —
|
|
13352
|
+
// the whole `text` event below still carries the reply for those.
|
|
13353
|
+
case "text_delta": {
|
|
13354
|
+
session.streamedThisCall = true;
|
|
13355
|
+
this.emitEvent(session.id, { type: "stream_delta", text: event.text, agentName, agentId, taskId });
|
|
13356
|
+
break;
|
|
13357
|
+
}
|
|
13358
|
+
case "thinking_delta": {
|
|
13359
|
+
session.streamedThisCall = true;
|
|
13360
|
+
this.emitEvent(session.id, { type: "thinking_delta", text: event.text, agentName, agentId, taskId });
|
|
13361
|
+
break;
|
|
13362
|
+
}
|
|
13160
13363
|
case "text": {
|
|
13161
13364
|
session.output += event.text ?? "";
|
|
13162
13365
|
session.lastActivityAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
13163
13366
|
if (event.final)
|
|
13164
13367
|
session.finalOutput += event.text ?? "";
|
|
13165
|
-
this.emitEvent(session.id, { type: "stream", text: event.text, final: event.final, agentName, agentId, taskId });
|
|
13368
|
+
this.emitEvent(session.id, { type: "stream", text: event.text, final: event.final, streamed: session.streamedThisCall, agentName, agentId, taskId });
|
|
13166
13369
|
break;
|
|
13167
13370
|
}
|
|
13168
13371
|
case "thinking": {
|
|
13169
|
-
this.emitEvent(session.id, { type: "thinking", text: event.text, agentName, agentId, taskId });
|
|
13372
|
+
this.emitEvent(session.id, { type: "thinking", text: event.text, streamed: session.streamedThisCall, agentName, agentId, taskId });
|
|
13170
13373
|
break;
|
|
13171
13374
|
}
|
|
13172
13375
|
case "tool_call": {
|
|
@@ -13199,9 +13402,11 @@ var init_session_manager = __esm({
|
|
|
13199
13402
|
agentId,
|
|
13200
13403
|
taskId,
|
|
13201
13404
|
e2eMs: event.durationMs,
|
|
13405
|
+
ttftMs: event.ttftMs,
|
|
13202
13406
|
thinkingEffort: session.thinkingEffort
|
|
13203
13407
|
});
|
|
13204
13408
|
session.lastContextTokens = (u.totalTokens ?? 0) + (u.cacheReadInputTokens ?? 0) + (u.cacheWriteInputTokens ?? 0);
|
|
13409
|
+
session.streamedThisCall = false;
|
|
13205
13410
|
break;
|
|
13206
13411
|
}
|
|
13207
13412
|
case "stop": {
|
|
@@ -13245,7 +13450,6 @@ var init_session_manager = __esm({
|
|
|
13245
13450
|
session.abortController = new AbortController();
|
|
13246
13451
|
const signal = session.abortController.signal;
|
|
13247
13452
|
resultText = "";
|
|
13248
|
-
session.draftReset?.();
|
|
13249
13453
|
const last = session.agent.messages[session.agent.messages.length - 1];
|
|
13250
13454
|
const input = attempt === 0 || last?.role !== "user" ? message : [];
|
|
13251
13455
|
try {
|
|
@@ -13279,6 +13483,7 @@ var init_session_manager = __esm({
|
|
|
13279
13483
|
break;
|
|
13280
13484
|
} catch (err) {
|
|
13281
13485
|
session.abortController = null;
|
|
13486
|
+
session.streamedThisCall = false;
|
|
13282
13487
|
const { kind, msg, errName, httpStatus } = classifyModelError(err);
|
|
13283
13488
|
if (signal.aborted || errName === "AbortError") {
|
|
13284
13489
|
if (session.interruptRequested) {
|
|
@@ -13410,6 +13615,9 @@ var init_session_manager = __esm({
|
|
|
13410
13615
|
session.selfKick = false;
|
|
13411
13616
|
result = await this.runAgentTurn(session, message);
|
|
13412
13617
|
console.debug(`[SessionManager] runSession ${sessionId} first turn done \u2014 result: ${result.slice(0, 150)}`);
|
|
13618
|
+
if (session.messageQueue.length > 0 && session.finalOutput !== "" && !session.turnError) {
|
|
13619
|
+
this.sendOpeningInterim(session);
|
|
13620
|
+
}
|
|
13413
13621
|
}
|
|
13414
13622
|
if (session.messageQueue.length > 0) {
|
|
13415
13623
|
await this.drainQueue(session);
|
|
@@ -13508,11 +13716,17 @@ ${result}`;
|
|
|
13508
13716
|
* wrap-up (a fresh task) rather than being a leftover from the original batch.
|
|
13509
13717
|
*
|
|
13510
13718
|
* Mid-tier parents are intentionally excluded (parentId !== null): their
|
|
13511
|
-
* tryReportToParent bubble-up already gates them on a fully-drained subtree.
|
|
13719
|
+
* tryReportToParent bubble-up already gates them on a fully-drained subtree.
|
|
13720
|
+
*
|
|
13721
|
+
* Returns two renderings of the same rows: `llm` (appended to the model
|
|
13722
|
+
* input, ISO stamps for a clock-less model) and `ui` (the `system` event the
|
|
13723
|
+
* admin shows — the message carries its own `timestamp`, so the header has no
|
|
13724
|
+
* clock and per-child times are relative ages, not raw UTC ISO). */
|
|
13512
13725
|
siblingStatusSuffix(session) {
|
|
13513
13726
|
if (session.parentId !== null)
|
|
13514
|
-
return "";
|
|
13515
|
-
const
|
|
13727
|
+
return { llm: "", ui: "" };
|
|
13728
|
+
const now = Date.now();
|
|
13729
|
+
const nowIso = new Date(now).toISOString();
|
|
13516
13730
|
const stillRunning = this.db.select({
|
|
13517
13731
|
agentName: agentSessions.agentName,
|
|
13518
13732
|
description: agentSessions.description,
|
|
@@ -13520,16 +13734,19 @@ ${result}`;
|
|
|
13520
13734
|
updatedAt: agentSessions.updatedAt
|
|
13521
13735
|
}).from(agentSessions).where(and6(eq9(agentSessions.parentId, session.id), isNull4(agentSessions.stoppedAt), isNull4(agentSessions.archivedAt))).all();
|
|
13522
13736
|
if (stillRunning.length === 0 && session.messageQueue.length === 0) {
|
|
13523
|
-
|
|
13737
|
+
const done = "All sub-agents you dispatched have completed. This is the final report \u2014 you may now consolidate and wrap up.";
|
|
13738
|
+
return { llm: `
|
|
13524
13739
|
|
|
13525
|
-
[System @ ${nowIso}]
|
|
13740
|
+
[System @ ${nowIso}] ${done}`, ui: `[System] ${done}` };
|
|
13526
13741
|
}
|
|
13527
|
-
const
|
|
13528
|
-
return `
|
|
13529
|
-
|
|
13530
|
-
[System @ ${nowIso}] Do NOT wrap up yet \u2014 ${stillRunning.length} sub-agent(s) still running, ${session.messageQueue.length} report(s) still queued.
|
|
13742
|
+
const head = `Do NOT wrap up yet \u2014 ${stillRunning.length} sub-agent(s) still running, ${session.messageQueue.length} report(s) still queued.
|
|
13531
13743
|
Still running:
|
|
13532
|
-
|
|
13744
|
+
`;
|
|
13745
|
+
const llmList = stillRunning.map((c) => ` - ${c.agentName} (created ${new Date(c.createdAt).toISOString()}, last active ${new Date(c.updatedAt).toISOString()}): ${c.description.slice(0, 80)}`).join("\n");
|
|
13746
|
+
const uiList = stillRunning.map((c) => ` - ${c.agentName} (started ${formatAge(now - c.createdAt)} ago, last active ${formatAge(now - c.updatedAt)} ago): ${c.description.slice(0, 80)}`).join("\n");
|
|
13747
|
+
return { llm: `
|
|
13748
|
+
|
|
13749
|
+
[System @ ${nowIso}] ${head}${llmList}`, ui: `[System] ${head}${uiList}` };
|
|
13533
13750
|
}
|
|
13534
13751
|
/** Drain queued messages (user→agent AND agent→agent share this one queue)
|
|
13535
13752
|
* after a session finishes a turn. The whole queue is folded into ONE turn:
|
|
@@ -13561,12 +13778,12 @@ ${list}`;
|
|
|
13561
13778
|
const merged = batch.map((q) => q.sourceSessionId ? `(from: session ${q.sourceSessionId})
|
|
13562
13779
|
${q.text}` : q.text).join("\n\n");
|
|
13563
13780
|
const images = batch.flatMap((q) => q.images ?? []);
|
|
13564
|
-
const suffix = batch.some((q) => q.sourceSessionId !== void 0) ? this.siblingStatusSuffix(session) : "";
|
|
13565
|
-
if (suffix && session.parentId === null) {
|
|
13566
|
-
this.emitEvent(session.id, { type: "system", text: suffix.
|
|
13781
|
+
const suffix = batch.some((q) => q.sourceSessionId !== void 0) ? this.siblingStatusSuffix(session) : { llm: "", ui: "" };
|
|
13782
|
+
if (suffix.ui && session.parentId === null) {
|
|
13783
|
+
this.emitEvent(session.id, { type: "system", text: suffix.ui, agentName: session.agentName });
|
|
13567
13784
|
}
|
|
13568
13785
|
const kickNote = kickFlagReset ? "\n\n[System] The continue_task flag you set last turn was reset by this interrupt. If the interrupted task is still unfinished after you answer, call continue_task again." : "";
|
|
13569
|
-
const input = this.buildInput(merged + suffix + kickNote, images.length > 0 ? images : void 0, session.supportsImage);
|
|
13786
|
+
const input = this.buildInput(merged + suffix.llm + kickNote, images.length > 0 ? images : void 0, session.supportsImage);
|
|
13570
13787
|
try {
|
|
13571
13788
|
await this.runAgentTurn(session, input);
|
|
13572
13789
|
} catch (err) {
|
|
@@ -13574,9 +13791,12 @@ ${q.text}` : q.text).join("\n\n");
|
|
|
13574
13791
|
console.error(`[SessionManager] Drain run error for ${session.id}: ${errMsg}`);
|
|
13575
13792
|
throw err;
|
|
13576
13793
|
}
|
|
13794
|
+
const nextTurnFollows = session.selfKick && !session.interruptRequested || session.messageQueue.length > 0;
|
|
13795
|
+
const isAnswer = session.selfKick || session.finalOutput !== "";
|
|
13796
|
+
if (nextTurnFollows && isAnswer)
|
|
13797
|
+
this.sendInterimReport(session, batch);
|
|
13577
13798
|
if (session.messageQueue.length === 0) {
|
|
13578
13799
|
if (session.selfKick && !session.interruptRequested) {
|
|
13579
|
-
this.sendInterimReport(session, batch);
|
|
13580
13800
|
session.messageQueue.push({ text: CONTINUE_TASK_KICK });
|
|
13581
13801
|
const taskId = session.parentId !== null ? session.id : void 0;
|
|
13582
13802
|
this.emitEvent(session.id, { type: "user", text: CONTINUE_TASK_KICK, agentName: "user", report: true, taskId });
|
|
@@ -13589,30 +13809,52 @@ ${q.text}` : q.text).join("\n\n");
|
|
|
13589
13809
|
}
|
|
13590
13810
|
}
|
|
13591
13811
|
}
|
|
13592
|
-
/**
|
|
13593
|
-
* otherwise lost to whoever asked: the end-of-run reports
|
|
13594
|
-
* deliverRelayReport) read only the LAST turn's output,
|
|
13595
|
-
* resets it. So when the batch just answered came
|
|
13596
|
-
* owes a report — its parent (sub-agent) or a
|
|
13597
|
-
* prefixed message) — forward the answer now.
|
|
13598
|
-
* report's state: no stoppedAt / agent_done
|
|
13599
|
-
* this child running), reply_to kept.
|
|
13600
|
-
* (G's messages carry neither mark):
|
|
13601
|
-
* fresh query_session from G would
|
|
13812
|
+
/** Interim report for a drained turn. A turn followed by another turn (kick /
|
|
13813
|
+
* queued message) is otherwise lost to whoever asked: the end-of-run reports
|
|
13814
|
+
* (tryReportToParent / deliverRelayReport) read only the LAST turn's output,
|
|
13815
|
+
* and the next runAgentTurn resets it. So when the batch just answered came
|
|
13816
|
+
* from the party this session owes a report — its parent (sub-agent) or a
|
|
13817
|
+
* relay caller (root, relay-prefixed message) — forward the answer now.
|
|
13818
|
+
* Neither path touches the final report's state: no stoppedAt / agent_done
|
|
13819
|
+
* (parent's sibling-status still sees this child running), reply_to kept.
|
|
13820
|
+
* Goal workers are deliberately excluded (G's messages carry neither mark):
|
|
13821
|
+
* an interim would wake G mid-round, and a fresh query_session from G would
|
|
13822
|
+
* interrupt W again. */
|
|
13602
13823
|
sendInterimReport(session, batch) {
|
|
13603
13824
|
const fromParent = session.parentId !== null && batch.some((q) => q.sourceSessionId === session.parentId);
|
|
13604
13825
|
const fromRelay = session.parentId === null && batch.some((q) => q.sourceSessionId === void 0 && q.text.includes(RELAY_CHANNEL_PREFIX));
|
|
13605
|
-
if (
|
|
13826
|
+
if (fromParent)
|
|
13827
|
+
this.deliverInterim(session, "parent");
|
|
13828
|
+
else if (fromRelay)
|
|
13829
|
+
this.deliverInterim(session, "relay");
|
|
13830
|
+
}
|
|
13831
|
+
/** Interim report for the OPENING turn (runSession's `hasFirst` turn): when it
|
|
13832
|
+
* ends naturally while the queue already holds a follow-up, the drain resets
|
|
13833
|
+
* output and the wrap-up is lost the same way. No batch to read the door from
|
|
13834
|
+
* here — the opening message is the dispatch itself — so the owed party is
|
|
13835
|
+
* whoever this session reports to at the end: its parent, or a relay caller
|
|
13836
|
+
* (reply_to row). Goal workers have neither, so they get nothing. */
|
|
13837
|
+
sendOpeningInterim(session) {
|
|
13838
|
+
if (session.parentId !== null)
|
|
13839
|
+
this.deliverInterim(session, "parent");
|
|
13840
|
+
else if (readReplyTo(this.db, session.id) !== null)
|
|
13841
|
+
this.deliverInterim(session, "relay");
|
|
13842
|
+
}
|
|
13843
|
+
/** Body = the WHOLE turn's text (`output`, not `finalOutput`): in a turn that
|
|
13844
|
+
* answered and then called continue_task the answer rides the tool_use
|
|
13845
|
+
* response, whose text is `final: false` and never reaches finalOutput. */
|
|
13846
|
+
deliverInterim(session, to) {
|
|
13847
|
+
if (session.turnError)
|
|
13606
13848
|
return;
|
|
13607
|
-
const answer = session.
|
|
13849
|
+
const answer = session.output;
|
|
13608
13850
|
if (!answer)
|
|
13609
13851
|
return;
|
|
13610
13852
|
const cap2 = config.limits.autoReportMax;
|
|
13611
13853
|
const body = answer.length > cap2 ? answer.slice(0, cap2) + `
|
|
13612
13854
|
|
|
13613
13855
|
[Interim report truncated: ${answer.length} chars total.]` : answer;
|
|
13614
|
-
if (
|
|
13615
|
-
const text6 = `[Interim report:
|
|
13856
|
+
if (to === "parent") {
|
|
13857
|
+
const text6 = `[Interim report \xB7 status: still running] Answered your message (below); the session is still working \u2014 its final report follows when done. Not the final result.
|
|
13616
13858
|
|
|
13617
13859
|
${body}`;
|
|
13618
13860
|
this.querySession(session.parentId, session.id, text6).catch((err) => {
|
|
@@ -13881,12 +14123,11 @@ ${cleaned}` : cleaned;
|
|
|
13881
14123
|
if (accessLevel !== void 0 && accessLevel !== session.accessLevel) {
|
|
13882
14124
|
session.accessLevel = accessLevel;
|
|
13883
14125
|
const savedMessages = session.agent.messages;
|
|
13884
|
-
const { agent, modelId, systemPrompt
|
|
14126
|
+
const { agent, modelId, systemPrompt } = await this.buildAgentInstance(session.agentId, sessionId, session.parentId, session.workingDir ?? void 0, accessLevel);
|
|
13885
14127
|
session.agent = agent;
|
|
13886
14128
|
session.agent.messages = savedMessages;
|
|
13887
14129
|
session.currentModelId = modelId;
|
|
13888
14130
|
session.systemPrompt = systemPrompt;
|
|
13889
|
-
session.draftReset = draftReset;
|
|
13890
14131
|
this.db.update(agentSessions).set({ accessLevel }).where(eq9(agentSessions.id, sessionId)).run();
|
|
13891
14132
|
}
|
|
13892
14133
|
const meta = this.db.select().from(agentSessions).where(eq9(agentSessions.id, sessionId)).get();
|
|
@@ -14361,7 +14602,7 @@ ${result.summary}`, agentName: "user", report: true, taskId });
|
|
|
14361
14602
|
if (!uiState)
|
|
14362
14603
|
return "not_found";
|
|
14363
14604
|
const messageLog = uiState.messageLog;
|
|
14364
|
-
let
|
|
14605
|
+
let start2 = -1;
|
|
14365
14606
|
let seen = -1;
|
|
14366
14607
|
for (let i = 0; i < messageLog.length; i++) {
|
|
14367
14608
|
if (messageLog[i].role !== "user")
|
|
@@ -14370,22 +14611,22 @@ ${result.summary}`, agentName: "user", report: true, taskId });
|
|
|
14370
14611
|
continue;
|
|
14371
14612
|
seen++;
|
|
14372
14613
|
if (seen === userOrdinal) {
|
|
14373
|
-
|
|
14614
|
+
start2 = i;
|
|
14374
14615
|
break;
|
|
14375
14616
|
}
|
|
14376
14617
|
}
|
|
14377
|
-
if (
|
|
14618
|
+
if (start2 === -1)
|
|
14378
14619
|
return "no_exchange";
|
|
14379
|
-
const target = stripImageMarkers(messageLog[
|
|
14620
|
+
const target = stripImageMarkers(messageLog[start2].content);
|
|
14380
14621
|
let rank = 0;
|
|
14381
|
-
for (let i = 0; i <=
|
|
14622
|
+
for (let i = 0; i <= start2; i++) {
|
|
14382
14623
|
const m = messageLog[i];
|
|
14383
14624
|
if (m.role === "user" && !m.taskId && stripImageMarkers(m.content) === target)
|
|
14384
14625
|
rank++;
|
|
14385
14626
|
}
|
|
14386
14627
|
const newLog = messageLog.slice();
|
|
14387
|
-
for (let i =
|
|
14388
|
-
if (i >
|
|
14628
|
+
for (let i = start2; i < newLog.length; i++) {
|
|
14629
|
+
if (i > start2 && newLog[i].role === "user")
|
|
14389
14630
|
break;
|
|
14390
14631
|
newLog[i] = { ...newLog[i], deleted: true };
|
|
14391
14632
|
}
|
|
@@ -14417,28 +14658,28 @@ ${result.summary}`, agentName: "user", report: true, taskId });
|
|
|
14417
14658
|
* Returns the new array, or null when no matching turn exists. */
|
|
14418
14659
|
deleteRawTurn(raw, target, rank) {
|
|
14419
14660
|
let seen = 0;
|
|
14420
|
-
let
|
|
14661
|
+
let start2 = -1;
|
|
14421
14662
|
for (let i = 0; i < raw.length; i++) {
|
|
14422
14663
|
if (!isRawTurnStart(raw[i]))
|
|
14423
14664
|
continue;
|
|
14424
14665
|
if (stripImageMarkers(stripTurnStamp(rawMessageText(raw[i]))) === target) {
|
|
14425
14666
|
seen++;
|
|
14426
14667
|
if (seen === rank) {
|
|
14427
|
-
|
|
14668
|
+
start2 = i;
|
|
14428
14669
|
break;
|
|
14429
14670
|
}
|
|
14430
14671
|
}
|
|
14431
14672
|
}
|
|
14432
|
-
if (
|
|
14673
|
+
if (start2 === -1)
|
|
14433
14674
|
return null;
|
|
14434
14675
|
let end = raw.length;
|
|
14435
|
-
for (let i =
|
|
14676
|
+
for (let i = start2 + 1; i < raw.length; i++) {
|
|
14436
14677
|
if (isRawTurnStart(raw[i])) {
|
|
14437
14678
|
end = i;
|
|
14438
14679
|
break;
|
|
14439
14680
|
}
|
|
14440
14681
|
}
|
|
14441
|
-
return [...raw.slice(0,
|
|
14682
|
+
return [...raw.slice(0, start2), ...raw.slice(end)];
|
|
14442
14683
|
}
|
|
14443
14684
|
// ── Phase 2: deleteSession (SQLite only) ──────────────────────────
|
|
14444
14685
|
/**
|
|
@@ -14999,7 +15240,7 @@ async function createHarness(opts) {
|
|
|
14999
15240
|
function interrupt() {
|
|
15000
15241
|
state.sm.interruptSession(state.sessionId);
|
|
15001
15242
|
}
|
|
15002
|
-
async function
|
|
15243
|
+
async function stop2() {
|
|
15003
15244
|
if (state.sm.isSessionRunning(state.sessionId)) {
|
|
15004
15245
|
await state.sm.stopSession(state.sessionId);
|
|
15005
15246
|
}
|
|
@@ -15076,7 +15317,7 @@ async function createHarness(opts) {
|
|
|
15076
15317
|
offEvent,
|
|
15077
15318
|
command,
|
|
15078
15319
|
interrupt,
|
|
15079
|
-
stop,
|
|
15320
|
+
stop: stop2,
|
|
15080
15321
|
destroy,
|
|
15081
15322
|
switchWorkspace,
|
|
15082
15323
|
getSessionTree,
|
|
@@ -16449,8 +16690,8 @@ function applyPathPick(value, ref, pick) {
|
|
|
16449
16690
|
const before = value.slice(0, markerStart);
|
|
16450
16691
|
const kind = ref.kind === "scope" ? "scope" : ref.kind === "image" ? "image" : pick.isImage ? "image" : "file";
|
|
16451
16692
|
const needsQuote = pick.insertPath.includes(" ");
|
|
16452
|
-
const
|
|
16453
|
-
return `${before}@${kind} ${
|
|
16693
|
+
const path74 = pick.isDir ? needsQuote ? `"${pick.insertPath}/` : `${pick.insertPath}/` : needsQuote ? `"${pick.insertPath}" ` : `${pick.insertPath} `;
|
|
16694
|
+
return `${before}@${kind} ${path74}`;
|
|
16454
16695
|
}
|
|
16455
16696
|
var init_path_suggest_engine = __esm({
|
|
16456
16697
|
"src/tui/path-suggest-engine.ts"() {
|
|
@@ -17948,7 +18189,9 @@ function extractMediaMessage(text6) {
|
|
|
17948
18189
|
const { text: stripped, mediaPaths } = extractMediaPaths(text6);
|
|
17949
18190
|
return { text: stripped.replace(/\n{3,}/g, "\n\n").trim(), mediaPaths };
|
|
17950
18191
|
}
|
|
17951
|
-
function isMediaPathAllowed(filePath, workspacePath) {
|
|
18192
|
+
function isMediaPathAllowed(filePath, workspacePath, accessLevel) {
|
|
18193
|
+
if (accessLevel === "full")
|
|
18194
|
+
return true;
|
|
17952
18195
|
const resolved = path40.resolve(filePath);
|
|
17953
18196
|
const ws = path40.resolve(workspacePath);
|
|
17954
18197
|
return resolved === ws || resolved.startsWith(ws + path40.sep) || isInTempDir(resolved);
|
|
@@ -18263,6 +18506,48 @@ function createFileRoutes() {
|
|
|
18263
18506
|
return c.json({ error: errorMessage }, 500);
|
|
18264
18507
|
}
|
|
18265
18508
|
});
|
|
18509
|
+
app2.put("/files/raw", async (c) => {
|
|
18510
|
+
try {
|
|
18511
|
+
const filePath = c.req.query("path");
|
|
18512
|
+
const projectId = c.req.query("projectId");
|
|
18513
|
+
const expectMtimeRaw = c.req.query("expectMtime");
|
|
18514
|
+
if (!filePath || !projectId) {
|
|
18515
|
+
return c.json({ error: "path and projectId are required" }, 400);
|
|
18516
|
+
}
|
|
18517
|
+
const expectMtime = expectMtimeRaw === void 0 ? void 0 : Number(expectMtimeRaw);
|
|
18518
|
+
if (expectMtime !== void 0 && !Number.isFinite(expectMtime)) {
|
|
18519
|
+
return c.json({ error: "expectMtime must be a number" }, 400);
|
|
18520
|
+
}
|
|
18521
|
+
const projectPath = await resolveProjectPath(projectId);
|
|
18522
|
+
if (!projectPath) {
|
|
18523
|
+
return c.json({ error: "Project not found" }, 404);
|
|
18524
|
+
}
|
|
18525
|
+
if (!validatePath(filePath, projectPath)) {
|
|
18526
|
+
return c.json({ error: "Path traversal not allowed" }, 403);
|
|
18527
|
+
}
|
|
18528
|
+
const absolutePath = path42.resolve(projectPath, filePath);
|
|
18529
|
+
let before;
|
|
18530
|
+
try {
|
|
18531
|
+
before = await fs35.stat(absolutePath);
|
|
18532
|
+
} catch (err) {
|
|
18533
|
+
if (err.code === "ENOENT")
|
|
18534
|
+
return c.json({ error: "File not found" }, 404);
|
|
18535
|
+
throw err;
|
|
18536
|
+
}
|
|
18537
|
+
if (before.isDirectory())
|
|
18538
|
+
return c.json({ error: "Cannot write a directory" }, 400);
|
|
18539
|
+
if (expectMtime !== void 0 && Math.round(before.mtimeMs) !== Math.round(expectMtime)) {
|
|
18540
|
+
return c.json({ error: "conflict", mtime: before.mtimeMs, size: before.size }, 409);
|
|
18541
|
+
}
|
|
18542
|
+
await fs35.writeFile(absolutePath, Buffer.from(await c.req.arrayBuffer()));
|
|
18543
|
+
const stat = await fs35.stat(absolutePath);
|
|
18544
|
+
return c.json({ ok: true, path: filePath, mtime: stat.mtimeMs, size: stat.size });
|
|
18545
|
+
} catch (err) {
|
|
18546
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
18547
|
+
console.log(`[Files] Error writing raw file: ${errorMessage}`);
|
|
18548
|
+
return c.json({ error: errorMessage }, 500);
|
|
18549
|
+
}
|
|
18550
|
+
});
|
|
18266
18551
|
app2.post("/files/new", async (c) => {
|
|
18267
18552
|
try {
|
|
18268
18553
|
const body = await c.req.json();
|
|
@@ -18378,7 +18663,8 @@ function createFileRoutes() {
|
|
|
18378
18663
|
wav: "audio/wav",
|
|
18379
18664
|
flac: "audio/flac",
|
|
18380
18665
|
aac: "audio/aac",
|
|
18381
|
-
m4a: "audio/mp4"
|
|
18666
|
+
m4a: "audio/mp4",
|
|
18667
|
+
wasm: "application/wasm"
|
|
18382
18668
|
};
|
|
18383
18669
|
app2.get("/files/download", async (c) => {
|
|
18384
18670
|
try {
|
|
@@ -18407,15 +18693,15 @@ function createFileRoutes() {
|
|
|
18407
18693
|
const disposition = inline ? `inline; filename="${encodeURIComponent(fileName)}"` : `attachment; filename="${encodeURIComponent(fileName)}"`;
|
|
18408
18694
|
const fileSize = stat.size;
|
|
18409
18695
|
const rangeHeader = c.req.header("range");
|
|
18410
|
-
let
|
|
18696
|
+
let start2 = 0;
|
|
18411
18697
|
let end = fileSize - 1;
|
|
18412
18698
|
let status = 200;
|
|
18413
18699
|
if (rangeHeader) {
|
|
18414
18700
|
const match = /^bytes=(\d+)-(\d*)$/.exec(rangeHeader);
|
|
18415
18701
|
if (match) {
|
|
18416
|
-
|
|
18702
|
+
start2 = parseInt(match[1], 10);
|
|
18417
18703
|
end = match[2] ? parseInt(match[2], 10) : fileSize - 1;
|
|
18418
|
-
if (isNaN(
|
|
18704
|
+
if (isNaN(start2) || isNaN(end) || start2 > end || end >= fileSize) {
|
|
18419
18705
|
return new Response(null, {
|
|
18420
18706
|
status: 416,
|
|
18421
18707
|
headers: { "Content-Range": `bytes */${fileSize}` }
|
|
@@ -18424,8 +18710,8 @@ function createFileRoutes() {
|
|
|
18424
18710
|
status = 206;
|
|
18425
18711
|
}
|
|
18426
18712
|
}
|
|
18427
|
-
const chunkSize = fileSize === 0 ? 0 : end -
|
|
18428
|
-
const nodeStream = fileSize === 0 ? Readable.from([]) : createReadStream(absolutePath, { start, end });
|
|
18713
|
+
const chunkSize = fileSize === 0 ? 0 : end - start2 + 1;
|
|
18714
|
+
const nodeStream = fileSize === 0 ? Readable.from([]) : createReadStream(absolutePath, { start: start2, end });
|
|
18429
18715
|
const req = c.req.raw;
|
|
18430
18716
|
if (req.signal) {
|
|
18431
18717
|
req.signal.addEventListener("abort", () => {
|
|
@@ -18440,7 +18726,7 @@ function createFileRoutes() {
|
|
|
18440
18726
|
"Accept-Ranges": "bytes"
|
|
18441
18727
|
};
|
|
18442
18728
|
if (status === 206) {
|
|
18443
|
-
headers["Content-Range"] = `bytes ${
|
|
18729
|
+
headers["Content-Range"] = `bytes ${start2}-${end}/${fileSize}`;
|
|
18444
18730
|
}
|
|
18445
18731
|
return new Response(webStream, { status, headers });
|
|
18446
18732
|
} catch (err) {
|
|
@@ -19565,7 +19851,6 @@ function getAvailableTools() {
|
|
|
19565
19851
|
if (_cachedTools)
|
|
19566
19852
|
return _cachedTools;
|
|
19567
19853
|
const tools = createWorkspaceTools("/tmp");
|
|
19568
|
-
const { tool: draft } = createDraftTool();
|
|
19569
19854
|
const relay = buildRelayTools({ workspaceRoot: "/tmp" }, "");
|
|
19570
19855
|
const relayChip = {
|
|
19571
19856
|
name: "relay_send",
|
|
@@ -19573,7 +19858,7 @@ function getAvailableTools() {
|
|
|
19573
19858
|
|
|
19574
19859
|
${relay[0].description}`
|
|
19575
19860
|
};
|
|
19576
|
-
_cachedTools =
|
|
19861
|
+
_cachedTools = tools.map((t2) => ({ name: t2.name, description: t2.description })).concat(relayChip);
|
|
19577
19862
|
return _cachedTools;
|
|
19578
19863
|
}
|
|
19579
19864
|
async function writeFileAtomic2(filePath, content) {
|
|
@@ -19766,6 +20051,8 @@ function createAgentConfigRoutes() {
|
|
|
19766
20051
|
const id = body.name.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff]+/g, "-").replace(/^-|-$/g, "");
|
|
19767
20052
|
if (!id)
|
|
19768
20053
|
return c.json({ error: "Invalid agent name" }, 400);
|
|
20054
|
+
if (id === GOAL_AGENT_ID && !config.goalModeEnabled)
|
|
20055
|
+
return c.json({ error: "Goal mode is disabled." }, 403);
|
|
19769
20056
|
const scope = body.scope ?? "global";
|
|
19770
20057
|
let baseDir;
|
|
19771
20058
|
if (scope === "workspace") {
|
|
@@ -20029,6 +20316,13 @@ ${body.description ?? ""}
|
|
|
20029
20316
|
if (!isSafeIdSegment(sessionId))
|
|
20030
20317
|
return c.json({ error: "Invalid session id" }, 400);
|
|
20031
20318
|
const dir = getSessionsDir(agentId, source, projectId || void 0);
|
|
20319
|
+
if (agentId === GOAL_AGENT_ID && !config.goalModeEnabled) {
|
|
20320
|
+
try {
|
|
20321
|
+
await fs39.access(path47.join(dir, `${sessionId}.json`));
|
|
20322
|
+
} catch {
|
|
20323
|
+
return c.json({ error: "Goal mode is disabled." }, 403);
|
|
20324
|
+
}
|
|
20325
|
+
}
|
|
20032
20326
|
await ensureDir2(dir);
|
|
20033
20327
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
20034
20328
|
const messages2 = body.messages ?? [];
|
|
@@ -20092,8 +20386,8 @@ var init_agent_configs = __esm({
|
|
|
20092
20386
|
"../server/dist/routes/agent-configs.js"() {
|
|
20093
20387
|
"use strict";
|
|
20094
20388
|
init_workspace_tools();
|
|
20095
|
-
init_draft_tool();
|
|
20096
20389
|
init_relay();
|
|
20390
|
+
init_goal_mode();
|
|
20097
20391
|
init_md_loader();
|
|
20098
20392
|
init_config();
|
|
20099
20393
|
init_db();
|
|
@@ -20445,8 +20739,8 @@ function detectOrphans(tree, declared) {
|
|
|
20445
20739
|
if (!block || typeof block !== "object")
|
|
20446
20740
|
continue;
|
|
20447
20741
|
for (const k of Object.keys(block)) {
|
|
20448
|
-
const
|
|
20449
|
-
if (!declaredMap.has(
|
|
20742
|
+
const path74 = `${namespaceId}.${kindWord}.${k}`;
|
|
20743
|
+
if (!declaredMap.has(path74)) {
|
|
20450
20744
|
out.push({ namespaceId, kind: kindWord === "params" ? "param" : "secret", key: k });
|
|
20451
20745
|
}
|
|
20452
20746
|
}
|
|
@@ -21532,7 +21826,11 @@ function rememberWechatContextToken(db, accountId, userId, token) {
|
|
|
21532
21826
|
return;
|
|
21533
21827
|
const tokens = existing.config.contextTokens ?? {};
|
|
21534
21828
|
if (tokens[userId] !== token) {
|
|
21535
|
-
|
|
21829
|
+
const ats = existing.config.contextTokenAts ?? {};
|
|
21830
|
+
patchConfig(db, accountId, {
|
|
21831
|
+
contextTokens: { ...tokens, [userId]: token },
|
|
21832
|
+
contextTokenAts: { ...ats, [userId]: Date.now() }
|
|
21833
|
+
});
|
|
21536
21834
|
}
|
|
21537
21835
|
_wechatContextTokenCache.set(key, token);
|
|
21538
21836
|
}
|
|
@@ -21754,14 +22052,14 @@ function createWebChannel(deps) {
|
|
|
21754
22052
|
return findActiveSessionId(sm, accountId, prefix, activeOverrides2, "full") ?? void 0;
|
|
21755
22053
|
}
|
|
21756
22054
|
function resolveWorkspace(account, override) {
|
|
21757
|
-
let
|
|
22055
|
+
let path74 = account.workspacePath;
|
|
21758
22056
|
if (override && override !== account.workspacePath) {
|
|
21759
22057
|
if (account.accessLevel !== "full") {
|
|
21760
22058
|
return { ok: false, error: "workspace override requires a full-access token" };
|
|
21761
22059
|
}
|
|
21762
|
-
|
|
22060
|
+
path74 = override;
|
|
21763
22061
|
}
|
|
21764
|
-
const resolved = resolveAccountWorkspace({ ...account, workspacePath:
|
|
22062
|
+
const resolved = resolveAccountWorkspace({ ...account, workspacePath: path74 });
|
|
21765
22063
|
if (!resolved)
|
|
21766
22064
|
return { ok: false, error: "workspace not found" };
|
|
21767
22065
|
return { ok: true, path: resolved };
|
|
@@ -22197,11 +22495,19 @@ var init_session_message = __esm({
|
|
|
22197
22495
|
}
|
|
22198
22496
|
});
|
|
22199
22497
|
|
|
22498
|
+
// ../core/dist/protocol/extension-frames.js
|
|
22499
|
+
var init_extension_frames = __esm({
|
|
22500
|
+
"../core/dist/protocol/extension-frames.js"() {
|
|
22501
|
+
"use strict";
|
|
22502
|
+
}
|
|
22503
|
+
});
|
|
22504
|
+
|
|
22200
22505
|
// ../core/dist/protocol/index.js
|
|
22201
22506
|
var init_protocol = __esm({
|
|
22202
22507
|
"../core/dist/protocol/index.js"() {
|
|
22203
22508
|
"use strict";
|
|
22204
22509
|
init_session_message();
|
|
22510
|
+
init_extension_frames();
|
|
22205
22511
|
}
|
|
22206
22512
|
});
|
|
22207
22513
|
|
|
@@ -23197,7 +23503,7 @@ var init_file_watcher = __esm({
|
|
|
23197
23503
|
];
|
|
23198
23504
|
IS_WIN32 = process.platform === "win32";
|
|
23199
23505
|
IGNORED_SEGMENT_SET = new Set(IGNORED_SEGMENTS);
|
|
23200
|
-
NATIVE_IGNORE = IS_WIN32 ? IGNORED_SEGMENTS : IGNORED_SEGMENTS.
|
|
23506
|
+
NATIVE_IGNORE = IS_WIN32 ? IGNORED_SEGMENTS : [...IGNORED_SEGMENTS, `**/{${IGNORED_SEGMENTS.join(",")}}/**`];
|
|
23201
23507
|
nativeOpChain = Promise.resolve();
|
|
23202
23508
|
WorkspaceWatcher = class {
|
|
23203
23509
|
subscription = null;
|
|
@@ -23231,8 +23537,8 @@ var init_file_watcher = __esm({
|
|
|
23231
23537
|
const epoch = ++this.startEpoch;
|
|
23232
23538
|
this.workspaceRoot = root;
|
|
23233
23539
|
try {
|
|
23234
|
-
const { default:
|
|
23235
|
-
const sub = await enqueueNativeOp(() =>
|
|
23540
|
+
const { default: watcher2 } = await import("@parcel/watcher");
|
|
23541
|
+
const sub = await enqueueNativeOp(() => watcher2.subscribe(root, (err, events) => {
|
|
23236
23542
|
if (err) {
|
|
23237
23543
|
console.warn(`[FileWatcher] watcher error on ${root}: ${err.message}`);
|
|
23238
23544
|
return;
|
|
@@ -23492,28 +23798,43 @@ function sendJson(ws, data) {
|
|
|
23492
23798
|
function sendWsNotification(event, state, turnId, ctx) {
|
|
23493
23799
|
const agentName = event.agentName ?? "default";
|
|
23494
23800
|
const taskId = event.taskId;
|
|
23801
|
+
const sessionId = ctx.sessionId;
|
|
23495
23802
|
switch (event.type) {
|
|
23803
|
+
// Streaming chunks ride the same frames as the whole-text events — the
|
|
23804
|
+
// admin appends chat:thinking / chat:stream by turnId either way. The whole
|
|
23805
|
+
// event that follows a streamed call is stamped `streamed` and dropped so
|
|
23806
|
+
// the client doesn't render the text twice.
|
|
23807
|
+
case "thinking_delta":
|
|
23808
|
+
sendJson(ctx.ws, { type: "chat:thinking", text: event.text ?? "", agentName, taskId, turnId, sessionId });
|
|
23809
|
+
break;
|
|
23810
|
+
case "stream_delta":
|
|
23811
|
+
sendJson(ctx.ws, { type: "chat:stream", text: event.text ?? "", agentName, taskId, turnId, sessionId });
|
|
23812
|
+
break;
|
|
23496
23813
|
case "thinking":
|
|
23497
|
-
|
|
23814
|
+
if (event.streamed)
|
|
23815
|
+
break;
|
|
23816
|
+
sendJson(ctx.ws, { type: "chat:thinking", text: event.text ?? "", agentName, taskId, turnId, sessionId });
|
|
23498
23817
|
break;
|
|
23499
23818
|
case "stream":
|
|
23500
|
-
|
|
23819
|
+
if (event.streamed)
|
|
23820
|
+
break;
|
|
23821
|
+
sendJson(ctx.ws, { type: "chat:stream", text: event.text ?? "", agentName, taskId, turnId, sessionId });
|
|
23501
23822
|
break;
|
|
23502
23823
|
case "agent_start":
|
|
23503
|
-
sendJson(ctx.ws, { type: "agent:start", agentName, task: event.text, taskId });
|
|
23824
|
+
sendJson(ctx.ws, { type: "agent:start", agentName, task: event.text, taskId, sessionId });
|
|
23504
23825
|
break;
|
|
23505
23826
|
case "agent_done":
|
|
23506
|
-
sendJson(ctx.ws, { type: "agent:done", agentName, taskId });
|
|
23827
|
+
sendJson(ctx.ws, { type: "agent:done", agentName, taskId, sessionId });
|
|
23507
23828
|
break;
|
|
23508
23829
|
case "tool_call":
|
|
23509
|
-
sendJson(ctx.ws, { type: "agent:tool_call", tool: event.toolName, toolUseId: event.toolUseId, input: event.toolInput, agentName, taskId, turnId });
|
|
23830
|
+
sendJson(ctx.ws, { type: "agent:tool_call", tool: event.toolName, toolUseId: event.toolUseId, input: event.toolInput, agentName, taskId, turnId, sessionId });
|
|
23510
23831
|
break;
|
|
23511
23832
|
case "tool_result":
|
|
23512
|
-
sendJson(ctx.ws, { type: "agent:tool_result", result: event.toolResult, toolUseId: event.toolUseId, agentName, taskId, durationMs: event.durationMs });
|
|
23833
|
+
sendJson(ctx.ws, { type: "agent:tool_result", result: event.toolResult, toolUseId: event.toolUseId, agentName, taskId, durationMs: event.durationMs, sessionId });
|
|
23513
23834
|
break;
|
|
23514
23835
|
case "followup_start":
|
|
23515
23836
|
case "queued_message":
|
|
23516
|
-
sendJson(ctx.ws, { type: "chat:followup", agentName });
|
|
23837
|
+
sendJson(ctx.ws, { type: "chat:followup", agentName, sessionId });
|
|
23517
23838
|
break;
|
|
23518
23839
|
case "usage":
|
|
23519
23840
|
if (!taskId) {
|
|
@@ -23523,28 +23844,29 @@ function sendWsNotification(event, state, turnId, ctx) {
|
|
|
23523
23844
|
outputTokens: state.outputTokens,
|
|
23524
23845
|
turnId,
|
|
23525
23846
|
modelId: event.modelId,
|
|
23526
|
-
usage: buildUsageData(event)
|
|
23847
|
+
usage: buildUsageData(event),
|
|
23848
|
+
sessionId
|
|
23527
23849
|
});
|
|
23528
23850
|
}
|
|
23529
23851
|
break;
|
|
23530
23852
|
case "complete":
|
|
23531
|
-
sendJson(ctx.ws, { type: "chat:complete", sessionId
|
|
23853
|
+
sendJson(ctx.ws, { type: "chat:complete", sessionId });
|
|
23532
23854
|
break;
|
|
23533
23855
|
case "context":
|
|
23534
|
-
sendJson(ctx.ws, { type: "agent:context", agentName, systemPrompt: event.systemPrompt, taskId });
|
|
23856
|
+
sendJson(ctx.ws, { type: "agent:context", agentName, systemPrompt: event.systemPrompt, taskId, sessionId });
|
|
23535
23857
|
break;
|
|
23536
23858
|
case "system":
|
|
23537
23859
|
if (!taskId && /^Compacting context \(\d+K tokens\)…$/.test(event.text ?? "")) {
|
|
23538
23860
|
sendJson(ctx.ws, { type: "compact:started" });
|
|
23539
23861
|
}
|
|
23540
|
-
sendJson(ctx.ws, { type: "chat:system", text: event.text ?? "", taskId, agentName });
|
|
23862
|
+
sendJson(ctx.ws, { type: "chat:system", text: event.text ?? "", taskId, agentName, sessionId });
|
|
23541
23863
|
break;
|
|
23542
23864
|
case "error":
|
|
23543
|
-
sendJson(ctx.ws, { type: "error", error: event.error, agentName, taskId });
|
|
23865
|
+
sendJson(ctx.ws, { type: "error", error: event.error, agentName, taskId, sessionId });
|
|
23544
23866
|
break;
|
|
23545
23867
|
case "user":
|
|
23546
23868
|
if (!taskId && !event.localEcho) {
|
|
23547
|
-
sendJson(ctx.ws, { type: "chat:user", text: event.text ?? "" });
|
|
23869
|
+
sendJson(ctx.ws, { type: "chat:user", text: event.text ?? "", sessionId });
|
|
23548
23870
|
}
|
|
23549
23871
|
break;
|
|
23550
23872
|
case "compacted":
|
|
@@ -23877,14 +24199,14 @@ function setupWebSocketHandler(deps) {
|
|
|
23877
24199
|
if (!client.sessionManager.isUIStateDirty(client.sessionId))
|
|
23878
24200
|
return;
|
|
23879
24201
|
const projectPath = resolveProjectPath2(client.projectId);
|
|
23880
|
-
const
|
|
23881
|
-
if (
|
|
24202
|
+
const snapshot2 = createSaveSnapshot(state);
|
|
24203
|
+
if (snapshot2.length === 0)
|
|
23882
24204
|
return;
|
|
23883
24205
|
const sessionInfo = client.sessionManager.getSessionById(client.sessionId);
|
|
23884
24206
|
client.sessionManager.persistSessionFile({
|
|
23885
24207
|
sessionId: client.sessionId,
|
|
23886
24208
|
projectPath,
|
|
23887
|
-
messages:
|
|
24209
|
+
messages: snapshot2,
|
|
23888
24210
|
contextTokens: state.contextTokens,
|
|
23889
24211
|
outputTokens: state.outputTokens,
|
|
23890
24212
|
agentId: sessionInfo?.agentId,
|
|
@@ -24367,17 +24689,18 @@ ${msg.message}` : markers.join("\n");
|
|
|
24367
24689
|
if (state2 && running) {
|
|
24368
24690
|
console.debug(`[WS] Session still running \u2014 synthesizing in-progress state`);
|
|
24369
24691
|
const agentName = detachedSession?.agentName ?? (state2.streamingAgent || "default");
|
|
24370
|
-
|
|
24692
|
+
const sessionId = client2.sessionId;
|
|
24693
|
+
sendJson(ws, { type: "chat:followup", agentName, replay: true, sessionId });
|
|
24371
24694
|
for (const block of state2.turnContentBlocks) {
|
|
24372
24695
|
if (block.type === "thinking") {
|
|
24373
|
-
sendJson(ws, { type: "chat:thinking", text: block.text, agentName, turnId: block.turnId, replay: true });
|
|
24696
|
+
sendJson(ws, { type: "chat:thinking", text: block.text, agentName, turnId: block.turnId, replay: true, sessionId });
|
|
24374
24697
|
} else if (block.type === "text") {
|
|
24375
|
-
sendJson(ws, { type: "chat:stream", text: block.text, agentName, turnId: block.turnId, replay: true });
|
|
24698
|
+
sendJson(ws, { type: "chat:stream", text: block.text, agentName, turnId: block.turnId, replay: true, sessionId });
|
|
24376
24699
|
} else {
|
|
24377
24700
|
const tc = block.toolCall;
|
|
24378
|
-
sendJson(ws, { type: "agent:tool_call", tool: tc.name, toolUseId: tc.toolUseId, input: tc.input, agentName, turnId: block.turnId, replay: true });
|
|
24701
|
+
sendJson(ws, { type: "agent:tool_call", tool: tc.name, toolUseId: tc.toolUseId, input: tc.input, agentName, turnId: block.turnId, replay: true, sessionId });
|
|
24379
24702
|
if (tc.output) {
|
|
24380
|
-
sendJson(ws, { type: "agent:tool_result", result: tc.output, toolUseId: tc.toolUseId, agentName, durationMs: tc.durationMs, replay: true });
|
|
24703
|
+
sendJson(ws, { type: "agent:tool_result", result: tc.output, toolUseId: tc.toolUseId, agentName, durationMs: tc.durationMs, replay: true, sessionId });
|
|
24381
24704
|
}
|
|
24382
24705
|
}
|
|
24383
24706
|
}
|
|
@@ -24681,14 +25004,14 @@ function resolveHaloCli() {
|
|
|
24681
25004
|
return process.platform === "win32" ? "halo.cmd" : "halo";
|
|
24682
25005
|
}
|
|
24683
25006
|
function listDescendants(rootPid) {
|
|
24684
|
-
let
|
|
25007
|
+
let snapshot2;
|
|
24685
25008
|
try {
|
|
24686
|
-
|
|
25009
|
+
snapshot2 = execFileSync("ps", ["-e", "-o", "pid=,ppid="], { encoding: "utf-8" });
|
|
24687
25010
|
} catch {
|
|
24688
25011
|
return [];
|
|
24689
25012
|
}
|
|
24690
25013
|
const childrenOf = /* @__PURE__ */ new Map();
|
|
24691
|
-
for (const line of
|
|
25014
|
+
for (const line of snapshot2.split("\n")) {
|
|
24692
25015
|
const parts = line.trim().split(/\s+/);
|
|
24693
25016
|
if (parts.length !== 2)
|
|
24694
25017
|
continue;
|
|
@@ -25574,88 +25897,810 @@ var init_cron = __esm({
|
|
|
25574
25897
|
}
|
|
25575
25898
|
});
|
|
25576
25899
|
|
|
25577
|
-
// ../server/dist/
|
|
25578
|
-
import
|
|
25579
|
-
|
|
25580
|
-
|
|
25900
|
+
// ../server/dist/extensions/registry.js
|
|
25901
|
+
import fs53 from "node:fs";
|
|
25902
|
+
import path61 from "node:path";
|
|
25903
|
+
function isExtensionId(id) {
|
|
25904
|
+
return ID_RE.test(id);
|
|
25905
|
+
}
|
|
25906
|
+
function extensionsRoot() {
|
|
25907
|
+
return globalExtensionsDir();
|
|
25908
|
+
}
|
|
25909
|
+
function parseManifest(raw, dirName, entryExists) {
|
|
25910
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
25911
|
+
return { error: "manifest must be a JSON object" };
|
|
25912
|
+
const m = raw;
|
|
25913
|
+
if (typeof m.id !== "string" || !ID_RE.test(m.id))
|
|
25914
|
+
return { error: "id must match ^[a-z0-9][a-z0-9_-]{0,63}$" };
|
|
25915
|
+
if (m.id !== dirName)
|
|
25916
|
+
return { error: `id mismatch: manifest "${m.id}" vs directory "${dirName}"` };
|
|
25917
|
+
if (typeof m.name !== "string" || m.name.trim() === "" || m.name.length > 64)
|
|
25918
|
+
return { error: "name must be a non-empty string (\u2264 64 chars)" };
|
|
25919
|
+
if (typeof m.version !== "string" || !VERSION_RE.test(m.version))
|
|
25920
|
+
return { error: "version must be semver (x.y.z[-pre])" };
|
|
25921
|
+
if (m.description !== void 0 && (typeof m.description !== "string" || m.description.length > 200))
|
|
25922
|
+
return { error: "description must be a string (\u2264 200 chars)" };
|
|
25923
|
+
if (!Array.isArray(m.extensions) || m.extensions.length === 0)
|
|
25924
|
+
return { error: "extensions must be a non-empty array" };
|
|
25925
|
+
for (const e of m.extensions) {
|
|
25926
|
+
if (typeof e !== "string" || !FILE_EXT_RE.test(e))
|
|
25927
|
+
return { error: `extensions entry "${String(e)}" must look like ".glb" (lowercase, with dot)` };
|
|
25928
|
+
}
|
|
25929
|
+
if (typeof m.entry !== "string" || m.entry === "" || m.entry.startsWith("/") || m.entry.split(/[\\/]/).includes("..")) {
|
|
25930
|
+
return { error: "entry must be a relative path inside the extension directory" };
|
|
25931
|
+
}
|
|
25932
|
+
if (!entryExists(m.entry))
|
|
25933
|
+
return { error: `entry not found: ${m.entry}` };
|
|
25934
|
+
let priority = "default";
|
|
25935
|
+
if (m.priority !== void 0) {
|
|
25936
|
+
if (m.priority !== "default" && m.priority !== "option")
|
|
25937
|
+
return { error: 'priority must be "default" or "option"' };
|
|
25938
|
+
priority = m.priority;
|
|
25939
|
+
}
|
|
25940
|
+
const capabilities = [];
|
|
25941
|
+
if (m.capabilities !== void 0) {
|
|
25942
|
+
if (!Array.isArray(m.capabilities))
|
|
25943
|
+
return { error: "capabilities must be an array" };
|
|
25944
|
+
for (const cap2 of m.capabilities) {
|
|
25945
|
+
if (typeof cap2 !== "string" || !CAPABILITIES.has(cap2))
|
|
25946
|
+
return { error: `unknown capability: ${String(cap2)}` };
|
|
25947
|
+
if (!capabilities.includes(cap2))
|
|
25948
|
+
capabilities.push(cap2);
|
|
25949
|
+
}
|
|
25950
|
+
}
|
|
25951
|
+
if (m.homepage !== void 0 && (typeof m.homepage !== "string" || !/^https?:\/\//.test(m.homepage)))
|
|
25952
|
+
return { error: "homepage must be an http(s) URL" };
|
|
25953
|
+
if (m.license !== void 0 && typeof m.license !== "string")
|
|
25954
|
+
return { error: "license must be a string" };
|
|
25955
|
+
return {
|
|
25956
|
+
id: m.id,
|
|
25957
|
+
name: m.name,
|
|
25958
|
+
version: m.version,
|
|
25959
|
+
...m.description !== void 0 ? { description: m.description } : {},
|
|
25960
|
+
extensions: m.extensions,
|
|
25961
|
+
entry: m.entry,
|
|
25962
|
+
priority,
|
|
25963
|
+
capabilities,
|
|
25964
|
+
...m.homepage !== void 0 ? { homepage: m.homepage } : {},
|
|
25965
|
+
...m.license !== void 0 ? { license: m.license } : {}
|
|
25966
|
+
};
|
|
25581
25967
|
}
|
|
25582
|
-
function
|
|
25583
|
-
|
|
25584
|
-
|
|
25585
|
-
|
|
25586
|
-
|
|
25587
|
-
|
|
25588
|
-
|
|
25589
|
-
|
|
25590
|
-
|
|
25591
|
-
|
|
25592
|
-
|
|
25968
|
+
function readExtensionDir(dir, id = path61.basename(dir)) {
|
|
25969
|
+
let raw;
|
|
25970
|
+
try {
|
|
25971
|
+
raw = JSON.parse(fs53.readFileSync(path61.join(dir, MANIFEST_FILE), "utf-8"));
|
|
25972
|
+
} catch (err) {
|
|
25973
|
+
const code = err.code;
|
|
25974
|
+
return { id, error: code === "ENOENT" ? `${MANIFEST_FILE} missing` : `${MANIFEST_FILE} unreadable: ${err instanceof Error ? err.message : String(err)}` };
|
|
25975
|
+
}
|
|
25976
|
+
const parsed = parseManifest(raw, id, (entry) => {
|
|
25977
|
+
try {
|
|
25978
|
+
return fs53.statSync(path61.join(dir, entry)).isFile();
|
|
25979
|
+
} catch {
|
|
25980
|
+
return false;
|
|
25981
|
+
}
|
|
25982
|
+
});
|
|
25983
|
+
if ("error" in parsed)
|
|
25984
|
+
return { id, error: parsed.error };
|
|
25985
|
+
let installedAt = 0;
|
|
25986
|
+
try {
|
|
25987
|
+
installedAt = Math.round(fs53.statSync(dir).mtimeMs);
|
|
25988
|
+
} catch {
|
|
25989
|
+
}
|
|
25990
|
+
return { ...parsed, installedAt };
|
|
25593
25991
|
}
|
|
25594
|
-
function
|
|
25595
|
-
|
|
25596
|
-
|
|
25597
|
-
|
|
25992
|
+
function scanExtensions() {
|
|
25993
|
+
const root = extensionsRoot();
|
|
25994
|
+
const extensions = [];
|
|
25995
|
+
const errors = [];
|
|
25996
|
+
let entries = [];
|
|
25997
|
+
try {
|
|
25998
|
+
entries = fs53.readdirSync(root, { withFileTypes: true });
|
|
25999
|
+
} catch {
|
|
25598
26000
|
}
|
|
25599
|
-
|
|
25600
|
-
|
|
25601
|
-
|
|
25602
|
-
|
|
26001
|
+
for (const entry of entries) {
|
|
26002
|
+
if (!entry.isDirectory() || entry.name.startsWith("."))
|
|
26003
|
+
continue;
|
|
26004
|
+
if (!ID_RE.test(entry.name)) {
|
|
26005
|
+
errors.push({ id: entry.name, error: "directory name is not a valid extension id" });
|
|
26006
|
+
continue;
|
|
26007
|
+
}
|
|
26008
|
+
const result = readExtensionDir(path61.join(root, entry.name));
|
|
26009
|
+
if ("error" in result)
|
|
26010
|
+
errors.push(result);
|
|
26011
|
+
else
|
|
26012
|
+
extensions.push(result);
|
|
25603
26013
|
}
|
|
26014
|
+
extensions.sort((a, b) => a.id.localeCompare(b.id));
|
|
26015
|
+
errors.sort((a, b) => a.id.localeCompare(b.id));
|
|
26016
|
+
snapshot = { extensions, errors };
|
|
26017
|
+
return snapshot;
|
|
25604
26018
|
}
|
|
25605
|
-
function
|
|
25606
|
-
|
|
26019
|
+
function getSnapshot() {
|
|
26020
|
+
return snapshot;
|
|
25607
26021
|
}
|
|
25608
|
-
|
|
25609
|
-
|
|
25610
|
-
startPendingRuns();
|
|
25611
|
-
startPendingApplies();
|
|
25612
|
-
broadcastChanges();
|
|
26022
|
+
function getExtension(id) {
|
|
26023
|
+
return snapshot.extensions.find((e) => e.id === id);
|
|
25613
26024
|
}
|
|
25614
|
-
function
|
|
25615
|
-
|
|
25616
|
-
|
|
25617
|
-
|
|
25618
|
-
|
|
25619
|
-
|
|
25620
|
-
|
|
25621
|
-
|
|
25622
|
-
|
|
25623
|
-
|
|
25624
|
-
|
|
25625
|
-
|
|
25626
|
-
|
|
25627
|
-
|
|
25628
|
-
|
|
25629
|
-
|
|
26025
|
+
function snapshotKey(s) {
|
|
26026
|
+
return [
|
|
26027
|
+
...s.extensions.map((e) => `${e.id}@${e.version}#${e.installedAt}`),
|
|
26028
|
+
...s.errors.map((e) => `!${e.id}:${e.error}`)
|
|
26029
|
+
].join("\n");
|
|
26030
|
+
}
|
|
26031
|
+
var MANIFEST_FILE, ID_RE, VERSION_RE, FILE_EXT_RE, CAPABILITIES, snapshot;
|
|
26032
|
+
var init_registry2 = __esm({
|
|
26033
|
+
"../server/dist/extensions/registry.js"() {
|
|
26034
|
+
"use strict";
|
|
26035
|
+
init_paths();
|
|
26036
|
+
MANIFEST_FILE = "halo-extension.json";
|
|
26037
|
+
ID_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
26038
|
+
VERSION_RE = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/;
|
|
26039
|
+
FILE_EXT_RE = /^\.[a-z0-9]+$/;
|
|
26040
|
+
CAPABILITIES = /* @__PURE__ */ new Set(["save"]);
|
|
26041
|
+
snapshot = { extensions: [], errors: [] };
|
|
25630
26042
|
}
|
|
25631
|
-
|
|
25632
|
-
|
|
25633
|
-
|
|
26043
|
+
});
|
|
26044
|
+
|
|
26045
|
+
// ../server/dist/extensions/install.js
|
|
26046
|
+
import fs54 from "node:fs";
|
|
26047
|
+
import path62 from "node:path";
|
|
26048
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
26049
|
+
import JSZip2 from "jszip";
|
|
26050
|
+
function safeEntryPath(name) {
|
|
26051
|
+
const posix = name.replace(/\\/g, "/");
|
|
26052
|
+
if (posix.startsWith("/") || /^[A-Za-z]:/.test(posix))
|
|
26053
|
+
throw new ExtensionInstallError(`zip entry has an absolute path: ${name}`);
|
|
26054
|
+
const segments = posix.split("/").filter((s) => s !== "" && s !== ".");
|
|
26055
|
+
if (segments.includes(".."))
|
|
26056
|
+
throw new ExtensionInstallError(`zip entry escapes the archive root: ${name}`);
|
|
26057
|
+
if (segments.some((s) => s.includes("\0")))
|
|
26058
|
+
throw new ExtensionInstallError(`zip entry name contains NUL: ${name}`);
|
|
26059
|
+
return segments.join("/");
|
|
26060
|
+
}
|
|
26061
|
+
async function unpackZip(zipBuf, dest) {
|
|
26062
|
+
let zip;
|
|
26063
|
+
try {
|
|
26064
|
+
zip = await JSZip2.loadAsync(zipBuf);
|
|
26065
|
+
} catch (err) {
|
|
26066
|
+
throw new ExtensionInstallError(`not a zip file: ${err instanceof Error ? err.message : String(err)}`);
|
|
26067
|
+
}
|
|
26068
|
+
const files = Object.values(zip.files);
|
|
26069
|
+
if (files.length === 0)
|
|
26070
|
+
throw new ExtensionInstallError("zip is empty");
|
|
26071
|
+
for (const entry of files) {
|
|
26072
|
+
const rel = safeEntryPath(entry.name);
|
|
26073
|
+
if (rel === "")
|
|
26074
|
+
continue;
|
|
26075
|
+
const perm = entry.unixPermissions;
|
|
26076
|
+
if (typeof perm === "number" && (perm & S_IFMT) === S_IFLNK)
|
|
26077
|
+
throw new ExtensionInstallError(`zip entry is a symlink: ${entry.name}`);
|
|
26078
|
+
const abs = path62.join(dest, rel);
|
|
26079
|
+
if (entry.dir) {
|
|
26080
|
+
fs54.mkdirSync(abs, { recursive: true });
|
|
25634
26081
|
continue;
|
|
25635
|
-
const row = db.select({ status: evolutionRuns.status }).from(evolutionRuns).where(eq16(evolutionRuns.id, id)).get();
|
|
25636
|
-
const finalStatus = row?.status;
|
|
25637
|
-
if (finalStatus && _lastRunStatus.get(id) !== finalStatus) {
|
|
25638
|
-
broadcast({ type: "evolution:run_changed", id, status: finalStatus });
|
|
25639
26082
|
}
|
|
25640
|
-
|
|
26083
|
+
fs54.mkdirSync(path62.dirname(abs), { recursive: true });
|
|
26084
|
+
fs54.writeFileSync(abs, await entry.async("nodebuffer"));
|
|
25641
26085
|
}
|
|
25642
|
-
|
|
25643
|
-
|
|
25644
|
-
|
|
25645
|
-
|
|
25646
|
-
|
|
25647
|
-
|
|
25648
|
-
|
|
25649
|
-
|
|
26086
|
+
}
|
|
26087
|
+
function stripWrapperDir(dir) {
|
|
26088
|
+
if (fs54.existsSync(path62.join(dir, MANIFEST_FILE)))
|
|
26089
|
+
return;
|
|
26090
|
+
const children = fs54.readdirSync(dir, { withFileTypes: true });
|
|
26091
|
+
if (children.length !== 1 || !children[0].isDirectory())
|
|
26092
|
+
return;
|
|
26093
|
+
const inner = path62.join(dir, children[0].name);
|
|
26094
|
+
if (!fs54.existsSync(path62.join(inner, MANIFEST_FILE)))
|
|
26095
|
+
return;
|
|
26096
|
+
for (const name of fs54.readdirSync(inner)) {
|
|
26097
|
+
fs54.renameSync(path62.join(inner, name), path62.join(dir, name));
|
|
26098
|
+
}
|
|
26099
|
+
fs54.rmdirSync(inner);
|
|
26100
|
+
}
|
|
26101
|
+
function rand() {
|
|
26102
|
+
return randomBytes2(3).toString("hex");
|
|
26103
|
+
}
|
|
26104
|
+
async function installExtensionZip(zipBuf, expectId) {
|
|
26105
|
+
const root = extensionsRoot();
|
|
26106
|
+
fs54.mkdirSync(root, { recursive: true });
|
|
26107
|
+
const stage = path62.join(root, `.tmp-unpack-${rand()}`);
|
|
26108
|
+
fs54.mkdirSync(stage);
|
|
26109
|
+
try {
|
|
26110
|
+
await unpackZip(zipBuf, stage);
|
|
26111
|
+
stripWrapperDir(stage);
|
|
26112
|
+
let manifestId;
|
|
26113
|
+
try {
|
|
26114
|
+
const raw = JSON.parse(fs54.readFileSync(path62.join(stage, MANIFEST_FILE), "utf-8"));
|
|
26115
|
+
manifestId = typeof raw.id === "string" ? raw.id : "";
|
|
26116
|
+
} catch (err) {
|
|
26117
|
+
throw new ExtensionInstallError(`${MANIFEST_FILE} missing or unreadable at the zip root: ${err instanceof Error ? err.message : String(err)}`);
|
|
26118
|
+
}
|
|
26119
|
+
if (!isExtensionId(manifestId))
|
|
26120
|
+
throw new ExtensionInstallError(`manifest id "${manifestId}" is not a valid extension id`);
|
|
26121
|
+
if (expectId !== void 0 && manifestId !== expectId)
|
|
26122
|
+
throw new ExtensionInstallError(`manifest id "${manifestId}" does not match requested "${expectId}"`);
|
|
26123
|
+
const verdict = readExtensionDir(stage, manifestId);
|
|
26124
|
+
if ("error" in verdict)
|
|
26125
|
+
throw new ExtensionInstallError(`invalid extension: ${verdict.error}`);
|
|
26126
|
+
const target = path62.join(root, manifestId);
|
|
26127
|
+
const old = path62.join(root, `.old-${manifestId}-${rand()}`);
|
|
26128
|
+
const hadOld = fs54.existsSync(target);
|
|
26129
|
+
if (hadOld)
|
|
26130
|
+
fs54.renameSync(target, old);
|
|
26131
|
+
try {
|
|
26132
|
+
fs54.renameSync(stage, target);
|
|
26133
|
+
} catch (err) {
|
|
26134
|
+
if (hadOld)
|
|
26135
|
+
fs54.renameSync(old, target);
|
|
26136
|
+
throw err;
|
|
25650
26137
|
}
|
|
25651
|
-
|
|
26138
|
+
if (hadOld)
|
|
26139
|
+
fs54.rmSync(old, { recursive: true, force: true });
|
|
26140
|
+
const info = readExtensionDir(target);
|
|
26141
|
+
if ("error" in info)
|
|
26142
|
+
throw new ExtensionInstallError(`installed but failed re-validation: ${info.error}`);
|
|
26143
|
+
console.log(`[Extensions] installed ${info.id}@${info.version}${hadOld ? " (replaced previous)" : ""}`);
|
|
26144
|
+
return info;
|
|
26145
|
+
} finally {
|
|
26146
|
+
fs54.rmSync(stage, { recursive: true, force: true });
|
|
25652
26147
|
}
|
|
25653
26148
|
}
|
|
25654
|
-
function
|
|
25655
|
-
|
|
25656
|
-
|
|
25657
|
-
const
|
|
25658
|
-
|
|
26149
|
+
function uninstallExtension(id) {
|
|
26150
|
+
if (!isExtensionId(id))
|
|
26151
|
+
throw new ExtensionInstallError(`"${id}" is not a valid extension id`);
|
|
26152
|
+
const dir = path62.join(extensionsRoot(), id);
|
|
26153
|
+
if (!fs54.existsSync(dir))
|
|
26154
|
+
return false;
|
|
26155
|
+
fs54.rmSync(dir, { recursive: true, force: true });
|
|
26156
|
+
console.log(`[Extensions] removed ${id}`);
|
|
26157
|
+
return true;
|
|
26158
|
+
}
|
|
26159
|
+
var ExtensionInstallError, S_IFMT, S_IFLNK;
|
|
26160
|
+
var init_install = __esm({
|
|
26161
|
+
"../server/dist/extensions/install.js"() {
|
|
26162
|
+
"use strict";
|
|
26163
|
+
init_registry2();
|
|
26164
|
+
ExtensionInstallError = class extends Error {
|
|
26165
|
+
};
|
|
26166
|
+
S_IFMT = 61440;
|
|
26167
|
+
S_IFLNK = 40960;
|
|
26168
|
+
}
|
|
26169
|
+
});
|
|
26170
|
+
|
|
26171
|
+
// ../server/dist/extensions/watcher.js
|
|
26172
|
+
import fs55 from "node:fs";
|
|
26173
|
+
function scheduleFlush() {
|
|
26174
|
+
if (flushTimer)
|
|
26175
|
+
return;
|
|
26176
|
+
flushTimer = setTimeout(() => {
|
|
26177
|
+
flushTimer = null;
|
|
26178
|
+
rescanAndBroadcast();
|
|
26179
|
+
}, 300);
|
|
26180
|
+
}
|
|
26181
|
+
function rescanAndBroadcast() {
|
|
26182
|
+
const snap = scanExtensions();
|
|
26183
|
+
const key = snapshotKey(snap);
|
|
26184
|
+
if (key === lastKey)
|
|
26185
|
+
return;
|
|
26186
|
+
lastKey = key;
|
|
26187
|
+
broadcast({ type: "extension:changed", ...snap });
|
|
26188
|
+
}
|
|
26189
|
+
function openWatcher(root) {
|
|
26190
|
+
try {
|
|
26191
|
+
watcher = fs55.watch(root, { persistent: false, recursive: false }, () => scheduleFlush());
|
|
26192
|
+
watcher.on("error", (err) => {
|
|
26193
|
+
console.warn(`[Extensions] watch error on ${root}: ${err.message} \u2014 retrying in 1s`);
|
|
26194
|
+
closeWatcher();
|
|
26195
|
+
restartTimer = setTimeout(() => {
|
|
26196
|
+
restartTimer = null;
|
|
26197
|
+
start();
|
|
26198
|
+
}, 1e3);
|
|
26199
|
+
scheduleFlush();
|
|
26200
|
+
});
|
|
26201
|
+
} catch (err) {
|
|
26202
|
+
console.warn(`[Extensions] failed to watch ${root}: ${err instanceof Error ? err.message : String(err)} \u2014 live extension updates disabled`);
|
|
26203
|
+
watcher = null;
|
|
26204
|
+
}
|
|
26205
|
+
}
|
|
26206
|
+
function closeWatcher() {
|
|
26207
|
+
if (watcher) {
|
|
26208
|
+
watcher.close();
|
|
26209
|
+
watcher = null;
|
|
26210
|
+
}
|
|
26211
|
+
}
|
|
26212
|
+
function start() {
|
|
26213
|
+
if (watcher)
|
|
26214
|
+
return;
|
|
26215
|
+
const root = extensionsRoot();
|
|
26216
|
+
try {
|
|
26217
|
+
fs55.mkdirSync(root, { recursive: true });
|
|
26218
|
+
} catch (err) {
|
|
26219
|
+
console.warn(`[Extensions] cannot create ${root}: ${err instanceof Error ? err.message : String(err)}`);
|
|
26220
|
+
}
|
|
26221
|
+
lastKey = snapshotKey(scanExtensions());
|
|
26222
|
+
openWatcher(root);
|
|
26223
|
+
const { extensions, errors } = getSnapshot();
|
|
26224
|
+
if (extensions.length > 0 || errors.length > 0) {
|
|
26225
|
+
console.log(`[Extensions] ${extensions.length} installed${errors.length ? `, ${errors.length} invalid` : ""}: ${[...extensions.map((e) => `${e.id}@${e.version}`), ...errors.map((e) => `${e.id}(!)`)].join(", ")}`);
|
|
26226
|
+
}
|
|
26227
|
+
}
|
|
26228
|
+
function stop() {
|
|
26229
|
+
if (flushTimer)
|
|
26230
|
+
clearTimeout(flushTimer);
|
|
26231
|
+
flushTimer = null;
|
|
26232
|
+
if (restartTimer)
|
|
26233
|
+
clearTimeout(restartTimer);
|
|
26234
|
+
restartTimer = null;
|
|
26235
|
+
closeWatcher();
|
|
26236
|
+
}
|
|
26237
|
+
var watcher, flushTimer, restartTimer, lastKey;
|
|
26238
|
+
var init_watcher = __esm({
|
|
26239
|
+
"../server/dist/extensions/watcher.js"() {
|
|
26240
|
+
"use strict";
|
|
26241
|
+
init_broadcast();
|
|
26242
|
+
init_registry2();
|
|
26243
|
+
watcher = null;
|
|
26244
|
+
flushTimer = null;
|
|
26245
|
+
restartTimer = null;
|
|
26246
|
+
lastKey = "";
|
|
26247
|
+
}
|
|
26248
|
+
});
|
|
26249
|
+
|
|
26250
|
+
// ../server/dist/middleware/auth.js
|
|
26251
|
+
import { Hono as Hono15 } from "hono";
|
|
26252
|
+
import { getCookie, setCookie } from "hono/cookie";
|
|
26253
|
+
import crypto3 from "node:crypto";
|
|
26254
|
+
function passwordHash() {
|
|
26255
|
+
return config.server.password ?? "";
|
|
26256
|
+
}
|
|
26257
|
+
function passwordEnvPlaintext() {
|
|
26258
|
+
return config.server.passwordEnvPlaintext;
|
|
26259
|
+
}
|
|
26260
|
+
function jwtSecret() {
|
|
26261
|
+
const secret = config.server.jwtSecret ?? "";
|
|
26262
|
+
if (!secret) {
|
|
26263
|
+
throw new Error("jwt_secret missing \u2014 run `halo setup` to initialize");
|
|
26264
|
+
}
|
|
26265
|
+
return Buffer.from(secret, "base64");
|
|
26266
|
+
}
|
|
26267
|
+
function base64url(data) {
|
|
26268
|
+
const buf = typeof data === "string" ? Buffer.from(data) : data;
|
|
26269
|
+
return buf.toString("base64url");
|
|
26270
|
+
}
|
|
26271
|
+
function jwtSign(payload) {
|
|
26272
|
+
const header = base64url(JSON.stringify({ alg: "HS256", typ: "JWT" }));
|
|
26273
|
+
const body = base64url(JSON.stringify(payload));
|
|
26274
|
+
const sig = crypto3.createHmac("sha256", jwtSecret()).update(`${header}.${body}`).digest();
|
|
26275
|
+
return `${header}.${body}.${base64url(sig)}`;
|
|
26276
|
+
}
|
|
26277
|
+
function jwtVerify(token) {
|
|
26278
|
+
const parts = token.split(".");
|
|
26279
|
+
if (parts.length !== 3)
|
|
26280
|
+
return null;
|
|
26281
|
+
const [header, body, sig] = parts;
|
|
26282
|
+
let secret;
|
|
26283
|
+
try {
|
|
26284
|
+
secret = jwtSecret();
|
|
26285
|
+
} catch {
|
|
26286
|
+
return null;
|
|
26287
|
+
}
|
|
26288
|
+
const expected = base64url(crypto3.createHmac("sha256", secret).update(`${header}.${body}`).digest());
|
|
26289
|
+
if (sig !== expected)
|
|
26290
|
+
return null;
|
|
26291
|
+
try {
|
|
26292
|
+
const payload = JSON.parse(Buffer.from(body, "base64url").toString());
|
|
26293
|
+
if (typeof payload.exp === "number" && Date.now() / 1e3 > payload.exp)
|
|
26294
|
+
return null;
|
|
26295
|
+
return payload;
|
|
26296
|
+
} catch {
|
|
26297
|
+
return null;
|
|
26298
|
+
}
|
|
26299
|
+
}
|
|
26300
|
+
function createToken() {
|
|
26301
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
26302
|
+
return jwtSign({ iat: now, exp: now + TOKEN_MAX_AGE });
|
|
26303
|
+
}
|
|
26304
|
+
function constantTimeEqualString(a, b) {
|
|
26305
|
+
const ab = Buffer.from(a, "utf-8");
|
|
26306
|
+
const bb = Buffer.from(b, "utf-8");
|
|
26307
|
+
const max = Math.max(ab.length, bb.length);
|
|
26308
|
+
const ap = Buffer.alloc(max);
|
|
26309
|
+
ab.copy(ap);
|
|
26310
|
+
const bp = Buffer.alloc(max);
|
|
26311
|
+
bb.copy(bp);
|
|
26312
|
+
const eq17 = crypto3.timingSafeEqual(ap, bp);
|
|
26313
|
+
return eq17 && ab.length === bb.length;
|
|
26314
|
+
}
|
|
26315
|
+
function validateToken(token) {
|
|
26316
|
+
if (!token)
|
|
26317
|
+
return null;
|
|
26318
|
+
const payload = jwtVerify(token);
|
|
26319
|
+
if (payload?.scope)
|
|
26320
|
+
return null;
|
|
26321
|
+
return payload;
|
|
26322
|
+
}
|
|
26323
|
+
function mintScopedToken(scope, maxAgeSec) {
|
|
26324
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
26325
|
+
return jwtSign({ scope, iat: now, exp: now + maxAgeSec });
|
|
26326
|
+
}
|
|
26327
|
+
function verifyScopedToken(token, scope) {
|
|
26328
|
+
if (!token)
|
|
26329
|
+
return false;
|
|
26330
|
+
const payload = jwtVerify(token);
|
|
26331
|
+
return payload !== null && payload.scope === scope;
|
|
26332
|
+
}
|
|
26333
|
+
function shouldRefresh(payload) {
|
|
26334
|
+
const iat = payload.iat;
|
|
26335
|
+
if (!iat)
|
|
26336
|
+
return true;
|
|
26337
|
+
return Date.now() / 1e3 - iat > REFRESH_AFTER;
|
|
26338
|
+
}
|
|
26339
|
+
function isAuthDisabled() {
|
|
26340
|
+
return passwordHash().length === 0 && passwordEnvPlaintext() == null;
|
|
26341
|
+
}
|
|
26342
|
+
function isAuthenticated(cookie) {
|
|
26343
|
+
if (isAuthDisabled())
|
|
26344
|
+
return true;
|
|
26345
|
+
return validateToken(cookie) !== null;
|
|
26346
|
+
}
|
|
26347
|
+
function getTokenFromCookieHeader(cookieHeader) {
|
|
26348
|
+
if (!cookieHeader)
|
|
26349
|
+
return void 0;
|
|
26350
|
+
const match = cookieHeader.match(new RegExp(`(?:^|;\\s*)${COOKIE_NAME}=([^;]+)`));
|
|
26351
|
+
return match?.[1];
|
|
26352
|
+
}
|
|
26353
|
+
function setTokenCookie(c, token) {
|
|
26354
|
+
setCookie(c, COOKIE_NAME, token, {
|
|
26355
|
+
path: "/",
|
|
26356
|
+
// WS upgrade requests carry cookies on the underlying HTTP handshake;
|
|
26357
|
+
// the server reads them via the Cookie header, never via JS — so the
|
|
26358
|
+
// cookie can stay httpOnly to avoid XSS exfiltration.
|
|
26359
|
+
httpOnly: true,
|
|
26360
|
+
maxAge: TOKEN_MAX_AGE,
|
|
26361
|
+
sameSite: "Lax"
|
|
26362
|
+
});
|
|
26363
|
+
}
|
|
26364
|
+
function createAuthRoutes() {
|
|
26365
|
+
const app2 = new Hono15();
|
|
26366
|
+
app2.post("/auth/login", async (c) => {
|
|
26367
|
+
const ip = getClientIp(c);
|
|
26368
|
+
if (isLockedOut2(ip)) {
|
|
26369
|
+
return c.json({ error: "Too many attempts, try again later" }, 429);
|
|
26370
|
+
}
|
|
26371
|
+
const body = await c.req.json().catch(() => ({ password: "" }));
|
|
26372
|
+
if (!body.password) {
|
|
26373
|
+
recordFailure2(ip);
|
|
26374
|
+
return c.json({ error: "Invalid password" }, 401);
|
|
26375
|
+
}
|
|
26376
|
+
const envPlain = passwordEnvPlaintext();
|
|
26377
|
+
const ok = envPlain != null ? constantTimeEqualString(body.password, envPlain) : await verifyPassword(body.password, passwordHash());
|
|
26378
|
+
if (!ok) {
|
|
26379
|
+
recordFailure2(ip);
|
|
26380
|
+
return c.json({ error: "Invalid password" }, 401);
|
|
26381
|
+
}
|
|
26382
|
+
clearFailures2(ip);
|
|
26383
|
+
const token = createToken();
|
|
26384
|
+
setTokenCookie(c, token);
|
|
26385
|
+
return c.json({ ok: true });
|
|
26386
|
+
});
|
|
26387
|
+
app2.get("/auth/check", (c) => {
|
|
26388
|
+
const token = getCookie(c, COOKIE_NAME);
|
|
26389
|
+
const payload = validateToken(token);
|
|
26390
|
+
if (payload) {
|
|
26391
|
+
if (shouldRefresh(payload)) {
|
|
26392
|
+
const newToken = createToken();
|
|
26393
|
+
setTokenCookie(c, newToken);
|
|
26394
|
+
}
|
|
26395
|
+
return c.json({ authenticated: true, badge: config.server.badge, goalModeEnabled: config.goalModeEnabled });
|
|
26396
|
+
}
|
|
26397
|
+
return c.json({ authenticated: false, badge: config.server.badge }, 401);
|
|
26398
|
+
});
|
|
26399
|
+
app2.post("/auth/logout", (c) => {
|
|
26400
|
+
setCookie(c, COOKIE_NAME, "", { path: "/", maxAge: 0 });
|
|
26401
|
+
return c.json({ ok: true });
|
|
26402
|
+
});
|
|
26403
|
+
app2.post("/auth/change-password", async (c) => {
|
|
26404
|
+
const body = await c.req.json().catch(() => ({}));
|
|
26405
|
+
const oldPassword = typeof body.oldPassword === "string" ? body.oldPassword : "";
|
|
26406
|
+
const newPassword = typeof body.newPassword === "string" ? body.newPassword : "";
|
|
26407
|
+
if (passwordEnvPlaintext() != null) {
|
|
26408
|
+
return c.json({ error: "Password is managed by the HALO_PASSWORD environment variable" }, 400);
|
|
26409
|
+
}
|
|
26410
|
+
if (!await verifyPassword(oldPassword, passwordHash())) {
|
|
26411
|
+
return c.json({ error: "Incorrect current password" }, 401);
|
|
26412
|
+
}
|
|
26413
|
+
const weak = passwordStrengthError(newPassword);
|
|
26414
|
+
if (weak)
|
|
26415
|
+
return c.json({ error: weak }, 400);
|
|
26416
|
+
if (newPassword === oldPassword) {
|
|
26417
|
+
return c.json({ error: "New password must differ from the current password" }, 400);
|
|
26418
|
+
}
|
|
26419
|
+
updateConfigLeaves({ "server.password": await hashPassword(newPassword) });
|
|
26420
|
+
return c.json({ ok: true });
|
|
26421
|
+
});
|
|
26422
|
+
return app2;
|
|
26423
|
+
}
|
|
26424
|
+
function passwordStrengthError(pw) {
|
|
26425
|
+
if (pw.length < 8)
|
|
26426
|
+
return "Password must be at least 8 characters";
|
|
26427
|
+
if (!/[A-Za-z]/.test(pw))
|
|
26428
|
+
return "Password must contain at least one letter";
|
|
26429
|
+
if (!/[0-9]/.test(pw))
|
|
26430
|
+
return "Password must contain at least one digit";
|
|
26431
|
+
return null;
|
|
26432
|
+
}
|
|
26433
|
+
function authMiddleware() {
|
|
26434
|
+
return async (c, next) => {
|
|
26435
|
+
const path74 = c.req.path;
|
|
26436
|
+
if (PUBLIC_PATHS.some((p) => path74 === p || path74.startsWith(p + "?"))) {
|
|
26437
|
+
return next();
|
|
26438
|
+
}
|
|
26439
|
+
if (EXTENSION_ASSET_PATH.test(path74)) {
|
|
26440
|
+
return next();
|
|
26441
|
+
}
|
|
26442
|
+
const token = getCookie(c, COOKIE_NAME);
|
|
26443
|
+
const payload = validateToken(token);
|
|
26444
|
+
if (!payload) {
|
|
26445
|
+
if (!path74.startsWith("/api/")) {
|
|
26446
|
+
return next();
|
|
26447
|
+
}
|
|
26448
|
+
return c.json({ error: "Unauthorized" }, 401);
|
|
26449
|
+
}
|
|
26450
|
+
if (shouldRefresh(payload)) {
|
|
26451
|
+
const newToken = createToken();
|
|
26452
|
+
setTokenCookie(c, newToken);
|
|
26453
|
+
}
|
|
26454
|
+
return next();
|
|
26455
|
+
};
|
|
26456
|
+
}
|
|
26457
|
+
var TOKEN_MAX_AGE, REFRESH_AFTER, COOKIE_NAME, ADMIN_LOGIN_BUCKET, isLockedOut2, recordFailure2, clearFailures2, PUBLIC_PATHS, EXTENSION_ASSET_PATH;
|
|
26458
|
+
var init_auth = __esm({
|
|
26459
|
+
"../server/dist/middleware/auth.js"() {
|
|
26460
|
+
"use strict";
|
|
26461
|
+
init_config();
|
|
26462
|
+
init_password_hash();
|
|
26463
|
+
init_setup_config();
|
|
26464
|
+
init_brute_force();
|
|
26465
|
+
TOKEN_MAX_AGE = config.auth.tokenMaxAge;
|
|
26466
|
+
REFRESH_AFTER = config.auth.refreshAfter;
|
|
26467
|
+
COOKIE_NAME = "halo_token";
|
|
26468
|
+
ADMIN_LOGIN_BUCKET = "admin-login";
|
|
26469
|
+
isLockedOut2 = (ip) => isLockedOut(ADMIN_LOGIN_BUCKET, ip);
|
|
26470
|
+
recordFailure2 = (ip) => recordFailure(ADMIN_LOGIN_BUCKET, ip);
|
|
26471
|
+
clearFailures2 = (ip) => clearFailures(ADMIN_LOGIN_BUCKET, ip);
|
|
26472
|
+
PUBLIC_PATHS = ["/api/auth/login", "/api/auth/check", "/api/auth/logout", "/api/health", "/api/web/chat", "/api/web/sessions", "/api/web/stop", "/api/web/history", "/api/web/subscribe", "/api/web/file", "/api/show/state", "/api/show/session", "/api/metrics"];
|
|
26473
|
+
EXTENSION_ASSET_PATH = /^\/api\/extensions\/[^/]+\/[^/]+\/[^/]+\/./;
|
|
26474
|
+
}
|
|
26475
|
+
});
|
|
26476
|
+
|
|
26477
|
+
// ../server/dist/routes/extensions.js
|
|
26478
|
+
import { Hono as Hono16 } from "hono";
|
|
26479
|
+
import fs56 from "node:fs";
|
|
26480
|
+
import path63 from "node:path";
|
|
26481
|
+
import { Readable as Readable2 } from "node:stream";
|
|
26482
|
+
function assetMime(fileName) {
|
|
26483
|
+
const ext = fileName.split(".").pop()?.toLowerCase() ?? "";
|
|
26484
|
+
return imageMimeFromExt(ext) ?? ASSET_MIME[ext] ?? "application/octet-stream";
|
|
26485
|
+
}
|
|
26486
|
+
function resolveAsset(id, version, rest) {
|
|
26487
|
+
if (!isExtensionId(id))
|
|
26488
|
+
return null;
|
|
26489
|
+
const info = getExtension(id);
|
|
26490
|
+
if (!info || info.version !== version)
|
|
26491
|
+
return null;
|
|
26492
|
+
const segments = rest.split("/");
|
|
26493
|
+
if (segments.some((s) => s === "" || s === "." || s === ".." || s.startsWith(".") || s.includes("\\")))
|
|
26494
|
+
return null;
|
|
26495
|
+
let dir;
|
|
26496
|
+
let real;
|
|
26497
|
+
try {
|
|
26498
|
+
dir = fs56.realpathSync(path63.join(extensionsRoot(), id));
|
|
26499
|
+
real = fs56.realpathSync(path63.join(dir, ...segments));
|
|
26500
|
+
} catch {
|
|
26501
|
+
return null;
|
|
26502
|
+
}
|
|
26503
|
+
if (real !== dir && !real.startsWith(dir + path63.sep))
|
|
26504
|
+
return null;
|
|
26505
|
+
try {
|
|
26506
|
+
if (!fs56.statSync(real).isFile())
|
|
26507
|
+
return null;
|
|
26508
|
+
} catch {
|
|
26509
|
+
return null;
|
|
26510
|
+
}
|
|
26511
|
+
return real;
|
|
26512
|
+
}
|
|
26513
|
+
function createExtensionRoutes() {
|
|
26514
|
+
const app2 = new Hono16();
|
|
26515
|
+
app2.get("/extensions", (c) => c.json(getSnapshot()));
|
|
26516
|
+
app2.get("/extensions/token", (c) => c.json({
|
|
26517
|
+
token: mintScopedToken(EXT_TOKEN_SCOPE, EXT_TOKEN_MAX_AGE),
|
|
26518
|
+
expiresAt: Date.now() + EXT_TOKEN_MAX_AGE * 1e3
|
|
26519
|
+
}));
|
|
26520
|
+
app2.post("/extensions/install", async (c) => {
|
|
26521
|
+
if (Number(c.req.header("content-length") ?? 0) > MAX_ZIP_BYTES)
|
|
26522
|
+
return c.json({ error: `zip exceeds ${MAX_ZIP_BYTES / 1024 / 1024} MB` }, 413);
|
|
26523
|
+
let file = null;
|
|
26524
|
+
try {
|
|
26525
|
+
const form = await c.req.formData();
|
|
26526
|
+
const f = form.get("file");
|
|
26527
|
+
file = f instanceof File ? f : null;
|
|
26528
|
+
} catch {
|
|
26529
|
+
return c.json({ error: "multipart form with a `file` field is required" }, 400);
|
|
26530
|
+
}
|
|
26531
|
+
if (!file)
|
|
26532
|
+
return c.json({ error: "`file` (zip) is required" }, 400);
|
|
26533
|
+
if (file.size > MAX_ZIP_BYTES)
|
|
26534
|
+
return c.json({ error: `zip exceeds ${MAX_ZIP_BYTES / 1024 / 1024} MB` }, 413);
|
|
26535
|
+
try {
|
|
26536
|
+
const info = await installExtensionZip(Buffer.from(await file.arrayBuffer()));
|
|
26537
|
+
rescanAndBroadcast();
|
|
26538
|
+
return c.json(info);
|
|
26539
|
+
} catch (err) {
|
|
26540
|
+
if (err instanceof ExtensionInstallError)
|
|
26541
|
+
return c.json({ error: err.message }, 400);
|
|
26542
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
26543
|
+
console.log(`[Extensions] install failed: ${msg}`);
|
|
26544
|
+
return c.json({ error: msg }, 500);
|
|
26545
|
+
}
|
|
26546
|
+
});
|
|
26547
|
+
app2.delete("/extensions/:id", (c) => {
|
|
26548
|
+
const id = c.req.param("id");
|
|
26549
|
+
if (!isExtensionId(id))
|
|
26550
|
+
return c.json({ error: "Invalid extension id" }, 400);
|
|
26551
|
+
try {
|
|
26552
|
+
if (!uninstallExtension(id))
|
|
26553
|
+
return c.json({ error: "Extension not installed" }, 404);
|
|
26554
|
+
rescanAndBroadcast();
|
|
26555
|
+
return c.json({ ok: true, id });
|
|
26556
|
+
} catch (err) {
|
|
26557
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
26558
|
+
console.log(`[Extensions] uninstall ${id} failed: ${msg}`);
|
|
26559
|
+
return c.json({ error: msg }, 500);
|
|
26560
|
+
}
|
|
26561
|
+
});
|
|
26562
|
+
app2.get("/extensions/:id/:version/:token/:rest{.+}", (c) => {
|
|
26563
|
+
if (!verifyScopedToken(c.req.param("token"), EXT_TOKEN_SCOPE))
|
|
26564
|
+
return c.json({ error: "Unauthorized" }, 401);
|
|
26565
|
+
const abs = resolveAsset(c.req.param("id"), c.req.param("version"), c.req.param("rest"));
|
|
26566
|
+
if (!abs)
|
|
26567
|
+
return c.json({ error: "Not found" }, 404);
|
|
26568
|
+
const stat = fs56.statSync(abs);
|
|
26569
|
+
const nodeStream = fs56.createReadStream(abs);
|
|
26570
|
+
c.req.raw.signal?.addEventListener("abort", () => nodeStream.destroy(), { once: true });
|
|
26571
|
+
return new Response(Readable2.toWeb(nodeStream), {
|
|
26572
|
+
headers: {
|
|
26573
|
+
"Content-Type": assetMime(abs),
|
|
26574
|
+
"Content-Length": String(stat.size),
|
|
26575
|
+
// The version segment changes on every upgrade, so the URL is a
|
|
26576
|
+
// content address as far as the browser is concerned.
|
|
26577
|
+
"Cache-Control": "public, max-age=31536000, immutable",
|
|
26578
|
+
// Module scripts / fetch / wasm / dynamic import are CORS-mode
|
|
26579
|
+
// requests; the iframe is same-origin today so this is a no-op, kept
|
|
26580
|
+
// so the assets keep loading if the host ever drops allow-same-origin.
|
|
26581
|
+
"Access-Control-Allow-Origin": "*",
|
|
26582
|
+
// The token is in the URL — keep it out of Referer headers.
|
|
26583
|
+
"Referrer-Policy": "no-referrer"
|
|
26584
|
+
}
|
|
26585
|
+
});
|
|
26586
|
+
});
|
|
26587
|
+
return app2;
|
|
26588
|
+
}
|
|
26589
|
+
var MAX_ZIP_BYTES, EXT_TOKEN_MAX_AGE, EXT_TOKEN_SCOPE, ASSET_MIME;
|
|
26590
|
+
var init_extensions = __esm({
|
|
26591
|
+
"../server/dist/routes/extensions.js"() {
|
|
26592
|
+
"use strict";
|
|
26593
|
+
init_dist();
|
|
26594
|
+
init_registry2();
|
|
26595
|
+
init_install();
|
|
26596
|
+
init_watcher();
|
|
26597
|
+
init_auth();
|
|
26598
|
+
MAX_ZIP_BYTES = 100 * 1024 * 1024;
|
|
26599
|
+
EXT_TOKEN_MAX_AGE = 24 * 60 * 60;
|
|
26600
|
+
EXT_TOKEN_SCOPE = "ext";
|
|
26601
|
+
ASSET_MIME = {
|
|
26602
|
+
html: "text/html; charset=utf-8",
|
|
26603
|
+
htm: "text/html; charset=utf-8",
|
|
26604
|
+
js: "text/javascript; charset=utf-8",
|
|
26605
|
+
mjs: "text/javascript; charset=utf-8",
|
|
26606
|
+
css: "text/css; charset=utf-8",
|
|
26607
|
+
json: "application/json; charset=utf-8",
|
|
26608
|
+
map: "application/json; charset=utf-8",
|
|
26609
|
+
wasm: "application/wasm",
|
|
26610
|
+
txt: "text/plain; charset=utf-8",
|
|
26611
|
+
md: "text/plain; charset=utf-8",
|
|
26612
|
+
woff: "font/woff",
|
|
26613
|
+
woff2: "font/woff2",
|
|
26614
|
+
ttf: "font/ttf",
|
|
26615
|
+
otf: "font/otf",
|
|
26616
|
+
hdr: "application/octet-stream",
|
|
26617
|
+
bin: "application/octet-stream"
|
|
26618
|
+
};
|
|
26619
|
+
}
|
|
26620
|
+
});
|
|
26621
|
+
|
|
26622
|
+
// ../server/dist/evolution/ticker.js
|
|
26623
|
+
import { eq as eq16, and as and12, or as or2, asc, isNull as isNull6, sql as sql2 } from "drizzle-orm";
|
|
26624
|
+
function setEvoSpawner(fn) {
|
|
26625
|
+
_spawner = fn;
|
|
26626
|
+
}
|
|
26627
|
+
function startEvoTicker() {
|
|
26628
|
+
if (_interval)
|
|
26629
|
+
return;
|
|
26630
|
+
_startupTimer2 = setTimeout(() => {
|
|
26631
|
+
_startupTimer2 = null;
|
|
26632
|
+
runTick().catch(logTickError);
|
|
26633
|
+
}, 2e3);
|
|
26634
|
+
_interval = setInterval(() => {
|
|
26635
|
+
runTick().catch(logTickError);
|
|
26636
|
+
}, TICK_INTERVAL_MS);
|
|
26637
|
+
console.log(`[EvoTicker] started (interval: ${TICK_INTERVAL_MS / 1e3}s)`);
|
|
26638
|
+
}
|
|
26639
|
+
function stopEvoTicker() {
|
|
26640
|
+
if (_startupTimer2) {
|
|
26641
|
+
clearTimeout(_startupTimer2);
|
|
26642
|
+
_startupTimer2 = null;
|
|
26643
|
+
}
|
|
26644
|
+
if (_interval) {
|
|
26645
|
+
clearInterval(_interval);
|
|
26646
|
+
_interval = null;
|
|
26647
|
+
console.log("[EvoTicker] stopped");
|
|
26648
|
+
}
|
|
26649
|
+
}
|
|
26650
|
+
function logTickError(err) {
|
|
26651
|
+
console.error(`[EvoTicker] tick failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
26652
|
+
}
|
|
26653
|
+
async function runTick() {
|
|
26654
|
+
markTimeouts();
|
|
26655
|
+
startPendingRuns();
|
|
26656
|
+
startPendingApplies();
|
|
26657
|
+
broadcastChanges();
|
|
26658
|
+
}
|
|
26659
|
+
function broadcastChanges() {
|
|
26660
|
+
const db = getEvoDb();
|
|
26661
|
+
const inFlightLimit = (config.evolution.maxConcurrentRun + config.evolution.maxConcurrentApply) * 3;
|
|
26662
|
+
const runRows = db.select({ id: evolutionRuns.id, status: evolutionRuns.status }).from(evolutionRuns).where(or2(eq16(evolutionRuns.status, "pending"), eq16(evolutionRuns.status, "running"))).limit(inFlightLimit).all();
|
|
26663
|
+
for (const r of runRows) {
|
|
26664
|
+
if (_lastRunStatus.get(r.id) !== r.status) {
|
|
26665
|
+
_lastRunStatus.set(r.id, r.status);
|
|
26666
|
+
broadcast({ type: "evolution:run_changed", id: r.id, status: r.status });
|
|
26667
|
+
}
|
|
26668
|
+
}
|
|
26669
|
+
const applyRows = db.select({ id: evolutionApplies.id, status: evolutionApplies.status }).from(evolutionApplies).where(or2(eq16(evolutionApplies.status, "pending"), eq16(evolutionApplies.status, "running"), eq16(evolutionApplies.status, "syncing"))).limit(inFlightLimit).all();
|
|
26670
|
+
for (const a of applyRows) {
|
|
26671
|
+
if (_lastApplyStatus.get(a.id) !== a.status) {
|
|
26672
|
+
_lastApplyStatus.set(a.id, a.status);
|
|
26673
|
+
broadcast({ type: "evolution:apply_changed", id: a.id, status: a.status });
|
|
26674
|
+
}
|
|
26675
|
+
}
|
|
26676
|
+
const aliveRunIds = new Set(runRows.map((r) => r.id));
|
|
26677
|
+
for (const id of [..._lastRunStatus.keys()]) {
|
|
26678
|
+
if (aliveRunIds.has(id))
|
|
26679
|
+
continue;
|
|
26680
|
+
const row = db.select({ status: evolutionRuns.status }).from(evolutionRuns).where(eq16(evolutionRuns.id, id)).get();
|
|
26681
|
+
const finalStatus = row?.status;
|
|
26682
|
+
if (finalStatus && _lastRunStatus.get(id) !== finalStatus) {
|
|
26683
|
+
broadcast({ type: "evolution:run_changed", id, status: finalStatus });
|
|
26684
|
+
}
|
|
26685
|
+
_lastRunStatus.delete(id);
|
|
26686
|
+
}
|
|
26687
|
+
const aliveApplyIds = new Set(applyRows.map((r) => r.id));
|
|
26688
|
+
for (const id of [..._lastApplyStatus.keys()]) {
|
|
26689
|
+
if (aliveApplyIds.has(id))
|
|
26690
|
+
continue;
|
|
26691
|
+
const row = db.select({ status: evolutionApplies.status }).from(evolutionApplies).where(eq16(evolutionApplies.id, id)).get();
|
|
26692
|
+
const finalStatus = row?.status;
|
|
26693
|
+
if (finalStatus && _lastApplyStatus.get(id) !== finalStatus) {
|
|
26694
|
+
broadcast({ type: "evolution:apply_changed", id, status: finalStatus });
|
|
26695
|
+
}
|
|
26696
|
+
_lastApplyStatus.delete(id);
|
|
26697
|
+
}
|
|
26698
|
+
}
|
|
26699
|
+
function markTimeouts() {
|
|
26700
|
+
const db = getEvoDb();
|
|
26701
|
+
const now = Date.now();
|
|
26702
|
+
const runCutoff = now - config.evolution.runTimeoutMinutes * 6e4;
|
|
26703
|
+
const applyCutoff = now - config.evolution.applyTimeoutMinutes * 6e4;
|
|
25659
26704
|
const maxAttempts = config.evolution.maxAttempts;
|
|
25660
26705
|
const runRows = db.select({
|
|
25661
26706
|
id: evolutionRuns.id,
|
|
@@ -25817,7 +26862,7 @@ var init_ticker = __esm({
|
|
|
25817
26862
|
});
|
|
25818
26863
|
|
|
25819
26864
|
// ../server/dist/evolution/spawn.js
|
|
25820
|
-
import
|
|
26865
|
+
import path64 from "node:path";
|
|
25821
26866
|
import { spawn as spawn4 } from "node:child_process";
|
|
25822
26867
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
25823
26868
|
var __dirname2, WRAPPER_PATH, realEvoSpawner;
|
|
@@ -25825,8 +26870,8 @@ var init_spawn = __esm({
|
|
|
25825
26870
|
"../server/dist/evolution/spawn.js"() {
|
|
25826
26871
|
"use strict";
|
|
25827
26872
|
init_child_env();
|
|
25828
|
-
__dirname2 =
|
|
25829
|
-
WRAPPER_PATH =
|
|
26873
|
+
__dirname2 = path64.dirname(fileURLToPath2(import.meta.url));
|
|
26874
|
+
WRAPPER_PATH = path64.resolve(__dirname2, "evo-wrapper.js");
|
|
25830
26875
|
realEvoSpawner = (mode, id) => {
|
|
25831
26876
|
const child = spawn4(process.execPath, [WRAPPER_PATH, `--mode=${mode}`, `--id=${id}`], {
|
|
25832
26877
|
detached: true,
|
|
@@ -25868,7 +26913,7 @@ async function shutdownChannels() {
|
|
|
25868
26913
|
_running.length = 0;
|
|
25869
26914
|
}
|
|
25870
26915
|
var _running;
|
|
25871
|
-
var
|
|
26916
|
+
var init_registry3 = __esm({
|
|
25872
26917
|
"../server/dist/channels/registry.js"() {
|
|
25873
26918
|
"use strict";
|
|
25874
26919
|
_running = [];
|
|
@@ -26223,6 +27268,7 @@ var init_inbound = __esm({
|
|
|
26223
27268
|
});
|
|
26224
27269
|
|
|
26225
27270
|
// ../server/dist/channels/telegram/handler.js
|
|
27271
|
+
import path65 from "node:path";
|
|
26226
27272
|
import { Bot, InputFile } from "grammy";
|
|
26227
27273
|
function inferMediaKind(filePath) {
|
|
26228
27274
|
const cls = classifyMedia(filePath);
|
|
@@ -26338,25 +27384,33 @@ function startTelegramChannel(deps) {
|
|
|
26338
27384
|
const route = bridge.getRoute(sessionId);
|
|
26339
27385
|
if (!route)
|
|
26340
27386
|
return;
|
|
26341
|
-
|
|
26342
|
-
|
|
26343
|
-
|
|
26344
|
-
|
|
26345
|
-
|
|
26346
|
-
|
|
26347
|
-
|
|
26348
|
-
|
|
26349
|
-
|
|
26350
|
-
|
|
26351
|
-
|
|
26352
|
-
|
|
26353
|
-
|
|
26354
|
-
|
|
26355
|
-
|
|
26356
|
-
|
|
26357
|
-
|
|
26358
|
-
|
|
26359
|
-
|
|
27387
|
+
try {
|
|
27388
|
+
if (!isMediaPathAllowed(filePath, account.workspacePath, account.accessLevel)) {
|
|
27389
|
+
throw new Error(`media path not allowed: ${filePath} (must be under the workspace or the temp dir; account access level ${account.accessLevel})`);
|
|
27390
|
+
}
|
|
27391
|
+
const kind = inferMediaKind(filePath);
|
|
27392
|
+
const file = new InputFile(filePath);
|
|
27393
|
+
switch (kind) {
|
|
27394
|
+
case "photo":
|
|
27395
|
+
await bot.api.sendPhoto(route.chatId, file);
|
|
27396
|
+
break;
|
|
27397
|
+
case "video":
|
|
27398
|
+
await bot.api.sendVideo(route.chatId, file);
|
|
27399
|
+
break;
|
|
27400
|
+
case "voice":
|
|
27401
|
+
await bot.api.sendVoice(route.chatId, file);
|
|
27402
|
+
break;
|
|
27403
|
+
case "document":
|
|
27404
|
+
await bot.api.sendDocument(route.chatId, file);
|
|
27405
|
+
break;
|
|
27406
|
+
}
|
|
27407
|
+
} catch (err) {
|
|
27408
|
+
console.log(`[Telegram] sendMedia ${filePath} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
27409
|
+
await bot.api.sendMessage(route.chatId, t("handler.upload_failed", getLang(account), {
|
|
27410
|
+
name: path65.basename(filePath),
|
|
27411
|
+
error: err instanceof Error ? err.message : String(err)
|
|
27412
|
+
})).catch(() => {
|
|
27413
|
+
});
|
|
26360
27414
|
}
|
|
26361
27415
|
}
|
|
26362
27416
|
})
|
|
@@ -26421,7 +27475,8 @@ async function runBot(args) {
|
|
|
26421
27475
|
bot.command("start", async (ctx) => {
|
|
26422
27476
|
await ctx.reply(t("handler.start_greeting", lang));
|
|
26423
27477
|
});
|
|
26424
|
-
for (const
|
|
27478
|
+
for (const command of DISPATCH_COMMANDS) {
|
|
27479
|
+
const cmd = command.slice(1);
|
|
26425
27480
|
bot.command(cmd, async (ctx) => {
|
|
26426
27481
|
const cmdCtx = buildCmdCtx4(ctx.from?.id ?? 0, ctx.chat?.id);
|
|
26427
27482
|
if (!cmdCtx) {
|
|
@@ -26635,9 +27690,9 @@ var init_handler3 = __esm({
|
|
|
26635
27690
|
init_event_adapter();
|
|
26636
27691
|
init_media_store();
|
|
26637
27692
|
init_accounts();
|
|
27693
|
+
init_commands2();
|
|
26638
27694
|
init_inbound();
|
|
26639
27695
|
init_i18n();
|
|
26640
|
-
init_commands();
|
|
26641
27696
|
init_media();
|
|
26642
27697
|
init_session_prefix();
|
|
26643
27698
|
MAX_TG_DOWNLOAD_BYTES = 20 * 1024 * 1024;
|
|
@@ -26645,19 +27700,19 @@ var init_handler3 = __esm({
|
|
|
26645
27700
|
});
|
|
26646
27701
|
|
|
26647
27702
|
// ../server/dist/routes/telegram.js
|
|
26648
|
-
import { Hono as
|
|
27703
|
+
import { Hono as Hono17 } from "hono";
|
|
26649
27704
|
import { Bot as Bot2 } from "grammy";
|
|
26650
|
-
import
|
|
27705
|
+
import fs57 from "node:fs";
|
|
26651
27706
|
function createTelegramRoutes(deps) {
|
|
26652
27707
|
const { db, channel } = deps;
|
|
26653
|
-
const app2 = new
|
|
27708
|
+
const app2 = new Hono17();
|
|
26654
27709
|
app2.get("/telegram/accounts", (c) => {
|
|
26655
27710
|
const accounts = listAccounts3(db).map((acc) => {
|
|
26656
27711
|
return {
|
|
26657
27712
|
accountId: acc.accountId,
|
|
26658
27713
|
botUsername: acc.botUsername,
|
|
26659
27714
|
workspacePath: acc.workspacePath,
|
|
26660
|
-
workspaceMissing: !
|
|
27715
|
+
workspaceMissing: !fs57.existsSync(acc.workspacePath),
|
|
26661
27716
|
label: acc.label,
|
|
26662
27717
|
enabled: acc.enabled,
|
|
26663
27718
|
accessLevel: acc.accessLevel,
|
|
@@ -26846,7 +27901,7 @@ var init_descriptor = __esm({
|
|
|
26846
27901
|
});
|
|
26847
27902
|
|
|
26848
27903
|
// ../server/dist/channels/wechat/api.js
|
|
26849
|
-
import
|
|
27904
|
+
import crypto4 from "node:crypto";
|
|
26850
27905
|
function buildClientVersion(version) {
|
|
26851
27906
|
const [ma, mi, pa] = version.split(".").map((p) => parseInt(p, 10));
|
|
26852
27907
|
return (ma & 255) << 16 | (mi & 255) << 8 | pa & 255;
|
|
@@ -26855,7 +27910,7 @@ function buildBaseInfo() {
|
|
|
26855
27910
|
return { channel_version: CHANNEL_VERSION };
|
|
26856
27911
|
}
|
|
26857
27912
|
function randomWechatUin() {
|
|
26858
|
-
const n =
|
|
27913
|
+
const n = crypto4.randomBytes(4).readUInt32BE(0);
|
|
26859
27914
|
return Buffer.from(String(n), "utf-8").toString("base64");
|
|
26860
27915
|
}
|
|
26861
27916
|
function commonHeaders() {
|
|
@@ -26990,7 +28045,12 @@ async function sendMessage(params) {
|
|
|
26990
28045
|
try {
|
|
26991
28046
|
const parsed = JSON.parse(raw);
|
|
26992
28047
|
if (parsed.ret !== void 0 && parsed.ret !== 0 || parsed.errcode !== void 0 && parsed.errcode !== 0) {
|
|
26993
|
-
|
|
28048
|
+
let hint = "";
|
|
28049
|
+
if (parsed.ret === -2) {
|
|
28050
|
+
const tokenAge = !params.body.msg?.context_token ? "context_token missing" : params.contextTokenAt ? `context_token age ${Math.round((Date.now() - params.contextTokenAt) / 6e4)}m` : "context_token age unknown";
|
|
28051
|
+
console.warn(`[WeChat:sendmessage] ret=-2 to=${params.body.msg?.to_user_id ?? ""} ${tokenAge}`);
|
|
28052
|
+
hint = ` (gateway rejected the message \u2014 payload too large (>16KB), no context_token, or context_token expired (only a new inbound message from the recipient refreshes it); ${tokenAge})`;
|
|
28053
|
+
}
|
|
26994
28054
|
throw new Error(`[WeChat:sendmessage] gateway error ret=${parsed.ret} errcode=${parsed.errcode} ${parsed.errmsg ?? ""}${hint}`);
|
|
26995
28055
|
}
|
|
26996
28056
|
} catch (err) {
|
|
@@ -27259,14 +28319,18 @@ var init_event_adapter2 = __esm({
|
|
|
27259
28319
|
try {
|
|
27260
28320
|
await this.deps.sendText(text6);
|
|
27261
28321
|
} catch (err) {
|
|
27262
|
-
|
|
28322
|
+
const msg = `sendText failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
28323
|
+
console.warn(`[WeChat] ${msg}`);
|
|
28324
|
+
this.deps.onSendError?.(msg);
|
|
27263
28325
|
}
|
|
27264
28326
|
}
|
|
27265
28327
|
for (const p of mediaPaths) {
|
|
27266
28328
|
try {
|
|
27267
28329
|
await this.deps.sendMedia(p);
|
|
27268
28330
|
} catch (err) {
|
|
27269
|
-
|
|
28331
|
+
const msg = `sendMedia ${p} failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
28332
|
+
console.warn(`[WeChat] ${msg}`);
|
|
28333
|
+
this.deps.onSendError?.(msg);
|
|
27270
28334
|
}
|
|
27271
28335
|
}
|
|
27272
28336
|
}
|
|
@@ -27275,7 +28339,7 @@ var init_event_adapter2 = __esm({
|
|
|
27275
28339
|
});
|
|
27276
28340
|
|
|
27277
28341
|
// ../server/dist/channels/wechat/cdn.js
|
|
27278
|
-
import { createDecipheriv, createCipheriv, randomBytes as
|
|
28342
|
+
import { createDecipheriv, createCipheriv, randomBytes as randomBytes3, createHash as createHash2 } from "node:crypto";
|
|
27279
28343
|
function decryptAesEcb(ciphertext, key) {
|
|
27280
28344
|
const decipher = createDecipheriv("aes-128-ecb", key, null);
|
|
27281
28345
|
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
@@ -27322,8 +28386,8 @@ async function downloadPlain(params) {
|
|
|
27322
28386
|
return fetchCdnBytes(url, params.label);
|
|
27323
28387
|
}
|
|
27324
28388
|
function prepareUpload(plaintext) {
|
|
27325
|
-
const filekey =
|
|
27326
|
-
const aeskey =
|
|
28389
|
+
const filekey = randomBytes3(16).toString("hex");
|
|
28390
|
+
const aeskey = randomBytes3(16);
|
|
27327
28391
|
return {
|
|
27328
28392
|
filekey,
|
|
27329
28393
|
aeskey,
|
|
@@ -27381,8 +28445,8 @@ var init_cdn = __esm({
|
|
|
27381
28445
|
});
|
|
27382
28446
|
|
|
27383
28447
|
// ../server/dist/channels/wechat/send-media.js
|
|
27384
|
-
import
|
|
27385
|
-
import
|
|
28448
|
+
import fs58 from "node:fs/promises";
|
|
28449
|
+
import path66 from "node:path";
|
|
27386
28450
|
function inferKind(filePath) {
|
|
27387
28451
|
const cls = classifyMedia(filePath);
|
|
27388
28452
|
if (cls === "image")
|
|
@@ -27392,9 +28456,9 @@ function inferKind(filePath) {
|
|
|
27392
28456
|
return "file";
|
|
27393
28457
|
}
|
|
27394
28458
|
async function sendMediaFile(params) {
|
|
27395
|
-
const { baseUrl, token, toUserId, contextToken, filePath } = params;
|
|
28459
|
+
const { baseUrl, token, toUserId, contextToken, contextTokenAt, filePath } = params;
|
|
27396
28460
|
const kind = params.kind ?? inferKind(filePath);
|
|
27397
|
-
const plaintext = await
|
|
28461
|
+
const plaintext = await fs58.readFile(filePath);
|
|
27398
28462
|
const prep = prepareUpload(plaintext);
|
|
27399
28463
|
console.log(`[WeChat] sendMediaFile: path=${filePath} kind=${kind} rawsize=${prep.rawsize} filesize=${prep.filesize}`);
|
|
27400
28464
|
const mediaType = kind === "image" ? UploadMediaType.IMAGE : kind === "video" ? UploadMediaType.VIDEO : UploadMediaType.FILE;
|
|
@@ -27432,7 +28496,7 @@ async function sendMediaFile(params) {
|
|
|
27432
28496
|
aesKeyBase64,
|
|
27433
28497
|
fileSizeCiphertext: prep.filesize,
|
|
27434
28498
|
fileSize: prep.rawsize,
|
|
27435
|
-
fileName:
|
|
28499
|
+
fileName: path66.basename(filePath)
|
|
27436
28500
|
});
|
|
27437
28501
|
const clientId = `halo-media-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
27438
28502
|
const body = {
|
|
@@ -27446,7 +28510,7 @@ async function sendMediaFile(params) {
|
|
|
27446
28510
|
context_token: contextToken
|
|
27447
28511
|
}
|
|
27448
28512
|
};
|
|
27449
|
-
await sendMessage({ baseUrl, token, body });
|
|
28513
|
+
await sendMessage({ baseUrl, token, body, contextTokenAt });
|
|
27450
28514
|
console.log(`[WeChat] sendMediaFile: sendMessage success clientId=${clientId}`);
|
|
27451
28515
|
return { clientId };
|
|
27452
28516
|
}
|
|
@@ -27595,14 +28659,16 @@ var init_login = __esm({
|
|
|
27595
28659
|
});
|
|
27596
28660
|
|
|
27597
28661
|
// ../server/dist/channels/wechat/handler.js
|
|
27598
|
-
import
|
|
27599
|
-
import
|
|
28662
|
+
import fs59 from "node:fs";
|
|
28663
|
+
import path67 from "node:path";
|
|
27600
28664
|
import QRCode from "qrcode";
|
|
27601
|
-
function wxRoute(fromUserId, contextToken) {
|
|
27602
|
-
return (prev) =>
|
|
27603
|
-
|
|
27604
|
-
|
|
27605
|
-
|
|
28665
|
+
function wxRoute(fromUserId, contextToken, contextTokenAt) {
|
|
28666
|
+
return (prev) => {
|
|
28667
|
+
if (contextToken !== void 0)
|
|
28668
|
+
return { fromUserId, contextToken, contextTokenAt: contextTokenAt ?? Date.now() };
|
|
28669
|
+
const carry = prev?.fromUserId === fromUserId ? prev : void 0;
|
|
28670
|
+
return { fromUserId, contextToken: carry?.contextToken, contextTokenAt: carry?.contextTokenAt };
|
|
28671
|
+
};
|
|
27606
28672
|
}
|
|
27607
28673
|
function startWechatChannel(deps) {
|
|
27608
28674
|
const { registry: registry2, db } = deps;
|
|
@@ -27630,24 +28696,29 @@ function startWechatChannel(deps) {
|
|
|
27630
28696
|
const route = bridge.getRoute(sessionId);
|
|
27631
28697
|
if (!route)
|
|
27632
28698
|
return;
|
|
27633
|
-
await sendToUser({ account, toUserId: route.fromUserId, text: chunk, contextToken: route.contextToken });
|
|
28699
|
+
await sendToUser({ account, toUserId: route.fromUserId, text: chunk, contextToken: route.contextToken, contextTokenAt: route.contextTokenAt });
|
|
27634
28700
|
},
|
|
27635
28701
|
sendMedia: async (filePath) => {
|
|
27636
28702
|
const route = bridge.getRoute(sessionId);
|
|
27637
28703
|
if (!route)
|
|
27638
28704
|
return;
|
|
27639
|
-
if (!isMediaPathAllowed(filePath, account.workspacePath)) {
|
|
27640
|
-
|
|
27641
|
-
return;
|
|
28705
|
+
if (!isMediaPathAllowed(filePath, account.workspacePath, account.accessLevel)) {
|
|
28706
|
+
throw new Error(`media path not allowed: ${filePath} (must be under the workspace or the temp dir; account access level ${account.accessLevel})`);
|
|
27642
28707
|
}
|
|
27643
28708
|
await sendMediaFile({
|
|
27644
28709
|
baseUrl: account.baseUrl,
|
|
27645
28710
|
token: account.botToken,
|
|
27646
28711
|
toUserId: route.fromUserId,
|
|
27647
28712
|
contextToken: route.contextToken,
|
|
28713
|
+
contextTokenAt: route.contextTokenAt,
|
|
27648
28714
|
filePath
|
|
27649
28715
|
});
|
|
27650
|
-
}
|
|
28716
|
+
},
|
|
28717
|
+
// A dropped reply used to be visible only in the server log. Record
|
|
28718
|
+
// it in the session so the user sees the turn ran but never landed.
|
|
28719
|
+
// appendNotification only writes the log row — it emits no event, so
|
|
28720
|
+
// nothing loops back into this responder as another send attempt.
|
|
28721
|
+
onSendError: (m) => registry2.getOrCreate(account.workspacePath).appendNotification(sessionId, `\u26A0\uFE0F WeChat delivery failed: ${m}`)
|
|
27651
28722
|
})
|
|
27652
28723
|
});
|
|
27653
28724
|
const restartSelf = () => {
|
|
@@ -27690,7 +28761,9 @@ function startWechatChannel(deps) {
|
|
|
27690
28761
|
}
|
|
27691
28762
|
function restoreReplyRoutes(args) {
|
|
27692
28763
|
const { registry: registry2, db, account, bridge } = args;
|
|
27693
|
-
const
|
|
28764
|
+
const config2 = getAccount(db, account.accountId)?.config ?? {};
|
|
28765
|
+
const tokens = config2.contextTokens ?? {};
|
|
28766
|
+
const ats = config2.contextTokenAts ?? {};
|
|
27694
28767
|
const users = Object.keys(tokens);
|
|
27695
28768
|
if (users.length === 0)
|
|
27696
28769
|
return;
|
|
@@ -27703,7 +28776,7 @@ function restoreReplyRoutes(args) {
|
|
|
27703
28776
|
workspacePath,
|
|
27704
28777
|
bridge,
|
|
27705
28778
|
sessionPrefix: buildWxSessionPrefix(userId),
|
|
27706
|
-
route: wxRoute(userId, tokens[userId])
|
|
28779
|
+
route: wxRoute(userId, tokens[userId], ats[userId])
|
|
27707
28780
|
});
|
|
27708
28781
|
if (sid)
|
|
27709
28782
|
console.log(`[WeChat] ${account.accountId} reply route restored for ${sid}`);
|
|
@@ -27887,6 +28960,7 @@ async function handleInbound(args) {
|
|
|
27887
28960
|
account: storedAccount,
|
|
27888
28961
|
toUserId: fromUserId,
|
|
27889
28962
|
contextToken: msg.context_token,
|
|
28963
|
+
contextTokenAt: Date.now(),
|
|
27890
28964
|
text: t("handler.workspace_missing", lang, { path: storedAccount.workspacePath })
|
|
27891
28965
|
});
|
|
27892
28966
|
return;
|
|
@@ -27935,7 +29009,7 @@ async function handleInbound(args) {
|
|
|
27935
29009
|
lang,
|
|
27936
29010
|
// If the session is currently compacting or mid-turn, send an immediate
|
|
27937
29011
|
// hint so the WeChat user doesn't stare at a silent chat for 30+ seconds.
|
|
27938
|
-
sendHint: (hint) => sendToUser({ account, toUserId: fromUserId, contextToken: msg.context_token, text: hint }),
|
|
29012
|
+
sendHint: (hint) => sendToUser({ account, toUserId: fromUserId, contextToken: msg.context_token, contextTokenAt: Date.now(), text: hint }),
|
|
27939
29013
|
uiText: userText || "[\u4EC5\u56FE\u7247]",
|
|
27940
29014
|
agentText: userText,
|
|
27941
29015
|
userTag: fromUserId,
|
|
@@ -28009,7 +29083,7 @@ async function handleSlashCommand(args) {
|
|
|
28009
29083
|
await reply(login.message);
|
|
28010
29084
|
return true;
|
|
28011
29085
|
}
|
|
28012
|
-
const tmpPath =
|
|
29086
|
+
const tmpPath = path67.join(tempDir(), `halo-qr-${login.sessionKey}.png`);
|
|
28013
29087
|
await QRCode.toFile(tmpPath, login.qrcodeUrl, { width: 256, margin: 2 });
|
|
28014
29088
|
await sendMediaFile({
|
|
28015
29089
|
baseUrl: account.baseUrl,
|
|
@@ -28018,7 +29092,7 @@ async function handleSlashCommand(args) {
|
|
|
28018
29092
|
contextToken,
|
|
28019
29093
|
filePath: tmpPath
|
|
28020
29094
|
});
|
|
28021
|
-
await
|
|
29095
|
+
await fs59.promises.unlink(tmpPath).catch(() => {
|
|
28022
29096
|
});
|
|
28023
29097
|
await reply(t("wechat.qr_sent", lang, { level, path: account.workspacePath }));
|
|
28024
29098
|
waitLogin({ sessionKey: login.sessionKey }).then(async (result2) => {
|
|
@@ -28061,7 +29135,7 @@ async function sendToUser(params) {
|
|
|
28061
29135
|
context_token: params.contextToken
|
|
28062
29136
|
}
|
|
28063
29137
|
};
|
|
28064
|
-
await sendMessage({ baseUrl: params.account.baseUrl, token: params.account.botToken, body });
|
|
29138
|
+
await sendMessage({ baseUrl: params.account.baseUrl, token: params.account.botToken, body, contextTokenAt: params.contextTokenAt });
|
|
28065
29139
|
}
|
|
28066
29140
|
function sleep2(ms, signal) {
|
|
28067
29141
|
return new Promise((resolve2, reject) => {
|
|
@@ -28095,20 +29169,20 @@ var init_handler4 = __esm({
|
|
|
28095
29169
|
RETRY_DELAY_MS = 2e3;
|
|
28096
29170
|
}
|
|
28097
29171
|
});
|
|
28098
|
-
|
|
28099
|
-
// ../server/dist/routes/wechat.js
|
|
28100
|
-
import { Hono as
|
|
28101
|
-
import
|
|
28102
|
-
import
|
|
29172
|
+
|
|
29173
|
+
// ../server/dist/routes/wechat.js
|
|
29174
|
+
import { Hono as Hono18 } from "hono";
|
|
29175
|
+
import path68 from "node:path";
|
|
29176
|
+
import fs60 from "node:fs";
|
|
28103
29177
|
function createWechatRoutes(deps) {
|
|
28104
29178
|
const { db, channel } = deps;
|
|
28105
|
-
const app2 = new
|
|
29179
|
+
const app2 = new Hono18();
|
|
28106
29180
|
app2.get("/wechat/accounts", (c) => {
|
|
28107
29181
|
const accounts = listAccounts4(db).map((acc) => {
|
|
28108
29182
|
return {
|
|
28109
29183
|
...stripSecrets(acc),
|
|
28110
29184
|
workspacePath: acc.workspacePath,
|
|
28111
|
-
workspaceMissing: !
|
|
29185
|
+
workspaceMissing: !fs60.existsSync(acc.workspacePath)
|
|
28112
29186
|
};
|
|
28113
29187
|
});
|
|
28114
29188
|
return c.json({ accounts });
|
|
@@ -28124,7 +29198,7 @@ function createWechatRoutes(deps) {
|
|
|
28124
29198
|
return c.json({ error: "sessionKey required" }, 400);
|
|
28125
29199
|
if (!body.workspacePath)
|
|
28126
29200
|
return c.json({ error: "workspacePath required" }, 400);
|
|
28127
|
-
if (!
|
|
29201
|
+
if (!path68.isAbsolute(body.workspacePath))
|
|
28128
29202
|
return c.json({ error: "workspacePath must be absolute" }, 400);
|
|
28129
29203
|
const levelError = accessLevelError(body.accessLevel, CHAT_ACCESS_LEVELS);
|
|
28130
29204
|
if (levelError)
|
|
@@ -28231,7 +29305,11 @@ function readContextToken(accountId, userId) {
|
|
|
28231
29305
|
const acct = getAccount(getChannelDb(), accountId);
|
|
28232
29306
|
const tokens = acct?.config?.contextTokens;
|
|
28233
29307
|
const v = tokens?.[userId];
|
|
28234
|
-
|
|
29308
|
+
if (typeof v !== "string" || v.length === 0)
|
|
29309
|
+
return {};
|
|
29310
|
+
const ats = acct?.config?.contextTokenAts;
|
|
29311
|
+
const at = ats?.[userId];
|
|
29312
|
+
return { token: v, at: typeof at === "number" ? at : void 0 };
|
|
28235
29313
|
}
|
|
28236
29314
|
async function dispatch2(accountId, text6, explicitChatId, media) {
|
|
28237
29315
|
const acct = getAccount4(getChannelDb(), accountId);
|
|
@@ -28244,12 +29322,12 @@ async function dispatch2(accountId, text6, explicitChatId, media) {
|
|
|
28244
29322
|
throw new Error("no wechat target \u2014 bind the account first (the QR-login owner becomes the default cron recipient)");
|
|
28245
29323
|
}
|
|
28246
29324
|
const out = [];
|
|
28247
|
-
const contextToken = readContextToken(accountId, chatId);
|
|
29325
|
+
const { token: contextToken, at: contextTokenAt } = readContextToken(accountId, chatId);
|
|
28248
29326
|
if (text6) {
|
|
28249
29327
|
const chunks = splitText(text6, WECHAT_TEXT_LIMIT);
|
|
28250
29328
|
for (const [i, chunk] of chunks.entries()) {
|
|
28251
29329
|
try {
|
|
28252
|
-
await sendToUser({ account: acct, toUserId: chatId, text: chunk, contextToken });
|
|
29330
|
+
await sendToUser({ account: acct, toUserId: chatId, text: chunk, contextToken, contextTokenAt });
|
|
28253
29331
|
} catch (err) {
|
|
28254
29332
|
throw new Error(`chunk ${i + 1}/${chunks.length}: ${err instanceof Error ? err.message : String(err)}`);
|
|
28255
29333
|
}
|
|
@@ -28268,6 +29346,7 @@ async function dispatch2(accountId, text6, explicitChatId, media) {
|
|
|
28268
29346
|
token: acct.botToken,
|
|
28269
29347
|
toUserId: chatId,
|
|
28270
29348
|
contextToken,
|
|
29349
|
+
contextTokenAt,
|
|
28271
29350
|
filePath
|
|
28272
29351
|
});
|
|
28273
29352
|
out.push({ channelType: "wechat", accountId, chatId, ok: true });
|
|
@@ -28334,20 +29413,20 @@ var init_descriptor2 = __esm({
|
|
|
28334
29413
|
});
|
|
28335
29414
|
|
|
28336
29415
|
// ../server/dist/routes/web.js
|
|
28337
|
-
import { Hono as
|
|
29416
|
+
import { Hono as Hono19 } from "hono";
|
|
28338
29417
|
import { streamSSE } from "hono/streaming";
|
|
28339
|
-
import
|
|
28340
|
-
import
|
|
29418
|
+
import crypto5 from "node:crypto";
|
|
29419
|
+
import fs61 from "node:fs";
|
|
28341
29420
|
function createWebRoutes(deps) {
|
|
28342
29421
|
const { db, channel } = deps;
|
|
28343
|
-
const app2 = new
|
|
29422
|
+
const app2 = new Hono19();
|
|
28344
29423
|
app2.get("/web/accounts", (c) => {
|
|
28345
29424
|
const accounts = listAccounts2(db).map((acc) => {
|
|
28346
29425
|
return {
|
|
28347
29426
|
accountId: acc.accountId,
|
|
28348
29427
|
token: acc.token,
|
|
28349
29428
|
workspacePath: acc.workspacePath,
|
|
28350
|
-
workspaceMissing: !
|
|
29429
|
+
workspaceMissing: !fs61.existsSync(acc.workspacePath),
|
|
28351
29430
|
label: acc.label,
|
|
28352
29431
|
enabled: acc.enabled,
|
|
28353
29432
|
accessLevel: acc.accessLevel,
|
|
@@ -28368,8 +29447,8 @@ function createWebRoutes(deps) {
|
|
|
28368
29447
|
const wsError = validateWorkspaceBody(body.workspacePath);
|
|
28369
29448
|
if (wsError)
|
|
28370
29449
|
return c.json({ error: wsError }, 400);
|
|
28371
|
-
const accountId =
|
|
28372
|
-
const token =
|
|
29450
|
+
const accountId = crypto5.randomUUID().slice(0, 8);
|
|
29451
|
+
const token = crypto5.randomBytes(24).toString("base64url");
|
|
28373
29452
|
insertAccount2(db, {
|
|
28374
29453
|
accountId,
|
|
28375
29454
|
token,
|
|
@@ -28530,30 +29609,30 @@ function createWebRoutes(deps) {
|
|
|
28530
29609
|
const filePath = c.req.query("path");
|
|
28531
29610
|
if (!filePath)
|
|
28532
29611
|
return c.json({ error: "path required" }, 400);
|
|
28533
|
-
const
|
|
28534
|
-
const resolved =
|
|
28535
|
-
const root =
|
|
28536
|
-
if (resolved !== root && !resolved.startsWith(root +
|
|
29612
|
+
const path74 = await import("node:path");
|
|
29613
|
+
const resolved = path74.default.resolve(account.workspacePath, filePath);
|
|
29614
|
+
const root = path74.default.resolve(account.workspacePath);
|
|
29615
|
+
if (resolved !== root && !resolved.startsWith(root + path74.default.sep)) {
|
|
28537
29616
|
return c.json({ error: "path traversal not allowed" }, 403);
|
|
28538
29617
|
}
|
|
28539
29618
|
let real;
|
|
28540
29619
|
let realRoot;
|
|
28541
29620
|
try {
|
|
28542
|
-
real =
|
|
28543
|
-
realRoot =
|
|
29621
|
+
real = fs61.realpathSync(resolved);
|
|
29622
|
+
realRoot = fs61.realpathSync(root);
|
|
28544
29623
|
} catch {
|
|
28545
29624
|
return c.json({ error: "file not found" }, 404);
|
|
28546
29625
|
}
|
|
28547
|
-
if (real !== realRoot && !real.startsWith(realRoot +
|
|
29626
|
+
if (real !== realRoot && !real.startsWith(realRoot + path74.default.sep)) {
|
|
28548
29627
|
return c.json({ error: "path traversal not allowed" }, 403);
|
|
28549
29628
|
}
|
|
28550
29629
|
if (isHiddenWorkspacePath(real, realRoot)) {
|
|
28551
29630
|
return c.json({ error: "path is not accessible" }, 403);
|
|
28552
29631
|
}
|
|
28553
|
-
const stat =
|
|
29632
|
+
const stat = fs61.statSync(real);
|
|
28554
29633
|
if (stat.isDirectory())
|
|
28555
29634
|
return c.json({ error: "cannot serve directory" }, 400);
|
|
28556
|
-
const ext =
|
|
29635
|
+
const ext = path74.default.extname(real).toLowerCase();
|
|
28557
29636
|
const nonImageMimes = {
|
|
28558
29637
|
".mp4": "video/mp4",
|
|
28559
29638
|
".webm": "video/webm",
|
|
@@ -28561,8 +29640,8 @@ function createWebRoutes(deps) {
|
|
|
28561
29640
|
".txt": "text/plain"
|
|
28562
29641
|
};
|
|
28563
29642
|
const contentType = imageMimeFromExt(ext) || nonImageMimes[ext] || "application/octet-stream";
|
|
28564
|
-
const fileName =
|
|
28565
|
-
const data =
|
|
29643
|
+
const fileName = path74.default.basename(resolved);
|
|
29644
|
+
const data = fs61.readFileSync(real);
|
|
28566
29645
|
c.header("content-type", contentType);
|
|
28567
29646
|
c.header("content-disposition", `inline; filename="${fileName}"`);
|
|
28568
29647
|
return c.body(data);
|
|
@@ -28893,10 +29972,10 @@ async function authTest(botToken) {
|
|
|
28893
29972
|
return slackPost("auth.test", botToken, {});
|
|
28894
29973
|
}
|
|
28895
29974
|
async function uploadFile(args) {
|
|
28896
|
-
const
|
|
28897
|
-
const
|
|
28898
|
-
const filename = args.filename ??
|
|
28899
|
-
const stat =
|
|
29975
|
+
const fs68 = await import("node:fs");
|
|
29976
|
+
const path74 = await import("node:path");
|
|
29977
|
+
const filename = args.filename ?? path74.basename(args.filePath);
|
|
29978
|
+
const stat = fs68.statSync(args.filePath);
|
|
28900
29979
|
if (!stat.isFile())
|
|
28901
29980
|
throw new Error(`[slack:uploadFile] not a regular file: ${args.filePath}`);
|
|
28902
29981
|
const length = stat.size;
|
|
@@ -28914,7 +29993,7 @@ async function uploadFile(args) {
|
|
|
28914
29993
|
if (step1Parsed.ok === false || !step1Parsed.upload_url || !step1Parsed.file_id) {
|
|
28915
29994
|
throw new Error(`[slack:files.getUploadURLExternal] ${step1Parsed.error ?? "unknown"}`);
|
|
28916
29995
|
}
|
|
28917
|
-
const buf =
|
|
29996
|
+
const buf = fs68.readFileSync(args.filePath);
|
|
28918
29997
|
const step2 = await fetch(step1Parsed.upload_url, {
|
|
28919
29998
|
method: "POST",
|
|
28920
29999
|
body: buf
|
|
@@ -29059,7 +30138,7 @@ var init_api2 = __esm({
|
|
|
29059
30138
|
});
|
|
29060
30139
|
|
|
29061
30140
|
// ../server/dist/channels/slack/handler.js
|
|
29062
|
-
import
|
|
30141
|
+
import path69 from "node:path";
|
|
29063
30142
|
import { WebSocket } from "ws";
|
|
29064
30143
|
function slashToBang(text6, commandNames) {
|
|
29065
30144
|
if (commandNames.length === 0)
|
|
@@ -29190,12 +30269,11 @@ function startSlackChannel(deps) {
|
|
|
29190
30269
|
const route = bridge.getRoute(sessionId);
|
|
29191
30270
|
if (!account || !route)
|
|
29192
30271
|
return;
|
|
29193
|
-
const resolved =
|
|
29194
|
-
if (!isMediaPathAllowed(resolved, account.workspacePath)) {
|
|
29195
|
-
console.warn(`[Slack] sendMedia blocked: ${filePath} not under workspace`);
|
|
29196
|
-
return;
|
|
29197
|
-
}
|
|
30272
|
+
const resolved = path69.resolve(filePath);
|
|
29198
30273
|
try {
|
|
30274
|
+
if (!isMediaPathAllowed(resolved, account.workspacePath, account.accessLevel)) {
|
|
30275
|
+
throw new Error(`media path not allowed: ${filePath} (must be under the workspace or the temp dir; account access level ${account.accessLevel})`);
|
|
30276
|
+
}
|
|
29199
30277
|
await uploadFile({
|
|
29200
30278
|
botToken: account.botToken,
|
|
29201
30279
|
channel: route.channelId,
|
|
@@ -29209,7 +30287,7 @@ function startSlackChannel(deps) {
|
|
|
29209
30287
|
channel: route.channelId,
|
|
29210
30288
|
threadTs: route.replyTs,
|
|
29211
30289
|
text: t("handler.upload_failed", getLang(account), {
|
|
29212
|
-
name:
|
|
30290
|
+
name: path69.basename(filePath),
|
|
29213
30291
|
error: err instanceof Error ? err.message : String(err)
|
|
29214
30292
|
})
|
|
29215
30293
|
}).catch(() => {
|
|
@@ -29471,11 +30549,11 @@ var init_handler5 = __esm({
|
|
|
29471
30549
|
});
|
|
29472
30550
|
|
|
29473
30551
|
// ../server/dist/routes/slack.js
|
|
29474
|
-
import { Hono as
|
|
29475
|
-
import
|
|
30552
|
+
import { Hono as Hono20 } from "hono";
|
|
30553
|
+
import fs62 from "node:fs";
|
|
29476
30554
|
function createSlackRoutes(deps) {
|
|
29477
30555
|
const { db, channel } = deps;
|
|
29478
|
-
const app2 = new
|
|
30556
|
+
const app2 = new Hono20();
|
|
29479
30557
|
app2.get("/slack/accounts/:id/search", async (c) => {
|
|
29480
30558
|
const id = c.req.param("id");
|
|
29481
30559
|
const q = c.req.query("q") ?? "";
|
|
@@ -29495,7 +30573,7 @@ function createSlackRoutes(deps) {
|
|
|
29495
30573
|
botUserId: a.botUserId,
|
|
29496
30574
|
teamId: a.teamId,
|
|
29497
30575
|
workspacePath: a.workspacePath,
|
|
29498
|
-
workspaceMissing: !
|
|
30576
|
+
workspaceMissing: !fs62.existsSync(a.workspacePath),
|
|
29499
30577
|
label: a.label,
|
|
29500
30578
|
enabled: a.enabled,
|
|
29501
30579
|
accessLevel: a.accessLevel,
|
|
@@ -30014,10 +31092,10 @@ async function replyMessage(args) {
|
|
|
30014
31092
|
return { message_id: parsed.data?.message_id ?? "" };
|
|
30015
31093
|
}
|
|
30016
31094
|
async function uploadImage(args) {
|
|
30017
|
-
const
|
|
30018
|
-
const
|
|
30019
|
-
const buf =
|
|
30020
|
-
const filename =
|
|
31095
|
+
const fs68 = await import("node:fs");
|
|
31096
|
+
const path74 = await import("node:path");
|
|
31097
|
+
const buf = fs68.readFileSync(args.filePath);
|
|
31098
|
+
const filename = path74.basename(args.filePath);
|
|
30021
31099
|
const send = async (token2) => {
|
|
30022
31100
|
const form = new FormData();
|
|
30023
31101
|
form.append("image_type", "message");
|
|
@@ -30048,10 +31126,10 @@ async function uploadImage(args) {
|
|
|
30048
31126
|
return { imageKey: parsed.data.image_key };
|
|
30049
31127
|
}
|
|
30050
31128
|
async function uploadFile2(args) {
|
|
30051
|
-
const
|
|
30052
|
-
const
|
|
30053
|
-
const buf =
|
|
30054
|
-
const filename = args.filename ??
|
|
31129
|
+
const fs68 = await import("node:fs");
|
|
31130
|
+
const path74 = await import("node:path");
|
|
31131
|
+
const buf = fs68.readFileSync(args.filePath);
|
|
31132
|
+
const filename = args.filename ?? path74.basename(args.filePath);
|
|
30055
31133
|
const send = async (token2) => {
|
|
30056
31134
|
const form = new FormData();
|
|
30057
31135
|
form.append("file_type", args.fileType);
|
|
@@ -30157,7 +31235,7 @@ var init_api3 = __esm({
|
|
|
30157
31235
|
});
|
|
30158
31236
|
|
|
30159
31237
|
// ../server/dist/channels/feishu/handler.js
|
|
30160
|
-
import
|
|
31238
|
+
import path70 from "node:path";
|
|
30161
31239
|
import * as Lark from "@larksuiteoapi/node-sdk";
|
|
30162
31240
|
function buildSessionPrefixForThread2(chatId, rootId) {
|
|
30163
31241
|
return sessionPrefix("feishu", `${chatId}:${rootId}`);
|
|
@@ -30360,12 +31438,11 @@ function startFeishuChannel(deps) {
|
|
|
30360
31438
|
const route = bridge.getRoute(sessionId);
|
|
30361
31439
|
if (!account || !route)
|
|
30362
31440
|
return;
|
|
30363
|
-
const resolved =
|
|
30364
|
-
if (!isMediaPathAllowed(resolved, account.workspacePath)) {
|
|
30365
|
-
console.warn(`[Feishu] sendMedia blocked: ${filePath} not under workspace`);
|
|
30366
|
-
return;
|
|
30367
|
-
}
|
|
31441
|
+
const resolved = path70.resolve(filePath);
|
|
30368
31442
|
try {
|
|
31443
|
+
if (!isMediaPathAllowed(resolved, account.workspacePath, account.accessLevel)) {
|
|
31444
|
+
throw new Error(`media path not allowed: ${filePath} (must be under the workspace or the temp dir; account access level ${account.accessLevel})`);
|
|
31445
|
+
}
|
|
30369
31446
|
await sendFeishuMedia({ account, ...route, filePath: resolved });
|
|
30370
31447
|
} catch (err) {
|
|
30371
31448
|
console.log(`[Feishu] sendMedia ${filePath} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -30373,7 +31450,7 @@ function startFeishuChannel(deps) {
|
|
|
30373
31450
|
account,
|
|
30374
31451
|
...route,
|
|
30375
31452
|
text: t("handler.upload_failed", getLang(account), {
|
|
30376
|
-
name:
|
|
31453
|
+
name: path70.basename(filePath),
|
|
30377
31454
|
error: err instanceof Error ? err.message : String(err)
|
|
30378
31455
|
})
|
|
30379
31456
|
}).catch(() => {
|
|
@@ -30543,7 +31620,7 @@ async function replyToInbound(args) {
|
|
|
30543
31620
|
async function sendFeishuMedia(args) {
|
|
30544
31621
|
const { account, inboundMessageId, isP2P, chatId, filePath } = args;
|
|
30545
31622
|
const cls = classifyMedia(filePath);
|
|
30546
|
-
const ext =
|
|
31623
|
+
const ext = path70.extname(filePath).toLowerCase();
|
|
30547
31624
|
if (cls === "image") {
|
|
30548
31625
|
const { imageKey } = await uploadImage({ appId: account.appId, appSecret: account.appSecret, filePath });
|
|
30549
31626
|
await replyToInboundRaw({
|
|
@@ -30670,11 +31747,11 @@ var init_handler6 = __esm({
|
|
|
30670
31747
|
});
|
|
30671
31748
|
|
|
30672
31749
|
// ../server/dist/routes/feishu.js
|
|
30673
|
-
import { Hono as
|
|
30674
|
-
import
|
|
31750
|
+
import { Hono as Hono21 } from "hono";
|
|
31751
|
+
import fs63 from "node:fs";
|
|
30675
31752
|
function createFeishuRoutes(deps) {
|
|
30676
31753
|
const { db, channel } = deps;
|
|
30677
|
-
const app2 = new
|
|
31754
|
+
const app2 = new Hono21();
|
|
30678
31755
|
app2.get("/feishu/accounts/:id/search", async (c) => {
|
|
30679
31756
|
const id = c.req.param("id");
|
|
30680
31757
|
const q = c.req.query("q") ?? "";
|
|
@@ -30695,7 +31772,7 @@ function createFeishuRoutes(deps) {
|
|
|
30695
31772
|
botOpenId: a.botOpenId,
|
|
30696
31773
|
hasEncryptKey: !!a.encryptKey,
|
|
30697
31774
|
workspacePath: a.workspacePath,
|
|
30698
|
-
workspaceMissing: !
|
|
31775
|
+
workspaceMissing: !fs63.existsSync(a.workspacePath),
|
|
30699
31776
|
label: a.label,
|
|
30700
31777
|
enabled: a.enabled,
|
|
30701
31778
|
accessLevel: a.accessLevel,
|
|
@@ -31068,8 +32145,8 @@ var init_event_adapter5 = __esm({
|
|
|
31068
32145
|
});
|
|
31069
32146
|
|
|
31070
32147
|
// ../server/dist/channels/wecom/handler.js
|
|
31071
|
-
import
|
|
31072
|
-
import
|
|
32148
|
+
import path71 from "node:path";
|
|
32149
|
+
import fs64 from "node:fs/promises";
|
|
31073
32150
|
import { WSClient as WSClient2, generateReqId } from "@wecom/aibot-node-sdk";
|
|
31074
32151
|
function pickConversation(body) {
|
|
31075
32152
|
const userid = body.from?.userid ?? "unknown";
|
|
@@ -31196,13 +32273,13 @@ async function replyText(wsClient, route, text6) {
|
|
|
31196
32273
|
}
|
|
31197
32274
|
async function sendWecomMedia(args) {
|
|
31198
32275
|
const { wsClient, route, filePath } = args;
|
|
31199
|
-
const ext =
|
|
32276
|
+
const ext = path71.extname(filePath).toLowerCase();
|
|
31200
32277
|
const cls = classifyMedia(filePath);
|
|
31201
32278
|
const type = cls === "image" && [".png", ".jpg", ".jpeg", ".gif"].includes(ext) ? "image" : cls === "video" && ext === ".mp4" ? "video" : "file";
|
|
31202
|
-
const buf = await
|
|
32279
|
+
const buf = await fs64.readFile(filePath);
|
|
31203
32280
|
if (buf.length > MAX_WECOM_DOWNLOAD_BYTES)
|
|
31204
32281
|
throw new Error("file exceeds 20MB");
|
|
31205
|
-
const { media_id: mediaId } = await wsClient.uploadMedia(buf, { type, filename:
|
|
32282
|
+
const { media_id: mediaId } = await wsClient.uploadMedia(buf, { type, filename: path71.basename(filePath) });
|
|
31206
32283
|
await wsClient.replyMedia({ headers: { req_id: route.reqId } }, type, mediaId);
|
|
31207
32284
|
}
|
|
31208
32285
|
function startWecomChannel(deps) {
|
|
@@ -31226,17 +32303,16 @@ function startWecomChannel(deps) {
|
|
|
31226
32303
|
const wsClient = states.get(accountId)?.wsClient;
|
|
31227
32304
|
if (!account || !route || !wsClient)
|
|
31228
32305
|
return;
|
|
31229
|
-
const resolved =
|
|
31230
|
-
if (!isMediaPathAllowed(resolved, account.workspacePath)) {
|
|
31231
|
-
console.warn(`[WeCom] sendMedia blocked: ${filePath} not under workspace`);
|
|
31232
|
-
return;
|
|
31233
|
-
}
|
|
32306
|
+
const resolved = path71.resolve(filePath);
|
|
31234
32307
|
try {
|
|
32308
|
+
if (!isMediaPathAllowed(resolved, account.workspacePath, account.accessLevel)) {
|
|
32309
|
+
throw new Error(`media path not allowed: ${filePath} (must be under the workspace or the temp dir; account access level ${account.accessLevel})`);
|
|
32310
|
+
}
|
|
31235
32311
|
await sendWecomMedia({ wsClient, route, filePath: resolved });
|
|
31236
32312
|
} catch (err) {
|
|
31237
32313
|
console.log(`[WeCom] sendMedia ${filePath} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
31238
32314
|
await replyText(wsClient, route, t("handler.upload_failed", getLang(account), {
|
|
31239
|
-
name:
|
|
32315
|
+
name: path71.basename(filePath),
|
|
31240
32316
|
error: err instanceof Error ? err.message : String(err)
|
|
31241
32317
|
})).catch(() => {
|
|
31242
32318
|
});
|
|
@@ -31453,17 +32529,17 @@ var init_handler7 = __esm({
|
|
|
31453
32529
|
});
|
|
31454
32530
|
|
|
31455
32531
|
// ../server/dist/routes/wecom.js
|
|
31456
|
-
import { Hono as
|
|
31457
|
-
import
|
|
32532
|
+
import { Hono as Hono22 } from "hono";
|
|
32533
|
+
import fs65 from "node:fs";
|
|
31458
32534
|
function createWecomRoutes(deps) {
|
|
31459
32535
|
const { db, channel } = deps;
|
|
31460
|
-
const app2 = new
|
|
32536
|
+
const app2 = new Hono22();
|
|
31461
32537
|
app2.get("/wecom/accounts", (c) => {
|
|
31462
32538
|
const accounts = listAccounts7(db).map((a) => ({
|
|
31463
32539
|
accountId: a.accountId,
|
|
31464
32540
|
botId: a.botId,
|
|
31465
32541
|
workspacePath: a.workspacePath,
|
|
31466
|
-
workspaceMissing: !
|
|
32542
|
+
workspaceMissing: !fs65.existsSync(a.workspacePath),
|
|
31467
32543
|
label: a.label,
|
|
31468
32544
|
enabled: a.enabled,
|
|
31469
32545
|
accessLevel: a.accessLevel,
|
|
@@ -31657,235 +32733,25 @@ var init_descriptors = __esm({
|
|
|
31657
32733
|
}
|
|
31658
32734
|
});
|
|
31659
32735
|
|
|
31660
|
-
// ../server/dist/middleware/auth.js
|
|
31661
|
-
import { Hono as Hono21 } from "hono";
|
|
31662
|
-
import { getCookie, setCookie } from "hono/cookie";
|
|
31663
|
-
import crypto5 from "node:crypto";
|
|
31664
|
-
function passwordHash() {
|
|
31665
|
-
return config.server.password ?? "";
|
|
31666
|
-
}
|
|
31667
|
-
function passwordEnvPlaintext() {
|
|
31668
|
-
return config.server.passwordEnvPlaintext;
|
|
31669
|
-
}
|
|
31670
|
-
function jwtSecret() {
|
|
31671
|
-
const secret = config.server.jwtSecret ?? "";
|
|
31672
|
-
if (!secret) {
|
|
31673
|
-
throw new Error("jwt_secret missing \u2014 run `halo setup` to initialize");
|
|
31674
|
-
}
|
|
31675
|
-
return Buffer.from(secret, "base64");
|
|
31676
|
-
}
|
|
31677
|
-
function base64url(data) {
|
|
31678
|
-
const buf = typeof data === "string" ? Buffer.from(data) : data;
|
|
31679
|
-
return buf.toString("base64url");
|
|
31680
|
-
}
|
|
31681
|
-
function jwtSign(payload) {
|
|
31682
|
-
const header = base64url(JSON.stringify({ alg: "HS256", typ: "JWT" }));
|
|
31683
|
-
const body = base64url(JSON.stringify(payload));
|
|
31684
|
-
const sig = crypto5.createHmac("sha256", jwtSecret()).update(`${header}.${body}`).digest();
|
|
31685
|
-
return `${header}.${body}.${base64url(sig)}`;
|
|
31686
|
-
}
|
|
31687
|
-
function jwtVerify(token) {
|
|
31688
|
-
const parts = token.split(".");
|
|
31689
|
-
if (parts.length !== 3)
|
|
31690
|
-
return null;
|
|
31691
|
-
const [header, body, sig] = parts;
|
|
31692
|
-
let secret;
|
|
31693
|
-
try {
|
|
31694
|
-
secret = jwtSecret();
|
|
31695
|
-
} catch {
|
|
31696
|
-
return null;
|
|
31697
|
-
}
|
|
31698
|
-
const expected = base64url(crypto5.createHmac("sha256", secret).update(`${header}.${body}`).digest());
|
|
31699
|
-
if (sig !== expected)
|
|
31700
|
-
return null;
|
|
31701
|
-
try {
|
|
31702
|
-
const payload = JSON.parse(Buffer.from(body, "base64url").toString());
|
|
31703
|
-
if (typeof payload.exp === "number" && Date.now() / 1e3 > payload.exp)
|
|
31704
|
-
return null;
|
|
31705
|
-
return payload;
|
|
31706
|
-
} catch {
|
|
31707
|
-
return null;
|
|
31708
|
-
}
|
|
31709
|
-
}
|
|
31710
|
-
function createToken() {
|
|
31711
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
31712
|
-
return jwtSign({ iat: now, exp: now + TOKEN_MAX_AGE });
|
|
31713
|
-
}
|
|
31714
|
-
function constantTimeEqualString(a, b) {
|
|
31715
|
-
const ab = Buffer.from(a, "utf-8");
|
|
31716
|
-
const bb = Buffer.from(b, "utf-8");
|
|
31717
|
-
const max = Math.max(ab.length, bb.length);
|
|
31718
|
-
const ap = Buffer.alloc(max);
|
|
31719
|
-
ab.copy(ap);
|
|
31720
|
-
const bp = Buffer.alloc(max);
|
|
31721
|
-
bb.copy(bp);
|
|
31722
|
-
const eq17 = crypto5.timingSafeEqual(ap, bp);
|
|
31723
|
-
return eq17 && ab.length === bb.length;
|
|
31724
|
-
}
|
|
31725
|
-
function validateToken(token) {
|
|
31726
|
-
if (!token)
|
|
31727
|
-
return null;
|
|
31728
|
-
return jwtVerify(token);
|
|
31729
|
-
}
|
|
31730
|
-
function shouldRefresh(payload) {
|
|
31731
|
-
const iat = payload.iat;
|
|
31732
|
-
if (!iat)
|
|
31733
|
-
return true;
|
|
31734
|
-
return Date.now() / 1e3 - iat > REFRESH_AFTER;
|
|
31735
|
-
}
|
|
31736
|
-
function isAuthDisabled() {
|
|
31737
|
-
return passwordHash().length === 0 && passwordEnvPlaintext() == null;
|
|
31738
|
-
}
|
|
31739
|
-
function isAuthenticated(cookie) {
|
|
31740
|
-
if (isAuthDisabled())
|
|
31741
|
-
return true;
|
|
31742
|
-
return validateToken(cookie) !== null;
|
|
31743
|
-
}
|
|
31744
|
-
function getTokenFromCookieHeader(cookieHeader) {
|
|
31745
|
-
if (!cookieHeader)
|
|
31746
|
-
return void 0;
|
|
31747
|
-
const match = cookieHeader.match(new RegExp(`(?:^|;\\s*)${COOKIE_NAME}=([^;]+)`));
|
|
31748
|
-
return match?.[1];
|
|
31749
|
-
}
|
|
31750
|
-
function setTokenCookie(c, token) {
|
|
31751
|
-
setCookie(c, COOKIE_NAME, token, {
|
|
31752
|
-
path: "/",
|
|
31753
|
-
// WS upgrade requests carry cookies on the underlying HTTP handshake;
|
|
31754
|
-
// the server reads them via the Cookie header, never via JS — so the
|
|
31755
|
-
// cookie can stay httpOnly to avoid XSS exfiltration.
|
|
31756
|
-
httpOnly: true,
|
|
31757
|
-
maxAge: TOKEN_MAX_AGE,
|
|
31758
|
-
sameSite: "Lax"
|
|
31759
|
-
});
|
|
31760
|
-
}
|
|
31761
|
-
function createAuthRoutes() {
|
|
31762
|
-
const app2 = new Hono21();
|
|
31763
|
-
app2.post("/auth/login", async (c) => {
|
|
31764
|
-
const ip = getClientIp(c);
|
|
31765
|
-
if (isLockedOut2(ip)) {
|
|
31766
|
-
return c.json({ error: "Too many attempts, try again later" }, 429);
|
|
31767
|
-
}
|
|
31768
|
-
const body = await c.req.json().catch(() => ({ password: "" }));
|
|
31769
|
-
if (!body.password) {
|
|
31770
|
-
recordFailure2(ip);
|
|
31771
|
-
return c.json({ error: "Invalid password" }, 401);
|
|
31772
|
-
}
|
|
31773
|
-
const envPlain = passwordEnvPlaintext();
|
|
31774
|
-
const ok = envPlain != null ? constantTimeEqualString(body.password, envPlain) : await verifyPassword(body.password, passwordHash());
|
|
31775
|
-
if (!ok) {
|
|
31776
|
-
recordFailure2(ip);
|
|
31777
|
-
return c.json({ error: "Invalid password" }, 401);
|
|
31778
|
-
}
|
|
31779
|
-
clearFailures2(ip);
|
|
31780
|
-
const token = createToken();
|
|
31781
|
-
setTokenCookie(c, token);
|
|
31782
|
-
return c.json({ ok: true });
|
|
31783
|
-
});
|
|
31784
|
-
app2.get("/auth/check", (c) => {
|
|
31785
|
-
const token = getCookie(c, COOKIE_NAME);
|
|
31786
|
-
const payload = validateToken(token);
|
|
31787
|
-
if (payload) {
|
|
31788
|
-
if (shouldRefresh(payload)) {
|
|
31789
|
-
const newToken = createToken();
|
|
31790
|
-
setTokenCookie(c, newToken);
|
|
31791
|
-
}
|
|
31792
|
-
return c.json({ authenticated: true, badge: config.server.badge });
|
|
31793
|
-
}
|
|
31794
|
-
return c.json({ authenticated: false, badge: config.server.badge }, 401);
|
|
31795
|
-
});
|
|
31796
|
-
app2.post("/auth/logout", (c) => {
|
|
31797
|
-
setCookie(c, COOKIE_NAME, "", { path: "/", maxAge: 0 });
|
|
31798
|
-
return c.json({ ok: true });
|
|
31799
|
-
});
|
|
31800
|
-
app2.post("/auth/change-password", async (c) => {
|
|
31801
|
-
const body = await c.req.json().catch(() => ({}));
|
|
31802
|
-
const oldPassword = typeof body.oldPassword === "string" ? body.oldPassword : "";
|
|
31803
|
-
const newPassword = typeof body.newPassword === "string" ? body.newPassword : "";
|
|
31804
|
-
if (passwordEnvPlaintext() != null) {
|
|
31805
|
-
return c.json({ error: "Password is managed by the HALO_PASSWORD environment variable" }, 400);
|
|
31806
|
-
}
|
|
31807
|
-
if (!await verifyPassword(oldPassword, passwordHash())) {
|
|
31808
|
-
return c.json({ error: "Incorrect current password" }, 401);
|
|
31809
|
-
}
|
|
31810
|
-
const weak = passwordStrengthError(newPassword);
|
|
31811
|
-
if (weak)
|
|
31812
|
-
return c.json({ error: weak }, 400);
|
|
31813
|
-
if (newPassword === oldPassword) {
|
|
31814
|
-
return c.json({ error: "New password must differ from the current password" }, 400);
|
|
31815
|
-
}
|
|
31816
|
-
updateConfigLeaves({ "server.password": await hashPassword(newPassword) });
|
|
31817
|
-
return c.json({ ok: true });
|
|
31818
|
-
});
|
|
31819
|
-
return app2;
|
|
31820
|
-
}
|
|
31821
|
-
function passwordStrengthError(pw) {
|
|
31822
|
-
if (pw.length < 8)
|
|
31823
|
-
return "Password must be at least 8 characters";
|
|
31824
|
-
if (!/[A-Za-z]/.test(pw))
|
|
31825
|
-
return "Password must contain at least one letter";
|
|
31826
|
-
if (!/[0-9]/.test(pw))
|
|
31827
|
-
return "Password must contain at least one digit";
|
|
31828
|
-
return null;
|
|
31829
|
-
}
|
|
31830
|
-
function authMiddleware() {
|
|
31831
|
-
return async (c, next) => {
|
|
31832
|
-
const path70 = c.req.path;
|
|
31833
|
-
if (PUBLIC_PATHS.some((p) => path70 === p || path70.startsWith(p + "?"))) {
|
|
31834
|
-
return next();
|
|
31835
|
-
}
|
|
31836
|
-
const token = getCookie(c, COOKIE_NAME);
|
|
31837
|
-
const payload = validateToken(token);
|
|
31838
|
-
if (!payload) {
|
|
31839
|
-
if (!path70.startsWith("/api/")) {
|
|
31840
|
-
return next();
|
|
31841
|
-
}
|
|
31842
|
-
return c.json({ error: "Unauthorized" }, 401);
|
|
31843
|
-
}
|
|
31844
|
-
if (shouldRefresh(payload)) {
|
|
31845
|
-
const newToken = createToken();
|
|
31846
|
-
setTokenCookie(c, newToken);
|
|
31847
|
-
}
|
|
31848
|
-
return next();
|
|
31849
|
-
};
|
|
31850
|
-
}
|
|
31851
|
-
var TOKEN_MAX_AGE, REFRESH_AFTER, COOKIE_NAME, ADMIN_LOGIN_BUCKET, isLockedOut2, recordFailure2, clearFailures2, PUBLIC_PATHS;
|
|
31852
|
-
var init_auth = __esm({
|
|
31853
|
-
"../server/dist/middleware/auth.js"() {
|
|
31854
|
-
"use strict";
|
|
31855
|
-
init_config();
|
|
31856
|
-
init_password_hash();
|
|
31857
|
-
init_setup_config();
|
|
31858
|
-
init_brute_force();
|
|
31859
|
-
TOKEN_MAX_AGE = config.auth.tokenMaxAge;
|
|
31860
|
-
REFRESH_AFTER = config.auth.refreshAfter;
|
|
31861
|
-
COOKIE_NAME = "halo_token";
|
|
31862
|
-
ADMIN_LOGIN_BUCKET = "admin-login";
|
|
31863
|
-
isLockedOut2 = (ip) => isLockedOut(ADMIN_LOGIN_BUCKET, ip);
|
|
31864
|
-
recordFailure2 = (ip) => recordFailure(ADMIN_LOGIN_BUCKET, ip);
|
|
31865
|
-
clearFailures2 = (ip) => clearFailures(ADMIN_LOGIN_BUCKET, ip);
|
|
31866
|
-
PUBLIC_PATHS = ["/api/auth/login", "/api/auth/check", "/api/auth/logout", "/api/health", "/api/web/chat", "/api/web/sessions", "/api/web/stop", "/api/web/history", "/api/web/subscribe", "/api/web/file", "/api/show/state", "/api/show/session", "/api/metrics"];
|
|
31867
|
-
}
|
|
31868
|
-
});
|
|
31869
|
-
|
|
31870
32736
|
// ../server/dist/index.js
|
|
31871
32737
|
var dist_exports = {};
|
|
31872
|
-
import { Hono as
|
|
32738
|
+
import { Hono as Hono23 } from "hono";
|
|
31873
32739
|
import { cors } from "hono/cors";
|
|
31874
32740
|
import { compress } from "hono/compress";
|
|
31875
32741
|
import { serve } from "@hono/node-server";
|
|
31876
32742
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
31877
32743
|
import { WebSocketServer } from "ws";
|
|
31878
|
-
import
|
|
31879
|
-
import
|
|
32744
|
+
import path72 from "node:path";
|
|
32745
|
+
import fs66 from "node:fs";
|
|
31880
32746
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
31881
32747
|
import { homedir as homedir24 } from "node:os";
|
|
31882
32748
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
31883
32749
|
function findProjectRoot() {
|
|
31884
32750
|
let dir = process.cwd();
|
|
31885
32751
|
for (let i = 0; i < 32; i++) {
|
|
31886
|
-
if (
|
|
32752
|
+
if (fs66.existsSync(path72.join(dir, "pnpm-workspace.yaml")))
|
|
31887
32753
|
return dir;
|
|
31888
|
-
const parent =
|
|
32754
|
+
const parent = path72.dirname(dir);
|
|
31889
32755
|
if (parent === dir)
|
|
31890
32756
|
break;
|
|
31891
32757
|
dir = parent;
|
|
@@ -31894,15 +32760,15 @@ function findProjectRoot() {
|
|
|
31894
32760
|
return process.cwd();
|
|
31895
32761
|
}
|
|
31896
32762
|
function acquireSingleInstanceLock(lockFile) {
|
|
31897
|
-
|
|
31898
|
-
const fd =
|
|
32763
|
+
fs66.mkdirSync(path72.dirname(lockFile), { recursive: true });
|
|
32764
|
+
const fd = fs66.openSync(lockFile, fs66.constants.O_RDWR | fs66.constants.O_CREAT, 420);
|
|
31899
32765
|
const result = spawnSync3("flock", ["-n", "3"], {
|
|
31900
32766
|
stdio: ["ignore", "ignore", "ignore", fd]
|
|
31901
32767
|
});
|
|
31902
32768
|
if (result.error) {
|
|
31903
32769
|
const content = readLockPid(fd);
|
|
31904
32770
|
if (content > 0 && isProcessAlive(content)) {
|
|
31905
|
-
|
|
32771
|
+
fs66.closeSync(fd);
|
|
31906
32772
|
console.error(`[Server] another halo server is already running (pid ${content}). kill it first.`);
|
|
31907
32773
|
process.exit(1);
|
|
31908
32774
|
}
|
|
@@ -31911,19 +32777,19 @@ function acquireSingleInstanceLock(lockFile) {
|
|
|
31911
32777
|
if (content > 0 && !isProcessAlive(content)) {
|
|
31912
32778
|
console.warn(`[Server] removing stale server.lock (pid ${content} not running)`);
|
|
31913
32779
|
} else {
|
|
31914
|
-
|
|
32780
|
+
fs66.closeSync(fd);
|
|
31915
32781
|
console.error(`[Server] another halo server is already running${content > 0 ? ` (pid ${content})` : ""}. kill it first.`);
|
|
31916
32782
|
process.exit(1);
|
|
31917
32783
|
}
|
|
31918
32784
|
}
|
|
31919
|
-
|
|
31920
|
-
|
|
31921
|
-
|
|
32785
|
+
fs66.ftruncateSync(fd);
|
|
32786
|
+
fs66.writeSync(fd, String(process.pid));
|
|
32787
|
+
fs66.fsyncSync(fd);
|
|
31922
32788
|
}
|
|
31923
32789
|
function readLockPid(fd) {
|
|
31924
32790
|
try {
|
|
31925
32791
|
const buf = Buffer.alloc(32);
|
|
31926
|
-
const n =
|
|
32792
|
+
const n = fs66.readSync(fd, buf, 0, 32, 0);
|
|
31927
32793
|
return parseInt(buf.subarray(0, n).toString("utf-8").trim(), 10) || 0;
|
|
31928
32794
|
} catch {
|
|
31929
32795
|
return 0;
|
|
@@ -31951,7 +32817,7 @@ function isProcessAlive(pid) {
|
|
|
31951
32817
|
}
|
|
31952
32818
|
}
|
|
31953
32819
|
function readGitSha() {
|
|
31954
|
-
if (!
|
|
32820
|
+
if (!fs66.existsSync(path72.join(PROJECT_ROOT, "pnpm-workspace.yaml")) || !fs66.existsSync(path72.join(PROJECT_ROOT, ".git")))
|
|
31955
32821
|
return null;
|
|
31956
32822
|
try {
|
|
31957
32823
|
const rev = spawnSync3("git", ["rev-parse", "--short", "HEAD"], { cwd: PROJECT_ROOT, encoding: "utf-8" });
|
|
@@ -31968,19 +32834,20 @@ function readGitSha() {
|
|
|
31968
32834
|
}
|
|
31969
32835
|
function resolveFrontendDir() {
|
|
31970
32836
|
if (process.env.HALO_FRONTEND_DIR)
|
|
31971
|
-
return
|
|
31972
|
-
const bundled =
|
|
31973
|
-
if (
|
|
32837
|
+
return path72.resolve(process.env.HALO_FRONTEND_DIR);
|
|
32838
|
+
const bundled = path72.resolve(__dirname3, "..", "admin-out");
|
|
32839
|
+
if (fs66.existsSync(path72.join(bundled, "index.html")))
|
|
31974
32840
|
return bundled;
|
|
31975
|
-
const sibling =
|
|
31976
|
-
if (
|
|
32841
|
+
const sibling = path72.resolve(__dirname3, "..", "..", "admin", "out");
|
|
32842
|
+
if (fs66.existsSync(path72.join(sibling, "index.html")))
|
|
31977
32843
|
return sibling;
|
|
31978
|
-
return
|
|
32844
|
+
return path72.resolve(PROJECT_ROOT, "packages", "admin", "out");
|
|
31979
32845
|
}
|
|
31980
32846
|
async function gracefulShutdown(signal) {
|
|
31981
32847
|
console.log(`[Server] Received ${signal}, shutting down gracefully...`);
|
|
31982
32848
|
stopEvoTicker();
|
|
31983
32849
|
stopArchiveDaemon();
|
|
32850
|
+
stop();
|
|
31984
32851
|
stopCronDaemon();
|
|
31985
32852
|
await shutdownChannels();
|
|
31986
32853
|
wss.clients.forEach((client) => {
|
|
@@ -31994,7 +32861,7 @@ async function gracefulShutdown(signal) {
|
|
|
31994
32861
|
await shutdownObservability();
|
|
31995
32862
|
process.exit(0);
|
|
31996
32863
|
}
|
|
31997
|
-
var __dirname3, PORT, HALO_VERSION, PROJECT_ROOT, HALO_HOME4, GIT_SHA, AGENTCORE, app, allowlist, authRoutes, fileRoutes, dataPreviewRoutes, gitRoutes, agentConfigRoutes, skillRoutes, settingsRoutes, evolutionRoutes, cronRoutes, channelDb, registry, sessionRoutes, sessionArchiveRoutes, showRoutes, metricsRoutes, commandRoutes, FRONTEND_DIR, server, wss;
|
|
32864
|
+
var __dirname3, PORT, HALO_VERSION, PROJECT_ROOT, HALO_HOME4, GIT_SHA, AGENTCORE, app, allowlist, authRoutes, fileRoutes, dataPreviewRoutes, gitRoutes, agentConfigRoutes, skillRoutes, settingsRoutes, evolutionRoutes, cronRoutes, extensionRoutes, channelDb, registry, sessionRoutes, sessionArchiveRoutes, showRoutes, metricsRoutes, commandRoutes, FRONTEND_DIR, server, wss;
|
|
31998
32865
|
var init_dist2 = __esm({
|
|
31999
32866
|
async "../server/dist/index.js"() {
|
|
32000
32867
|
"use strict";
|
|
@@ -32023,11 +32890,13 @@ var init_dist2 = __esm({
|
|
|
32023
32890
|
init_runs_db();
|
|
32024
32891
|
init_runner();
|
|
32025
32892
|
init_cron();
|
|
32893
|
+
init_extensions();
|
|
32894
|
+
init_watcher();
|
|
32026
32895
|
init_evo_db();
|
|
32027
32896
|
init_ticker();
|
|
32028
32897
|
init_archive();
|
|
32029
32898
|
init_spawn();
|
|
32030
|
-
|
|
32899
|
+
init_registry3();
|
|
32031
32900
|
init_descriptors();
|
|
32032
32901
|
init_auth();
|
|
32033
32902
|
init_logger();
|
|
@@ -32036,13 +32905,13 @@ var init_dist2 = __esm({
|
|
|
32036
32905
|
init_sandbox();
|
|
32037
32906
|
init_init();
|
|
32038
32907
|
init_git_ssh();
|
|
32039
|
-
__dirname3 =
|
|
32908
|
+
__dirname3 = path72.dirname(fileURLToPath3(import.meta.url));
|
|
32040
32909
|
PORT = config.server.port;
|
|
32041
|
-
HALO_VERSION = "1.
|
|
32910
|
+
HALO_VERSION = "1.5.0-alpha";
|
|
32042
32911
|
PROJECT_ROOT = findProjectRoot();
|
|
32043
|
-
HALO_HOME4 =
|
|
32912
|
+
HALO_HOME4 = path72.join(homedir24(), ".halo");
|
|
32044
32913
|
GIT_SHA = readGitSha();
|
|
32045
|
-
if (!
|
|
32914
|
+
if (!fs66.existsSync(path72.join(HALO_HOME4, "global", ".template-version"))) {
|
|
32046
32915
|
process.stderr.write("\x1B[31m[Server] ~/.halo/global/ not initialized. Run `halo setup` first.\x1B[0m\n");
|
|
32047
32916
|
process.exit(1);
|
|
32048
32917
|
}
|
|
@@ -32063,20 +32932,8 @@ var init_dist2 = __esm({
|
|
|
32063
32932
|
process.exit(1);
|
|
32064
32933
|
}
|
|
32065
32934
|
if (!AGENTCORE)
|
|
32066
|
-
acquireSingleInstanceLock(
|
|
32067
|
-
|
|
32068
|
-
const declared = new Set(commandRegistry.listDescriptors().filter((d) => d.source === "builtin").map((d) => d.slashName));
|
|
32069
|
-
const dispatched = new Set(DISPATCH_COMMANDS);
|
|
32070
|
-
const declaredServer = new Set(commandRegistry.listDescriptors().filter((d) => d.type === "server" && d.source === "builtin").map((d) => d.slashName));
|
|
32071
|
-
const missingDispatch = [...declaredServer].filter((n) => !dispatched.has(n));
|
|
32072
|
-
const orphanDispatch = [...dispatched].filter((n) => !declared.has(n));
|
|
32073
|
-
if (missingDispatch.length > 0) {
|
|
32074
|
-
throw new Error(`[Server] Command descriptors without a dispatch case: ${missingDispatch.join(", ")}. Either add a case in channels/shared/commands.ts dispatchCommand or change the descriptor type to 'client'.`);
|
|
32075
|
-
}
|
|
32076
|
-
if (orphanDispatch.length > 0) {
|
|
32077
|
-
throw new Error(`[Server] Dispatch cases without a descriptor: ${orphanDispatch.join(", ")}. Add a registerDescriptor entry in commands/index.ts or remove the dispatch case.`);
|
|
32078
|
-
}
|
|
32079
|
-
}
|
|
32935
|
+
acquireSingleInstanceLock(path72.join(HALO_HOME4, "global", "server.lock"));
|
|
32936
|
+
commandRegistry.assertDispatchCommands(DISPATCH_COMMANDS);
|
|
32080
32937
|
await initObservability();
|
|
32081
32938
|
initLogger();
|
|
32082
32939
|
await initBwrapCheck();
|
|
@@ -32089,7 +32946,7 @@ var init_dist2 = __esm({
|
|
|
32089
32946
|
ensureSshAgent();
|
|
32090
32947
|
console.log(`[Server] Halo home: ${HALO_HOME4}`);
|
|
32091
32948
|
console.log("[Server] Services initialized (ModelRuntime)");
|
|
32092
|
-
app = new
|
|
32949
|
+
app = new Hono23();
|
|
32093
32950
|
allowlist = config.server.corsOrigins;
|
|
32094
32951
|
app.use("/*", cors({
|
|
32095
32952
|
origin: allowlist.length > 0 ? (origin) => origin && allowlist.includes(origin) ? origin : null : (origin) => origin ?? "*",
|
|
@@ -32136,11 +32993,15 @@ var init_dist2 = __esm({
|
|
|
32136
32993
|
app.route("/api", evolutionRoutes);
|
|
32137
32994
|
cronRoutes = createCronRoutes();
|
|
32138
32995
|
app.route("/api", cronRoutes);
|
|
32139
|
-
|
|
32996
|
+
extensionRoutes = createExtensionRoutes();
|
|
32997
|
+
app.route("/api", extensionRoutes);
|
|
32998
|
+
if (!AGENTCORE)
|
|
32999
|
+
start();
|
|
33000
|
+
channelDb = createChannelDb(path72.join(HALO_HOME4, "secrets"));
|
|
32140
33001
|
setChannelDb(channelDb);
|
|
32141
|
-
setEvoDb(createEvoDb(
|
|
32142
|
-
setCronDb(createCronDb(
|
|
32143
|
-
setRunsDb(createRunsDb(
|
|
33002
|
+
setEvoDb(createEvoDb(path72.join(HALO_HOME4, "global")));
|
|
33003
|
+
setCronDb(createCronDb(path72.join(HALO_HOME4, "global")));
|
|
33004
|
+
setRunsDb(createRunsDb(path72.join(HALO_HOME4, "global")));
|
|
32144
33005
|
setEvoSpawner(realEvoSpawner);
|
|
32145
33006
|
if (!AGENTCORE) {
|
|
32146
33007
|
startEvoTicker();
|
|
@@ -32150,7 +33011,7 @@ var init_dist2 = __esm({
|
|
|
32150
33011
|
setRelayRegistry(registry);
|
|
32151
33012
|
setCronSessionRegistry(registry);
|
|
32152
33013
|
for (const ws of listRunningWorkspaces()) {
|
|
32153
|
-
if (!
|
|
33014
|
+
if (!fs66.existsSync(path72.join(ws, ".halo")))
|
|
32154
33015
|
continue;
|
|
32155
33016
|
if (!claimWorkspaceRuntime(ws))
|
|
32156
33017
|
continue;
|
|
@@ -32179,11 +33040,11 @@ var init_dist2 = __esm({
|
|
|
32179
33040
|
console.log(`[Server] AgentCore runtime mode \u2014 workspace: ${config.server.agentcoreWorkspace}`);
|
|
32180
33041
|
}
|
|
32181
33042
|
FRONTEND_DIR = resolveFrontendDir();
|
|
32182
|
-
app.use("/*", serveStatic({ root:
|
|
33043
|
+
app.use("/*", serveStatic({ root: path72.relative(process.cwd(), FRONTEND_DIR) }));
|
|
32183
33044
|
app.get("/*", (c) => {
|
|
32184
|
-
const indexPath =
|
|
33045
|
+
const indexPath = path72.join(FRONTEND_DIR, "index.html");
|
|
32185
33046
|
try {
|
|
32186
|
-
const html =
|
|
33047
|
+
const html = fs66.readFileSync(indexPath, "utf-8");
|
|
32187
33048
|
return c.html(html);
|
|
32188
33049
|
} catch {
|
|
32189
33050
|
return c.text("Frontend not built. Run: cd packages/admin && npx next build", 503);
|
|
@@ -32973,11 +33834,11 @@ process.emitWarning = (warning, ...args) => {
|
|
|
32973
33834
|
|
|
32974
33835
|
// src/index.ts
|
|
32975
33836
|
init_server_supervisor();
|
|
32976
|
-
import
|
|
32977
|
-
import
|
|
33837
|
+
import path73 from "node:path";
|
|
33838
|
+
import fs67 from "node:fs";
|
|
32978
33839
|
import { homedir as homedir25 } from "node:os";
|
|
32979
33840
|
import { parseArgs as parseArgs2 } from "node:util";
|
|
32980
|
-
var VERSION = "1.
|
|
33841
|
+
var VERSION = "1.5.0-alpha";
|
|
32981
33842
|
var HELP_TOP = `Usage: halo <command> [options]
|
|
32982
33843
|
|
|
32983
33844
|
Commands:
|
|
@@ -33234,8 +34095,8 @@ async function cmdSetup(argv = []) {
|
|
|
33234
34095
|
const { buildBindProviderPlan: buildBindProviderPlan2, KEEP_CURRENT: KEEP_CURRENT2, AWS_CHAIN_PROVIDER: AWS_CHAIN_PROVIDER2 } = await Promise.resolve().then(() => (init_setup_bind_provider(), setup_bind_provider_exports));
|
|
33235
34096
|
const { promptText: promptText2, promptPassword: promptPassword2, promptSelect: promptSelect2, promptMultiSelect: promptMultiSelect2 } = await Promise.resolve().then(() => (init_setup_prompts(), setup_prompts_exports));
|
|
33236
34097
|
const nonInteractive = argv.includes("--non-interactive") || argv.includes("-y");
|
|
33237
|
-
const haloHome2 =
|
|
33238
|
-
const globalDir2 =
|
|
34098
|
+
const haloHome2 = path73.join(homedir25(), ".halo");
|
|
34099
|
+
const globalDir2 = path73.join(haloHome2, "global");
|
|
33239
34100
|
ensureHaloHome2(haloHome2);
|
|
33240
34101
|
process.stderr.write(`[setup] ${haloHome2}/ ready
|
|
33241
34102
|
`);
|
|
@@ -33578,7 +34439,7 @@ async function cmdUpgrade() {
|
|
|
33578
34439
|
`);
|
|
33579
34440
|
process.exit(1);
|
|
33580
34441
|
}
|
|
33581
|
-
const broken = verifyNativeModules2(
|
|
34442
|
+
const broken = verifyNativeModules2(path73.join(rootDir, PKG));
|
|
33582
34443
|
if (broken.length > 0) {
|
|
33583
34444
|
process.stderr.write(
|
|
33584
34445
|
`
|
|
@@ -33609,7 +34470,7 @@ halo: upgrade failed (npm exited ${install.status}). ${hint}
|
|
|
33609
34470
|
async function cmdAgents(flags) {
|
|
33610
34471
|
const { initRuntime: initRuntime2, listAgents: listAgents2 } = await Promise.resolve().then(() => (init_harness(), harness_exports));
|
|
33611
34472
|
await initRuntime2();
|
|
33612
|
-
const workspace =
|
|
34473
|
+
const workspace = path73.resolve(flags.workspace ?? process.cwd());
|
|
33613
34474
|
const agents = await listAgents2(workspace);
|
|
33614
34475
|
if (agents.length === 0) {
|
|
33615
34476
|
process.stderr.write("No agents found.\n");
|
|
@@ -33625,7 +34486,7 @@ async function cmdAgents(flags) {
|
|
|
33625
34486
|
async function cmdSessions(flags) {
|
|
33626
34487
|
const { initRuntime: initRuntime2, listSessions: listSessions2 } = await Promise.resolve().then(() => (init_harness(), harness_exports));
|
|
33627
34488
|
await initRuntime2();
|
|
33628
|
-
const workspace =
|
|
34489
|
+
const workspace = path73.resolve(flags.workspace ?? process.cwd());
|
|
33629
34490
|
const sessions = listSessions2(workspace);
|
|
33630
34491
|
if (sessions.length === 0) {
|
|
33631
34492
|
process.stderr.write("No sessions found.\n");
|
|
@@ -33738,11 +34599,11 @@ ${stdinContent}` : stdinContent;
|
|
|
33738
34599
|
harness.destroy();
|
|
33739
34600
|
}
|
|
33740
34601
|
}
|
|
33741
|
-
var SERVER_LOCK_PATH =
|
|
33742
|
-
var SERVER_LOG_PATH =
|
|
34602
|
+
var SERVER_LOCK_PATH = path73.join(homedir25(), ".halo", "global", "server.lock");
|
|
34603
|
+
var SERVER_LOG_PATH = path73.join(homedir25(), ".halo", "logs", "server.log");
|
|
33743
34604
|
function readServerPid() {
|
|
33744
34605
|
try {
|
|
33745
|
-
const txt =
|
|
34606
|
+
const txt = fs67.readFileSync(SERVER_LOCK_PATH, "utf-8").trim();
|
|
33746
34607
|
const n = parseInt(txt, 10);
|
|
33747
34608
|
return Number.isFinite(n) && n > 0 ? n : null;
|
|
33748
34609
|
} catch {
|
|
@@ -33829,8 +34690,8 @@ async function cmdServerStart(argv, _unused) {
|
|
|
33829
34690
|
return;
|
|
33830
34691
|
}
|
|
33831
34692
|
const { spawn: spawn5 } = await import("node:child_process");
|
|
33832
|
-
|
|
33833
|
-
const out =
|
|
34693
|
+
fs67.mkdirSync(path73.dirname(SERVER_LOG_PATH), { recursive: true });
|
|
34694
|
+
const out = fs67.openSync(SERVER_LOG_PATH, "a");
|
|
33834
34695
|
const child = spawn5(process.argv[0], [process.argv[1], "server", "start"], {
|
|
33835
34696
|
detached: true,
|
|
33836
34697
|
stdio: ["ignore", out, out],
|
|
@@ -33931,7 +34792,7 @@ async function cmdServerLogs(argv) {
|
|
|
33931
34792
|
lines: { type: "string", short: "n" }
|
|
33932
34793
|
}
|
|
33933
34794
|
});
|
|
33934
|
-
if (!
|
|
34795
|
+
if (!fs67.existsSync(SERVER_LOG_PATH)) {
|
|
33935
34796
|
process.stderr.write(`No log file at ${SERVER_LOG_PATH}
|
|
33936
34797
|
`);
|
|
33937
34798
|
return;
|
|
@@ -33949,7 +34810,7 @@ function exitMissingSetup() {
|
|
|
33949
34810
|
process.exit(1);
|
|
33950
34811
|
}
|
|
33951
34812
|
function isHaloHomeReady() {
|
|
33952
|
-
return
|
|
34813
|
+
return fs67.existsSync(path73.join(homedir25(), ".halo", "global", ".template-version"));
|
|
33953
34814
|
}
|
|
33954
34815
|
async function main2() {
|
|
33955
34816
|
const argv = process.argv.slice(2);
|