@turmind/halo 1.5.10 → 1.5.11
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/admin-out/404.html +1 -1
- package/admin-out/_next/static/chunks/{388-d5950f264d91625a.js → 941-4719e7343b54b9bd.js} +1 -1
- package/admin-out/_next/static/chunks/989-86b0f677f69ccf3b.js +1 -0
- package/admin-out/_next/static/chunks/app/page-4792aa1c79f7e7f1.js +1 -0
- package/admin-out/_next/static/css/97f3ac570664d044.css +3 -0
- package/admin-out/index.html +1 -1
- package/admin-out/index.txt +5 -5
- package/bundled-docs/design/agent.md +7 -5
- package/bundled-docs/design/halo-city.md +6 -0
- package/bundled-docs/design/prompt-system.md +1 -1
- package/bundled-docs/design/relay.md +6 -6
- package/bundled-docs/design/session.md +12 -11
- package/bundled-docs/design/web.md +33 -5
- package/bundled-docs/dev/acp-adapter.md +59 -175
- package/bundled-docs/dev/api.md +5 -2
- package/bundled-docs/dev/deploy.md +1 -1
- package/bundled-docs/dev/desktop-packaging.md +14 -0
- package/bundled-docs/dev/env.md +2 -2
- package/bundled-docs/dev/tools.md +8 -8
- package/bundled-docs/guide/README.md +1 -1
- package/bundled-docs/guide/channels/README.md +3 -3
- package/bundled-docs/guide/channels/acp.md +67 -42
- package/bundled-docs/guide/channels/feishu.md +1 -1
- package/bundled-docs/guide/channels/slack.md +1 -1
- package/bundled-docs/guide/channels/telegram.md +1 -1
- package/bundled-docs/guide/channels/web.md +10 -8
- package/bundled-docs/guide/channels/wechat.md +1 -1
- package/bundled-docs/guide/channels/wecom.md +1 -1
- package/bundled-docs/guide/chat.md +0 -1
- package/bundled-docs/guide/cli.md +1 -2
- package/bundled-docs/guide/getting-started.md +1 -1
- package/bundled-docs/guide/skills.md +1 -2
- package/bundled-docs/requirements/chat.md +14 -0
- package/bundled-docs/requirements/command.md +1 -2
- package/bundled-docs/requirements/explorer.md +1 -0
- package/bundled-docs/requirements/terminal.md +15 -0
- package/dist/index.js +810 -333
- package/package.json +1 -1
- package/templates/agents/__score__/AGENT.md +1 -1
- package/templates/agents/default/agent.yaml +0 -1
- package/templates/prompts/all/TOOL_SHELL.windows.md +2 -1
- package/templates/skills/skill/SKILL.md +1 -1
- package/templates/skills/workspace/stage.py +1 -1
- package/admin-out/_next/static/chunks/989-1797287c46139412.js +0 -1
- package/admin-out/_next/static/chunks/app/page-44ab337b2f071e4d.js +0 -1
- package/admin-out/_next/static/css/d53aa54212002364.css +0 -3
- package/templates/skills/acp/SKILL.md +0 -243
- package/templates/skills/acp/templates/SKILL.md.tmpl +0 -75
- package/templates/skills/acp/templates/ask.py +0 -177
- package/templates/skills/acp/templates/config.yaml.tmpl +0 -38
- /package/admin-out/_next/static/{A-Rjx2yltZDKaSO1qVv7K → GO2W5JoiC46FIxLjtFAXx}/_buildManifest.js +0 -0
- /package/admin-out/_next/static/{A-Rjx2yltZDKaSO1qVv7K → GO2W5JoiC46FIxLjtFAXx}/_ssgManifest.js +0 -0
package/dist/index.js
CHANGED
|
@@ -2967,6 +2967,7 @@ Error: tool "${tu.name}" was not run \u2014 the response hit the output token li
|
|
|
2967
2967
|
const inputErrors = new Map(result.toolCalls.filter((tc) => tc.inputError).map((tc) => [tc.id, tc.inputError]));
|
|
2968
2968
|
const toolResults = [];
|
|
2969
2969
|
let shouldEndTurn = false;
|
|
2970
|
+
let cutId = null;
|
|
2970
2971
|
try {
|
|
2971
2972
|
for (const tu of toolUseBlocks) {
|
|
2972
2973
|
if (options?.cancelSignal?.aborted)
|
|
@@ -3006,8 +3007,10 @@ Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
|
3006
3007
|
isError = true;
|
|
3007
3008
|
}
|
|
3008
3009
|
}
|
|
3009
|
-
if (options?.cancelSignal?.aborted)
|
|
3010
|
+
if (options?.cancelSignal?.aborted) {
|
|
3011
|
+
cutId = tu.id;
|
|
3010
3012
|
return;
|
|
3013
|
+
}
|
|
3011
3014
|
const uiCap = config.limits.toolResultUiMax;
|
|
3012
3015
|
const resultTextFull = resultText.length > uiCap ? resultText.slice(0, uiCap) + `
|
|
3013
3016
|
|
|
@@ -3053,6 +3056,18 @@ Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
|
3053
3056
|
};
|
|
3054
3057
|
}
|
|
3055
3058
|
} finally {
|
|
3059
|
+
const answered = new Set(toolResults.map((b) => b.tool_use_id));
|
|
3060
|
+
for (const tu of toolUseBlocks) {
|
|
3061
|
+
if (answered.has(tu.id) || tu.id === cutId)
|
|
3062
|
+
continue;
|
|
3063
|
+
toolResults.push({
|
|
3064
|
+
type: "tool_result",
|
|
3065
|
+
tool_use_id: tu.id,
|
|
3066
|
+
content: `${TOOL_ERROR_MARKER}
|
|
3067
|
+
Error: tool "${tu.name}" was not run \u2014 the turn was interrupted before this call started. Safe to re-issue if still needed.`,
|
|
3068
|
+
is_error: true
|
|
3069
|
+
});
|
|
3070
|
+
}
|
|
3056
3071
|
if (toolResults.length > 0) {
|
|
3057
3072
|
this.messages.push({ role: "user", content: toolResults });
|
|
3058
3073
|
}
|
|
@@ -3780,6 +3795,11 @@ function ensureHaloHome(haloHome2) {
|
|
|
3780
3795
|
console.log(`[Init] Removed stale \`${staleId}\` skill (merged into \`web-search\`)`);
|
|
3781
3796
|
}
|
|
3782
3797
|
}
|
|
3798
|
+
const staleAcpSkill = path12.join(globalDir2, "skills", "acp");
|
|
3799
|
+
if (fs9.existsSync(staleAcpSkill)) {
|
|
3800
|
+
fs9.rmSync(staleAcpSkill, { recursive: true, force: true });
|
|
3801
|
+
console.log("[Init] Removed stale `acp` skill (outbound ACP client dropped)");
|
|
3802
|
+
}
|
|
3783
3803
|
const docsSrc = resolveDocsSource();
|
|
3784
3804
|
if (!docsSrc) {
|
|
3785
3805
|
console.log(`[Init] No docs source found \u2014 skipping platform docs seed. Searched: ${DOCS_SRC_CANDIDATES.join(", ")}`);
|
|
@@ -3846,7 +3866,7 @@ var init_init = __esm({
|
|
|
3846
3866
|
path12.resolve(__dirname, "..", "bundled-docs"),
|
|
3847
3867
|
path12.resolve(__dirname, "..", "..", "..", ".halo", "docs")
|
|
3848
3868
|
];
|
|
3849
|
-
TEMPLATE_VERSION =
|
|
3869
|
+
TEMPLATE_VERSION = 92;
|
|
3850
3870
|
VERSION_FILE = ".template-version";
|
|
3851
3871
|
SKIP_NAMES = /* @__PURE__ */ new Set([".DS_Store", "schema.sql", "__pycache__", ".pytest_cache"]);
|
|
3852
3872
|
BUILTIN_AGENT_IDS = /* @__PURE__ */ new Set([
|
|
@@ -3865,11 +3885,6 @@ var init_init = __esm({
|
|
|
3865
3885
|
"skill",
|
|
3866
3886
|
"workspace",
|
|
3867
3887
|
"cron",
|
|
3868
|
-
// Meta-skill: walks the user through generating a per-remote
|
|
3869
|
-
// `ask-<label>` ACP binding skill. The generated bindings live in
|
|
3870
|
-
// user-owned skill dirs (workspace or global) and aren't templated;
|
|
3871
|
-
// only this generator itself is platform-owned.
|
|
3872
|
-
"acp",
|
|
3873
3888
|
// Always-on file delivery primitive: every channel handler (web /
|
|
3874
3889
|
// wechat / telegram / slack / feishu) intercepts `MEDIA:<path>` from
|
|
3875
3890
|
// the agent's reply and uploads it. The skill body teaches the agent
|
|
@@ -4043,6 +4058,12 @@ async function callerTeamFor(sm, sessionId) {
|
|
|
4043
4058
|
function isSameTree(a, b) {
|
|
4044
4059
|
return a.split(">")[0] === b.split(">")[0];
|
|
4045
4060
|
}
|
|
4061
|
+
function isSelfOrAncestor(callerId, targetId) {
|
|
4062
|
+
return callerId === targetId || callerId.startsWith(targetId + ">");
|
|
4063
|
+
}
|
|
4064
|
+
function selfOrAncestorRefusal(verb, targetId) {
|
|
4065
|
+
return `cannot ${verb} session ${targetId}: it is your own session or one of its ancestors, so the ${verb} would wait on the very turn making this call. To end your work, just finish this turn.`;
|
|
4066
|
+
}
|
|
4046
4067
|
function buildSessionTools(sm, sessionId) {
|
|
4047
4068
|
const startSessionTool = {
|
|
4048
4069
|
name: "start_session",
|
|
@@ -4192,6 +4213,9 @@ ${params.message}` : params.message;
|
|
|
4192
4213
|
if (!isSameTree(params.session_id, sessionId)) {
|
|
4193
4214
|
return JSON.stringify({ code: 1, error: `session ${params.session_id} not found` });
|
|
4194
4215
|
}
|
|
4216
|
+
if (isSelfOrAncestor(sessionId, params.session_id)) {
|
|
4217
|
+
return JSON.stringify({ code: 1, error: selfOrAncestorRefusal("stop", params.session_id) });
|
|
4218
|
+
}
|
|
4195
4219
|
await sm.stopSession(params.session_id);
|
|
4196
4220
|
return JSON.stringify({ code: 0, message: `Session ${params.session_id} stopped.` });
|
|
4197
4221
|
} catch (err) {
|
|
@@ -4213,6 +4237,9 @@ ${params.message}` : params.message;
|
|
|
4213
4237
|
if (!isSameTree(params.session_id, sessionId)) {
|
|
4214
4238
|
return JSON.stringify({ code: 1, error: `session ${params.session_id} not found` });
|
|
4215
4239
|
}
|
|
4240
|
+
if (isSelfOrAncestor(sessionId, params.session_id)) {
|
|
4241
|
+
return JSON.stringify({ code: 1, error: selfOrAncestorRefusal("archive", params.session_id) });
|
|
4242
|
+
}
|
|
4216
4243
|
const count = await sm.archiveSessionTree(params.session_id);
|
|
4217
4244
|
return JSON.stringify({ code: 0, message: `Archived ${count} session(s).` });
|
|
4218
4245
|
} catch (err) {
|
|
@@ -7130,7 +7157,7 @@ function resolveTarget(workspace) {
|
|
|
7130
7157
|
function buildRelayTools(host, callerSessionId) {
|
|
7131
7158
|
const relaySend = {
|
|
7132
7159
|
name: "relay_send",
|
|
7133
|
-
description: "Dispatch a message to a session in ANOTHER workspace on this server. Creates the session if `session_id` does not exist there (with `agent_id`, or the workspace's default agent). If the session is busy the message is queued and the current step is softly interrupted (finishes its current tool, then reads your message) \u2014 use this for follow-ups and corrections too. Returns immediately; when the target's whole subtree finishes, its wrap-up is delivered to you as a `[Relay report \xB7 \u2026]` message. If a busy target answers your message and then resumes the task it interrupted, that answer arrives first as a `[Relay interim report \xB7 \u2026]` \u2014 the final report still follows. Do not poll \u2014 the report arrives on its own. Returns JSON with code 0 on success.",
|
|
7160
|
+
description: "Dispatch a message to a session in ANOTHER workspace on this server. Creates the session if `session_id` does not exist there (with `agent_id`, or the workspace's default agent). If the session is busy the message is queued and the current step is softly interrupted (finishes its current batch of tool calls, then reads your message) \u2014 use this for follow-ups and corrections too. Returns immediately; when the target's whole subtree finishes, its wrap-up is delivered to you as a `[Relay report \xB7 \u2026]` message. If a busy target answers your message and then resumes the task it interrupted, that answer arrives first as a `[Relay interim report \xB7 \u2026]` \u2014 the final report still follows. Do not poll \u2014 the report arrives on its own. Returns JSON with code 0 on success.",
|
|
7134
7161
|
inputSchema: {
|
|
7135
7162
|
type: "object",
|
|
7136
7163
|
properties: {
|
|
@@ -7174,6 +7201,9 @@ function buildRelayTools(host, callerSessionId) {
|
|
|
7174
7201
|
if (typeof resolved === "string")
|
|
7175
7202
|
return resolved;
|
|
7176
7203
|
const { wsPath, target } = resolved;
|
|
7204
|
+
if (target === host && isSelfOrAncestor(callerSessionId, params.session_id)) {
|
|
7205
|
+
return jsonErr2(`cannot relay to session ${params.session_id}: it is your own session or one of its ancestors, so its report would come back to you. Talk to your parent through your final reply, or use the session tools.`);
|
|
7206
|
+
}
|
|
7177
7207
|
if (!target.getSessionById(params.session_id)) {
|
|
7178
7208
|
if (hard)
|
|
7179
7209
|
return jsonErr2("session not found");
|
|
@@ -7208,6 +7238,9 @@ ${params.message}`;
|
|
|
7208
7238
|
const { target } = resolved;
|
|
7209
7239
|
if (!target.getSessionById(params.session_id))
|
|
7210
7240
|
return jsonErr2("session not found");
|
|
7241
|
+
if (target === host && isSelfOrAncestor(callerSessionId, params.session_id)) {
|
|
7242
|
+
return jsonErr2(selfOrAncestorRefusal("stop", params.session_id));
|
|
7243
|
+
}
|
|
7211
7244
|
await target.stopSession(params.session_id);
|
|
7212
7245
|
return JSON.stringify({ code: 0, message: `Session ${params.session_id} stopped.` });
|
|
7213
7246
|
} catch (err) {
|
|
@@ -7276,6 +7309,7 @@ var init_relay = __esm({
|
|
|
7276
7309
|
init_schema();
|
|
7277
7310
|
init_config();
|
|
7278
7311
|
init_commands2();
|
|
7312
|
+
init_session_tools();
|
|
7279
7313
|
RELAY_CHANNEL_PREFIX = "[channel: relay | from: ";
|
|
7280
7314
|
_registry = null;
|
|
7281
7315
|
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>`.";
|
|
@@ -7481,27 +7515,27 @@ var init_workspace = __esm({
|
|
|
7481
7515
|
constructor(projectRoot) {
|
|
7482
7516
|
this.projectRoot = projectRoot;
|
|
7483
7517
|
}
|
|
7484
|
-
async readFile(
|
|
7485
|
-
const fullPath = this.validatePath(
|
|
7518
|
+
async readFile(path77) {
|
|
7519
|
+
const fullPath = this.validatePath(path77);
|
|
7486
7520
|
return readFile(fullPath, "utf-8");
|
|
7487
7521
|
}
|
|
7488
|
-
async writeFile(
|
|
7489
|
-
const fullPath = this.validatePath(
|
|
7522
|
+
async writeFile(path77, content) {
|
|
7523
|
+
const fullPath = this.validatePath(path77);
|
|
7490
7524
|
const dir = resolve(fullPath, "..");
|
|
7491
7525
|
await mkdir(dir, { recursive: true });
|
|
7492
7526
|
await writeFile(fullPath, content, "utf-8");
|
|
7493
7527
|
}
|
|
7494
|
-
validatePath(
|
|
7495
|
-
const fullPath = isAbsolute(
|
|
7528
|
+
validatePath(path77) {
|
|
7529
|
+
const fullPath = isAbsolute(path77) ? resolve(path77) : resolve(this.projectRoot, path77);
|
|
7496
7530
|
const root = resolve(this.projectRoot);
|
|
7497
7531
|
if (fullPath !== root && !fullPath.startsWith(root + sep)) {
|
|
7498
|
-
throw new WorkspaceError(`Path "${
|
|
7532
|
+
throw new WorkspaceError(`Path "${path77}" is outside the workspace directory "${this.projectRoot}"`);
|
|
7499
7533
|
}
|
|
7500
7534
|
try {
|
|
7501
7535
|
const real = realpathSync2(fullPath);
|
|
7502
7536
|
const realRoot = realpathSync2(root);
|
|
7503
7537
|
if (real !== realRoot && !real.startsWith(realRoot + sep)) {
|
|
7504
|
-
throw new WorkspaceError(`Path "${
|
|
7538
|
+
throw new WorkspaceError(`Path "${path77}" is outside the workspace directory "${this.projectRoot}"`);
|
|
7505
7539
|
}
|
|
7506
7540
|
} catch (err) {
|
|
7507
7541
|
if (err instanceof WorkspaceError)
|
|
@@ -7688,12 +7722,12 @@ var init_git_manager = __esm({
|
|
|
7688
7722
|
const code = parts[0];
|
|
7689
7723
|
const status = code[0];
|
|
7690
7724
|
const isRenameOrCopy = status === "R" || status === "C";
|
|
7691
|
-
const
|
|
7692
|
-
if (!
|
|
7725
|
+
const path77 = isRenameOrCopy ? parts[2] : parts[1];
|
|
7726
|
+
if (!path77 || seen.has(path77))
|
|
7693
7727
|
continue;
|
|
7694
|
-
seen.add(
|
|
7728
|
+
seen.add(path77);
|
|
7695
7729
|
files.push({
|
|
7696
|
-
path:
|
|
7730
|
+
path: path77,
|
|
7697
7731
|
status,
|
|
7698
7732
|
from: isRenameOrCopy ? parts[1] : void 0
|
|
7699
7733
|
});
|
|
@@ -7841,9 +7875,9 @@ var init_git_manager = __esm({
|
|
|
7841
7875
|
* empty for an added file or the root commit) and `modified` is the version
|
|
7842
7876
|
* at `commit` (empty for a deletion). The working-tree/staged path above is
|
|
7843
7877
|
* left entirely untouched (commit === undefined preserves existing behavior). */
|
|
7844
|
-
async getFileDiff(
|
|
7878
|
+
async getFileDiff(path77, staged, from, commit) {
|
|
7845
7879
|
if (commit) {
|
|
7846
|
-
const parentPath = from ||
|
|
7880
|
+
const parentPath = from || path77;
|
|
7847
7881
|
let original2 = "";
|
|
7848
7882
|
try {
|
|
7849
7883
|
original2 = await this.git.show([`${commit}^:${parentPath}`]);
|
|
@@ -7852,13 +7886,13 @@ var init_git_manager = __esm({
|
|
|
7852
7886
|
}
|
|
7853
7887
|
let modified2 = "";
|
|
7854
7888
|
try {
|
|
7855
|
-
modified2 = await this.git.show([`${commit}:${
|
|
7889
|
+
modified2 = await this.git.show([`${commit}:${path77}`]);
|
|
7856
7890
|
} catch {
|
|
7857
7891
|
modified2 = "";
|
|
7858
7892
|
}
|
|
7859
7893
|
return { original: original2, modified: modified2 };
|
|
7860
7894
|
}
|
|
7861
|
-
const headPath = from ||
|
|
7895
|
+
const headPath = from || path77;
|
|
7862
7896
|
let original = "";
|
|
7863
7897
|
try {
|
|
7864
7898
|
original = await this.git.show([`HEAD:${headPath}`]);
|
|
@@ -7868,13 +7902,13 @@ var init_git_manager = __esm({
|
|
|
7868
7902
|
let modified = "";
|
|
7869
7903
|
if (staged) {
|
|
7870
7904
|
try {
|
|
7871
|
-
modified = await this.git.show([`:${
|
|
7905
|
+
modified = await this.git.show([`:${path77}`]);
|
|
7872
7906
|
} catch {
|
|
7873
7907
|
modified = "";
|
|
7874
7908
|
}
|
|
7875
7909
|
} else {
|
|
7876
7910
|
try {
|
|
7877
|
-
modified = await this.workspace.readFile(
|
|
7911
|
+
modified = await this.workspace.readFile(path77);
|
|
7878
7912
|
} catch {
|
|
7879
7913
|
modified = "";
|
|
7880
7914
|
}
|
|
@@ -12171,7 +12205,7 @@ async function initObservability() {
|
|
|
12171
12205
|
return true;
|
|
12172
12206
|
}
|
|
12173
12207
|
const { registerSdk: registerSdk2 } = await Promise.resolve().then(() => (init_otel_sdk(), otel_sdk_exports));
|
|
12174
|
-
providers = registerSdk2(process.env.OTEL_SERVICE_NAME, "1.5.
|
|
12208
|
+
providers = registerSdk2(process.env.OTEL_SERVICE_NAME, "1.5.11");
|
|
12175
12209
|
console.log(`[Observability] OTLP export \u2192 ${process.env.OTEL_EXPORTER_OTLP_ENDPOINT} (service.name=${process.env.OTEL_SERVICE_NAME}, capture_content=${config.observability.captureContent})`);
|
|
12176
12210
|
return true;
|
|
12177
12211
|
}
|
|
@@ -12574,7 +12608,7 @@ var init_session_manager = __esm({
|
|
|
12574
12608
|
init_session_skill_commands();
|
|
12575
12609
|
init_session_state_store();
|
|
12576
12610
|
init_genai_spans();
|
|
12577
|
-
CONTINUE_TASK_KICK = `[System] You called continue_task: the task interrupted earlier is not finished. Resume it now from where it stopped. Any tool call marked "[tool execution interrupted \u2014 no result]"
|
|
12611
|
+
CONTINUE_TASK_KICK = `[System] You called continue_task: the task interrupted earlier is not finished. Resume it now from where it stopped. Any tool call marked "[tool execution interrupted \u2014 no result]" was cut off mid-run and its result is lost, and any marked "was not run" never started \u2014 re-issue either if you still need the result (this overrides the interrupted marker's do-not-retry note \u2014 you explicitly asked to continue).`;
|
|
12578
12612
|
SessionManager = class _SessionManager {
|
|
12579
12613
|
// The fields below are marked `internal:` because they're consumed by
|
|
12580
12614
|
// helpers in this directory (session-tools.ts). Treat them as
|
|
@@ -13319,6 +13353,7 @@ var init_session_manager = __esm({
|
|
|
13319
13353
|
message = i === -1 ? [{ type: "text", text: stamp }, ...message] : message.map((b, j) => j === i && b.type === "text" ? { type: "text", text: stamped(b.text) } : b);
|
|
13320
13354
|
}
|
|
13321
13355
|
beginTurn(session, message);
|
|
13356
|
+
let batchRan = false;
|
|
13322
13357
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
13323
13358
|
session.abortController = new AbortController();
|
|
13324
13359
|
const signal = session.abortController.signal;
|
|
@@ -13333,6 +13368,10 @@ var init_session_manager = __esm({
|
|
|
13333
13368
|
this.trimImages(session);
|
|
13334
13369
|
await this.maybeAutoCompact(session);
|
|
13335
13370
|
this.saveAgentState(session);
|
|
13371
|
+
if (session.interruptRequested && batchRan) {
|
|
13372
|
+
console.debug(`[SessionManager] Soft interrupt at batch boundary in ${session.id}`);
|
|
13373
|
+
session.abortController?.abort(abortReason("interrupt"));
|
|
13374
|
+
}
|
|
13336
13375
|
}
|
|
13337
13376
|
});
|
|
13338
13377
|
for await (const event of iter) {
|
|
@@ -13341,13 +13380,10 @@ var init_session_manager = __esm({
|
|
|
13341
13380
|
if (event.type === "text") {
|
|
13342
13381
|
resultText += event.text ?? "";
|
|
13343
13382
|
}
|
|
13383
|
+
if (event.type === "tool_result")
|
|
13384
|
+
batchRan = true;
|
|
13344
13385
|
onAgentEvent(session, event);
|
|
13345
13386
|
this.processSessionEvent(session, event);
|
|
13346
|
-
if (session.interruptRequested && event.type === "tool_result") {
|
|
13347
|
-
console.debug(`[SessionManager] Graceful interrupt after tool result in ${session.id}`);
|
|
13348
|
-
session.abortController?.abort(abortReason("interrupt"));
|
|
13349
|
-
this.markPendingToolCallsInterrupted(session.id);
|
|
13350
|
-
}
|
|
13351
13387
|
}
|
|
13352
13388
|
session.abortController = null;
|
|
13353
13389
|
if (session.interruptRequested) {
|
|
@@ -13765,15 +13801,18 @@ ${body}`;
|
|
|
13765
13801
|
}
|
|
13766
13802
|
// ── Session operations ─────────────────────────────────────────────
|
|
13767
13803
|
/**
|
|
13768
|
-
* UI-only repair for
|
|
13804
|
+
* UI-only repair for a hard-aborted turn: emit a synthetic `tool_result` for
|
|
13769
13805
|
* every pending tool call (tool_call seen, no output yet) in the session's
|
|
13770
13806
|
* cached UI log. On abort, runAgentTurn's consumer loop breaks on
|
|
13771
13807
|
* `signal.aborted` BEFORE processing the in-flight tool's real tool_result
|
|
13772
|
-
* event, so without this the UI block stays "running" forever.
|
|
13773
|
-
*
|
|
13774
|
-
*
|
|
13775
|
-
*
|
|
13776
|
-
*
|
|
13808
|
+
* event, so without this the UI block stays "running" forever. The batch
|
|
13809
|
+
* runs serially and setToolResult fills the first pending entry, so the
|
|
13810
|
+
* first pending call is the one that was running (`[interrupted by user]`,
|
|
13811
|
+
* model side: repair's do-not-retry marker) and the rest never started
|
|
13812
|
+
* (`[not run — interrupted]`, model side: agent-loop's "not run" result).
|
|
13813
|
+
* Routing through the normal emitEvent pipeline means ui-log-builder
|
|
13814
|
+
* persistence, admin WS push, and TUI rendering all pick it up unchanged.
|
|
13815
|
+
* Never touches agent.messages. Idempotent: completed tools
|
|
13777
13816
|
* (output already set) are never overwritten. Call BEFORE awaiting the
|
|
13778
13817
|
* aborted turn's promise — its finally emits `complete`, which flushes and
|
|
13779
13818
|
* clears the pending buffers this scans.
|
|
@@ -13790,12 +13829,12 @@ ${body}`;
|
|
|
13790
13829
|
if (pending.length === 0)
|
|
13791
13830
|
return;
|
|
13792
13831
|
const session = this.sessions.get(sessionId);
|
|
13793
|
-
for (const tc of pending) {
|
|
13832
|
+
for (const [i, tc] of pending.entries()) {
|
|
13794
13833
|
this.emitEvent(sessionId, {
|
|
13795
13834
|
type: "tool_result",
|
|
13796
13835
|
toolName: tc.name,
|
|
13797
13836
|
toolUseId: tc.toolUseId,
|
|
13798
|
-
toolResult: "[interrupted by user]",
|
|
13837
|
+
toolResult: i === 0 ? "[interrupted by user]" : "[not run \u2014 interrupted]",
|
|
13799
13838
|
agentName: session?.agentName,
|
|
13800
13839
|
agentId: session?.agentId,
|
|
13801
13840
|
taskId: sessionId === rootId ? void 0 : sessionId
|
|
@@ -13811,7 +13850,7 @@ ${body}`;
|
|
|
13811
13850
|
* single semantic shared by esc (TUI / admin) and the `interrupt_session`
|
|
13812
13851
|
* tool:
|
|
13813
13852
|
*
|
|
13814
|
-
* - abort the in-flight turn at once (not at the next
|
|
13853
|
+
* - abort the in-flight turn at once (not at the next batch boundary),
|
|
13815
13854
|
* - do NOT discard `messageQueue`: once the aborted turn unwinds,
|
|
13816
13855
|
* runSession's runFn drains every queued message into ONE merged follow-up
|
|
13817
13856
|
* turn (drainQueue). An empty queue just goes idle.
|
|
@@ -14213,6 +14252,10 @@ ${result.summary}`, agentName: "user", report: true, taskId });
|
|
|
14213
14252
|
isSessionCompacting(sessionId) {
|
|
14214
14253
|
return this.sessions.get(sessionId)?.isCompacting ?? false;
|
|
14215
14254
|
}
|
|
14255
|
+
/** Any message waiting in the session's queue (a turn / endCompact drains it) */
|
|
14256
|
+
hasQueuedMessages(sessionId) {
|
|
14257
|
+
return (this.sessions.get(sessionId)?.messageQueue.length ?? 0) > 0;
|
|
14258
|
+
}
|
|
14216
14259
|
/** Check if a session is running */
|
|
14217
14260
|
isSessionRunning(sessionId) {
|
|
14218
14261
|
const session = this.sessions.get(sessionId);
|
|
@@ -14739,7 +14782,7 @@ ${result.summary}`, agentName: "user", report: true, taskId });
|
|
|
14739
14782
|
/** Enqueue a user message directly (admin WS busy/compacting queue path).
|
|
14740
14783
|
* Pushes onto the single messageQueue with no `sourceSessionId` (a user
|
|
14741
14784
|
* message), and requests a SOFT interrupt so a busy turn yields after its
|
|
14742
|
-
* current tool — same effect as sendUserMessage's busy branch, minus the
|
|
14785
|
+
* current tool batch — same effect as sendUserMessage's busy branch, minus the
|
|
14743
14786
|
* idle/run path (the caller already established the session is busy or
|
|
14744
14787
|
* compacting). A compact outside a live model loop (manual /compact, or the
|
|
14745
14788
|
* turn-end auto-compact — abortController already null) has nothing to
|
|
@@ -14747,7 +14790,7 @@ ${result.summary}`, agentName: "user", report: true, taskId });
|
|
|
14747
14790
|
* compacting branch): the drain would snapshot it into resumedAfterInterrupt
|
|
14748
14791
|
* and let continue_task arm a kick in a turn that interrupted nothing. A
|
|
14749
14792
|
* mid-turn auto-compact (beforeCallModel) does interrupt a live turn — the
|
|
14750
|
-
* flag is set, so the turn yields
|
|
14793
|
+
* flag is set, so the turn yields at its next batch boundary instead of running on
|
|
14751
14794
|
* to its natural end with the message still queued. */
|
|
14752
14795
|
async enqueueUserMessage(sessionId, text6, images) {
|
|
14753
14796
|
const session = this.sessions.get(sessionId);
|
|
@@ -16731,8 +16774,8 @@ function applyPathPick(value, ref, pick) {
|
|
|
16731
16774
|
const before = value.slice(0, markerStart);
|
|
16732
16775
|
const kind = ref.kind === "scope" ? "scope" : ref.kind === "image" ? "image" : pick.isImage ? "image" : "file";
|
|
16733
16776
|
const needsQuote = pick.insertPath.includes(" ");
|
|
16734
|
-
const
|
|
16735
|
-
return `${before}@${kind} ${
|
|
16777
|
+
const path77 = pick.isDir ? needsQuote ? `"${pick.insertPath}/` : `${pick.insertPath}/` : needsQuote ? `"${pick.insertPath}" ` : `${pick.insertPath} `;
|
|
16778
|
+
return `${before}@${kind} ${path77}`;
|
|
16736
16779
|
}
|
|
16737
16780
|
var init_path_suggest_engine = __esm({
|
|
16738
16781
|
"src/tui/path-suggest-engine.ts"() {
|
|
@@ -17691,7 +17734,9 @@ function App({ harness, verbose }) {
|
|
|
17691
17734
|
let cancelled = false;
|
|
17692
17735
|
harness.getSessionMessages(harness.sessionId).then((msgs) => {
|
|
17693
17736
|
if (cancelled || !msgs || msgs.length === 0) return;
|
|
17694
|
-
const
|
|
17737
|
+
const all = messagesToBlocks(msgs);
|
|
17738
|
+
const hidden = all.length - REPLAY_BLOCKS;
|
|
17739
|
+
const blocks = hidden > 0 ? [{ id: nextId(), kind: "system", text: `\u2500\u2500 ${hidden} earlier messages hidden \xB7 ctrl+o for the full log \u2500\u2500` }, ...all.slice(-REPLAY_BLOCKS)] : all;
|
|
17695
17740
|
if (blocks.length > 0) dispatch6({ type: "load-history", blocks });
|
|
17696
17741
|
}).catch(() => {
|
|
17697
17742
|
});
|
|
@@ -17954,7 +17999,7 @@ ${lines.join("\n")}` });
|
|
|
17954
17999
|
] })
|
|
17955
18000
|
] });
|
|
17956
18001
|
}
|
|
17957
|
-
var MAX_CONTEXT_TOKENS_FALLBACK, blockSeq, ANSI2;
|
|
18002
|
+
var MAX_CONTEXT_TOKENS_FALLBACK, blockSeq, REPLAY_BLOCKS, ANSI2;
|
|
17958
18003
|
var init_app = __esm({
|
|
17959
18004
|
"src/tui/app.tsx"() {
|
|
17960
18005
|
"use strict";
|
|
@@ -17970,6 +18015,7 @@ var init_app = __esm({
|
|
|
17970
18015
|
init_log_viewer();
|
|
17971
18016
|
MAX_CONTEXT_TOKENS_FALLBACK = 272e3;
|
|
17972
18017
|
blockSeq = 0;
|
|
18018
|
+
REPLAY_BLOCKS = 100;
|
|
17973
18019
|
ANSI2 = {
|
|
17974
18020
|
reset: "\x1B[0m",
|
|
17975
18021
|
dim: "\x1B[2m",
|
|
@@ -20795,8 +20841,8 @@ function detectOrphans(tree, declared) {
|
|
|
20795
20841
|
if (!block || typeof block !== "object")
|
|
20796
20842
|
continue;
|
|
20797
20843
|
for (const k of Object.keys(block)) {
|
|
20798
|
-
const
|
|
20799
|
-
if (!declaredMap.has(
|
|
20844
|
+
const path77 = `${namespaceId}.${kindWord}.${k}`;
|
|
20845
|
+
if (!declaredMap.has(path77)) {
|
|
20800
20846
|
out.push({ namespaceId, kind: kindWord === "params" ? "param" : "secret", key: k });
|
|
20801
20847
|
}
|
|
20802
20848
|
}
|
|
@@ -21877,11 +21923,18 @@ var init_session_prefix = __esm({
|
|
|
21877
21923
|
});
|
|
21878
21924
|
|
|
21879
21925
|
// ../server/dist/channels/web/handler.js
|
|
21926
|
+
import fs49 from "node:fs";
|
|
21880
21927
|
function buildWebSessionPrefix(accountId) {
|
|
21881
21928
|
return sessionPrefix("web", accountId);
|
|
21882
21929
|
}
|
|
21883
21930
|
function canAddressSession(account, sessionId) {
|
|
21884
|
-
|
|
21931
|
+
const prefix = buildWebSessionPrefix(account.accountId);
|
|
21932
|
+
return account.accessLevel === "full" || sessionId.startsWith(prefix) || isOwnGoalSession(account, sessionId, prefix);
|
|
21933
|
+
}
|
|
21934
|
+
function isOwnGoalSession(account, sessionId, prefix) {
|
|
21935
|
+
if (!sessionId.startsWith("goal_") || !hasWorkspaceHalo(account.workspacePath))
|
|
21936
|
+
return false;
|
|
21937
|
+
return readGoalState(getWorkspaceDb(account.workspacePath).db, sessionId)?.workerSessionId.startsWith(prefix) ?? false;
|
|
21885
21938
|
}
|
|
21886
21939
|
function createWebChannel(deps) {
|
|
21887
21940
|
const { registry: registry2, db } = deps;
|
|
@@ -21890,24 +21943,24 @@ function createWebChannel(deps) {
|
|
|
21890
21943
|
const prefix = buildWebSessionPrefix(accountId);
|
|
21891
21944
|
return findActiveSessionId(sm, accountId, prefix, activeOverrides2, "full") ?? void 0;
|
|
21892
21945
|
}
|
|
21893
|
-
function resolveWorkspace(account, override) {
|
|
21894
|
-
let
|
|
21946
|
+
function resolveWorkspace(account, override, scaffold = true) {
|
|
21947
|
+
let path77 = account.workspacePath;
|
|
21895
21948
|
if (override && override !== account.workspacePath) {
|
|
21896
21949
|
if (account.accessLevel !== "full") {
|
|
21897
21950
|
return { ok: false, error: "workspace override requires a full-access token" };
|
|
21898
21951
|
}
|
|
21899
|
-
|
|
21952
|
+
path77 = override;
|
|
21900
21953
|
}
|
|
21901
|
-
const resolved = resolveAccountWorkspace({ ...account, workspacePath:
|
|
21954
|
+
const resolved = scaffold ? resolveAccountWorkspace({ ...account, workspacePath: path77 }) : fs49.existsSync(path77) ? path77 : null;
|
|
21902
21955
|
if (!resolved)
|
|
21903
21956
|
return { ok: false, error: "workspace not found" };
|
|
21904
21957
|
return { ok: true, path: resolved };
|
|
21905
21958
|
}
|
|
21906
|
-
function resolveRequest(token, workspaceOverride) {
|
|
21959
|
+
function resolveRequest(token, workspaceOverride, scaffold = true) {
|
|
21907
21960
|
const account = getAccountByToken(db, token);
|
|
21908
21961
|
if (!account || !account.enabled)
|
|
21909
21962
|
return { ok: false, error: "Invalid or disabled token" };
|
|
21910
|
-
const ws = resolveWorkspace(account, workspaceOverride);
|
|
21963
|
+
const ws = resolveWorkspace(account, workspaceOverride, scaffold);
|
|
21911
21964
|
if (!ws.ok)
|
|
21912
21965
|
return ws;
|
|
21913
21966
|
return { ok: true, account, workspace: ws.path };
|
|
@@ -22119,7 +22172,9 @@ function createWebChannel(deps) {
|
|
|
22119
22172
|
const state = sm.getUIState(sessionId);
|
|
22120
22173
|
if (!state)
|
|
22121
22174
|
return { sessionId, messages: [], running: false };
|
|
22122
|
-
const
|
|
22175
|
+
const snapshot3 = createSaveSnapshot(state);
|
|
22176
|
+
const since = opts?.since;
|
|
22177
|
+
const messages2 = since === void 0 ? snapshot3 : snapshot3.filter((m) => !m.taskId && m.timestamp >= since);
|
|
22123
22178
|
const running = sm.isSessionRunning(sessionId);
|
|
22124
22179
|
return { sessionId, messages: messages2, running };
|
|
22125
22180
|
}
|
|
@@ -22138,6 +22193,21 @@ function createWebChannel(deps) {
|
|
|
22138
22193
|
await sm.createSession(agent.id, null, `Web: ${account.label || account.accountId}`, void 0, sessionId, void 0, accessLevel);
|
|
22139
22194
|
return { ok: true, sessionId };
|
|
22140
22195
|
}
|
|
22196
|
+
function listSessions2(token, opts) {
|
|
22197
|
+
const req = resolveRequest(token, opts?.workspace, false);
|
|
22198
|
+
if (!req.ok)
|
|
22199
|
+
return req;
|
|
22200
|
+
if (!hasWorkspaceHalo(req.workspace))
|
|
22201
|
+
return { ok: true, workspace: req.workspace, sessions: [], nextCursor: null };
|
|
22202
|
+
const sm = registry2.getOrCreate(req.workspace);
|
|
22203
|
+
const page = sm.listSessions({ rootOnly: true, prefix: buildWebSessionPrefix(req.account.accountId), limit: WEB_LIST_PAGE, cursor: opts?.cursor });
|
|
22204
|
+
return {
|
|
22205
|
+
ok: true,
|
|
22206
|
+
workspace: req.workspace,
|
|
22207
|
+
sessions: page.sessions.map((s) => ({ sessionId: s.id, title: s.title || s.description || null, updatedAt: s.updatedAt })),
|
|
22208
|
+
nextCursor: page.nextCursor
|
|
22209
|
+
};
|
|
22210
|
+
}
|
|
22141
22211
|
async function* subscribe(token, signal, opts) {
|
|
22142
22212
|
const req = resolveRequest(token, opts?.workspace);
|
|
22143
22213
|
if (!req.ok) {
|
|
@@ -22151,9 +22221,16 @@ function createWebChannel(deps) {
|
|
|
22151
22221
|
return;
|
|
22152
22222
|
}
|
|
22153
22223
|
yield sseData({ type: "session", sessionId });
|
|
22154
|
-
|
|
22224
|
+
const listener = listenSession(sm, sessionId);
|
|
22225
|
+
const busy = sm.isSessionRunning(sessionId) || sm.isSessionCompacting(sessionId) && sm.hasQueuedMessages(sessionId);
|
|
22226
|
+
if (!busy) {
|
|
22227
|
+
listener.close();
|
|
22228
|
+
yield sseData({ type: "complete" });
|
|
22229
|
+
return;
|
|
22230
|
+
}
|
|
22231
|
+
yield* listener.events(signal);
|
|
22155
22232
|
}
|
|
22156
|
-
return { handleMessage, handleStop, getHistory, subscribe, createSession };
|
|
22233
|
+
return { handleMessage, handleStop, getHistory, subscribe, createSession, listSessions: listSessions2 };
|
|
22157
22234
|
}
|
|
22158
22235
|
function sseData(obj) {
|
|
22159
22236
|
return `data: ${JSON.stringify(obj)}
|
|
@@ -22196,12 +22273,14 @@ function createMediaBuffer() {
|
|
|
22196
22273
|
const out = flushAll() + sseData({ type: "thinking", text: event.text ?? "" });
|
|
22197
22274
|
return out;
|
|
22198
22275
|
}
|
|
22276
|
+
// `toolUseId` pairs a result with its call (the ACP adapter uses it as
|
|
22277
|
+
// the stable toolCallId); additive — older clients ignore it.
|
|
22199
22278
|
case "tool_call": {
|
|
22200
|
-
const out = flushAll() + sseData({ type: "tool_call", toolName: event.toolName, toolInput: event.toolInput });
|
|
22279
|
+
const out = flushAll() + sseData({ type: "tool_call", toolName: event.toolName, toolUseId: event.toolUseId, toolInput: event.toolInput });
|
|
22201
22280
|
return out;
|
|
22202
22281
|
}
|
|
22203
22282
|
case "tool_result": {
|
|
22204
|
-
const out = flushAll() + sseData({ type: "tool_result", toolName: event.toolName, result: event.toolResult?.slice(0, 500) });
|
|
22283
|
+
const out = flushAll() + sseData({ type: "tool_result", toolName: event.toolName, toolUseId: event.toolUseId, result: event.toolResult?.slice(0, 500) });
|
|
22205
22284
|
return out;
|
|
22206
22285
|
}
|
|
22207
22286
|
case "complete": {
|
|
@@ -22223,6 +22302,7 @@ function createMediaBuffer() {
|
|
|
22223
22302
|
}
|
|
22224
22303
|
};
|
|
22225
22304
|
}
|
|
22305
|
+
var WEB_LIST_PAGE;
|
|
22226
22306
|
var init_handler = __esm({
|
|
22227
22307
|
"../server/dist/channels/web/handler.js"() {
|
|
22228
22308
|
"use strict";
|
|
@@ -22235,9 +22315,11 @@ var init_handler = __esm({
|
|
|
22235
22315
|
init_commands2();
|
|
22236
22316
|
init_agent_loader();
|
|
22237
22317
|
init_db();
|
|
22318
|
+
init_init();
|
|
22238
22319
|
init_goal_mode();
|
|
22239
22320
|
init_i18n();
|
|
22240
22321
|
init_session_prefix();
|
|
22322
|
+
WEB_LIST_PAGE = 50;
|
|
22241
22323
|
}
|
|
22242
22324
|
});
|
|
22243
22325
|
|
|
@@ -22297,13 +22379,13 @@ var init_session_types = __esm({
|
|
|
22297
22379
|
|
|
22298
22380
|
// ../server/dist/routes/halo-city.js
|
|
22299
22381
|
import { Hono as Hono10 } from "hono";
|
|
22300
|
-
import
|
|
22382
|
+
import fs50 from "node:fs";
|
|
22301
22383
|
import path58 from "node:path";
|
|
22302
22384
|
import Database6 from "better-sqlite3";
|
|
22303
22385
|
function scanSkills2(dir) {
|
|
22304
22386
|
let names;
|
|
22305
22387
|
try {
|
|
22306
|
-
names =
|
|
22388
|
+
names = fs50.readdirSync(dir);
|
|
22307
22389
|
} catch {
|
|
22308
22390
|
return [];
|
|
22309
22391
|
}
|
|
@@ -22312,7 +22394,7 @@ function scanSkills2(dir) {
|
|
|
22312
22394
|
const skillMd = path58.join(dir, entry, "SKILL.md");
|
|
22313
22395
|
let stat;
|
|
22314
22396
|
try {
|
|
22315
|
-
stat =
|
|
22397
|
+
stat = fs50.statSync(skillMd);
|
|
22316
22398
|
if (!stat.isFile())
|
|
22317
22399
|
continue;
|
|
22318
22400
|
} catch {
|
|
@@ -22321,7 +22403,7 @@ function scanSkills2(dir) {
|
|
|
22321
22403
|
let meta = _skillCache.get(skillMd, stat.mtimeMs);
|
|
22322
22404
|
if (!meta) {
|
|
22323
22405
|
try {
|
|
22324
|
-
const { name, description, command } = parseSkillFrontmatter(
|
|
22406
|
+
const { name, description, command } = parseSkillFrontmatter(fs50.readFileSync(skillMd, "utf-8"));
|
|
22325
22407
|
meta = _skillCache.set(skillMd, stat.mtimeMs, { id: entry, name: name || entry, description: description ?? "", command });
|
|
22326
22408
|
} catch {
|
|
22327
22409
|
continue;
|
|
@@ -22421,7 +22503,7 @@ function snapshotWorkspace(sm, wsPath, label) {
|
|
|
22421
22503
|
function roReader(rawPath) {
|
|
22422
22504
|
let wsPath;
|
|
22423
22505
|
try {
|
|
22424
|
-
wsPath =
|
|
22506
|
+
wsPath = fs50.realpathSync(rawPath);
|
|
22425
22507
|
} catch {
|
|
22426
22508
|
return null;
|
|
22427
22509
|
}
|
|
@@ -22429,7 +22511,7 @@ function roReader(rawPath) {
|
|
|
22429
22511
|
if (cached)
|
|
22430
22512
|
return cached;
|
|
22431
22513
|
const dbPath = path58.join(wsPath, ".halo", "halo.db");
|
|
22432
|
-
if (!
|
|
22514
|
+
if (!fs50.existsSync(dbPath))
|
|
22433
22515
|
return null;
|
|
22434
22516
|
try {
|
|
22435
22517
|
const db = new Database6(dbPath, { readonly: true, fileMustExist: true });
|
|
@@ -22457,7 +22539,7 @@ function dropRoReader(rawPath) {
|
|
|
22457
22539
|
return;
|
|
22458
22540
|
let wsPath = rawPath;
|
|
22459
22541
|
try {
|
|
22460
|
-
wsPath =
|
|
22542
|
+
wsPath = fs50.realpathSync(rawPath);
|
|
22461
22543
|
} catch {
|
|
22462
22544
|
}
|
|
22463
22545
|
const r = _roReaders.get(wsPath);
|
|
@@ -22554,7 +22636,7 @@ function workspaceBirth(wsPath) {
|
|
|
22554
22636
|
let b = _birthCache.get(wsPath);
|
|
22555
22637
|
if (b === void 0) {
|
|
22556
22638
|
try {
|
|
22557
|
-
b =
|
|
22639
|
+
b = fs50.statSync(wsPath).birthtimeMs || fs50.statSync(wsPath).ctimeMs;
|
|
22558
22640
|
} catch {
|
|
22559
22641
|
b = 0;
|
|
22560
22642
|
}
|
|
@@ -22566,7 +22648,7 @@ function discoverWorkspaces(registry2) {
|
|
|
22566
22648
|
const out = /* @__PURE__ */ new Map();
|
|
22567
22649
|
const add = (p, label) => {
|
|
22568
22650
|
try {
|
|
22569
|
-
const resolved =
|
|
22651
|
+
const resolved = fs50.realpathSync(p);
|
|
22570
22652
|
if (!out.has(resolved))
|
|
22571
22653
|
out.set(resolved, label);
|
|
22572
22654
|
} catch {
|
|
@@ -22630,7 +22712,7 @@ function createShowRoutes(registry2) {
|
|
|
22630
22712
|
if (!globalView) {
|
|
22631
22713
|
let allowed = false;
|
|
22632
22714
|
try {
|
|
22633
|
-
allowed =
|
|
22715
|
+
allowed = fs50.realpathSync(wsPath) === fs50.realpathSync(account.workspacePath);
|
|
22634
22716
|
} catch {
|
|
22635
22717
|
}
|
|
22636
22718
|
if (!allowed || !canAddressSession(account, id))
|
|
@@ -22826,7 +22908,7 @@ var init_commands3 = __esm({
|
|
|
22826
22908
|
// ../server/dist/routes/agentcore.js
|
|
22827
22909
|
import { Hono as Hono13 } from "hono";
|
|
22828
22910
|
import path59 from "node:path";
|
|
22829
|
-
import
|
|
22911
|
+
import fs51 from "node:fs";
|
|
22830
22912
|
function sanitizeRuntimeId(runtimeSessionId) {
|
|
22831
22913
|
return runtimeSessionId.replace(/[^a-zA-Z0-9_-]/g, "-").slice(0, 80);
|
|
22832
22914
|
}
|
|
@@ -22835,7 +22917,7 @@ function haloSessionId(runtimeSessionId) {
|
|
|
22835
22917
|
}
|
|
22836
22918
|
function userWorkspace(base, runtimeSessionId) {
|
|
22837
22919
|
const dir = path59.join(base, "users", sanitizeRuntimeId(runtimeSessionId));
|
|
22838
|
-
|
|
22920
|
+
fs51.mkdirSync(dir, { recursive: true });
|
|
22839
22921
|
return dir;
|
|
22840
22922
|
}
|
|
22841
22923
|
function acceptChunk(uploads, msg) {
|
|
@@ -23372,17 +23454,17 @@ function passwordStrengthError(pw) {
|
|
|
23372
23454
|
}
|
|
23373
23455
|
function authMiddleware() {
|
|
23374
23456
|
return async (c, next) => {
|
|
23375
|
-
const
|
|
23376
|
-
if (PUBLIC_PATHS.some((p) =>
|
|
23457
|
+
const path77 = c.req.path;
|
|
23458
|
+
if (PUBLIC_PATHS.some((p) => path77 === p || path77.startsWith(p + "?"))) {
|
|
23377
23459
|
return next();
|
|
23378
23460
|
}
|
|
23379
|
-
if (EXTENSION_ASSET_PATH.test(
|
|
23461
|
+
if (EXTENSION_ASSET_PATH.test(path77)) {
|
|
23380
23462
|
return next();
|
|
23381
23463
|
}
|
|
23382
23464
|
const token = getCookie(c, COOKIE_NAME);
|
|
23383
23465
|
const payload = validateToken(token);
|
|
23384
23466
|
if (!payload) {
|
|
23385
|
-
if (!
|
|
23467
|
+
if (!path77.startsWith("/api/")) {
|
|
23386
23468
|
return next();
|
|
23387
23469
|
}
|
|
23388
23470
|
return c.json({ error: "Unauthorized" }, 401);
|
|
@@ -23644,7 +23726,7 @@ var init_upgrade_router = __esm({
|
|
|
23644
23726
|
});
|
|
23645
23727
|
|
|
23646
23728
|
// ../server/dist/ws/file-watcher.js
|
|
23647
|
-
import
|
|
23729
|
+
import fs52 from "node:fs";
|
|
23648
23730
|
import path60 from "node:path";
|
|
23649
23731
|
function enqueueNativeOp(op) {
|
|
23650
23732
|
const result = nativeOpChain.then(op);
|
|
@@ -23754,7 +23836,7 @@ var init_file_watcher = __esm({
|
|
|
23754
23836
|
}
|
|
23755
23837
|
let root = workspaceRoot;
|
|
23756
23838
|
try {
|
|
23757
|
-
root =
|
|
23839
|
+
root = fs52.realpathSync(workspaceRoot);
|
|
23758
23840
|
} catch {
|
|
23759
23841
|
}
|
|
23760
23842
|
if (this.subscription && this.workspaceRoot === root)
|
|
@@ -23812,7 +23894,7 @@ var init_file_watcher = __esm({
|
|
|
23812
23894
|
} else {
|
|
23813
23895
|
let isDir = false;
|
|
23814
23896
|
try {
|
|
23815
|
-
isDir =
|
|
23897
|
+
isDir = fs52.statSync(absPath).isDirectory();
|
|
23816
23898
|
} catch {
|
|
23817
23899
|
}
|
|
23818
23900
|
action = isDir ? "addDir" : "add";
|
|
@@ -23888,7 +23970,7 @@ var init_file_watcher = __esm({
|
|
|
23888
23970
|
});
|
|
23889
23971
|
|
|
23890
23972
|
// ../server/dist/ws/git-dir-watcher.js
|
|
23891
|
-
import
|
|
23973
|
+
import fs53 from "node:fs";
|
|
23892
23974
|
import path61 from "node:path";
|
|
23893
23975
|
var GitDirWatcher;
|
|
23894
23976
|
var init_git_dir_watcher = __esm({
|
|
@@ -23914,7 +23996,7 @@ var init_git_dir_watcher = __esm({
|
|
|
23914
23996
|
this.stop();
|
|
23915
23997
|
this.workspaceRoot = workspaceRoot;
|
|
23916
23998
|
const gitDir = path61.join(workspaceRoot, ".git");
|
|
23917
|
-
if (
|
|
23999
|
+
if (fs53.existsSync(gitDir)) {
|
|
23918
24000
|
this.watchGitDir(gitDir);
|
|
23919
24001
|
} else {
|
|
23920
24002
|
this.watchParentForGit(workspaceRoot);
|
|
@@ -23923,7 +24005,7 @@ var init_git_dir_watcher = __esm({
|
|
|
23923
24005
|
/** Watch the `.git` dir for index/HEAD changes (steady state). */
|
|
23924
24006
|
watchGitDir(gitDir) {
|
|
23925
24007
|
try {
|
|
23926
|
-
this.watcher =
|
|
24008
|
+
this.watcher = fs53.watch(gitDir, { persistent: true, recursive: false }, (_eventType, filename) => {
|
|
23927
24009
|
this.handleGitEvent(filename);
|
|
23928
24010
|
});
|
|
23929
24011
|
this.watcher.on("error", (err) => {
|
|
@@ -23942,7 +24024,7 @@ var init_git_dir_watcher = __esm({
|
|
|
23942
24024
|
* Exists solely during the non-repo window; upgraded away on first `.git`. */
|
|
23943
24025
|
watchParentForGit(workspaceRoot) {
|
|
23944
24026
|
try {
|
|
23945
|
-
this.watcher =
|
|
24027
|
+
this.watcher = fs53.watch(workspaceRoot, { persistent: true, recursive: false }, (_eventType, filename) => {
|
|
23946
24028
|
this.handleParentEvent(filename);
|
|
23947
24029
|
});
|
|
23948
24030
|
this.watcher.on("error", (err) => {
|
|
@@ -23979,7 +24061,7 @@ var init_git_dir_watcher = __esm({
|
|
|
23979
24061
|
if (!root)
|
|
23980
24062
|
return;
|
|
23981
24063
|
const gitDir = path61.join(root, ".git");
|
|
23982
|
-
if (!
|
|
24064
|
+
if (!fs53.existsSync(gitDir))
|
|
23983
24065
|
return;
|
|
23984
24066
|
this.closeWatcher();
|
|
23985
24067
|
this.watchGitDir(gitDir);
|
|
@@ -25001,9 +25083,9 @@ import Database7 from "better-sqlite3";
|
|
|
25001
25083
|
import { drizzle as drizzle5 } from "drizzle-orm/better-sqlite3";
|
|
25002
25084
|
import { sqliteTable as sqliteTable5, text as text5, integer as integer5 } from "drizzle-orm/sqlite-core";
|
|
25003
25085
|
import path63 from "node:path";
|
|
25004
|
-
import
|
|
25086
|
+
import fs54 from "node:fs";
|
|
25005
25087
|
function createCronDb(globalDir2) {
|
|
25006
|
-
|
|
25088
|
+
fs54.mkdirSync(globalDir2, { recursive: true });
|
|
25007
25089
|
const dbPath = path63.join(globalDir2, "cron.db");
|
|
25008
25090
|
const sqlite = new Database7(dbPath);
|
|
25009
25091
|
sqlite.pragma("journal_mode = WAL");
|
|
@@ -25234,7 +25316,7 @@ var init_dispatcher = __esm({
|
|
|
25234
25316
|
|
|
25235
25317
|
// ../server/dist/cron/runner.js
|
|
25236
25318
|
import { spawn as spawn3, execFileSync } from "node:child_process";
|
|
25237
|
-
import
|
|
25319
|
+
import fs55 from "node:fs";
|
|
25238
25320
|
import path64 from "node:path";
|
|
25239
25321
|
import { homedir as homedir23 } from "node:os";
|
|
25240
25322
|
import { Cron } from "croner";
|
|
@@ -25311,7 +25393,7 @@ function effectiveSessionId(job) {
|
|
|
25311
25393
|
function sessionKeyFor(workspacePath, sessionId) {
|
|
25312
25394
|
let ws;
|
|
25313
25395
|
try {
|
|
25314
|
-
ws =
|
|
25396
|
+
ws = fs55.realpathSync(workspacePath);
|
|
25315
25397
|
} catch {
|
|
25316
25398
|
ws = path64.resolve(workspacePath);
|
|
25317
25399
|
}
|
|
@@ -25386,7 +25468,7 @@ function sweepOrphanRuns(opts) {
|
|
|
25386
25468
|
}
|
|
25387
25469
|
}
|
|
25388
25470
|
function startCronDaemon() {
|
|
25389
|
-
|
|
25471
|
+
fs55.mkdirSync(logsDir(), { recursive: true });
|
|
25390
25472
|
sweepOrphanRuns();
|
|
25391
25473
|
reloadAll();
|
|
25392
25474
|
_daemonTimers.push(setInterval(pruneOldLogs, DAY_MS2));
|
|
@@ -25566,7 +25648,7 @@ async function runJob(jobId, triggerKind) {
|
|
|
25566
25648
|
const runId = newRunId();
|
|
25567
25649
|
const startedAt = Date.now();
|
|
25568
25650
|
const logPath = logPathFor(runId);
|
|
25569
|
-
|
|
25651
|
+
fs55.mkdirSync(logsDir(), { recursive: true });
|
|
25570
25652
|
const sessionId = effectiveSessionId(job);
|
|
25571
25653
|
const sessionKey = sessionKeyFor(job.workspacePath, sessionId);
|
|
25572
25654
|
const host = _sessionRegistry?.peek(job.workspacePath);
|
|
@@ -25606,7 +25688,7 @@ async function runJob(jobId, triggerKind) {
|
|
|
25606
25688
|
}).run();
|
|
25607
25689
|
broadcast({ type: "cron:run_changed", jobId, runId, status: "running" });
|
|
25608
25690
|
const targets = parseTargets(job.targets);
|
|
25609
|
-
if (!
|
|
25691
|
+
if (!fs55.existsSync(job.workspacePath)) {
|
|
25610
25692
|
finalize(runId, jobId, {
|
|
25611
25693
|
status: "failed",
|
|
25612
25694
|
output: null,
|
|
@@ -25623,7 +25705,7 @@ async function runJob(jobId, triggerKind) {
|
|
|
25623
25705
|
args.push("--access", accessLevel);
|
|
25624
25706
|
const [spawnBin, spawnArgs] = process.platform === "win32" && cli.endsWith(".cmd") ? ["cmd.exe", ["/c", cli, ...args]] : [cli, args];
|
|
25625
25707
|
const timeoutSec = job.timeoutSec ?? CLI_TIMEOUT_SEC;
|
|
25626
|
-
const teeStream =
|
|
25708
|
+
const teeStream = fs55.createWriteStream(logPath, { flags: "a" });
|
|
25627
25709
|
teeStream.write(`=== ${(/* @__PURE__ */ new Date()).toISOString()} cron run ${runId} job=${jobId} trigger=${triggerKind} ===
|
|
25628
25710
|
`);
|
|
25629
25711
|
teeStream.write(`agent=${job.agentId} session=${sessionId}${accessLevel ? ` access=${accessLevel}` : ""} workspace=${job.workspacePath} schedule=${job.schedule}
|
|
@@ -25812,15 +25894,15 @@ function pruneOldRuns(jobId) {
|
|
|
25812
25894
|
}
|
|
25813
25895
|
function pruneOldLogs() {
|
|
25814
25896
|
const dir = logsDir();
|
|
25815
|
-
if (!
|
|
25897
|
+
if (!fs55.existsSync(dir))
|
|
25816
25898
|
return;
|
|
25817
25899
|
const cutoff = Date.now() - LOG_RETENTION_DAYS * DAY_MS2;
|
|
25818
|
-
for (const name of
|
|
25900
|
+
for (const name of fs55.readdirSync(dir)) {
|
|
25819
25901
|
const p = path64.join(dir, name);
|
|
25820
25902
|
try {
|
|
25821
|
-
const stat =
|
|
25903
|
+
const stat = fs55.statSync(p);
|
|
25822
25904
|
if (stat.mtimeMs < cutoff)
|
|
25823
|
-
|
|
25905
|
+
fs55.rmSync(p, { force: true });
|
|
25824
25906
|
} catch {
|
|
25825
25907
|
}
|
|
25826
25908
|
}
|
|
@@ -25859,7 +25941,7 @@ var init_runner = __esm({
|
|
|
25859
25941
|
// ../server/dist/routes/cron.js
|
|
25860
25942
|
import { Hono as Hono15 } from "hono";
|
|
25861
25943
|
import { eq as eq15, desc as desc3, lt as lt6, and as and11 } from "drizzle-orm";
|
|
25862
|
-
import
|
|
25944
|
+
import fs56 from "node:fs/promises";
|
|
25863
25945
|
import { Cron as Cron2 } from "croner";
|
|
25864
25946
|
function newJobId() {
|
|
25865
25947
|
return `cron-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -26153,7 +26235,7 @@ function createCronRoutes() {
|
|
|
26153
26235
|
return c.json({ log: null });
|
|
26154
26236
|
let log2 = null;
|
|
26155
26237
|
try {
|
|
26156
|
-
log2 = await
|
|
26238
|
+
log2 = await fs56.readFile(row.logPath, "utf-8");
|
|
26157
26239
|
} catch {
|
|
26158
26240
|
}
|
|
26159
26241
|
return c.json({ log: log2 });
|
|
@@ -26217,7 +26299,7 @@ var init_cron = __esm({
|
|
|
26217
26299
|
});
|
|
26218
26300
|
|
|
26219
26301
|
// ../server/dist/extensions/install.js
|
|
26220
|
-
import
|
|
26302
|
+
import fs57 from "node:fs";
|
|
26221
26303
|
import path65 from "node:path";
|
|
26222
26304
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
26223
26305
|
import JSZip2 from "jszip";
|
|
@@ -26251,41 +26333,41 @@ async function unpackZip(zipBuf, dest) {
|
|
|
26251
26333
|
throw new ExtensionInstallError(`zip entry is a symlink: ${entry.name}`);
|
|
26252
26334
|
const abs = path65.join(dest, rel);
|
|
26253
26335
|
if (entry.dir) {
|
|
26254
|
-
|
|
26336
|
+
fs57.mkdirSync(abs, { recursive: true });
|
|
26255
26337
|
continue;
|
|
26256
26338
|
}
|
|
26257
|
-
|
|
26258
|
-
|
|
26339
|
+
fs57.mkdirSync(path65.dirname(abs), { recursive: true });
|
|
26340
|
+
fs57.writeFileSync(abs, await entry.async("nodebuffer"));
|
|
26259
26341
|
}
|
|
26260
26342
|
}
|
|
26261
26343
|
function stripWrapperDir(dir) {
|
|
26262
|
-
if (
|
|
26344
|
+
if (fs57.existsSync(path65.join(dir, MANIFEST_FILE)))
|
|
26263
26345
|
return;
|
|
26264
|
-
const children =
|
|
26346
|
+
const children = fs57.readdirSync(dir, { withFileTypes: true });
|
|
26265
26347
|
if (children.length !== 1 || !children[0].isDirectory())
|
|
26266
26348
|
return;
|
|
26267
26349
|
const inner = path65.join(dir, children[0].name);
|
|
26268
|
-
if (!
|
|
26350
|
+
if (!fs57.existsSync(path65.join(inner, MANIFEST_FILE)))
|
|
26269
26351
|
return;
|
|
26270
|
-
for (const name of
|
|
26271
|
-
|
|
26352
|
+
for (const name of fs57.readdirSync(inner)) {
|
|
26353
|
+
fs57.renameSync(path65.join(inner, name), path65.join(dir, name));
|
|
26272
26354
|
}
|
|
26273
|
-
|
|
26355
|
+
fs57.rmdirSync(inner);
|
|
26274
26356
|
}
|
|
26275
26357
|
function rand() {
|
|
26276
26358
|
return randomBytes2(3).toString("hex");
|
|
26277
26359
|
}
|
|
26278
26360
|
async function installExtensionZip(zipBuf, expectId) {
|
|
26279
26361
|
const root = extensionsRoot();
|
|
26280
|
-
|
|
26362
|
+
fs57.mkdirSync(root, { recursive: true });
|
|
26281
26363
|
const stage = path65.join(root, `.tmp-unpack-${rand()}`);
|
|
26282
|
-
|
|
26364
|
+
fs57.mkdirSync(stage);
|
|
26283
26365
|
try {
|
|
26284
26366
|
await unpackZip(zipBuf, stage);
|
|
26285
26367
|
stripWrapperDir(stage);
|
|
26286
26368
|
let manifestId;
|
|
26287
26369
|
try {
|
|
26288
|
-
const raw = JSON.parse(
|
|
26370
|
+
const raw = JSON.parse(fs57.readFileSync(path65.join(stage, MANIFEST_FILE), "utf-8"));
|
|
26289
26371
|
manifestId = typeof raw.id === "string" ? raw.id : "";
|
|
26290
26372
|
} catch (err) {
|
|
26291
26373
|
throw new ExtensionInstallError(`${MANIFEST_FILE} missing or unreadable at the zip root: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -26299,34 +26381,34 @@ async function installExtensionZip(zipBuf, expectId) {
|
|
|
26299
26381
|
throw new ExtensionInstallError(`invalid extension: ${verdict.error}`);
|
|
26300
26382
|
const target = path65.join(root, manifestId);
|
|
26301
26383
|
const old = path65.join(root, `.old-${manifestId}-${rand()}`);
|
|
26302
|
-
const hadOld =
|
|
26384
|
+
const hadOld = fs57.existsSync(target);
|
|
26303
26385
|
if (hadOld)
|
|
26304
|
-
|
|
26386
|
+
fs57.renameSync(target, old);
|
|
26305
26387
|
try {
|
|
26306
|
-
|
|
26388
|
+
fs57.renameSync(stage, target);
|
|
26307
26389
|
} catch (err) {
|
|
26308
26390
|
if (hadOld)
|
|
26309
|
-
|
|
26391
|
+
fs57.renameSync(old, target);
|
|
26310
26392
|
throw err;
|
|
26311
26393
|
}
|
|
26312
26394
|
if (hadOld)
|
|
26313
|
-
|
|
26395
|
+
fs57.rmSync(old, { recursive: true, force: true });
|
|
26314
26396
|
const info = readExtensionDir(target);
|
|
26315
26397
|
if ("error" in info)
|
|
26316
26398
|
throw new ExtensionInstallError(`installed but failed re-validation: ${info.error}`);
|
|
26317
26399
|
console.log(`[Extensions] installed ${info.id}@${info.version}${hadOld ? " (replaced previous)" : ""}`);
|
|
26318
26400
|
return info;
|
|
26319
26401
|
} finally {
|
|
26320
|
-
|
|
26402
|
+
fs57.rmSync(stage, { recursive: true, force: true });
|
|
26321
26403
|
}
|
|
26322
26404
|
}
|
|
26323
26405
|
function uninstallExtension(id) {
|
|
26324
26406
|
if (!isExtensionId(id))
|
|
26325
26407
|
throw new ExtensionInstallError(`"${id}" is not a valid extension id`);
|
|
26326
26408
|
const dir = path65.join(extensionsRoot(), id);
|
|
26327
|
-
if (!
|
|
26409
|
+
if (!fs57.existsSync(dir))
|
|
26328
26410
|
return false;
|
|
26329
|
-
|
|
26411
|
+
fs57.rmSync(dir, { recursive: true, force: true });
|
|
26330
26412
|
console.log(`[Extensions] removed ${id}`);
|
|
26331
26413
|
return true;
|
|
26332
26414
|
}
|
|
@@ -26343,7 +26425,7 @@ var init_install2 = __esm({
|
|
|
26343
26425
|
});
|
|
26344
26426
|
|
|
26345
26427
|
// ../server/dist/extensions/watcher.js
|
|
26346
|
-
import
|
|
26428
|
+
import fs58 from "node:fs";
|
|
26347
26429
|
function scheduleFlush() {
|
|
26348
26430
|
if (flushTimer)
|
|
26349
26431
|
return;
|
|
@@ -26362,7 +26444,7 @@ function rescanAndBroadcast() {
|
|
|
26362
26444
|
}
|
|
26363
26445
|
function openWatcher(root) {
|
|
26364
26446
|
try {
|
|
26365
|
-
watcher =
|
|
26447
|
+
watcher = fs58.watch(root, { persistent: false, recursive: false }, () => scheduleFlush());
|
|
26366
26448
|
watcher.on("error", (err) => {
|
|
26367
26449
|
console.warn(`[Extensions] watch error on ${root}: ${err.message} \u2014 retrying in 1s`);
|
|
26368
26450
|
closeWatcher();
|
|
@@ -26388,7 +26470,7 @@ function start() {
|
|
|
26388
26470
|
return;
|
|
26389
26471
|
const root = extensionsRoot();
|
|
26390
26472
|
try {
|
|
26391
|
-
|
|
26473
|
+
fs58.mkdirSync(root, { recursive: true });
|
|
26392
26474
|
} catch (err) {
|
|
26393
26475
|
console.warn(`[Extensions] cannot create ${root}: ${err instanceof Error ? err.message : String(err)}`);
|
|
26394
26476
|
}
|
|
@@ -26423,7 +26505,7 @@ var init_watcher = __esm({
|
|
|
26423
26505
|
|
|
26424
26506
|
// ../server/dist/routes/extensions.js
|
|
26425
26507
|
import { Hono as Hono16 } from "hono";
|
|
26426
|
-
import
|
|
26508
|
+
import fs59 from "node:fs";
|
|
26427
26509
|
import path66 from "node:path";
|
|
26428
26510
|
import { Readable as Readable2 } from "node:stream";
|
|
26429
26511
|
function assetMime(fileName) {
|
|
@@ -26442,15 +26524,15 @@ function resolveAsset(id, version, rest) {
|
|
|
26442
26524
|
let dir;
|
|
26443
26525
|
let real;
|
|
26444
26526
|
try {
|
|
26445
|
-
dir =
|
|
26446
|
-
real =
|
|
26527
|
+
dir = fs59.realpathSync(path66.join(extensionsRoot(), id));
|
|
26528
|
+
real = fs59.realpathSync(path66.join(dir, ...segments));
|
|
26447
26529
|
} catch {
|
|
26448
26530
|
return null;
|
|
26449
26531
|
}
|
|
26450
26532
|
if (real !== dir && !real.startsWith(dir + path66.sep))
|
|
26451
26533
|
return null;
|
|
26452
26534
|
try {
|
|
26453
|
-
if (!
|
|
26535
|
+
if (!fs59.statSync(real).isFile())
|
|
26454
26536
|
return null;
|
|
26455
26537
|
} catch {
|
|
26456
26538
|
return null;
|
|
@@ -26512,8 +26594,8 @@ function createExtensionRoutes() {
|
|
|
26512
26594
|
const abs = resolveAsset(c.req.param("id"), c.req.param("version"), c.req.param("rest"));
|
|
26513
26595
|
if (!abs)
|
|
26514
26596
|
return c.json({ error: "Not found" }, 404);
|
|
26515
|
-
const stat =
|
|
26516
|
-
const nodeStream =
|
|
26597
|
+
const stat = fs59.statSync(abs);
|
|
26598
|
+
const nodeStream = fs59.createReadStream(abs);
|
|
26517
26599
|
c.req.raw.signal?.addEventListener("abort", () => nodeStream.destroy(), { once: true });
|
|
26518
26600
|
return new Response(Readable2.toWeb(nodeStream), {
|
|
26519
26601
|
headers: {
|
|
@@ -26567,7 +26649,7 @@ var init_extensions = __esm({
|
|
|
26567
26649
|
});
|
|
26568
26650
|
|
|
26569
26651
|
// ../server/dist/models/watcher.js
|
|
26570
|
-
import
|
|
26652
|
+
import fs60 from "node:fs";
|
|
26571
26653
|
function snapshot2() {
|
|
26572
26654
|
const { effective } = loadProviders();
|
|
26573
26655
|
return {
|
|
@@ -26595,7 +26677,7 @@ function reloadAndBroadcast() {
|
|
|
26595
26677
|
}
|
|
26596
26678
|
function openWatcher2(root) {
|
|
26597
26679
|
try {
|
|
26598
|
-
watcher2 =
|
|
26680
|
+
watcher2 = fs60.watch(root, { persistent: false, recursive: false }, () => scheduleFlush2());
|
|
26599
26681
|
watcher2.on("error", (err) => {
|
|
26600
26682
|
console.warn(`[Models] watch error on ${root}: ${err.message} \u2014 retrying in 1s`);
|
|
26601
26683
|
closeWatcher2();
|
|
@@ -26621,7 +26703,7 @@ function start2() {
|
|
|
26621
26703
|
return;
|
|
26622
26704
|
const root = globalHubModelsDir();
|
|
26623
26705
|
try {
|
|
26624
|
-
|
|
26706
|
+
fs60.mkdirSync(root, { recursive: true });
|
|
26625
26707
|
} catch (err) {
|
|
26626
26708
|
console.warn(`[Models] cannot create ${root}: ${err instanceof Error ? err.message : String(err)}`);
|
|
26627
26709
|
}
|
|
@@ -27738,12 +27820,12 @@ var init_handler3 = __esm({
|
|
|
27738
27820
|
});
|
|
27739
27821
|
|
|
27740
27822
|
// ../server/dist/routes/channel-accounts.js
|
|
27741
|
-
import
|
|
27823
|
+
import fs61 from "node:fs";
|
|
27742
27824
|
function accountListFields(a) {
|
|
27743
27825
|
return {
|
|
27744
27826
|
accountId: a.accountId,
|
|
27745
27827
|
workspacePath: a.workspacePath,
|
|
27746
|
-
workspaceMissing: !
|
|
27828
|
+
workspaceMissing: !fs61.existsSync(a.workspacePath),
|
|
27747
27829
|
label: a.label,
|
|
27748
27830
|
enabled: a.enabled,
|
|
27749
27831
|
accessLevel: a.accessLevel,
|
|
@@ -28385,7 +28467,7 @@ var init_cdn = __esm({
|
|
|
28385
28467
|
});
|
|
28386
28468
|
|
|
28387
28469
|
// ../server/dist/channels/wechat/send-media.js
|
|
28388
|
-
import
|
|
28470
|
+
import fs62 from "node:fs/promises";
|
|
28389
28471
|
import path68 from "node:path";
|
|
28390
28472
|
function inferKind(filePath) {
|
|
28391
28473
|
const cls = classifyMedia(filePath);
|
|
@@ -28398,7 +28480,7 @@ function inferKind(filePath) {
|
|
|
28398
28480
|
async function sendMediaFile(params) {
|
|
28399
28481
|
const { baseUrl, token, toUserId, contextToken, contextTokenAt, filePath } = params;
|
|
28400
28482
|
const kind = params.kind ?? inferKind(filePath);
|
|
28401
|
-
const plaintext = await
|
|
28483
|
+
const plaintext = await fs62.readFile(filePath);
|
|
28402
28484
|
const prep = prepareUpload(plaintext);
|
|
28403
28485
|
console.log(`[WeChat] sendMediaFile: path=${filePath} kind=${kind} rawsize=${prep.rawsize} filesize=${prep.filesize}`);
|
|
28404
28486
|
const mediaType = kind === "image" ? UploadMediaType.IMAGE : kind === "video" ? UploadMediaType.VIDEO : UploadMediaType.FILE;
|
|
@@ -28599,7 +28681,7 @@ var init_login = __esm({
|
|
|
28599
28681
|
});
|
|
28600
28682
|
|
|
28601
28683
|
// ../server/dist/channels/wechat/handler.js
|
|
28602
|
-
import
|
|
28684
|
+
import fs63 from "node:fs";
|
|
28603
28685
|
import path69 from "node:path";
|
|
28604
28686
|
import QRCode from "qrcode";
|
|
28605
28687
|
function wxRoute(fromUserId, contextToken, contextTokenAt) {
|
|
@@ -29030,7 +29112,7 @@ async function handleSlashCommand(args) {
|
|
|
29030
29112
|
contextToken,
|
|
29031
29113
|
filePath: tmpPath
|
|
29032
29114
|
});
|
|
29033
|
-
await
|
|
29115
|
+
await fs63.promises.unlink(tmpPath).catch(() => {
|
|
29034
29116
|
});
|
|
29035
29117
|
await reply(t("wechat.qr_sent", lang, { level, path: account.workspacePath }));
|
|
29036
29118
|
waitLogin({ sessionKey: login.sessionKey }).then(async (result2) => {
|
|
@@ -29111,7 +29193,7 @@ var init_handler4 = __esm({
|
|
|
29111
29193
|
// ../server/dist/routes/wechat.js
|
|
29112
29194
|
import { Hono as Hono18 } from "hono";
|
|
29113
29195
|
import path70 from "node:path";
|
|
29114
|
-
import
|
|
29196
|
+
import fs64 from "node:fs";
|
|
29115
29197
|
function createWechatRoutes(deps) {
|
|
29116
29198
|
const { db, channel } = deps;
|
|
29117
29199
|
const app2 = new Hono18();
|
|
@@ -29120,7 +29202,7 @@ function createWechatRoutes(deps) {
|
|
|
29120
29202
|
return {
|
|
29121
29203
|
...stripSecrets(acc),
|
|
29122
29204
|
workspacePath: acc.workspacePath,
|
|
29123
|
-
workspaceMissing: !
|
|
29205
|
+
workspaceMissing: !fs64.existsSync(acc.workspacePath)
|
|
29124
29206
|
};
|
|
29125
29207
|
});
|
|
29126
29208
|
return c.json({ accounts });
|
|
@@ -29349,7 +29431,26 @@ var init_descriptor2 = __esm({
|
|
|
29349
29431
|
import { Hono as Hono19 } from "hono";
|
|
29350
29432
|
import { streamSSE } from "hono/streaming";
|
|
29351
29433
|
import crypto5 from "node:crypto";
|
|
29352
|
-
import
|
|
29434
|
+
import fs65 from "node:fs";
|
|
29435
|
+
function streamWithKeepalive(c, chunks) {
|
|
29436
|
+
return streamSSE(c, async (stream) => {
|
|
29437
|
+
const timer = setInterval(() => {
|
|
29438
|
+
void stream.write(": keepalive\n\n");
|
|
29439
|
+
}, SSE_KEEPALIVE_MS);
|
|
29440
|
+
try {
|
|
29441
|
+
for await (const chunk of chunks()) {
|
|
29442
|
+
await stream.write(chunk);
|
|
29443
|
+
}
|
|
29444
|
+
} finally {
|
|
29445
|
+
clearInterval(timer);
|
|
29446
|
+
}
|
|
29447
|
+
});
|
|
29448
|
+
}
|
|
29449
|
+
function epochParam(raw) {
|
|
29450
|
+
if (raw === void 0 || raw === "")
|
|
29451
|
+
return void 0;
|
|
29452
|
+
return /^\d+$/.test(raw) ? Number(raw) : null;
|
|
29453
|
+
}
|
|
29353
29454
|
function createWebRoutes(deps) {
|
|
29354
29455
|
const { db, channel } = deps;
|
|
29355
29456
|
const app2 = new Hono19();
|
|
@@ -29443,11 +29544,7 @@ function createWebRoutes(deps) {
|
|
|
29443
29544
|
const denied = sessionOverrideError(c, auth.account, opts.sessionId);
|
|
29444
29545
|
if (denied)
|
|
29445
29546
|
return denied;
|
|
29446
|
-
return
|
|
29447
|
-
for await (const chunk of channel.handleMessage(token, body.message ?? "", body.images, opts, c.req.raw.signal)) {
|
|
29448
|
-
await stream.write(chunk);
|
|
29449
|
-
}
|
|
29450
|
-
});
|
|
29547
|
+
return streamWithKeepalive(c, () => channel.handleMessage(token, body.message ?? "", body.images, opts, c.req.raw.signal));
|
|
29451
29548
|
});
|
|
29452
29549
|
app2.post("/web/sessions", async (c) => {
|
|
29453
29550
|
const auth = authToken(c);
|
|
@@ -29463,6 +29560,18 @@ function createWebRoutes(deps) {
|
|
|
29463
29560
|
return c.json({ error: result.error }, 403);
|
|
29464
29561
|
return c.json({ sessionId: result.sessionId });
|
|
29465
29562
|
});
|
|
29563
|
+
app2.get("/web/sessions", (c) => {
|
|
29564
|
+
const auth = authToken(c);
|
|
29565
|
+
if (!auth.ok)
|
|
29566
|
+
return auth.response;
|
|
29567
|
+
const cursor = epochParam(c.req.query("cursor"));
|
|
29568
|
+
if (cursor === null)
|
|
29569
|
+
return c.json({ error: "Invalid cursor" }, 400);
|
|
29570
|
+
const result = channel.listSessions(auth.token, { workspace: readOverrides(c).workspace, cursor });
|
|
29571
|
+
if (!result.ok)
|
|
29572
|
+
return c.json({ error: result.error }, 403);
|
|
29573
|
+
return c.json({ workspace: result.workspace, sessions: result.sessions, nextCursor: result.nextCursor });
|
|
29574
|
+
});
|
|
29466
29575
|
app2.post("/web/stop", async (c) => {
|
|
29467
29576
|
const auth = authToken(c);
|
|
29468
29577
|
if (!auth.ok)
|
|
@@ -29482,7 +29591,10 @@ function createWebRoutes(deps) {
|
|
|
29482
29591
|
const denied = sessionOverrideError(c, auth.account, overrides.sessionId);
|
|
29483
29592
|
if (denied)
|
|
29484
29593
|
return denied;
|
|
29485
|
-
const
|
|
29594
|
+
const since = epochParam(c.req.query("since"));
|
|
29595
|
+
if (since === null)
|
|
29596
|
+
return c.json({ error: "Invalid since" }, 400);
|
|
29597
|
+
const result = channel.getHistory(auth.token, { ...overrides, since });
|
|
29486
29598
|
if (!result) {
|
|
29487
29599
|
if (overrides.sessionId)
|
|
29488
29600
|
return c.json({ error: "session not found" }, 404);
|
|
@@ -29500,11 +29612,7 @@ function createWebRoutes(deps) {
|
|
|
29500
29612
|
const denied = sessionOverrideError(c, auth.account, opts.sessionId);
|
|
29501
29613
|
if (denied)
|
|
29502
29614
|
return denied;
|
|
29503
|
-
return
|
|
29504
|
-
for await (const chunk of channel.subscribe(auth.token, abortController.signal, opts)) {
|
|
29505
|
-
await stream.write(chunk);
|
|
29506
|
-
}
|
|
29507
|
-
});
|
|
29615
|
+
return streamWithKeepalive(c, () => channel.subscribe(auth.token, abortController.signal, opts));
|
|
29508
29616
|
});
|
|
29509
29617
|
app2.get("/web/file", async (c) => {
|
|
29510
29618
|
const auth = authToken(c);
|
|
@@ -29514,30 +29622,30 @@ function createWebRoutes(deps) {
|
|
|
29514
29622
|
const filePath = c.req.query("path");
|
|
29515
29623
|
if (!filePath)
|
|
29516
29624
|
return c.json({ error: "path required" }, 400);
|
|
29517
|
-
const
|
|
29518
|
-
const resolved =
|
|
29519
|
-
const root =
|
|
29520
|
-
if (resolved !== root && !resolved.startsWith(root +
|
|
29625
|
+
const path77 = await import("node:path");
|
|
29626
|
+
const resolved = path77.default.resolve(account.workspacePath, filePath);
|
|
29627
|
+
const root = path77.default.resolve(account.workspacePath);
|
|
29628
|
+
if (resolved !== root && !resolved.startsWith(root + path77.default.sep)) {
|
|
29521
29629
|
return c.json({ error: "path traversal not allowed" }, 403);
|
|
29522
29630
|
}
|
|
29523
29631
|
let real;
|
|
29524
29632
|
let realRoot;
|
|
29525
29633
|
try {
|
|
29526
|
-
real =
|
|
29527
|
-
realRoot =
|
|
29634
|
+
real = fs65.realpathSync(resolved);
|
|
29635
|
+
realRoot = fs65.realpathSync(root);
|
|
29528
29636
|
} catch {
|
|
29529
29637
|
return c.json({ error: "file not found" }, 404);
|
|
29530
29638
|
}
|
|
29531
|
-
if (real !== realRoot && !real.startsWith(realRoot +
|
|
29639
|
+
if (real !== realRoot && !real.startsWith(realRoot + path77.default.sep)) {
|
|
29532
29640
|
return c.json({ error: "path traversal not allowed" }, 403);
|
|
29533
29641
|
}
|
|
29534
29642
|
if (isHiddenWorkspacePath(real, realRoot)) {
|
|
29535
29643
|
return c.json({ error: "path is not accessible" }, 403);
|
|
29536
29644
|
}
|
|
29537
|
-
const stat =
|
|
29645
|
+
const stat = fs65.statSync(real);
|
|
29538
29646
|
if (stat.isDirectory())
|
|
29539
29647
|
return c.json({ error: "cannot serve directory" }, 400);
|
|
29540
|
-
const ext =
|
|
29648
|
+
const ext = path77.default.extname(real).toLowerCase();
|
|
29541
29649
|
const nonImageMimes = {
|
|
29542
29650
|
".mp4": "video/mp4",
|
|
29543
29651
|
".webm": "video/webm",
|
|
@@ -29545,14 +29653,15 @@ function createWebRoutes(deps) {
|
|
|
29545
29653
|
".txt": "text/plain"
|
|
29546
29654
|
};
|
|
29547
29655
|
const contentType = imageMimeFromExt(ext) || nonImageMimes[ext] || "application/octet-stream";
|
|
29548
|
-
const fileName =
|
|
29549
|
-
const data =
|
|
29656
|
+
const fileName = path77.default.basename(resolved);
|
|
29657
|
+
const data = fs65.readFileSync(real);
|
|
29550
29658
|
c.header("content-type", contentType);
|
|
29551
29659
|
c.header("content-disposition", `inline; filename="${fileName}"`);
|
|
29552
29660
|
return c.body(data);
|
|
29553
29661
|
});
|
|
29554
29662
|
return app2;
|
|
29555
29663
|
}
|
|
29664
|
+
var SSE_KEEPALIVE_MS;
|
|
29556
29665
|
var init_web = __esm({
|
|
29557
29666
|
"../server/dist/routes/web.js"() {
|
|
29558
29667
|
"use strict";
|
|
@@ -29564,6 +29673,7 @@ var init_web = __esm({
|
|
|
29564
29673
|
init_web_token();
|
|
29565
29674
|
init_sandbox();
|
|
29566
29675
|
init_workspace_path();
|
|
29676
|
+
SSE_KEEPALIVE_MS = 15e3;
|
|
29567
29677
|
}
|
|
29568
29678
|
});
|
|
29569
29679
|
|
|
@@ -29766,10 +29876,10 @@ async function authTest(botToken) {
|
|
|
29766
29876
|
return slackPost("auth.test", botToken, {});
|
|
29767
29877
|
}
|
|
29768
29878
|
async function uploadFile(args) {
|
|
29769
|
-
const
|
|
29770
|
-
const
|
|
29771
|
-
const filename = args.filename ??
|
|
29772
|
-
const stat =
|
|
29879
|
+
const fs69 = await import("node:fs");
|
|
29880
|
+
const path77 = await import("node:path");
|
|
29881
|
+
const filename = args.filename ?? path77.basename(args.filePath);
|
|
29882
|
+
const stat = fs69.statSync(args.filePath);
|
|
29773
29883
|
if (!stat.isFile())
|
|
29774
29884
|
throw new Error(`[slack:uploadFile] not a regular file: ${args.filePath}`);
|
|
29775
29885
|
const length = stat.size;
|
|
@@ -29787,7 +29897,7 @@ async function uploadFile(args) {
|
|
|
29787
29897
|
if (step1Parsed.ok === false || !step1Parsed.upload_url || !step1Parsed.file_id) {
|
|
29788
29898
|
throw new Error(`[slack:files.getUploadURLExternal] ${step1Parsed.error ?? "unknown"}`);
|
|
29789
29899
|
}
|
|
29790
|
-
const buf =
|
|
29900
|
+
const buf = fs69.readFileSync(args.filePath);
|
|
29791
29901
|
const step2 = await fetch(step1Parsed.upload_url, {
|
|
29792
29902
|
method: "POST",
|
|
29793
29903
|
body: buf
|
|
@@ -30705,10 +30815,10 @@ async function replyMessage(args) {
|
|
|
30705
30815
|
return { message_id: parsed.data?.message_id ?? "" };
|
|
30706
30816
|
}
|
|
30707
30817
|
async function uploadImage(args) {
|
|
30708
|
-
const
|
|
30709
|
-
const
|
|
30710
|
-
const buf =
|
|
30711
|
-
const filename =
|
|
30818
|
+
const fs69 = await import("node:fs");
|
|
30819
|
+
const path77 = await import("node:path");
|
|
30820
|
+
const buf = fs69.readFileSync(args.filePath);
|
|
30821
|
+
const filename = path77.basename(args.filePath);
|
|
30712
30822
|
const send = async (token) => {
|
|
30713
30823
|
const form = new FormData();
|
|
30714
30824
|
form.append("image_type", "message");
|
|
@@ -30724,10 +30834,10 @@ async function uploadImage(args) {
|
|
|
30724
30834
|
return { imageKey: parsed.data.image_key };
|
|
30725
30835
|
}
|
|
30726
30836
|
async function uploadFile2(args) {
|
|
30727
|
-
const
|
|
30728
|
-
const
|
|
30729
|
-
const buf =
|
|
30730
|
-
const filename = args.filename ??
|
|
30837
|
+
const fs69 = await import("node:fs");
|
|
30838
|
+
const path77 = await import("node:path");
|
|
30839
|
+
const buf = fs69.readFileSync(args.filePath);
|
|
30840
|
+
const filename = args.filename ?? path77.basename(args.filePath);
|
|
30731
30841
|
const send = async (token) => {
|
|
30732
30842
|
const form = new FormData();
|
|
30733
30843
|
form.append("file_type", args.fileType);
|
|
@@ -31554,7 +31664,7 @@ var init_event_adapter5 = __esm({
|
|
|
31554
31664
|
|
|
31555
31665
|
// ../server/dist/channels/wecom/handler.js
|
|
31556
31666
|
import path73 from "node:path";
|
|
31557
|
-
import
|
|
31667
|
+
import fs66 from "node:fs/promises";
|
|
31558
31668
|
import { WSClient as WSClient2, generateReqId } from "@wecom/aibot-node-sdk";
|
|
31559
31669
|
function pickConversation(body) {
|
|
31560
31670
|
const userid = body.from?.userid ?? "unknown";
|
|
@@ -31687,7 +31797,7 @@ async function sendWecomMedia(args) {
|
|
|
31687
31797
|
const ext = path73.extname(filePath).toLowerCase();
|
|
31688
31798
|
const cls = classifyMedia(filePath);
|
|
31689
31799
|
const type = cls === "image" && [".png", ".jpg", ".jpeg", ".gif"].includes(ext) ? "image" : cls === "video" && ext === ".mp4" ? "video" : "file";
|
|
31690
|
-
const buf = await
|
|
31800
|
+
const buf = await fs66.readFile(filePath);
|
|
31691
31801
|
if (buf.length > MAX_WECOM_DOWNLOAD_BYTES)
|
|
31692
31802
|
throw new Error("file exceeds 20MB");
|
|
31693
31803
|
const { media_id: mediaId } = await wsClient.uploadMedia(buf, { type, filename: path73.basename(filePath) });
|
|
@@ -32145,14 +32255,14 @@ import { serve } from "@hono/node-server";
|
|
|
32145
32255
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
32146
32256
|
import { WebSocketServer as WebSocketServer2 } from "ws";
|
|
32147
32257
|
import path74 from "node:path";
|
|
32148
|
-
import
|
|
32258
|
+
import fs67 from "node:fs";
|
|
32149
32259
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
32150
32260
|
import { homedir as homedir24 } from "node:os";
|
|
32151
32261
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
32152
32262
|
function findProjectRoot() {
|
|
32153
32263
|
let dir = process.cwd();
|
|
32154
32264
|
for (let i = 0; i < 32; i++) {
|
|
32155
|
-
if (
|
|
32265
|
+
if (fs67.existsSync(path74.join(dir, "pnpm-workspace.yaml")))
|
|
32156
32266
|
return dir;
|
|
32157
32267
|
const parent = path74.dirname(dir);
|
|
32158
32268
|
if (parent === dir)
|
|
@@ -32163,15 +32273,15 @@ function findProjectRoot() {
|
|
|
32163
32273
|
return process.cwd();
|
|
32164
32274
|
}
|
|
32165
32275
|
function acquireSingleInstanceLock(lockFile) {
|
|
32166
|
-
|
|
32167
|
-
const fd =
|
|
32276
|
+
fs67.mkdirSync(path74.dirname(lockFile), { recursive: true });
|
|
32277
|
+
const fd = fs67.openSync(lockFile, fs67.constants.O_RDWR | fs67.constants.O_CREAT, 420);
|
|
32168
32278
|
const result = spawnSync3("flock", ["-n", "3"], {
|
|
32169
32279
|
stdio: ["ignore", "ignore", "ignore", fd]
|
|
32170
32280
|
});
|
|
32171
32281
|
if (result.error) {
|
|
32172
32282
|
const content = readLockPid(fd);
|
|
32173
32283
|
if (content > 0 && isProcessAlive(content)) {
|
|
32174
|
-
|
|
32284
|
+
fs67.closeSync(fd);
|
|
32175
32285
|
console.error(`[Server] another halo server is already running (pid ${content}). kill it first.`);
|
|
32176
32286
|
process.exit(1);
|
|
32177
32287
|
}
|
|
@@ -32180,19 +32290,19 @@ function acquireSingleInstanceLock(lockFile) {
|
|
|
32180
32290
|
if (content > 0 && !isProcessAlive(content)) {
|
|
32181
32291
|
console.warn(`[Server] removing stale server.lock (pid ${content} not running)`);
|
|
32182
32292
|
} else {
|
|
32183
|
-
|
|
32293
|
+
fs67.closeSync(fd);
|
|
32184
32294
|
console.error(`[Server] another halo server is already running${content > 0 ? ` (pid ${content})` : ""}. kill it first.`);
|
|
32185
32295
|
process.exit(1);
|
|
32186
32296
|
}
|
|
32187
32297
|
}
|
|
32188
|
-
|
|
32189
|
-
|
|
32190
|
-
|
|
32298
|
+
fs67.ftruncateSync(fd);
|
|
32299
|
+
fs67.writeSync(fd, String(process.pid));
|
|
32300
|
+
fs67.fsyncSync(fd);
|
|
32191
32301
|
}
|
|
32192
32302
|
function readLockPid(fd) {
|
|
32193
32303
|
try {
|
|
32194
32304
|
const buf = Buffer.alloc(32);
|
|
32195
|
-
const n =
|
|
32305
|
+
const n = fs67.readSync(fd, buf, 0, 32, 0);
|
|
32196
32306
|
return parseInt(buf.subarray(0, n).toString("utf-8").trim(), 10) || 0;
|
|
32197
32307
|
} catch {
|
|
32198
32308
|
return 0;
|
|
@@ -32220,7 +32330,7 @@ function isProcessAlive(pid) {
|
|
|
32220
32330
|
}
|
|
32221
32331
|
}
|
|
32222
32332
|
function readGitSha() {
|
|
32223
|
-
if (!
|
|
32333
|
+
if (!fs67.existsSync(path74.join(PROJECT_ROOT, "pnpm-workspace.yaml")) || !fs67.existsSync(path74.join(PROJECT_ROOT, ".git")))
|
|
32224
32334
|
return null;
|
|
32225
32335
|
try {
|
|
32226
32336
|
const rev = spawnSync3("git", ["rev-parse", "--short", "HEAD"], { cwd: PROJECT_ROOT, encoding: "utf-8" });
|
|
@@ -32243,10 +32353,10 @@ function resolveFrontendDir() {
|
|
|
32243
32353
|
if (process.env.HALO_FRONTEND_DIR)
|
|
32244
32354
|
return path74.resolve(process.env.HALO_FRONTEND_DIR);
|
|
32245
32355
|
const bundled = path74.resolve(__dirname3, "..", "admin-out");
|
|
32246
|
-
if (
|
|
32356
|
+
if (fs67.existsSync(path74.join(bundled, "index.html")))
|
|
32247
32357
|
return bundled;
|
|
32248
32358
|
const sibling = path74.resolve(__dirname3, "..", "..", "admin", "out");
|
|
32249
|
-
if (
|
|
32359
|
+
if (fs67.existsSync(path74.join(sibling, "index.html")))
|
|
32250
32360
|
return sibling;
|
|
32251
32361
|
return path74.resolve(PROJECT_ROOT, "packages", "admin", "out");
|
|
32252
32362
|
}
|
|
@@ -32327,11 +32437,11 @@ var init_dist2 = __esm({
|
|
|
32327
32437
|
init_git_ssh();
|
|
32328
32438
|
__dirname3 = path74.dirname(fileURLToPath3(import.meta.url));
|
|
32329
32439
|
PORT = config.server.port;
|
|
32330
|
-
HALO_VERSION = "1.5.
|
|
32440
|
+
HALO_VERSION = "1.5.11";
|
|
32331
32441
|
PROJECT_ROOT = findProjectRoot();
|
|
32332
32442
|
HALO_HOME4 = path74.join(homedir24(), ".halo");
|
|
32333
32443
|
GIT_SHA = readGitSha();
|
|
32334
|
-
if (!
|
|
32444
|
+
if (!fs67.existsSync(path74.join(HALO_HOME4, "global", ".template-version"))) {
|
|
32335
32445
|
process.stderr.write("\x1B[31m[Server] ~/.halo/global/ not initialized. Run `halo setup` first.\x1B[0m\n");
|
|
32336
32446
|
process.exit(1);
|
|
32337
32447
|
}
|
|
@@ -32424,7 +32534,7 @@ var init_dist2 = __esm({
|
|
|
32424
32534
|
setCronSessionRegistry(registry);
|
|
32425
32535
|
if (OWNS_RUNTIMES) {
|
|
32426
32536
|
for (const ws of listRunningWorkspaces()) {
|
|
32427
|
-
if (!
|
|
32537
|
+
if (!fs67.existsSync(path74.join(ws, ".halo")))
|
|
32428
32538
|
continue;
|
|
32429
32539
|
if (!claimWorkspaceRuntime(ws))
|
|
32430
32540
|
continue;
|
|
@@ -32458,7 +32568,7 @@ var init_dist2 = __esm({
|
|
|
32458
32568
|
app.get("/*", (c) => {
|
|
32459
32569
|
const indexPath = path74.join(FRONTEND_DIR, "index.html");
|
|
32460
32570
|
try {
|
|
32461
|
-
const html =
|
|
32571
|
+
const html = fs67.readFileSync(indexPath, "utf-8");
|
|
32462
32572
|
return c.html(html);
|
|
32463
32573
|
} catch {
|
|
32464
32574
|
return c.text("Frontend not built. Run: cd packages/admin && npx next build", 503);
|
|
@@ -32619,21 +32729,57 @@ var init_halo_client = __esm({
|
|
|
32619
32729
|
throw new Error("halo session create: missing sessionId");
|
|
32620
32730
|
return data.sessionId;
|
|
32621
32731
|
}
|
|
32622
|
-
/** GET /api/web/
|
|
32623
|
-
*
|
|
32624
|
-
async
|
|
32625
|
-
const
|
|
32626
|
-
|
|
32627
|
-
|
|
32628
|
-
|
|
32732
|
+
/** GET /api/web/subscribe as SSE — same frames as `chat`. Ends after a
|
|
32733
|
+
* single `complete` when the session isn't running. */
|
|
32734
|
+
async *subscribe(workspace, sessionId, signal) {
|
|
32735
|
+
const res = await fetch(this.sessionUrl("/api/web/subscribe", workspace, sessionId), {
|
|
32736
|
+
headers: this.authHeaders({ "x-token": this.opts.token }),
|
|
32737
|
+
signal
|
|
32738
|
+
});
|
|
32739
|
+
if (!res.ok || !res.body) {
|
|
32740
|
+
const msg = await safeText(res);
|
|
32741
|
+
throw new Error(`halo subscribe ${res.status}: ${msg}`);
|
|
32742
|
+
}
|
|
32743
|
+
yield* parseSseStream(res.body);
|
|
32744
|
+
}
|
|
32745
|
+
/** GET /api/web/history for an explicit sessionId; null when the server
|
|
32746
|
+
* has no such session (404). `since` (epoch ms) = only root-log rows
|
|
32747
|
+
* stamped at or after it — omitted for the full log. */
|
|
32748
|
+
async history(workspace, sessionId, signal, since) {
|
|
32749
|
+
const url = this.sessionUrl("/api/web/history", workspace, sessionId);
|
|
32750
|
+
if (since !== void 0)
|
|
32751
|
+
url.searchParams.set("since", String(since));
|
|
32752
|
+
const res = await fetch(url, {
|
|
32753
|
+
headers: this.authHeaders({ "x-token": this.opts.token }),
|
|
32754
|
+
signal
|
|
32755
|
+
});
|
|
32629
32756
|
if (res.status === 404)
|
|
32630
|
-
return
|
|
32757
|
+
return null;
|
|
32631
32758
|
if (!res.ok) {
|
|
32632
32759
|
const msg = await safeText(res);
|
|
32633
32760
|
throw new Error(`halo history ${res.status}: ${msg}`);
|
|
32634
32761
|
}
|
|
32635
|
-
|
|
32636
|
-
|
|
32762
|
+
return await res.json();
|
|
32763
|
+
}
|
|
32764
|
+
/** GET /api/web/sessions — one page of the token's own root sessions,
|
|
32765
|
+
* newest first. `workspace` is the server-resolved absolute path. */
|
|
32766
|
+
async listSessions(workspace, cursor) {
|
|
32767
|
+
const url = new URL(`${this.opts.baseUrl}/api/web/sessions`);
|
|
32768
|
+
url.searchParams.set("workspace", workspace);
|
|
32769
|
+
if (cursor !== void 0)
|
|
32770
|
+
url.searchParams.set("cursor", String(cursor));
|
|
32771
|
+
const res = await fetch(url, { headers: this.authHeaders({ "x-token": this.opts.token }) });
|
|
32772
|
+
if (!res.ok) {
|
|
32773
|
+
const msg = await safeText(res);
|
|
32774
|
+
throw new Error(`halo session list ${res.status}: ${msg}`);
|
|
32775
|
+
}
|
|
32776
|
+
return await res.json();
|
|
32777
|
+
}
|
|
32778
|
+
sessionUrl(path77, workspace, sessionId) {
|
|
32779
|
+
const url = new URL(`${this.opts.baseUrl}${path77}`);
|
|
32780
|
+
url.searchParams.set("workspace", workspace);
|
|
32781
|
+
url.searchParams.set("sessionId", sessionId);
|
|
32782
|
+
return url;
|
|
32637
32783
|
}
|
|
32638
32784
|
/** POST /api/web/stop. */
|
|
32639
32785
|
async stop(workspace, sessionId) {
|
|
@@ -32657,26 +32803,218 @@ var init_halo_client = __esm({
|
|
|
32657
32803
|
}
|
|
32658
32804
|
});
|
|
32659
32805
|
|
|
32806
|
+
// ../acp-adapter/dist/acp-updates.js
|
|
32807
|
+
function toolKind(toolName) {
|
|
32808
|
+
return TOOL_KINDS[toolName] ?? "other";
|
|
32809
|
+
}
|
|
32810
|
+
function toolTitle(toolName, input) {
|
|
32811
|
+
const arg = titleArg(input)?.replace(/\s+/g, " ").trim();
|
|
32812
|
+
return arg ? `${toolName}: ${arg.slice(0, TITLE_ARG_MAX)}` : toolName;
|
|
32813
|
+
}
|
|
32814
|
+
function titleArg(input) {
|
|
32815
|
+
if (typeof input === "string") {
|
|
32816
|
+
if (!input.startsWith("{"))
|
|
32817
|
+
return input;
|
|
32818
|
+
try {
|
|
32819
|
+
return titleArg(JSON.parse(input));
|
|
32820
|
+
} catch {
|
|
32821
|
+
return input;
|
|
32822
|
+
}
|
|
32823
|
+
}
|
|
32824
|
+
const value = input;
|
|
32825
|
+
if (!value || typeof value !== "object")
|
|
32826
|
+
return void 0;
|
|
32827
|
+
for (const key of TITLE_ARGS) {
|
|
32828
|
+
const v = value[key];
|
|
32829
|
+
if (typeof v === "string" && v)
|
|
32830
|
+
return v;
|
|
32831
|
+
}
|
|
32832
|
+
return void 0;
|
|
32833
|
+
}
|
|
32834
|
+
function textChunk(kind, text6) {
|
|
32835
|
+
return { sessionUpdate: kind, content: { type: "text", text: text6 } };
|
|
32836
|
+
}
|
|
32837
|
+
function toolResultContent(text6) {
|
|
32838
|
+
return [{ type: "content", content: { type: "text", text: text6 } }];
|
|
32839
|
+
}
|
|
32840
|
+
function replayToolCall(tc, fallbackId) {
|
|
32841
|
+
const output = tc.output?.slice(0, REPLAY_OUTPUT_MAX);
|
|
32842
|
+
return {
|
|
32843
|
+
sessionUpdate: "tool_call",
|
|
32844
|
+
toolCallId: tc.toolUseId || `replay-${fallbackId}`,
|
|
32845
|
+
title: toolTitle(tc.name, tc.input),
|
|
32846
|
+
kind: toolKind(tc.name),
|
|
32847
|
+
status: "completed",
|
|
32848
|
+
rawInput: tc.input,
|
|
32849
|
+
...output !== void 0 ? { rawOutput: output, content: toolResultContent(output) } : {}
|
|
32850
|
+
};
|
|
32851
|
+
}
|
|
32852
|
+
function replayUpdates(messages2) {
|
|
32853
|
+
const out = [];
|
|
32854
|
+
messages2.forEach((m, i) => {
|
|
32855
|
+
if (m.deleted || m.taskId)
|
|
32856
|
+
return;
|
|
32857
|
+
if (m.role === "user") {
|
|
32858
|
+
if (m.content)
|
|
32859
|
+
out.push(textChunk("user_message_chunk", m.content));
|
|
32860
|
+
return;
|
|
32861
|
+
}
|
|
32862
|
+
if (m.role !== "assistant")
|
|
32863
|
+
return;
|
|
32864
|
+
const msgKey = m.id ?? String(i);
|
|
32865
|
+
if (m.contentBlocks && m.contentBlocks.length > 0) {
|
|
32866
|
+
m.contentBlocks.forEach((b, j) => {
|
|
32867
|
+
if (b.type === "tool_call")
|
|
32868
|
+
out.push(replayToolCall(b.toolCall, `${msgKey}-${j}`));
|
|
32869
|
+
else if (b.text)
|
|
32870
|
+
out.push(textChunk(b.type === "thinking" ? "agent_thought_chunk" : "agent_message_chunk", b.text));
|
|
32871
|
+
});
|
|
32872
|
+
return;
|
|
32873
|
+
}
|
|
32874
|
+
m.toolCalls?.forEach((tc, j) => out.push(replayToolCall(tc, `${msgKey}-${j}`)));
|
|
32875
|
+
if (m.content)
|
|
32876
|
+
out.push(textChunk("agent_message_chunk", m.content));
|
|
32877
|
+
});
|
|
32878
|
+
return out;
|
|
32879
|
+
}
|
|
32880
|
+
function stripMediaLines(text6) {
|
|
32881
|
+
return text6.split(/(?<=\n)/).map((line) => line.replace(MEDIA_MARKER_RE2, "")).join("");
|
|
32882
|
+
}
|
|
32883
|
+
function replyAfterPrompt(messages2, prompt) {
|
|
32884
|
+
let start3 = -1;
|
|
32885
|
+
for (let i = messages2.length - 1; i >= 0; i--) {
|
|
32886
|
+
const m = messages2[i];
|
|
32887
|
+
if (m.role === "user" && (m.content === prompt || m.content.startsWith(prompt + "\n\n"))) {
|
|
32888
|
+
start3 = i;
|
|
32889
|
+
break;
|
|
32890
|
+
}
|
|
32891
|
+
}
|
|
32892
|
+
if (start3 === -1)
|
|
32893
|
+
return "";
|
|
32894
|
+
return stripMediaLines(messages2.slice(start3 + 1).filter((m) => m.role === "assistant" && !m.deleted && !m.taskId).map((m) => m.content).join(""));
|
|
32895
|
+
}
|
|
32896
|
+
var TOOL_KINDS, TITLE_ARGS, TITLE_ARG_MAX, REPLAY_OUTPUT_MAX, MEDIA_MARKER_RE2;
|
|
32897
|
+
var init_acp_updates = __esm({
|
|
32898
|
+
"../acp-adapter/dist/acp-updates.js"() {
|
|
32899
|
+
"use strict";
|
|
32900
|
+
TOOL_KINDS = {
|
|
32901
|
+
file_read: "read",
|
|
32902
|
+
file_write: "edit",
|
|
32903
|
+
file_edit: "edit",
|
|
32904
|
+
shell_exec: "execute",
|
|
32905
|
+
grep: "search",
|
|
32906
|
+
glob: "search",
|
|
32907
|
+
file_list: "search",
|
|
32908
|
+
web_fetch: "fetch"
|
|
32909
|
+
};
|
|
32910
|
+
TITLE_ARGS = ["command", "pattern", "url", "path"];
|
|
32911
|
+
TITLE_ARG_MAX = 80;
|
|
32912
|
+
REPLAY_OUTPUT_MAX = 500;
|
|
32913
|
+
MEDIA_MARKER_RE2 = /^MEDIA:\s*(\S.*?)\s*$/gm;
|
|
32914
|
+
}
|
|
32915
|
+
});
|
|
32916
|
+
|
|
32917
|
+
// ../acp-adapter/dist/version.js
|
|
32918
|
+
var ADAPTER_VERSION;
|
|
32919
|
+
var init_version = __esm({
|
|
32920
|
+
"../acp-adapter/dist/version.js"() {
|
|
32921
|
+
"use strict";
|
|
32922
|
+
ADAPTER_VERSION = "0.1.0";
|
|
32923
|
+
}
|
|
32924
|
+
});
|
|
32925
|
+
|
|
32660
32926
|
// ../acp-adapter/dist/adapter.js
|
|
32927
|
+
import path75 from "node:path";
|
|
32928
|
+
function composePrompt(blocks) {
|
|
32929
|
+
const segments = [];
|
|
32930
|
+
let run = "";
|
|
32931
|
+
const paragraph = (s) => {
|
|
32932
|
+
if (run)
|
|
32933
|
+
segments.push(run);
|
|
32934
|
+
run = "";
|
|
32935
|
+
segments.push(s);
|
|
32936
|
+
};
|
|
32937
|
+
const images = [];
|
|
32938
|
+
for (const b of blocks) {
|
|
32939
|
+
if (b.type === "text") {
|
|
32940
|
+
run += b.text ?? "";
|
|
32941
|
+
} else if (b.type === "image" && b.data && b.mimeType) {
|
|
32942
|
+
images.push({ data: b.data, mimeType: b.mimeType });
|
|
32943
|
+
} else if (b.type === "resource" && b.resource) {
|
|
32944
|
+
const { uri = "", text: text6 } = b.resource;
|
|
32945
|
+
if (typeof text6 === "string") {
|
|
32946
|
+
const fence = "`".repeat(Math.max(3, longestBacktickRun(text6) + 1));
|
|
32947
|
+
paragraph(`[resource: ${uri}]
|
|
32948
|
+
${fence}
|
|
32949
|
+
${text6}
|
|
32950
|
+
${fence}`);
|
|
32951
|
+
} else {
|
|
32952
|
+
paragraph(`[binary resource omitted: ${uri}]`);
|
|
32953
|
+
}
|
|
32954
|
+
} else if (b.type === "resource_link") {
|
|
32955
|
+
paragraph(`[resource link: ${[b.name, b.uri].filter(Boolean).join(" ")}]`);
|
|
32956
|
+
} else {
|
|
32957
|
+
process.stderr.write(`[acp-adapter] dropping ${b.type} content block (not supported)
|
|
32958
|
+
`);
|
|
32959
|
+
}
|
|
32960
|
+
}
|
|
32961
|
+
if (run)
|
|
32962
|
+
segments.push(run);
|
|
32963
|
+
return { text: segments.join("\n\n"), images };
|
|
32964
|
+
}
|
|
32965
|
+
function longestBacktickRun(s) {
|
|
32966
|
+
let max = 0;
|
|
32967
|
+
for (const m of s.matchAll(/`+/g))
|
|
32968
|
+
max = Math.max(max, m[0].length);
|
|
32969
|
+
return max;
|
|
32970
|
+
}
|
|
32971
|
+
function nonEmpty(v) {
|
|
32972
|
+
return typeof v === "string" && v ? v : void 0;
|
|
32973
|
+
}
|
|
32974
|
+
function errMessage(err) {
|
|
32975
|
+
return err instanceof Error ? err.message : String(err);
|
|
32976
|
+
}
|
|
32977
|
+
function sleep3(ms, signal) {
|
|
32978
|
+
return new Promise((resolve2) => {
|
|
32979
|
+
if (signal.aborted)
|
|
32980
|
+
return resolve2();
|
|
32981
|
+
const done = () => {
|
|
32982
|
+
clearTimeout(timer);
|
|
32983
|
+
signal.removeEventListener("abort", done);
|
|
32984
|
+
resolve2();
|
|
32985
|
+
};
|
|
32986
|
+
const timer = setTimeout(done, ms);
|
|
32987
|
+
signal.addEventListener("abort", done);
|
|
32988
|
+
});
|
|
32989
|
+
}
|
|
32661
32990
|
function newError(message, code = -32603) {
|
|
32662
32991
|
const e = new Error(message);
|
|
32663
32992
|
e.code = code;
|
|
32664
32993
|
return e;
|
|
32665
32994
|
}
|
|
32666
|
-
var AcpAdapter;
|
|
32995
|
+
var RETRY_DELAYS_MS, CANCEL_GRACE_MS, SETTLE_SKEW_MS, AcpAdapter;
|
|
32667
32996
|
var init_adapter = __esm({
|
|
32668
32997
|
"../acp-adapter/dist/adapter.js"() {
|
|
32669
32998
|
"use strict";
|
|
32670
32999
|
init_halo_client();
|
|
33000
|
+
init_acp_updates();
|
|
33001
|
+
init_version();
|
|
33002
|
+
RETRY_DELAYS_MS = [1e3, 2e3, 4e3, 8e3, 16e3];
|
|
33003
|
+
CANCEL_GRACE_MS = 5e3;
|
|
33004
|
+
SETTLE_SKEW_MS = 5 * 6e4;
|
|
32671
33005
|
AcpAdapter = class {
|
|
32672
33006
|
conn;
|
|
32673
33007
|
config;
|
|
32674
33008
|
client;
|
|
33009
|
+
retryDelaysMs;
|
|
33010
|
+
cancelGraceMs;
|
|
32675
33011
|
sessions = /* @__PURE__ */ new Map();
|
|
32676
|
-
constructor(conn, config3) {
|
|
33012
|
+
constructor(conn, config3, deps = {}) {
|
|
32677
33013
|
this.conn = conn;
|
|
32678
33014
|
this.config = config3;
|
|
32679
|
-
this.client = new HaloClient({ baseUrl: config3.baseUrl, token: config3.token, headers: config3.headers });
|
|
33015
|
+
this.client = deps.client ?? new HaloClient({ baseUrl: config3.baseUrl, token: config3.token, headers: config3.headers });
|
|
33016
|
+
this.retryDelaysMs = deps.retryDelaysMs ?? RETRY_DELAYS_MS;
|
|
33017
|
+
this.cancelGraceMs = deps.cancelGraceMs ?? CANCEL_GRACE_MS;
|
|
32680
33018
|
this.registerHandlers();
|
|
32681
33019
|
}
|
|
32682
33020
|
registerHandlers() {
|
|
@@ -32684,7 +33022,11 @@ var init_adapter = __esm({
|
|
|
32684
33022
|
this.conn.onRequest("authenticate", () => this.handleAuthenticate());
|
|
32685
33023
|
this.conn.onRequest("session/new", (params) => this.handleSessionNew(params));
|
|
32686
33024
|
this.conn.onRequest("session/load", (params) => this.handleSessionLoad(params));
|
|
33025
|
+
this.conn.onRequest("session/list", (params) => this.handleSessionList(params));
|
|
32687
33026
|
this.conn.onRequest("session/prompt", (params) => this.handleSessionPrompt(params));
|
|
33027
|
+
this.conn.onNotification("session/cancel", (params) => {
|
|
33028
|
+
this.handleSessionCancel(params);
|
|
33029
|
+
});
|
|
32688
33030
|
this.conn.onRequest("session/cancel", (params) => this.handleSessionCancel(params));
|
|
32689
33031
|
}
|
|
32690
33032
|
/**
|
|
@@ -32698,13 +33040,15 @@ var init_adapter = __esm({
|
|
|
32698
33040
|
protocolVersion: 1,
|
|
32699
33041
|
agentCapabilities: {
|
|
32700
33042
|
loadSession: true,
|
|
33043
|
+
sessionCapabilities: { list: {} },
|
|
32701
33044
|
promptCapabilities: {
|
|
32702
33045
|
image: true,
|
|
32703
33046
|
audio: false,
|
|
32704
33047
|
embeddedContext: true
|
|
32705
33048
|
}
|
|
32706
33049
|
},
|
|
32707
|
-
authMethods: []
|
|
33050
|
+
authMethods: [],
|
|
33051
|
+
agentInfo: { name: "halo", title: "Halo", version: ADAPTER_VERSION }
|
|
32708
33052
|
};
|
|
32709
33053
|
}
|
|
32710
33054
|
/** Authentication is satisfied by the adapter's launch flags; ACP
|
|
@@ -32730,37 +33074,66 @@ var init_adapter = __esm({
|
|
|
32730
33074
|
/**
|
|
32731
33075
|
* Resume a session whose id the ACP client persisted from an earlier
|
|
32732
33076
|
* `session/new`. Adapter doesn't store anything on disk — the client
|
|
32733
|
-
* is the source of truth for "which sessions are mine". We
|
|
33077
|
+
* is the source of truth for "which sessions are mine". We:
|
|
32734
33078
|
*
|
|
32735
|
-
* 1.
|
|
32736
|
-
*
|
|
32737
|
-
*
|
|
32738
|
-
*
|
|
33079
|
+
* 1. Fetch its history (`/api/web/history` with the explicit id —
|
|
33080
|
+
* 404 when the row is gone → invalid-params error).
|
|
33081
|
+
* 2. Replay the whole conversation as `session/update`s, all of them
|
|
33082
|
+
* before the response (ACP session-setup "Loading Sessions").
|
|
33083
|
+
* 3. Register it in our in-memory map so the same prompt / cancel
|
|
32739
33084
|
* paths work for it.
|
|
32740
33085
|
*
|
|
32741
|
-
*
|
|
32742
|
-
*
|
|
33086
|
+
* `cwd` / `mcpServers` are accepted and ignored: the agent runs in the
|
|
33087
|
+
* server-side workspace and can't reach the client's local MCP servers.
|
|
33088
|
+
* A turn still running server-side is replayed as far as it got; the
|
|
33089
|
+
* live tail isn't attached.
|
|
32743
33090
|
*/
|
|
32744
33091
|
async handleSessionLoad(params) {
|
|
32745
33092
|
const p = params;
|
|
32746
33093
|
if (!p.sessionId)
|
|
32747
33094
|
throw newError("invalid params: sessionId required", -32602);
|
|
32748
|
-
const
|
|
32749
|
-
if (!
|
|
33095
|
+
const history = await this.client.history(this.config.workspace, p.sessionId);
|
|
33096
|
+
if (!history)
|
|
32750
33097
|
throw newError(`unknown session: ${p.sessionId}`, -32602);
|
|
32751
33098
|
if (!this.sessions.has(p.sessionId)) {
|
|
32752
33099
|
this.sessions.set(p.sessionId, { workspace: this.config.workspace });
|
|
32753
33100
|
}
|
|
32754
|
-
|
|
33101
|
+
for (const update of replayUpdates(history.messages)) {
|
|
33102
|
+
this.conn.notify("session/update", { sessionId: p.sessionId, update });
|
|
33103
|
+
}
|
|
33104
|
+
return {};
|
|
33105
|
+
}
|
|
33106
|
+
/**
|
|
33107
|
+
* One page of the token's own root sessions (`GET /api/web/sessions`),
|
|
33108
|
+
* newest first. Every session's `cwd` is the server-resolved workspace —
|
|
33109
|
+
* where the agent actually runs — so a `cwd` filter matches only that
|
|
33110
|
+
* path; anything else is an empty page, not an error (spec). The cursor
|
|
33111
|
+
* is the server's `nextCursor` (an updatedAt epoch-ms) as an opaque
|
|
33112
|
+
* string; anything that doesn't parse back is invalid params.
|
|
33113
|
+
*/
|
|
33114
|
+
async handleSessionList(params) {
|
|
33115
|
+
const p = params ?? {};
|
|
33116
|
+
const cursor = p.cursor || void 0;
|
|
33117
|
+
if (cursor !== void 0 && !/^\d+$/.test(cursor))
|
|
33118
|
+
throw newError(`invalid cursor: ${cursor}`, -32602);
|
|
33119
|
+
const page = await this.client.listSessions(this.config.workspace, cursor === void 0 ? void 0 : Number(cursor));
|
|
33120
|
+
if (p.cwd !== void 0 && path75.resolve(p.cwd) !== path75.resolve(page.workspace))
|
|
33121
|
+
return { sessions: [] };
|
|
33122
|
+
return {
|
|
33123
|
+
sessions: page.sessions.map((s) => ({
|
|
33124
|
+
sessionId: s.sessionId,
|
|
33125
|
+
cwd: page.workspace,
|
|
33126
|
+
...s.title ? { title: s.title } : {},
|
|
33127
|
+
updatedAt: new Date(s.updatedAt).toISOString()
|
|
33128
|
+
})),
|
|
33129
|
+
...page.nextCursor !== null ? { nextCursor: String(page.nextCursor) } : {}
|
|
33130
|
+
};
|
|
32755
33131
|
}
|
|
32756
33132
|
/**
|
|
32757
33133
|
* Forward the user's prompt to halo and stream back updates.
|
|
32758
33134
|
*
|
|
32759
|
-
* ACP `session/prompt.params.prompt` is an array of content blocks
|
|
32760
|
-
*
|
|
32761
|
-
* concatenate; image blocks become halo `images[]` entries.
|
|
32762
|
-
* Resource / embedded-context blocks are ignored with a warning to
|
|
32763
|
-
* stderr (we don't have a way to ship them to halo yet).
|
|
33135
|
+
* ACP `session/prompt.params.prompt` is an array of content blocks;
|
|
33136
|
+
* `composePrompt` turns it into halo's `message` + `images[]`.
|
|
32764
33137
|
*/
|
|
32765
33138
|
async handleSessionPrompt(params) {
|
|
32766
33139
|
const p = params;
|
|
@@ -32769,57 +33142,167 @@ var init_adapter = __esm({
|
|
|
32769
33142
|
const state = this.sessions.get(p.sessionId);
|
|
32770
33143
|
if (!state)
|
|
32771
33144
|
throw newError(`unknown session: ${p.sessionId}`, -32602);
|
|
32772
|
-
if (state.
|
|
33145
|
+
if (state.turn) {
|
|
32773
33146
|
throw newError("prompt already in progress for this session", -32600);
|
|
32774
33147
|
}
|
|
32775
|
-
const
|
|
32776
|
-
const text6 = blocks.filter((b) => b.type === "text").map((b) => b.text ?? "").join("");
|
|
32777
|
-
const images = [];
|
|
32778
|
-
for (const b of blocks) {
|
|
32779
|
-
if (b.type === "image" && b.data && b.mimeType) {
|
|
32780
|
-
images.push({ data: b.data, mimeType: b.mimeType });
|
|
32781
|
-
} else if (b.type === "resource" || b.type === "resource_link") {
|
|
32782
|
-
process.stderr.write(`[acp-adapter] dropping ${b.type} content block (not yet supported)
|
|
32783
|
-
`);
|
|
32784
|
-
}
|
|
32785
|
-
}
|
|
33148
|
+
const { text: text6, images } = composePrompt(p.prompt ?? []);
|
|
32786
33149
|
if (!text6 && images.length === 0) {
|
|
32787
33150
|
throw newError("invalid params: empty prompt", -32602);
|
|
32788
33151
|
}
|
|
32789
|
-
const
|
|
32790
|
-
|
|
32791
|
-
|
|
32792
|
-
|
|
32793
|
-
|
|
33152
|
+
const turn = {
|
|
33153
|
+
cancel: new AbortController(),
|
|
33154
|
+
http: new AbortController(),
|
|
33155
|
+
haloSessionId: p.sessionId,
|
|
33156
|
+
prompt: text6,
|
|
33157
|
+
startedAt: Date.now(),
|
|
33158
|
+
fallbackIds: 0,
|
|
33159
|
+
streamed: "",
|
|
33160
|
+
contentEvents: 0,
|
|
33161
|
+
announcedTools: /* @__PURE__ */ new Set()
|
|
33162
|
+
};
|
|
33163
|
+
state.turn = turn;
|
|
32794
33164
|
try {
|
|
32795
|
-
|
|
33165
|
+
const stopReason = await this.runTurn(p.sessionId, state, turn, {
|
|
32796
33166
|
message: text6,
|
|
32797
33167
|
images: images.length > 0 ? images : void 0,
|
|
32798
33168
|
workspace: state.workspace,
|
|
32799
|
-
sessionId,
|
|
33169
|
+
sessionId: p.sessionId,
|
|
32800
33170
|
agentId: this.config.agentId
|
|
32801
|
-
}
|
|
32802
|
-
|
|
32803
|
-
|
|
32804
|
-
|
|
32805
|
-
|
|
32806
|
-
|
|
32807
|
-
|
|
32808
|
-
|
|
32809
|
-
|
|
33171
|
+
});
|
|
33172
|
+
return { stopReason };
|
|
33173
|
+
} finally {
|
|
33174
|
+
turn.http.abort();
|
|
33175
|
+
state.turn = void 0;
|
|
33176
|
+
}
|
|
33177
|
+
}
|
|
33178
|
+
/**
|
|
33179
|
+
* One prompt turn: the /chat stream, then — when the session was busy
|
|
33180
|
+
* (`queued`, the message drains in the running turn) or the stream died
|
|
33181
|
+
* before `complete` — /subscribe until the terminal `complete`. A cancel
|
|
33182
|
+
* at any point resolves `cancelled`, once the stream is drained or the
|
|
33183
|
+
* grace period ran out.
|
|
33184
|
+
*/
|
|
33185
|
+
async runTurn(acpSessionId, state, turn, chat) {
|
|
33186
|
+
let result;
|
|
33187
|
+
try {
|
|
33188
|
+
result = await this.pump(acpSessionId, turn, this.client.chat(chat, turn.http.signal));
|
|
32810
33189
|
} catch (err) {
|
|
32811
|
-
|
|
32812
|
-
|
|
32813
|
-
|
|
32814
|
-
|
|
32815
|
-
|
|
32816
|
-
|
|
33190
|
+
if (turn.cancel.signal.aborted)
|
|
33191
|
+
return "cancelled";
|
|
33192
|
+
this.sendChunk(acpSessionId, "agent_message_chunk", `[adapter error] ${errMessage(err)}`);
|
|
33193
|
+
return "end_turn";
|
|
33194
|
+
}
|
|
33195
|
+
if (turn.cancel.signal.aborted)
|
|
33196
|
+
return "cancelled";
|
|
33197
|
+
if (result === "end")
|
|
33198
|
+
return "end_turn";
|
|
33199
|
+
if (result === "queued") {
|
|
33200
|
+
result = await this.pumpSafe(acpSessionId, turn, this.client.subscribe(state.workspace, turn.haloSessionId, turn.http.signal));
|
|
33201
|
+
if (turn.cancel.signal.aborted)
|
|
33202
|
+
return "cancelled";
|
|
33203
|
+
if (result !== "dropped")
|
|
33204
|
+
return this.settle(acpSessionId, state, turn);
|
|
33205
|
+
}
|
|
33206
|
+
return this.reconnect(acpSessionId, state, turn);
|
|
33207
|
+
}
|
|
33208
|
+
/**
|
|
33209
|
+
* The stream ended without `complete` and without a cancel (proxy idle
|
|
33210
|
+
* timeout, network blip, server restart). Re-attach via /subscribe with
|
|
33211
|
+
* backoff; a re-attach that delivers frames resets the budget, so a long
|
|
33212
|
+
* task can survive repeated drops. Once a re-attached stream completes
|
|
33213
|
+
* (or subscribe reports the run already over) the reply is settled
|
|
33214
|
+
* against history — text produced while detached is lost from the live
|
|
33215
|
+
* stream.
|
|
33216
|
+
*/
|
|
33217
|
+
async reconnect(acpSessionId, state, turn) {
|
|
33218
|
+
let failures = 0;
|
|
33219
|
+
while (failures < this.retryDelaysMs.length) {
|
|
33220
|
+
await sleep3(this.retryDelaysMs[failures], turn.cancel.signal);
|
|
33221
|
+
if (turn.cancel.signal.aborted)
|
|
33222
|
+
return "cancelled";
|
|
33223
|
+
const before = turn.contentEvents;
|
|
33224
|
+
const result = await this.pumpSafe(acpSessionId, turn, this.client.subscribe(state.workspace, turn.haloSessionId, turn.http.signal));
|
|
33225
|
+
if (turn.cancel.signal.aborted)
|
|
33226
|
+
return "cancelled";
|
|
33227
|
+
if (result !== "dropped")
|
|
33228
|
+
return this.settle(acpSessionId, state, turn);
|
|
33229
|
+
failures = turn.contentEvents > before ? 0 : failures + 1;
|
|
33230
|
+
}
|
|
33231
|
+
this.sendChunk(acpSessionId, "agent_message_chunk", "[adapter error] connection lost");
|
|
33232
|
+
return "end_turn";
|
|
33233
|
+
}
|
|
33234
|
+
/** Route a stream's events until a terminal one. A stream that ends or
|
|
33235
|
+
* throws after opening is 'dropped'; one that throws before its first
|
|
33236
|
+
* event rethrows (the request itself failed). */
|
|
33237
|
+
async pump(acpSessionId, turn, stream) {
|
|
33238
|
+
let opened = false;
|
|
33239
|
+
try {
|
|
33240
|
+
for await (const ev of stream) {
|
|
33241
|
+
opened = true;
|
|
33242
|
+
const action = this.routeEvent(acpSessionId, turn, ev);
|
|
33243
|
+
if (action)
|
|
33244
|
+
return action;
|
|
32817
33245
|
}
|
|
32818
|
-
}
|
|
32819
|
-
|
|
33246
|
+
} catch (err) {
|
|
33247
|
+
if (!opened)
|
|
33248
|
+
throw err;
|
|
33249
|
+
if (!turn.http.signal.aborted)
|
|
33250
|
+
process.stderr.write(`[acp-adapter] stream dropped: ${errMessage(err)}
|
|
33251
|
+
`);
|
|
33252
|
+
}
|
|
33253
|
+
return "dropped";
|
|
33254
|
+
}
|
|
33255
|
+
/** `pump` for re-attach attempts, where a failed request is just one
|
|
33256
|
+
* more dropped attempt. */
|
|
33257
|
+
async pumpSafe(acpSessionId, turn, stream) {
|
|
33258
|
+
try {
|
|
33259
|
+
return await this.pump(acpSessionId, turn, stream);
|
|
33260
|
+
} catch (err) {
|
|
33261
|
+
if (!turn.http.signal.aborted)
|
|
33262
|
+
process.stderr.write(`[acp-adapter] subscribe failed: ${errMessage(err)}
|
|
33263
|
+
`);
|
|
33264
|
+
return "dropped";
|
|
33265
|
+
}
|
|
33266
|
+
}
|
|
33267
|
+
/**
|
|
33268
|
+
* After a re-attach: compare what was streamed with the reply the
|
|
33269
|
+
* server logged for this prompt. Streamed text that is a prefix gets
|
|
33270
|
+
* only the missing tail; anything else (a gap mid-reply) gets the whole
|
|
33271
|
+
* reply again behind a marker, so the client never silently shows a
|
|
33272
|
+
* reply with a hole in it.
|
|
33273
|
+
*/
|
|
33274
|
+
async settle(acpSessionId, state, turn) {
|
|
33275
|
+
await this.settleFromHistory(acpSessionId, state, turn);
|
|
33276
|
+
return turn.cancel.signal.aborted ? "cancelled" : "end_turn";
|
|
33277
|
+
}
|
|
33278
|
+
async settleFromHistory(acpSessionId, state, turn) {
|
|
33279
|
+
let reply;
|
|
33280
|
+
try {
|
|
33281
|
+
const since = Math.max(0, turn.startedAt - SETTLE_SKEW_MS);
|
|
33282
|
+
const history = await this.client.history(state.workspace, turn.haloSessionId, turn.http.signal, since);
|
|
33283
|
+
if (!history)
|
|
33284
|
+
return;
|
|
33285
|
+
reply = replyAfterPrompt(history.messages, turn.prompt);
|
|
33286
|
+
} catch (err) {
|
|
33287
|
+
if (!turn.http.signal.aborted)
|
|
33288
|
+
process.stderr.write(`[acp-adapter] history after reconnect failed: ${errMessage(err)}
|
|
33289
|
+
`);
|
|
33290
|
+
return;
|
|
32820
33291
|
}
|
|
32821
|
-
|
|
33292
|
+
if (!reply || turn.streamed.endsWith(reply))
|
|
33293
|
+
return;
|
|
33294
|
+
const missing = reply.startsWith(turn.streamed) ? reply.slice(turn.streamed.length) : `[reconnected \u2014 full reply]
|
|
33295
|
+
${reply}`;
|
|
33296
|
+
this.sendChunk(acpSessionId, "agent_message_chunk", missing);
|
|
33297
|
+
turn.streamed = reply;
|
|
32822
33298
|
}
|
|
33299
|
+
/**
|
|
33300
|
+
* ACP cancel: stop the halo session, then let the prompt keep reading
|
|
33301
|
+
* so the stop's own updates (interrupted tool rows, the final text)
|
|
33302
|
+
* reach the client BEFORE the `cancelled` response (prompt-turn
|
|
33303
|
+
* "Cancellation"). If no `complete` arrives within the grace period the
|
|
33304
|
+
* stream is aborted and the prompt resolves `cancelled` anyway.
|
|
33305
|
+
*/
|
|
32823
33306
|
handleSessionCancel(params) {
|
|
32824
33307
|
const p = params;
|
|
32825
33308
|
if (!p.sessionId)
|
|
@@ -32827,10 +33310,13 @@ var init_adapter = __esm({
|
|
|
32827
33310
|
const state = this.sessions.get(p.sessionId);
|
|
32828
33311
|
if (!state)
|
|
32829
33312
|
return null;
|
|
32830
|
-
|
|
32831
|
-
|
|
32832
|
-
|
|
32833
|
-
|
|
33313
|
+
const turn = state.turn;
|
|
33314
|
+
if (turn && !turn.cancel.signal.aborted) {
|
|
33315
|
+
turn.cancel.abort();
|
|
33316
|
+
setTimeout(() => turn.http.abort(), this.cancelGraceMs).unref();
|
|
33317
|
+
}
|
|
33318
|
+
void this.client.stop(state.workspace, turn?.haloSessionId ?? p.sessionId).catch((err) => {
|
|
33319
|
+
process.stderr.write(`[acp-adapter] cancel stop failed: ${errMessage(err)}
|
|
32834
33320
|
`);
|
|
32835
33321
|
});
|
|
32836
33322
|
return null;
|
|
@@ -32838,17 +33324,23 @@ var init_adapter = __esm({
|
|
|
32838
33324
|
/**
|
|
32839
33325
|
* Translate one halo SSE event into an ACP session/update
|
|
32840
33326
|
* notification. Returns 'end' when the event terminates the prompt
|
|
32841
|
-
* (complete / error), '
|
|
32842
|
-
*
|
|
33327
|
+
* (complete / error), 'queued' when the session was busy, or null to
|
|
33328
|
+
* continue.
|
|
32843
33329
|
*/
|
|
32844
|
-
routeEvent(acpSessionId,
|
|
33330
|
+
routeEvent(acpSessionId, turn, ev) {
|
|
33331
|
+
if (ev.type !== "session")
|
|
33332
|
+
turn.contentEvents++;
|
|
32845
33333
|
switch (ev.type) {
|
|
32846
33334
|
case "session":
|
|
33335
|
+
if (typeof ev.sessionId === "string" && ev.sessionId)
|
|
33336
|
+
turn.haloSessionId = ev.sessionId;
|
|
32847
33337
|
return null;
|
|
32848
33338
|
case "stream": {
|
|
32849
33339
|
const text6 = typeof ev.text === "string" ? ev.text : "";
|
|
32850
|
-
if (text6)
|
|
33340
|
+
if (text6) {
|
|
33341
|
+
turn.streamed += text6;
|
|
32851
33342
|
this.sendChunk(acpSessionId, "agent_message_chunk", text6);
|
|
33343
|
+
}
|
|
32852
33344
|
return null;
|
|
32853
33345
|
}
|
|
32854
33346
|
case "thinking": {
|
|
@@ -32859,60 +33351,34 @@ var init_adapter = __esm({
|
|
|
32859
33351
|
}
|
|
32860
33352
|
case "tool_call": {
|
|
32861
33353
|
const toolName = typeof ev.toolName === "string" ? ev.toolName : "tool";
|
|
32862
|
-
const callId = `${acpSessionId}-${toolName}-${
|
|
32863
|
-
|
|
32864
|
-
this.
|
|
32865
|
-
sessionId: acpSessionId,
|
|
32866
|
-
update: {
|
|
32867
|
-
sessionUpdate: "tool_call",
|
|
32868
|
-
toolCallId: callId,
|
|
32869
|
-
title: toolName,
|
|
32870
|
-
kind: "other",
|
|
32871
|
-
status: "in_progress",
|
|
32872
|
-
rawInput: ev.toolInput
|
|
32873
|
-
}
|
|
32874
|
-
});
|
|
33354
|
+
const callId = nonEmpty(ev.toolUseId) ?? `${acpSessionId}-${toolName}-${turn.startedAt.toString(36)}-${++turn.fallbackIds}`;
|
|
33355
|
+
turn.lastToolCall = { callId, toolName };
|
|
33356
|
+
this.announceTool(acpSessionId, turn, callId, toolName, ev.toolInput);
|
|
32875
33357
|
return null;
|
|
32876
33358
|
}
|
|
32877
33359
|
case "tool_result": {
|
|
32878
33360
|
const evToolName = typeof ev.toolName === "string" ? ev.toolName : void 0;
|
|
32879
33361
|
const result = typeof ev.result === "string" ? ev.result : "";
|
|
32880
|
-
const last =
|
|
32881
|
-
|
|
32882
|
-
|
|
32883
|
-
|
|
32884
|
-
|
|
32885
|
-
|
|
32886
|
-
toolCallId: last.callId,
|
|
32887
|
-
status: "completed",
|
|
32888
|
-
content: [{ type: "content", content: { type: "text", text: result } }]
|
|
32889
|
-
}
|
|
32890
|
-
});
|
|
32891
|
-
state.lastToolCall = void 0;
|
|
32892
|
-
return null;
|
|
33362
|
+
const last = turn.lastToolCall;
|
|
33363
|
+
let callId = nonEmpty(ev.toolUseId);
|
|
33364
|
+
if (!callId && last && (!evToolName || evToolName === last.toolName))
|
|
33365
|
+
callId = last.callId;
|
|
33366
|
+
if (!callId) {
|
|
33367
|
+
callId = `${acpSessionId}-${evToolName ?? "tool"}-orphan-${turn.startedAt.toString(36)}-${++turn.fallbackIds}`;
|
|
32893
33368
|
}
|
|
32894
|
-
|
|
32895
|
-
|
|
32896
|
-
this.conn.notify("session/update", {
|
|
32897
|
-
sessionId: acpSessionId,
|
|
32898
|
-
update: {
|
|
32899
|
-
sessionUpdate: "tool_call",
|
|
32900
|
-
toolCallId: callId,
|
|
32901
|
-
title: toolName,
|
|
32902
|
-
kind: "other",
|
|
32903
|
-
status: "in_progress"
|
|
32904
|
-
}
|
|
32905
|
-
});
|
|
33369
|
+
if (!turn.announcedTools.has(callId))
|
|
33370
|
+
this.announceTool(acpSessionId, turn, callId, evToolName ?? "tool", void 0);
|
|
32906
33371
|
this.conn.notify("session/update", {
|
|
32907
33372
|
sessionId: acpSessionId,
|
|
32908
33373
|
update: {
|
|
32909
33374
|
sessionUpdate: "tool_call_update",
|
|
32910
33375
|
toolCallId: callId,
|
|
32911
33376
|
status: "completed",
|
|
32912
|
-
content:
|
|
33377
|
+
content: toolResultContent(result)
|
|
32913
33378
|
}
|
|
32914
33379
|
});
|
|
32915
|
-
|
|
33380
|
+
if (last?.callId === callId)
|
|
33381
|
+
turn.lastToolCall = void 0;
|
|
32916
33382
|
return null;
|
|
32917
33383
|
}
|
|
32918
33384
|
case "error": {
|
|
@@ -32920,10 +33386,8 @@ var init_adapter = __esm({
|
|
|
32920
33386
|
this.sendChunk(acpSessionId, "agent_message_chunk", `[error] ${errText2}`);
|
|
32921
33387
|
return "end";
|
|
32922
33388
|
}
|
|
32923
|
-
case "queued":
|
|
32924
|
-
|
|
32925
|
-
return "end";
|
|
32926
|
-
}
|
|
33389
|
+
case "queued":
|
|
33390
|
+
return "queued";
|
|
32927
33391
|
case "file": {
|
|
32928
33392
|
const filePath = typeof ev.path === "string" ? ev.path : "";
|
|
32929
33393
|
if (filePath)
|
|
@@ -32939,15 +33403,28 @@ var init_adapter = __esm({
|
|
|
32939
33403
|
return null;
|
|
32940
33404
|
}
|
|
32941
33405
|
}
|
|
32942
|
-
|
|
33406
|
+
/** `tool_call` notification. No `locations`: paths are server-side,
|
|
33407
|
+
* the client couldn't open them. */
|
|
33408
|
+
announceTool(acpSessionId, turn, callId, toolName, toolInput) {
|
|
33409
|
+
turn.announcedTools.add(callId);
|
|
32943
33410
|
this.conn.notify("session/update", {
|
|
32944
33411
|
sessionId: acpSessionId,
|
|
32945
33412
|
update: {
|
|
32946
|
-
sessionUpdate:
|
|
32947
|
-
|
|
33413
|
+
sessionUpdate: "tool_call",
|
|
33414
|
+
toolCallId: callId,
|
|
33415
|
+
title: toolTitle(toolName, toolInput),
|
|
33416
|
+
kind: toolKind(toolName),
|
|
33417
|
+
status: "in_progress",
|
|
33418
|
+
...toolInput !== void 0 ? { rawInput: toolInput } : {}
|
|
32948
33419
|
}
|
|
32949
33420
|
});
|
|
32950
33421
|
}
|
|
33422
|
+
sendChunk(acpSessionId, kind, text6) {
|
|
33423
|
+
this.conn.notify("session/update", {
|
|
33424
|
+
sessionId: acpSessionId,
|
|
33425
|
+
update: textChunk(kind, text6)
|
|
33426
|
+
});
|
|
33427
|
+
}
|
|
32951
33428
|
};
|
|
32952
33429
|
}
|
|
32953
33430
|
});
|
|
@@ -33254,11 +33731,11 @@ process.emitWarning = (warning, ...args) => {
|
|
|
33254
33731
|
|
|
33255
33732
|
// src/index.ts
|
|
33256
33733
|
init_server_supervisor();
|
|
33257
|
-
import
|
|
33258
|
-
import
|
|
33734
|
+
import path76 from "node:path";
|
|
33735
|
+
import fs68 from "node:fs";
|
|
33259
33736
|
import { homedir as homedir25 } from "node:os";
|
|
33260
33737
|
import { parseArgs as parseArgs2 } from "node:util";
|
|
33261
|
-
var VERSION = "1.5.
|
|
33738
|
+
var VERSION = "1.5.11";
|
|
33262
33739
|
var HELP_TOP = `Usage: halo <command> [options]
|
|
33263
33740
|
|
|
33264
33741
|
Commands:
|
|
@@ -33545,8 +34022,8 @@ async function cmdSetup(argv = []) {
|
|
|
33545
34022
|
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));
|
|
33546
34023
|
const { promptText: promptText2, promptPassword: promptPassword2, promptSelect: promptSelect2, promptMultiSelect: promptMultiSelect2 } = await Promise.resolve().then(() => (init_setup_prompts(), setup_prompts_exports));
|
|
33547
34024
|
const nonInteractive = argv.includes("--non-interactive") || argv.includes("-y");
|
|
33548
|
-
const haloHome2 =
|
|
33549
|
-
const globalDir2 =
|
|
34025
|
+
const haloHome2 = path76.join(homedir25(), ".halo");
|
|
34026
|
+
const globalDir2 = path76.join(haloHome2, "global");
|
|
33550
34027
|
ensureHaloHome2(haloHome2);
|
|
33551
34028
|
process.stderr.write(`[setup] ${haloHome2}/ ready
|
|
33552
34029
|
`);
|
|
@@ -33889,7 +34366,7 @@ async function cmdUpgrade() {
|
|
|
33889
34366
|
`);
|
|
33890
34367
|
process.exit(1);
|
|
33891
34368
|
}
|
|
33892
|
-
const broken = verifyNativeModules2(
|
|
34369
|
+
const broken = verifyNativeModules2(path76.join(rootDir, PKG));
|
|
33893
34370
|
if (broken.length > 0) {
|
|
33894
34371
|
process.stderr.write(
|
|
33895
34372
|
`
|
|
@@ -33920,7 +34397,7 @@ halo: upgrade failed (npm exited ${install.status}). ${hint}
|
|
|
33920
34397
|
async function cmdAgents(flags) {
|
|
33921
34398
|
const { initRuntime: initRuntime2, listAgents: listAgents2 } = await Promise.resolve().then(() => (init_harness(), harness_exports));
|
|
33922
34399
|
await initRuntime2();
|
|
33923
|
-
const workspace =
|
|
34400
|
+
const workspace = path76.resolve(flags.workspace ?? process.cwd());
|
|
33924
34401
|
const agents = await listAgents2(workspace);
|
|
33925
34402
|
if (agents.length === 0) {
|
|
33926
34403
|
process.stderr.write("No agents found.\n");
|
|
@@ -33936,7 +34413,7 @@ async function cmdAgents(flags) {
|
|
|
33936
34413
|
async function cmdSessions(flags) {
|
|
33937
34414
|
const { initRuntime: initRuntime2, listSessions: listSessions2 } = await Promise.resolve().then(() => (init_harness(), harness_exports));
|
|
33938
34415
|
await initRuntime2();
|
|
33939
|
-
const workspace =
|
|
34416
|
+
const workspace = path76.resolve(flags.workspace ?? process.cwd());
|
|
33940
34417
|
const sessions = listSessions2(workspace);
|
|
33941
34418
|
if (sessions.length === 0) {
|
|
33942
34419
|
process.stderr.write("No sessions found.\n");
|
|
@@ -33967,7 +34444,7 @@ async function cmdModels(argv) {
|
|
|
33967
34444
|
process.exitCode = 1;
|
|
33968
34445
|
return;
|
|
33969
34446
|
}
|
|
33970
|
-
const plan = mod.planModelsInstall(
|
|
34447
|
+
const plan = mod.planModelsInstall(path76.resolve(dir));
|
|
33971
34448
|
for (const line of mod.formatPlan(plan)) process.stdout.write(`${line}
|
|
33972
34449
|
`);
|
|
33973
34450
|
if (plan.needsConfirm && !values.yes) {
|
|
@@ -34079,11 +34556,11 @@ ${stdinContent}` : stdinContent;
|
|
|
34079
34556
|
harness.destroy();
|
|
34080
34557
|
}
|
|
34081
34558
|
}
|
|
34082
|
-
var SERVER_LOCK_PATH =
|
|
34083
|
-
var SERVER_LOG_PATH =
|
|
34559
|
+
var SERVER_LOCK_PATH = path76.join(homedir25(), ".halo", "global", "server.lock");
|
|
34560
|
+
var SERVER_LOG_PATH = path76.join(homedir25(), ".halo", "logs", "server.log");
|
|
34084
34561
|
function readServerPid() {
|
|
34085
34562
|
try {
|
|
34086
|
-
const txt =
|
|
34563
|
+
const txt = fs68.readFileSync(SERVER_LOCK_PATH, "utf-8").trim();
|
|
34087
34564
|
const n = parseInt(txt, 10);
|
|
34088
34565
|
return Number.isFinite(n) && n > 0 ? n : null;
|
|
34089
34566
|
} catch {
|
|
@@ -34170,8 +34647,8 @@ async function cmdServerStart(argv, _unused) {
|
|
|
34170
34647
|
return;
|
|
34171
34648
|
}
|
|
34172
34649
|
const { spawn: spawn5 } = await import("node:child_process");
|
|
34173
|
-
|
|
34174
|
-
const out =
|
|
34650
|
+
fs68.mkdirSync(path76.dirname(SERVER_LOG_PATH), { recursive: true });
|
|
34651
|
+
const out = fs68.openSync(SERVER_LOG_PATH, "a");
|
|
34175
34652
|
const child = spawn5(process.argv[0], [process.argv[1], "server", "start"], {
|
|
34176
34653
|
detached: true,
|
|
34177
34654
|
stdio: ["ignore", out, out],
|
|
@@ -34272,7 +34749,7 @@ async function cmdServerLogs(argv) {
|
|
|
34272
34749
|
lines: { type: "string", short: "n" }
|
|
34273
34750
|
}
|
|
34274
34751
|
});
|
|
34275
|
-
if (!
|
|
34752
|
+
if (!fs68.existsSync(SERVER_LOG_PATH)) {
|
|
34276
34753
|
process.stderr.write(`No log file at ${SERVER_LOG_PATH}
|
|
34277
34754
|
`);
|
|
34278
34755
|
return;
|
|
@@ -34290,7 +34767,7 @@ function exitMissingSetup() {
|
|
|
34290
34767
|
process.exit(1);
|
|
34291
34768
|
}
|
|
34292
34769
|
function isHaloHomeReady() {
|
|
34293
|
-
return
|
|
34770
|
+
return fs68.existsSync(path76.join(homedir25(), ".halo", "global", ".template-version"));
|
|
34294
34771
|
}
|
|
34295
34772
|
async function main2() {
|
|
34296
34773
|
const argv = process.argv.slice(2);
|