@shipers-dev/multi 0.51.0 → 0.63.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1645 -284
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
|
+
import { createRequire } from "node:module";
|
|
3
4
|
var __defProp = Object.defineProperty;
|
|
4
5
|
var __returnValue = (v) => v;
|
|
5
6
|
function __exportSetter(name, newValue) {
|
|
@@ -15,6 +16,7 @@ var __export = (target, all) => {
|
|
|
15
16
|
});
|
|
16
17
|
};
|
|
17
18
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
19
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
18
20
|
|
|
19
21
|
// ../../node_modules/effect/dist/esm/Function.js
|
|
20
22
|
function pipe(a, ab, bc, cd, de, ef, fg, gh, hi) {
|
|
@@ -27970,7 +27972,7 @@ function finalize(ctx, schema) {
|
|
|
27970
27972
|
result.$schema = "http://json-schema.org/draft-07/schema#";
|
|
27971
27973
|
} else if (ctx.target === "draft-04") {
|
|
27972
27974
|
result.$schema = "http://json-schema.org/draft-04/schema#";
|
|
27973
|
-
} else if (ctx.target === "openapi-3.0") {}
|
|
27975
|
+
} else if (ctx.target === "openapi-3.0") {}
|
|
27974
27976
|
if (ctx.external?.uri) {
|
|
27975
27977
|
const id3 = ctx.external.registry.get(schema)?.id;
|
|
27976
27978
|
if (!id3)
|
|
@@ -28235,7 +28237,7 @@ var formatMap, stringProcessor = (schema, ctx, _json, _params) => {
|
|
|
28235
28237
|
if (val === undefined) {
|
|
28236
28238
|
if (ctx.unrepresentable === "throw") {
|
|
28237
28239
|
throw new Error("Literal `undefined` cannot be represented in JSON Schema");
|
|
28238
|
-
}
|
|
28240
|
+
}
|
|
28239
28241
|
} else if (typeof val === "bigint") {
|
|
28240
28242
|
if (ctx.unrepresentable === "throw") {
|
|
28241
28243
|
throw new Error("BigInt literals cannot be represented in JSON Schema");
|
|
@@ -33196,6 +33198,35 @@ function isPidAlive(pid) {
|
|
|
33196
33198
|
return false;
|
|
33197
33199
|
}
|
|
33198
33200
|
}
|
|
33201
|
+
function listAdapterProcs() {
|
|
33202
|
+
try {
|
|
33203
|
+
const r = spawnSync("ps", ["-axo", "pid=,ppid=,command="], { encoding: "utf8" });
|
|
33204
|
+
if (r.status !== 0)
|
|
33205
|
+
return [];
|
|
33206
|
+
const out = [];
|
|
33207
|
+
for (const line of (r.stdout || "").split(`
|
|
33208
|
+
`)) {
|
|
33209
|
+
const m = line.match(/^\s*(\d+)\s+(\d+)\s+(.*)$/);
|
|
33210
|
+
if (!m)
|
|
33211
|
+
continue;
|
|
33212
|
+
const command = m[3];
|
|
33213
|
+
if (!command.includes("claude-code-acp") && !command.includes("acpx"))
|
|
33214
|
+
continue;
|
|
33215
|
+
out.push({ pid: parseInt(m[1], 10), ppid: parseInt(m[2], 10), command });
|
|
33216
|
+
}
|
|
33217
|
+
return out;
|
|
33218
|
+
} catch {
|
|
33219
|
+
return [];
|
|
33220
|
+
}
|
|
33221
|
+
}
|
|
33222
|
+
function killOne(pid, log3, label, command) {
|
|
33223
|
+
log3(`[reap] ${label} pid=${pid} cmd=${command.slice(0, 120)}`);
|
|
33224
|
+
tryKill(pid, "SIGTERM");
|
|
33225
|
+
setTimeout(() => {
|
|
33226
|
+
if (isPidAlive(pid))
|
|
33227
|
+
tryKill(pid, "SIGKILL");
|
|
33228
|
+
}, 1500).unref?.();
|
|
33229
|
+
}
|
|
33199
33230
|
function reapStaleAdapters(log3 = () => {}) {
|
|
33200
33231
|
ensureDir();
|
|
33201
33232
|
let entries2 = [];
|
|
@@ -33247,8 +33278,37 @@ function reapStaleAdapters(log3 = () => {}) {
|
|
|
33247
33278
|
} catch {}
|
|
33248
33279
|
reaped++;
|
|
33249
33280
|
}
|
|
33281
|
+
const accountedFor = new Set;
|
|
33282
|
+
try {
|
|
33283
|
+
for (const name of readdirSync2(ADAPTERS_DIR)) {
|
|
33284
|
+
const m = name.match(/^(\d+)\.json$/);
|
|
33285
|
+
if (m)
|
|
33286
|
+
accountedFor.add(parseInt(m[1], 10));
|
|
33287
|
+
}
|
|
33288
|
+
} catch {}
|
|
33289
|
+
for (const p of listAdapterProcs()) {
|
|
33290
|
+
if (p.ppid !== 1)
|
|
33291
|
+
continue;
|
|
33292
|
+
if (accountedFor.has(p.pid))
|
|
33293
|
+
continue;
|
|
33294
|
+
killOne(p.pid, log3, "killing pidfile-less orphan", p.command);
|
|
33295
|
+
reaped++;
|
|
33296
|
+
}
|
|
33250
33297
|
return reaped;
|
|
33251
33298
|
}
|
|
33299
|
+
function killDaemonChildren(daemonPid, log3 = () => {}) {
|
|
33300
|
+
let killed = 0;
|
|
33301
|
+
for (const p of listAdapterProcs()) {
|
|
33302
|
+
if (p.ppid !== daemonPid)
|
|
33303
|
+
continue;
|
|
33304
|
+
killOne(p.pid, log3, "daemon-exit killing", p.command);
|
|
33305
|
+
try {
|
|
33306
|
+
unlinkSync3(pidfilePath(p.pid));
|
|
33307
|
+
} catch {}
|
|
33308
|
+
killed++;
|
|
33309
|
+
}
|
|
33310
|
+
return killed;
|
|
33311
|
+
}
|
|
33252
33312
|
var ADAPTERS_DIR;
|
|
33253
33313
|
var init_adapter_pidfile = __esm(() => {
|
|
33254
33314
|
ADAPTERS_DIR = join7(homedir(), ".multi", "adapters");
|
|
@@ -33456,8 +33516,20 @@ async function runAcp(opts) {
|
|
|
33456
33516
|
return { stopReason, sessionId: activeSessionId, summaryText };
|
|
33457
33517
|
} finally {
|
|
33458
33518
|
try {
|
|
33459
|
-
child.kill();
|
|
33519
|
+
child.kill("SIGTERM");
|
|
33460
33520
|
} catch {}
|
|
33521
|
+
const killTimer = setTimeout(() => {
|
|
33522
|
+
try {
|
|
33523
|
+
child.kill("SIGKILL");
|
|
33524
|
+
} catch {}
|
|
33525
|
+
}, 3000);
|
|
33526
|
+
try {
|
|
33527
|
+
killTimer.unref?.();
|
|
33528
|
+
} catch {}
|
|
33529
|
+
try {
|
|
33530
|
+
await child.exited;
|
|
33531
|
+
} catch {}
|
|
33532
|
+
clearTimeout(killTimer);
|
|
33461
33533
|
if (typeof child.pid === "number")
|
|
33462
33534
|
removeAdapterPidfile(child.pid);
|
|
33463
33535
|
}
|
|
@@ -34148,12 +34220,12 @@ function parsePlanBlocks(text) {
|
|
|
34148
34220
|
}
|
|
34149
34221
|
return { actions, errors: errors3 };
|
|
34150
34222
|
}
|
|
34151
|
-
var PLAN_SCHEMA_VERSION =
|
|
34223
|
+
var PLAN_SCHEMA_VERSION = 10, Priority, AssigneeType, IssueStatus, SessionRole, SkillFile, EvalPolicy, PlanActionSchema, PlanEnvelopeSchema, UiBlockSchema, UI_FENCE_RE, FENCE_RE;
|
|
34152
34224
|
var init_plans = __esm(() => {
|
|
34153
34225
|
init_zod();
|
|
34154
34226
|
Priority = exports_external.enum(["low", "medium", "high"]);
|
|
34155
34227
|
AssigneeType = exports_external.enum(["human", "agent"]);
|
|
34156
|
-
IssueStatus = exports_external.enum(["todo", "in_progress", "blocked", "done", "archived", "failed", "stopped"
|
|
34228
|
+
IssueStatus = exports_external.enum(["todo", "in_progress", "blocked", "done", "archived", "failed", "stopped"]);
|
|
34157
34229
|
SessionRole = exports_external.enum(["implementer", "reviewer", "test-fixer"]);
|
|
34158
34230
|
SkillFile = exports_external.object({ path: exports_external.string().min(1), content: exports_external.string() });
|
|
34159
34231
|
EvalPolicy = exports_external.object({
|
|
@@ -34193,6 +34265,14 @@ var init_plans = __esm(() => {
|
|
|
34193
34265
|
id: exports_external.string().min(1),
|
|
34194
34266
|
assignee_id: exports_external.string().min(1)
|
|
34195
34267
|
}),
|
|
34268
|
+
exports_external.object({
|
|
34269
|
+
type: exports_external.literal("handoff"),
|
|
34270
|
+
target_agent_id: exports_external.string().min(1),
|
|
34271
|
+
prompt: exports_external.string().min(1).max(8000),
|
|
34272
|
+
expect: exports_external.enum(["summary", "patch"]).optional(),
|
|
34273
|
+
return_to: exports_external.string().min(1).optional(),
|
|
34274
|
+
title: exports_external.string().min(1).max(200).optional()
|
|
34275
|
+
}),
|
|
34196
34276
|
exports_external.object({
|
|
34197
34277
|
type: exports_external.literal("issue.comment"),
|
|
34198
34278
|
id: exports_external.string().min(1),
|
|
@@ -34222,6 +34302,18 @@ var init_plans = __esm(() => {
|
|
|
34222
34302
|
query: exports_external.string().min(1).max(500),
|
|
34223
34303
|
limit: exports_external.number().int().min(1).max(100).optional()
|
|
34224
34304
|
}),
|
|
34305
|
+
exports_external.object({
|
|
34306
|
+
type: exports_external.literal("issue.get"),
|
|
34307
|
+
id: exports_external.string().min(1)
|
|
34308
|
+
}),
|
|
34309
|
+
exports_external.object({
|
|
34310
|
+
type: exports_external.literal("issue.start"),
|
|
34311
|
+
id: exports_external.string().min(1)
|
|
34312
|
+
}),
|
|
34313
|
+
exports_external.object({
|
|
34314
|
+
type: exports_external.literal("issue.stop"),
|
|
34315
|
+
id: exports_external.string().min(1)
|
|
34316
|
+
}),
|
|
34225
34317
|
exports_external.object({
|
|
34226
34318
|
type: exports_external.literal("agent.create"),
|
|
34227
34319
|
name: exports_external.string().min(1).max(120),
|
|
@@ -34268,6 +34360,20 @@ var init_plans = __esm(() => {
|
|
|
34268
34360
|
score: exports_external.number().min(0).max(1),
|
|
34269
34361
|
feedback: exports_external.string().min(1).max(8000),
|
|
34270
34362
|
scores: exports_external.record(exports_external.string(), exports_external.number().min(0).max(1)).optional()
|
|
34363
|
+
}),
|
|
34364
|
+
exports_external.object({
|
|
34365
|
+
type: exports_external.literal("memory.search"),
|
|
34366
|
+
project_id: exports_external.string().optional(),
|
|
34367
|
+
query: exports_external.string().min(1).max(1000),
|
|
34368
|
+
limit: exports_external.number().int().min(1).max(50).optional()
|
|
34369
|
+
}),
|
|
34370
|
+
exports_external.object({
|
|
34371
|
+
type: exports_external.literal("memory.write"),
|
|
34372
|
+
project_id: exports_external.string().optional(),
|
|
34373
|
+
text: exports_external.string().min(1).max(20000),
|
|
34374
|
+
summary: exports_external.string().max(2000).optional(),
|
|
34375
|
+
kind: exports_external.string().min(1).max(64).optional(),
|
|
34376
|
+
source_id: exports_external.string().min(1).max(256).optional()
|
|
34271
34377
|
})
|
|
34272
34378
|
]);
|
|
34273
34379
|
PlanEnvelopeSchema = exports_external.object({
|
|
@@ -34345,13 +34451,13 @@ var init_chat = __esm(() => {
|
|
|
34345
34451
|
title: exports_external.string().min(1).max(200),
|
|
34346
34452
|
primary_agent_id: exports_external.string().nullable().optional(),
|
|
34347
34453
|
device_id: exports_external.string().nullable().optional(),
|
|
34348
|
-
runtime: exports_external.string().nullable().optional()
|
|
34454
|
+
runtime: exports_external.string().nullable().optional(),
|
|
34455
|
+
working_dir: exports_external.string().nullable().optional(),
|
|
34456
|
+
branch: exports_external.string().nullable().optional()
|
|
34349
34457
|
});
|
|
34350
34458
|
UpdateChatBodySchema = exports_external.object({
|
|
34351
34459
|
title: exports_external.string().min(1).max(200).optional(),
|
|
34352
|
-
primary_agent_id: exports_external.string().nullable().optional()
|
|
34353
|
-
device_id: exports_external.string().nullable().optional(),
|
|
34354
|
-
runtime: exports_external.string().nullable().optional()
|
|
34460
|
+
primary_agent_id: exports_external.string().nullable().optional()
|
|
34355
34461
|
});
|
|
34356
34462
|
});
|
|
34357
34463
|
// ../lib/index.ts
|
|
@@ -34665,14 +34771,28 @@ function log3(msg) {
|
|
|
34665
34771
|
appendFileSync4(LOG_PATH3, line);
|
|
34666
34772
|
process.stdout.write(line);
|
|
34667
34773
|
}
|
|
34668
|
-
function nestedIssueBase(apiUrl, wsId, projectId, issueId) {
|
|
34669
|
-
return `${apiUrl}/api/workspaces/${wsId}/projects/${projectId}/issues/${issueId}`;
|
|
34670
|
-
}
|
|
34671
34774
|
async function patchIssueStatus(apiUrl, wsId, issueId, status3) {
|
|
34775
|
+
if (status3 === "todo") {
|
|
34776
|
+
throw new Error("patchIssueStatus: cannot return an issue to todo (Phase 4 one-way invariant)");
|
|
34777
|
+
}
|
|
34672
34778
|
if (!wsId)
|
|
34673
34779
|
return { success: false, error: "no workspace id" };
|
|
34674
34780
|
return apiClient.post(`${apiUrl}/api/workspaces/${wsId}/agent/issues/mutate`, { action: "update", id: issueId, status: status3 });
|
|
34675
34781
|
}
|
|
34782
|
+
async function postBoundChatMessage(apiUrl, task, body, authorKind = "agent") {
|
|
34783
|
+
const wsId = task?.tenant_workspace_id ?? null;
|
|
34784
|
+
if (!wsId)
|
|
34785
|
+
return;
|
|
34786
|
+
try {
|
|
34787
|
+
await apiClient.post(`${apiUrl}/api/workspaces/${wsId}/agent/issues/mutate`, {
|
|
34788
|
+
action: "comment",
|
|
34789
|
+
id: task.issue_id,
|
|
34790
|
+
body
|
|
34791
|
+
});
|
|
34792
|
+
} catch (e) {
|
|
34793
|
+
log3(`[bound-chat] post failed: ${String(e).slice(0, 200)}`);
|
|
34794
|
+
}
|
|
34795
|
+
}
|
|
34676
34796
|
async function markStopped(apiUrl, task, reason) {
|
|
34677
34797
|
const wsId = task?.tenant_workspace_id ?? null;
|
|
34678
34798
|
const pid = task?.project_id ?? null;
|
|
@@ -34682,14 +34802,7 @@ async function markStopped(apiUrl, task, reason) {
|
|
|
34682
34802
|
await patchIssueStatus(apiUrl, wsId, issueId, "stopped");
|
|
34683
34803
|
} catch {}
|
|
34684
34804
|
try {
|
|
34685
|
-
|
|
34686
|
-
await apiClient.post(`${nestedIssueBase(apiUrl, wsId, pid, issueId)}/comments`, {
|
|
34687
|
-
author_type: "agent",
|
|
34688
|
-
author_id: "daemon",
|
|
34689
|
-
author_name: "daemon",
|
|
34690
|
-
body: `⏹ Stopped: ${reason}`
|
|
34691
|
-
});
|
|
34692
|
-
}
|
|
34805
|
+
await postBoundChatMessage(apiUrl, task, `⏹ Stopped: ${reason}`);
|
|
34693
34806
|
} catch {}
|
|
34694
34807
|
await postStream(apiUrl, issueId, "stopped", { reason });
|
|
34695
34808
|
}
|
|
@@ -34803,48 +34916,18 @@ async function handleRunTask(apiUrl, deviceId, task, detected, ctx) {
|
|
|
34803
34916
|
if (task.from_comment_id && task.tenant_workspace_id && task.project_id) {
|
|
34804
34917
|
const baseDir = workingDir || join12(MULTI_DIR4, "tmp", issueId);
|
|
34805
34918
|
const inDir = join12(baseDir, ".multi-in", task.from_comment_id);
|
|
34806
|
-
|
|
34807
|
-
|
|
34808
|
-
|
|
34919
|
+
try {
|
|
34920
|
+
attachmentRefs = await downloadCommentAttachments(apiUrl, task.tenant_workspace_id, task.project_id, issueId, task.from_comment_id, inDir);
|
|
34921
|
+
if (attachmentRefs.length)
|
|
34922
|
+
log3(` fetched ${attachmentRefs.length} attachment(s) → ${inDir}`);
|
|
34923
|
+
} catch (e) {
|
|
34924
|
+
log3(`[from_comment_id] attachment fetch failed — comments table may be gone (Phase 6): ${String(e).slice(0, 200)}`);
|
|
34925
|
+
}
|
|
34809
34926
|
}
|
|
34810
34927
|
const outDir = join12(workingDir || join12(MULTI_DIR4, "tmp", issueId), ".multi-out");
|
|
34811
|
-
let liveCommentId;
|
|
34812
|
-
let liveBody = "";
|
|
34813
34928
|
let hadError = false;
|
|
34814
34929
|
let hasAssistantText = false;
|
|
34815
|
-
let liveCommentPromise = null;
|
|
34816
34930
|
let memorySummary = null;
|
|
34817
|
-
const ensureLiveComment = () => {
|
|
34818
|
-
if (liveCommentId)
|
|
34819
|
-
return Promise.resolve();
|
|
34820
|
-
if (!ISSUE_BASE)
|
|
34821
|
-
return Promise.resolve();
|
|
34822
|
-
if (!liveCommentPromise) {
|
|
34823
|
-
liveCommentPromise = apiClient.post(`${ISSUE_BASE}/comments`, {
|
|
34824
|
-
author_type: "agent",
|
|
34825
|
-
author_id: task.agent_id,
|
|
34826
|
-
author_name: "agent",
|
|
34827
|
-
body: "…"
|
|
34828
|
-
}).then((res) => {
|
|
34829
|
-
liveCommentId = res.data?.id;
|
|
34830
|
-
});
|
|
34831
|
-
}
|
|
34832
|
-
return liveCommentPromise;
|
|
34833
|
-
};
|
|
34834
|
-
const patchLive = async (body) => {
|
|
34835
|
-
if (!liveCommentId || !ISSUE_BASE)
|
|
34836
|
-
return;
|
|
34837
|
-
try {
|
|
34838
|
-
await apiClient.patch(`${ISSUE_BASE}/comments/${liveCommentId}`, { body: body || "…" });
|
|
34839
|
-
} catch {}
|
|
34840
|
-
};
|
|
34841
|
-
const postComment = async (body) => {
|
|
34842
|
-
if (!ISSUE_BASE)
|
|
34843
|
-
return;
|
|
34844
|
-
try {
|
|
34845
|
-
await apiClient.post(`${ISSUE_BASE}/comments`, { author_type: "agent", author_id: task.agent_id, author_name: "agent", body });
|
|
34846
|
-
} catch {}
|
|
34847
|
-
};
|
|
34848
34931
|
const turn = {
|
|
34849
34932
|
blocks: [],
|
|
34850
34933
|
tools: new Map,
|
|
@@ -34947,18 +35030,7 @@ _${bits.join(" · ")}_`);
|
|
|
34947
35030
|
|
|
34948
35031
|
`) || "…";
|
|
34949
35032
|
};
|
|
34950
|
-
|
|
34951
|
-
const schedulePatch = () => {
|
|
34952
|
-
if (renderScheduled)
|
|
34953
|
-
return;
|
|
34954
|
-
renderScheduled = true;
|
|
34955
|
-
queueMicrotask(async () => {
|
|
34956
|
-
renderScheduled = false;
|
|
34957
|
-
try {
|
|
34958
|
-
await patchLive(render2());
|
|
34959
|
-
} catch {}
|
|
34960
|
-
});
|
|
34961
|
-
};
|
|
35033
|
+
const schedulePatch = () => {};
|
|
34962
35034
|
const eventHandler = async (event) => {
|
|
34963
35035
|
if (event.event_type === "error")
|
|
34964
35036
|
hadError = true;
|
|
@@ -34971,7 +35043,6 @@ _${bits.join(" · ")}_`);
|
|
|
34971
35043
|
break;
|
|
34972
35044
|
}
|
|
34973
35045
|
case "assistant_text": {
|
|
34974
|
-
await ensureLiveComment();
|
|
34975
35046
|
appendText(p.text);
|
|
34976
35047
|
hasAssistantText = true;
|
|
34977
35048
|
schedulePatch();
|
|
@@ -34979,7 +35050,6 @@ _${bits.join(" · ")}_`);
|
|
|
34979
35050
|
}
|
|
34980
35051
|
case "stdout": {
|
|
34981
35052
|
if (p.line) {
|
|
34982
|
-
await ensureLiveComment();
|
|
34983
35053
|
appendText((turn.blocks.length ? `
|
|
34984
35054
|
` : "") + p.line);
|
|
34985
35055
|
hasAssistantText = true;
|
|
@@ -34988,14 +35058,12 @@ _${bits.join(" · ")}_`);
|
|
|
34988
35058
|
break;
|
|
34989
35059
|
}
|
|
34990
35060
|
case "tool_call": {
|
|
34991
|
-
await ensureLiveComment();
|
|
34992
35061
|
const id3 = p.id || `anon-${turn.tools.size}`;
|
|
34993
35062
|
upsertTool(id3, { tool: p.tool, kind: p.kind, status: p.status, input: p.input });
|
|
34994
35063
|
schedulePatch();
|
|
34995
35064
|
break;
|
|
34996
35065
|
}
|
|
34997
35066
|
case "tool_result": {
|
|
34998
|
-
await ensureLiveComment();
|
|
34999
35067
|
const lastToolId = [...turn.blocks].reverse().find((b) => b.kind === "tool")?.id;
|
|
35000
35068
|
const id3 = p.tool_use_id || lastToolId;
|
|
35001
35069
|
const entry = id3 ? turn.tools.get(id3) : undefined;
|
|
@@ -35012,7 +35080,6 @@ _${bits.join(" · ")}_`);
|
|
|
35012
35080
|
break;
|
|
35013
35081
|
}
|
|
35014
35082
|
case "result": {
|
|
35015
|
-
await ensureLiveComment();
|
|
35016
35083
|
if (p.is_error)
|
|
35017
35084
|
hadError = true;
|
|
35018
35085
|
if (!hasAssistantText && p.result) {
|
|
@@ -35024,7 +35091,6 @@ _${bits.join(" · ")}_`);
|
|
|
35024
35091
|
break;
|
|
35025
35092
|
}
|
|
35026
35093
|
case "error": {
|
|
35027
|
-
await ensureLiveComment();
|
|
35028
35094
|
turn.error = p.message || "error";
|
|
35029
35095
|
schedulePatch();
|
|
35030
35096
|
break;
|
|
@@ -35251,10 +35317,33 @@ ${userPart}` : userPart;
|
|
|
35251
35317
|
for await (const event of runner(task))
|
|
35252
35318
|
await eventHandler(event);
|
|
35253
35319
|
}
|
|
35254
|
-
|
|
35255
|
-
const
|
|
35256
|
-
|
|
35257
|
-
|
|
35320
|
+
const finalBody = (() => {
|
|
35321
|
+
const parts2 = [];
|
|
35322
|
+
for (const b of turn.blocks) {
|
|
35323
|
+
if (b.kind === "text") {
|
|
35324
|
+
if (b.text)
|
|
35325
|
+
parts2.push(b.text);
|
|
35326
|
+
} else if (b.kind === "tool") {
|
|
35327
|
+
const t = turn.tools.get(b.id);
|
|
35328
|
+
if (t)
|
|
35329
|
+
parts2.push(`- \uD83D\uDD27 ${t.tool}${t.status === "completed" ? " ✓" : t.status === "failed" ? " ✗" : ""}`);
|
|
35330
|
+
}
|
|
35331
|
+
}
|
|
35332
|
+
return parts2.join(`
|
|
35333
|
+
`).trim();
|
|
35334
|
+
})();
|
|
35335
|
+
if (finalBody)
|
|
35336
|
+
await postBoundChatMessage(apiUrl, task, finalBody);
|
|
35337
|
+
if (existsSync12(outDir)) {
|
|
35338
|
+
const files = readdirSync3(outDir).filter((f) => {
|
|
35339
|
+
try {
|
|
35340
|
+
return statSync2(join12(outDir, f)).isFile();
|
|
35341
|
+
} catch {
|
|
35342
|
+
return false;
|
|
35343
|
+
}
|
|
35344
|
+
});
|
|
35345
|
+
if (files.length > 0)
|
|
35346
|
+
log3(` [Phase 4.5] agent wrote ${files.length} file(s) to .multi-out; attachments via chat upload not wired in this path`);
|
|
35258
35347
|
}
|
|
35259
35348
|
if (ctx) {
|
|
35260
35349
|
const fullText = turn.blocks.filter((b) => b.kind === "text").map((b) => b.text).join(`
|
|
@@ -35264,15 +35353,14 @@ ${userPart}` : userPart;
|
|
|
35264
35353
|
const summary5 = await executePlanActions(apiUrl, task, actions, ctx, parseErrors);
|
|
35265
35354
|
if (summary5) {
|
|
35266
35355
|
try {
|
|
35267
|
-
|
|
35268
|
-
await apiClient.post(`${ISSUE_BASE}/comments`, { author_type: "agent", author_id: task.agent_id, author_name: "agent", body: summary5 });
|
|
35356
|
+
await postBoundChatMessage(apiUrl, task, summary5);
|
|
35269
35357
|
} catch {}
|
|
35270
35358
|
}
|
|
35271
35359
|
}
|
|
35272
35360
|
}
|
|
35273
35361
|
if (!hasAssistantText && !hadError) {
|
|
35274
35362
|
const stopReason = turn.result?.stopReason || "unknown";
|
|
35275
|
-
await
|
|
35363
|
+
await postBoundChatMessage(apiUrl, task, `⚠️ Agent returned no output (stopReason=${stopReason}). Adapter may be stuck on a stale session — try starting a new issue or clearing session_id.`);
|
|
35276
35364
|
log3(` ⚠ ${task.key} produced no assistant output (stopReason=${stopReason})`);
|
|
35277
35365
|
}
|
|
35278
35366
|
if (ctx?.runEntry?.stopped) {
|
|
@@ -35305,7 +35393,7 @@ ${userPart}` : userPart;
|
|
|
35305
35393
|
log3(` ⏹ ${task.key} stopped (${msg})`);
|
|
35306
35394
|
} else {
|
|
35307
35395
|
await postStream(apiUrl, issueId, "error", { message: msg });
|
|
35308
|
-
await
|
|
35396
|
+
await postBoundChatMessage(apiUrl, task, `❌ spawn error: ${msg}`);
|
|
35309
35397
|
if (ISSUE_BASE)
|
|
35310
35398
|
await apiClient.post(`${ISSUE_BASE}/fail`, {});
|
|
35311
35399
|
log3(` ✗ ${task.key} failed: ${msg}`);
|
|
@@ -35366,15 +35454,24 @@ Issue actions:
|
|
|
35366
35454
|
{"type":"issue.delete","id":"<issue id or key>"},
|
|
35367
35455
|
{"type":"issue.delete_where","status":"todo"},
|
|
35368
35456
|
{"type":"issue.list","status":"todo","assignee_id":"<agent id>","limit":20},
|
|
35369
|
-
{"type":"issue.search","query":"flaky tests","limit":10}
|
|
35457
|
+
{"type":"issue.search","query":"flaky tests","limit":10},
|
|
35458
|
+
{"type":"issue.get","id":"<issue id or key>"},
|
|
35459
|
+
{"type":"issue.start","id":"<issue id or key>"},
|
|
35460
|
+
{"type":"issue.stop","id":"<issue id or key>"},
|
|
35461
|
+
{"type":"memory.search","query":"how does the deploy pipeline work","limit":10},
|
|
35462
|
+
{"type":"memory.write","text":"Long-form note worth remembering across sessions...","summary":"short index hint","kind":"agent_note"}
|
|
35370
35463
|
]}
|
|
35371
35464
|
\`\`\`
|
|
35372
35465
|
|
|
35373
35466
|
Status values: \`todo\` | \`in_progress\` | \`blocked\` | \`done\` | \`archived\` | \`failed\`. **Use \`blocked\` (NOT \`done\`) when you are pausing to wait on a human decision** — confirmation, a choice between approaches, or missing context. Marking such an issue \`done\` is wrong: the work isn't finished, you're waiting on review. Use \`archived\` to hide a completed/abandoned issue from default views.
|
|
35374
35467
|
|
|
35468
|
+
**\`todo\` is a one-way exit.** Once an issue moves to any other status, it cannot return to \`todo\`. Use \`blocked\` to pause an issue that needs human input.
|
|
35469
|
+
|
|
35375
35470
|
Prefer the bulk \`issue.delete_where\` over \`issue.list\` + per-issue \`issue.delete\` when the user's intent matches a filter ("delete all todo issues", "remove anything assigned to agent X"). It runs in one turn instead of two.
|
|
35376
35471
|
|
|
35377
|
-
Read actions (\`issue.list\`, \`issue.search\`) return the matched rows in the action summary
|
|
35472
|
+
Read actions (\`issue.list\`, \`issue.search\`, \`issue.get\`, \`memory.search\`) return the matched rows in the action summary message posted to the issue's bound chat. Use them to look up issue ids/keys before \`update\` / \`delegate\` / \`issue.delete\` ONLY when the per-row filter isn't enough (e.g. you need to inspect titles before acting). Use \`issue.get\` to deep-read a single issue (status, autonomy, assignee, live dispatch, bound chat) before deciding to start/stop it. \`issue.start\` dispatches the assigned agent (no-op if no agent is assigned). \`issue.stop\` gracefully stops the live dispatch; safe to call on a non-running issue.
|
|
35473
|
+
|
|
35474
|
+
Memory actions are project-scoped persistent storage (FTS5 + vector). Use \`memory.search\` to recall facts learned in past sessions; the hits land in the next-turn context like \`issue.search\`. Use \`memory.write\` sparingly to record durable notes (decisions, gotchas, references) that will be useful to future agents on this project — not transient task state. \`kind\` defaults to \`agent_note\`; use a stable kind string (e.g. \`decision\`, \`gotcha\`) if you want to filter later.
|
|
35378
35475
|
|
|
35379
35476
|
Agent + skill self-service (use sparingly — only when you genuinely need a new capability that isn't covered by an existing agent or skill):
|
|
35380
35477
|
|
|
@@ -35424,7 +35521,7 @@ async function executePlanActions(apiUrl, parentTask, actions, ctx, parseErrors
|
|
|
35424
35521
|
results.push({ type: "note", status: "note", message: `${blocked3} non-update action(s) blocked (planning depth limit ${PLANNING_DEPTH_LIMIT})` });
|
|
35425
35522
|
}
|
|
35426
35523
|
}
|
|
35427
|
-
const SUBCAPS = { "agent.create": 2, "skill.create": 3, "skill.attach": 5, "skill.detach": 5, "agent.update": 5, "session.create": 3 };
|
|
35524
|
+
const SUBCAPS = { "agent.create": 2, "skill.create": 3, "skill.attach": 5, "skill.detach": 5, "agent.update": 5, "session.create": 3, "memory.search": 5, "memory.write": 5, "issue.get": 10, "issue.start": 3, "issue.stop": 3 };
|
|
35428
35525
|
const counts = {};
|
|
35429
35526
|
actions = actions.filter((a) => {
|
|
35430
35527
|
const cap = SUBCAPS[a.type];
|
|
@@ -35606,6 +35703,93 @@ async function executePlanActions(apiUrl, parentTask, actions, ctx, parseErrors
|
|
|
35606
35703
|
lines.push(` - **${r.key}** [${r.status}] ${r.title}`);
|
|
35607
35704
|
}
|
|
35608
35705
|
results.push({ type: "issue.search", status: "ok", count: rows.length, query: a.query });
|
|
35706
|
+
} else if (a.type === "issue.get") {
|
|
35707
|
+
const res = await apiClient.post(queryUrl, { kind: "get", id: a.id }, { headers });
|
|
35708
|
+
if (!res.success) {
|
|
35709
|
+
lines.push(`- [err] issue.get ${a.id}: ${res.error || res.status}`);
|
|
35710
|
+
results.push({ type: "issue.get", status: "error", error: String(res.error || res.status), label: a.id });
|
|
35711
|
+
continue;
|
|
35712
|
+
}
|
|
35713
|
+
const r = res.data || {};
|
|
35714
|
+
const dispatchLine = r.live_dispatch ? ` dispatch=${r.live_dispatch.status}` : " dispatch=none";
|
|
35715
|
+
lines.push(`- [ok] issue.get **${r.key}** [${r.status}]${r.assignee_id ? ` @${r.assignee_id}` : ""} autonomy=${r.autonomy_level}${dispatchLine}`);
|
|
35716
|
+
lines.push(` - ${r.title}`);
|
|
35717
|
+
if (r.bound_chat_id)
|
|
35718
|
+
lines.push(` - bound chat: ${r.bound_chat_id}`);
|
|
35719
|
+
if (r.live_dispatch?.last_error)
|
|
35720
|
+
lines.push(` - last error: ${r.live_dispatch.last_error}`);
|
|
35721
|
+
results.push({ type: "issue.get", status: "ok", issue_id: r.id, key: r.key, issue_status: r.status, autonomy_level: r.autonomy_level, assignee_id: r.assignee_id, bound_chat_id: r.bound_chat_id, live_dispatch_status: r.live_dispatch?.status ?? null });
|
|
35722
|
+
} else if (a.type === "issue.start") {
|
|
35723
|
+
const res = await apiClient.post(mutateUrl, { action: "start", id: a.id }, { headers });
|
|
35724
|
+
if (!res.success) {
|
|
35725
|
+
lines.push(`- [err] issue.start ${a.id}: ${res.error || res.status}`);
|
|
35726
|
+
results.push({ type: "issue.start", status: "error", error: String(res.error || res.status), label: a.id });
|
|
35727
|
+
continue;
|
|
35728
|
+
}
|
|
35729
|
+
const r = res.data || {};
|
|
35730
|
+
lines.push(r.dispatched ? `- [ok] issue.start ${r.key || a.id} -> dispatched` : `- [warn] issue.start ${r.key || a.id} not dispatched (autonomy=${r.autonomy_level} or no online device)`);
|
|
35731
|
+
results.push({ type: "issue.start", status: "ok", issue_id: r.id || a.id, key: r.key, dispatched: !!r.dispatched, dispatch_id: r.dispatch_id ?? null, autonomy_level: r.autonomy_level });
|
|
35732
|
+
} else if (a.type === "issue.stop") {
|
|
35733
|
+
const res = await apiClient.post(mutateUrl, { action: "stop", id: a.id }, { headers });
|
|
35734
|
+
if (!res.success) {
|
|
35735
|
+
lines.push(`- [err] issue.stop ${a.id}: ${res.error || res.status}`);
|
|
35736
|
+
results.push({ type: "issue.stop", status: "error", error: String(res.error || res.status), label: a.id });
|
|
35737
|
+
continue;
|
|
35738
|
+
}
|
|
35739
|
+
const r = res.data || {};
|
|
35740
|
+
lines.push(r.was_running ? `- [ok] issue.stop ${r.key || a.id} -> stopped` : `- [ok] issue.stop ${r.key || a.id} (nothing was running)`);
|
|
35741
|
+
results.push({ type: "issue.stop", status: "ok", issue_id: r.id || a.id, key: r.key, was_running: !!r.was_running });
|
|
35742
|
+
} else if (a.type === "memory.search") {
|
|
35743
|
+
const projectId = a.project_id || parentProjectId;
|
|
35744
|
+
if (!projectId) {
|
|
35745
|
+
lines.push(`- [err] memory.search "${a.query}": no project_id`);
|
|
35746
|
+
results.push({ type: "memory.search", status: "error", error: "no project_id", label: a.query });
|
|
35747
|
+
continue;
|
|
35748
|
+
}
|
|
35749
|
+
const limit = a.limit ?? 10;
|
|
35750
|
+
const qs = new URLSearchParams({ project_id: projectId, q: a.query, limit: String(limit) });
|
|
35751
|
+
const res = await apiClient.get(`${apiUrl}/api/memory/search?${qs.toString()}`);
|
|
35752
|
+
if (!res.success) {
|
|
35753
|
+
lines.push(`- [err] memory.search "${a.query}": ${res.error || res.status}`);
|
|
35754
|
+
results.push({ type: "memory.search", status: "error", error: String(res.error || res.status), label: a.query });
|
|
35755
|
+
continue;
|
|
35756
|
+
}
|
|
35757
|
+
const rows = Array.isArray(res.data?.rows) ? res.data.rows : [];
|
|
35758
|
+
if (!rows.length) {
|
|
35759
|
+
lines.push(`- [ok] memory.search "${a.query}": 0 hits`);
|
|
35760
|
+
results.push({ type: "memory.search", status: "ok", count: 0, query: a.query });
|
|
35761
|
+
continue;
|
|
35762
|
+
}
|
|
35763
|
+
lines.push(`- [ok] memory.search "${a.query}": ${rows.length} hit(s)`);
|
|
35764
|
+
for (const r of rows) {
|
|
35765
|
+
const summary5 = (r.summary || r.text || "").replace(/\s+/g, " ").slice(0, 200);
|
|
35766
|
+
lines.push(` - [${r.source_kind || "memory"}] ${summary5}`);
|
|
35767
|
+
}
|
|
35768
|
+
results.push({ type: "memory.search", status: "ok", count: rows.length, query: a.query });
|
|
35769
|
+
} else if (a.type === "memory.write") {
|
|
35770
|
+
const projectId = a.project_id || parentProjectId;
|
|
35771
|
+
if (!projectId) {
|
|
35772
|
+
lines.push(`- [err] memory.write: no project_id`);
|
|
35773
|
+
results.push({ type: "memory.write", status: "error", error: "no project_id" });
|
|
35774
|
+
continue;
|
|
35775
|
+
}
|
|
35776
|
+
const body = {
|
|
35777
|
+
project_id: projectId,
|
|
35778
|
+
source_kind: a.kind || "agent_note",
|
|
35779
|
+
source_id: a.source_id || `${parentTask.agent_id || "agent"}:${parentTask.issue_id || ""}:${Date.now()}`,
|
|
35780
|
+
agent_id: parentTask.agent_id || null,
|
|
35781
|
+
text: a.text,
|
|
35782
|
+
summary: a.summary || null
|
|
35783
|
+
};
|
|
35784
|
+
const res = await apiClient.post(`${apiUrl}/api/memory/write`, body);
|
|
35785
|
+
if (!res.success) {
|
|
35786
|
+
lines.push(`- [err] memory.write: ${res.error || res.status}`);
|
|
35787
|
+
results.push({ type: "memory.write", status: "error", error: String(res.error || res.status) });
|
|
35788
|
+
continue;
|
|
35789
|
+
}
|
|
35790
|
+
const row = res.data?.row || {};
|
|
35791
|
+
lines.push(`- [ok] memory.write -> ${row.id || "(no id)"}${row.embedding_id ? " (embedded)" : ""}`);
|
|
35792
|
+
results.push({ type: "memory.write", status: "ok", memory_id: row.id, embedded: !!row.embedding_id });
|
|
35609
35793
|
} else if (a.type === "agent.create") {
|
|
35610
35794
|
if (!parentWsId) {
|
|
35611
35795
|
lines.push(`- [err] agent.create "${a.name}": no tenant workspace id`);
|
|
@@ -35847,56 +36031,6 @@ function authTokenHeader() {
|
|
|
35847
36031
|
return null;
|
|
35848
36032
|
}
|
|
35849
36033
|
}
|
|
35850
|
-
async function uploadOutputDir(apiUrl, wsId, projectId, issueId, commentId, dir) {
|
|
35851
|
-
if (!existsSync12(dir))
|
|
35852
|
-
return 0;
|
|
35853
|
-
const files = [];
|
|
35854
|
-
const walk = (d, depth = 0) => {
|
|
35855
|
-
if (depth > 3)
|
|
35856
|
-
return;
|
|
35857
|
-
for (const name of readdirSync3(d)) {
|
|
35858
|
-
const p = join12(d, name);
|
|
35859
|
-
try {
|
|
35860
|
-
const st = statSync2(p);
|
|
35861
|
-
if (st.isDirectory())
|
|
35862
|
-
walk(p, depth + 1);
|
|
35863
|
-
else if (st.isFile())
|
|
35864
|
-
files.push(p);
|
|
35865
|
-
} catch {}
|
|
35866
|
-
}
|
|
35867
|
-
};
|
|
35868
|
-
walk(dir);
|
|
35869
|
-
if (!files.length)
|
|
35870
|
-
return 0;
|
|
35871
|
-
const token = authTokenHeader();
|
|
35872
|
-
const issueBase = `${apiUrl}/api/workspaces/${wsId}/projects/${projectId}/issues/${issueId}`;
|
|
35873
|
-
let uploaded = 0;
|
|
35874
|
-
for (const f of files) {
|
|
35875
|
-
try {
|
|
35876
|
-
const data = readFileSync10(f);
|
|
35877
|
-
const form = new FormData;
|
|
35878
|
-
const blob = new Blob([data]);
|
|
35879
|
-
form.append("file", blob, f.split("/").pop() || "file");
|
|
35880
|
-
const headers = {};
|
|
35881
|
-
if (token)
|
|
35882
|
-
headers.Authorization = token;
|
|
35883
|
-
const res = await fetch(`${issueBase}/comments/${commentId}/attachments`, {
|
|
35884
|
-
method: "POST",
|
|
35885
|
-
body: form,
|
|
35886
|
-
headers
|
|
35887
|
-
});
|
|
35888
|
-
if (res.ok) {
|
|
35889
|
-
uploaded++;
|
|
35890
|
-
try {
|
|
35891
|
-
unlinkSync6(f);
|
|
35892
|
-
} catch {}
|
|
35893
|
-
}
|
|
35894
|
-
} catch (e) {
|
|
35895
|
-
log3(`upload failed for ${f}: ${String(e)}`);
|
|
35896
|
-
}
|
|
35897
|
-
}
|
|
35898
|
-
return uploaded;
|
|
35899
|
-
}
|
|
35900
36034
|
function pickRunner(detected, preferType) {
|
|
35901
36035
|
const forceStub = process.env.MULTI_STUB === "1";
|
|
35902
36036
|
if (forceStub || !detected.length)
|
|
@@ -36170,6 +36304,116 @@ var init_run_task = __esm(() => {
|
|
|
36170
36304
|
SKILLS_DIR2 = join12(MULTI_DIR4, "skills");
|
|
36171
36305
|
});
|
|
36172
36306
|
|
|
36307
|
+
// src/_impl/worktree-create.ts
|
|
36308
|
+
var exports_worktree_create = {};
|
|
36309
|
+
__export(exports_worktree_create, {
|
|
36310
|
+
removeWorktree: () => removeWorktree2,
|
|
36311
|
+
materialiseWorktree: () => materialiseWorktree,
|
|
36312
|
+
codename: () => codename
|
|
36313
|
+
});
|
|
36314
|
+
import { homedir as homedir3 } from "os";
|
|
36315
|
+
import { existsSync as existsSync13 } from "fs";
|
|
36316
|
+
import { join as join13 } from "path";
|
|
36317
|
+
function codename() {
|
|
36318
|
+
const c = CITY_CODENAMES[Math.floor(Math.random() * CITY_CODENAMES.length)];
|
|
36319
|
+
const suffix = Math.random().toString(36).slice(2, 6);
|
|
36320
|
+
return `${c}-${suffix}`;
|
|
36321
|
+
}
|
|
36322
|
+
async function run5(cmd, cwd) {
|
|
36323
|
+
const proc = Bun.spawn(cmd, { cwd, stdout: "pipe", stderr: "pipe" });
|
|
36324
|
+
const [stdout, stderr] = await Promise.all([
|
|
36325
|
+
new Response(proc.stdout).text(),
|
|
36326
|
+
new Response(proc.stderr).text()
|
|
36327
|
+
]);
|
|
36328
|
+
await proc.exited;
|
|
36329
|
+
return { ok: proc.exitCode === 0, stdout: stdout.trim(), stderr: stderr.trim() };
|
|
36330
|
+
}
|
|
36331
|
+
async function materialiseWorktree(opts) {
|
|
36332
|
+
const { baseDir, name, log: log4 } = opts;
|
|
36333
|
+
if (!existsSync13(baseDir)) {
|
|
36334
|
+
throw new Error(`base_dir does not exist: ${baseDir}`);
|
|
36335
|
+
}
|
|
36336
|
+
const top = await run5(["git", "rev-parse", "--show-toplevel"], baseDir);
|
|
36337
|
+
if (!top.ok || !top.stdout) {
|
|
36338
|
+
throw new Error(`base_dir is not in a git repo: ${baseDir}`);
|
|
36339
|
+
}
|
|
36340
|
+
const repoRoot = top.stdout;
|
|
36341
|
+
let baseBranch = opts.baseBranch;
|
|
36342
|
+
if (!baseBranch) {
|
|
36343
|
+
const head5 = await run5(["git", "rev-parse", "--abbrev-ref", "HEAD"], repoRoot);
|
|
36344
|
+
baseBranch = head5.ok ? head5.stdout : "HEAD";
|
|
36345
|
+
}
|
|
36346
|
+
const worktreesDir = join13(homedir3(), ".multi", "worktrees");
|
|
36347
|
+
const path = join13(worktreesDir, name);
|
|
36348
|
+
if (existsSync13(path)) {
|
|
36349
|
+
throw new Error(`worktree path already exists: ${path}`);
|
|
36350
|
+
}
|
|
36351
|
+
const branch = `multi/${name}`;
|
|
36352
|
+
log4(`worktree: creating ${path} (branch ${branch} off ${baseBranch})`);
|
|
36353
|
+
const create = await run5(["git", "worktree", "add", "-b", branch, path, baseBranch], repoRoot);
|
|
36354
|
+
if (!create.ok) {
|
|
36355
|
+
throw new Error(`git worktree add failed: ${create.stderr || create.stdout}`);
|
|
36356
|
+
}
|
|
36357
|
+
return { path, branch, base_branch: baseBranch, name };
|
|
36358
|
+
}
|
|
36359
|
+
async function removeWorktree2(path, log4) {
|
|
36360
|
+
if (!existsSync13(path)) {
|
|
36361
|
+
return { ok: true };
|
|
36362
|
+
}
|
|
36363
|
+
const r = await run5(["git", "worktree", "remove", "--force", path], path);
|
|
36364
|
+
if (!r.ok) {
|
|
36365
|
+
log4(`worktree remove failed: ${r.stderr || r.stdout}`);
|
|
36366
|
+
return { ok: false, error: r.stderr || r.stdout };
|
|
36367
|
+
}
|
|
36368
|
+
return { ok: true };
|
|
36369
|
+
}
|
|
36370
|
+
var CITY_CODENAMES;
|
|
36371
|
+
var init_worktree_create = __esm(() => {
|
|
36372
|
+
CITY_CODENAMES = [
|
|
36373
|
+
"raleigh",
|
|
36374
|
+
"yokohama",
|
|
36375
|
+
"sydney",
|
|
36376
|
+
"porto",
|
|
36377
|
+
"kyoto",
|
|
36378
|
+
"bristol",
|
|
36379
|
+
"oslo",
|
|
36380
|
+
"havana",
|
|
36381
|
+
"lisbon",
|
|
36382
|
+
"tallinn",
|
|
36383
|
+
"salem",
|
|
36384
|
+
"marrakech",
|
|
36385
|
+
"perth",
|
|
36386
|
+
"boise",
|
|
36387
|
+
"nantes",
|
|
36388
|
+
"munich",
|
|
36389
|
+
"asheville",
|
|
36390
|
+
"stockholm",
|
|
36391
|
+
"halifax",
|
|
36392
|
+
"kolkata",
|
|
36393
|
+
"tucson",
|
|
36394
|
+
"valencia",
|
|
36395
|
+
"kazan",
|
|
36396
|
+
"savannah",
|
|
36397
|
+
"lyon",
|
|
36398
|
+
"iquitos",
|
|
36399
|
+
"wuhan",
|
|
36400
|
+
"leeds",
|
|
36401
|
+
"ankara",
|
|
36402
|
+
"auckland",
|
|
36403
|
+
"geneva",
|
|
36404
|
+
"izmir",
|
|
36405
|
+
"anchorage",
|
|
36406
|
+
"split",
|
|
36407
|
+
"kigali",
|
|
36408
|
+
"miami",
|
|
36409
|
+
"kazan",
|
|
36410
|
+
"krakow",
|
|
36411
|
+
"bilbao",
|
|
36412
|
+
"calgary",
|
|
36413
|
+
"dakar"
|
|
36414
|
+
];
|
|
36415
|
+
});
|
|
36416
|
+
|
|
36173
36417
|
// ../lib/chat-doc.ts
|
|
36174
36418
|
import { LoroDoc, LoroMap, LoroList } from "loro-crdt";
|
|
36175
36419
|
function appendMessage(doc2, msg) {
|
|
@@ -36259,8 +36503,8 @@ var init_chat_doc = __esm(() => {
|
|
|
36259
36503
|
});
|
|
36260
36504
|
|
|
36261
36505
|
// src/_impl/chat-peer.ts
|
|
36262
|
-
import { existsSync as
|
|
36263
|
-
import { dirname as dirname10, join as
|
|
36506
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync10, readFileSync as readFileSync11, writeFileSync as writeFileSync8 } from "fs";
|
|
36507
|
+
import { dirname as dirname10, join as join14 } from "path";
|
|
36264
36508
|
import { LoroDoc as LoroDoc2 } from "loro-crdt";
|
|
36265
36509
|
|
|
36266
36510
|
class ChatPeer {
|
|
@@ -36272,19 +36516,20 @@ class ChatPeer {
|
|
|
36272
36516
|
dirtySinceWrite = 0;
|
|
36273
36517
|
seenIds = new Set;
|
|
36274
36518
|
closed = false;
|
|
36519
|
+
localSubs = new Map;
|
|
36275
36520
|
firstFrameSinceConnect = true;
|
|
36276
36521
|
reconnectTimer = null;
|
|
36277
36522
|
constructor(opts) {
|
|
36278
36523
|
this.opts = opts;
|
|
36279
36524
|
this.chatId = opts.chatId;
|
|
36280
|
-
this.snapshotPath =
|
|
36525
|
+
this.snapshotPath = join14(MULTI_DIR, "chats", `${opts.chatId}.loro`);
|
|
36281
36526
|
this.doc = this.loadFromDisk();
|
|
36282
36527
|
for (const m of listMessages(this.doc))
|
|
36283
36528
|
this.seenIds.add(m.id);
|
|
36284
36529
|
}
|
|
36285
36530
|
loadFromDisk() {
|
|
36286
36531
|
try {
|
|
36287
|
-
if (
|
|
36532
|
+
if (existsSync14(this.snapshotPath)) {
|
|
36288
36533
|
const bytes = readFileSync11(this.snapshotPath);
|
|
36289
36534
|
return importSnapshot(new Uint8Array(bytes));
|
|
36290
36535
|
}
|
|
@@ -36292,7 +36537,7 @@ class ChatPeer {
|
|
|
36292
36537
|
return new LoroDoc2;
|
|
36293
36538
|
}
|
|
36294
36539
|
persist() {
|
|
36295
|
-
if (!
|
|
36540
|
+
if (!existsSync14(dirname10(this.snapshotPath)))
|
|
36296
36541
|
mkdirSync10(dirname10(this.snapshotPath), { recursive: true });
|
|
36297
36542
|
writeFileSync8(this.snapshotPath, exportSnapshot(this.doc));
|
|
36298
36543
|
this.dirtySinceWrite = 0;
|
|
@@ -36448,14 +36693,70 @@ class ChatPeer {
|
|
|
36448
36693
|
}, 3000);
|
|
36449
36694
|
}
|
|
36450
36695
|
sendLoro(update5) {
|
|
36451
|
-
if (!this.ws || this.ws.readyState !== 1)
|
|
36452
|
-
return;
|
|
36453
36696
|
const frame = new Uint8Array(1 + update5.byteLength);
|
|
36454
36697
|
frame[0] = FRAME_LORO;
|
|
36455
36698
|
frame.set(update5, 1);
|
|
36699
|
+
if (this.ws && this.ws.readyState === 1) {
|
|
36700
|
+
try {
|
|
36701
|
+
this.ws.send(frame);
|
|
36702
|
+
} catch {}
|
|
36703
|
+
}
|
|
36704
|
+
this.broadcastLocal(frame, null);
|
|
36705
|
+
}
|
|
36706
|
+
broadcastLocal(frame, except) {
|
|
36707
|
+
for (const [token, send] of this.localSubs) {
|
|
36708
|
+
if (token === except)
|
|
36709
|
+
continue;
|
|
36710
|
+
try {
|
|
36711
|
+
send(frame);
|
|
36712
|
+
} catch {}
|
|
36713
|
+
}
|
|
36714
|
+
}
|
|
36715
|
+
addLocalSubscriber(send) {
|
|
36716
|
+
const token = Symbol("local-chat-sub");
|
|
36717
|
+
this.localSubs.set(token, send);
|
|
36718
|
+
return {
|
|
36719
|
+
token,
|
|
36720
|
+
close: () => {
|
|
36721
|
+
this.localSubs.delete(token);
|
|
36722
|
+
}
|
|
36723
|
+
};
|
|
36724
|
+
}
|
|
36725
|
+
buildSnapshotGreet() {
|
|
36726
|
+
const snap = exportSnapshot(this.doc);
|
|
36727
|
+
const frame = new Uint8Array(1 + snap.byteLength);
|
|
36728
|
+
frame[0] = FRAME_LORO;
|
|
36729
|
+
frame.set(snap, 1);
|
|
36730
|
+
return frame;
|
|
36731
|
+
}
|
|
36732
|
+
ingestLocalUpdate(update5, fromToken) {
|
|
36733
|
+
const before2 = new Set(this.seenIds);
|
|
36456
36734
|
try {
|
|
36457
|
-
this.
|
|
36458
|
-
} catch {
|
|
36735
|
+
applyUpdate(this.doc, update5);
|
|
36736
|
+
} catch {
|
|
36737
|
+
return;
|
|
36738
|
+
}
|
|
36739
|
+
const frame = new Uint8Array(1 + update5.byteLength);
|
|
36740
|
+
frame[0] = FRAME_LORO;
|
|
36741
|
+
frame.set(update5, 1);
|
|
36742
|
+
if (this.ws && this.ws.readyState === 1) {
|
|
36743
|
+
try {
|
|
36744
|
+
this.ws.send(frame);
|
|
36745
|
+
} catch {}
|
|
36746
|
+
}
|
|
36747
|
+
this.broadcastLocal(frame, fromToken);
|
|
36748
|
+
for (const m of listMessages(this.doc)) {
|
|
36749
|
+
if (before2.has(m.id))
|
|
36750
|
+
continue;
|
|
36751
|
+
this.seenIds.add(m.id);
|
|
36752
|
+
if (m.author?.kind === "user" && !m.partial && this.opts.onUserMessage) {
|
|
36753
|
+
const cb = this.opts.onUserMessage;
|
|
36754
|
+
Promise.resolve().then(() => cb(m, this)).catch((e) => this.opts.log(`[chat ${this.chatId}] onUserMessage error: ${e.message}`));
|
|
36755
|
+
}
|
|
36756
|
+
}
|
|
36757
|
+
this.dirtySinceWrite++;
|
|
36758
|
+
if (this.dirtySinceWrite >= 8)
|
|
36759
|
+
this.persist();
|
|
36459
36760
|
}
|
|
36460
36761
|
sendJson(obj) {
|
|
36461
36762
|
if (!this.ws || this.ws.readyState !== 1)
|
|
@@ -36475,13 +36776,20 @@ class ChatPeer {
|
|
|
36475
36776
|
const tag3 = buf[0];
|
|
36476
36777
|
if (tag3 === FRAME_LORO) {
|
|
36477
36778
|
const before2 = new Set(this.seenIds);
|
|
36779
|
+
const updateBytes = buf.subarray(1);
|
|
36478
36780
|
try {
|
|
36479
|
-
applyUpdate(this.doc,
|
|
36781
|
+
applyUpdate(this.doc, updateBytes);
|
|
36480
36782
|
} catch {
|
|
36481
36783
|
return;
|
|
36482
36784
|
}
|
|
36483
36785
|
const isFirstFrame = this.firstFrameSinceConnect;
|
|
36484
36786
|
this.firstFrameSinceConnect = false;
|
|
36787
|
+
if (!isFirstFrame && this.localSubs.size > 0) {
|
|
36788
|
+
const frame = new Uint8Array(1 + updateBytes.byteLength);
|
|
36789
|
+
frame[0] = FRAME_LORO;
|
|
36790
|
+
frame.set(updateBytes, 1);
|
|
36791
|
+
this.broadcastLocal(frame, null);
|
|
36792
|
+
}
|
|
36485
36793
|
for (const m of listMessages(this.doc)) {
|
|
36486
36794
|
if (before2.has(m.id))
|
|
36487
36795
|
continue;
|
|
@@ -36837,6 +37145,100 @@ async function executeChatPlanActions(actionsIn, parseErrors, ctx) {
|
|
|
36837
37145
|
lines.push(` - **${r.key}** [${r.status}] ${r.title}`);
|
|
36838
37146
|
results.push({ type: "issue.search", status: "ok", count: rows.length, query: a.query });
|
|
36839
37147
|
tally(true);
|
|
37148
|
+
} else if (a.type === "issue.get") {
|
|
37149
|
+
const res = await apiClient.post(queryUrl, { kind: "get", id: a.id }, { headers });
|
|
37150
|
+
if (!res.success) {
|
|
37151
|
+
lines.push(`- [err] issue.get ${a.id}: ${res.error || res.status}`);
|
|
37152
|
+
results.push({ type: "issue.get", status: "error", error: String(res.error || res.status), label: a.id });
|
|
37153
|
+
tally(false);
|
|
37154
|
+
continue;
|
|
37155
|
+
}
|
|
37156
|
+
const r = res.data || {};
|
|
37157
|
+
const dispatchLine = r.live_dispatch ? ` dispatch=${r.live_dispatch.status}` : " dispatch=none";
|
|
37158
|
+
lines.push(`- [ok] issue.get **${r.key}** [${r.status}]${r.assignee_id ? ` @${r.assignee_id}` : ""} autonomy=${r.autonomy_level}${dispatchLine}`);
|
|
37159
|
+
lines.push(` - ${r.title}`);
|
|
37160
|
+
if (r.bound_chat_id)
|
|
37161
|
+
lines.push(` - bound chat: ${r.bound_chat_id}`);
|
|
37162
|
+
if (r.live_dispatch?.last_error)
|
|
37163
|
+
lines.push(` - last error: ${r.live_dispatch.last_error}`);
|
|
37164
|
+
results.push({ type: "issue.get", status: "ok", issue_id: r.id, key: r.key, issue_status: r.status, autonomy_level: r.autonomy_level, assignee_id: r.assignee_id, bound_chat_id: r.bound_chat_id, live_dispatch_status: r.live_dispatch?.status ?? null });
|
|
37165
|
+
tally(true);
|
|
37166
|
+
} else if (a.type === "issue.start") {
|
|
37167
|
+
const res = await apiClient.post(mutateUrl, { action: "start", id: a.id }, { headers });
|
|
37168
|
+
if (!res.success) {
|
|
37169
|
+
lines.push(`- [err] issue.start ${a.id}: ${res.error || res.status}`);
|
|
37170
|
+
results.push({ type: "issue.start", status: "error", error: String(res.error || res.status), label: a.id });
|
|
37171
|
+
tally(false);
|
|
37172
|
+
continue;
|
|
37173
|
+
}
|
|
37174
|
+
const r = res.data || {};
|
|
37175
|
+
lines.push(r.dispatched ? `- [ok] issue.start ${r.key || a.id} -> dispatched` : `- [warn] issue.start ${r.key || a.id} not dispatched (autonomy=${r.autonomy_level} or no online device)`);
|
|
37176
|
+
results.push({ type: "issue.start", status: "ok", issue_id: r.id || a.id, key: r.key, dispatched: !!r.dispatched, dispatch_id: r.dispatch_id ?? null, autonomy_level: r.autonomy_level });
|
|
37177
|
+
tally(true);
|
|
37178
|
+
} else if (a.type === "issue.stop") {
|
|
37179
|
+
const res = await apiClient.post(mutateUrl, { action: "stop", id: a.id }, { headers });
|
|
37180
|
+
if (!res.success) {
|
|
37181
|
+
lines.push(`- [err] issue.stop ${a.id}: ${res.error || res.status}`);
|
|
37182
|
+
results.push({ type: "issue.stop", status: "error", error: String(res.error || res.status), label: a.id });
|
|
37183
|
+
tally(false);
|
|
37184
|
+
continue;
|
|
37185
|
+
}
|
|
37186
|
+
const r = res.data || {};
|
|
37187
|
+
lines.push(r.was_running ? `- [ok] issue.stop ${r.key || a.id} -> stopped` : `- [ok] issue.stop ${r.key || a.id} (nothing was running)`);
|
|
37188
|
+
results.push({ type: "issue.stop", status: "ok", issue_id: r.id || a.id, key: r.key, was_running: !!r.was_running });
|
|
37189
|
+
tally(true);
|
|
37190
|
+
} else if (a.type === "memory.search") {
|
|
37191
|
+
const project_id = a.project_id || ctx.projectId;
|
|
37192
|
+
if (!project_id) {
|
|
37193
|
+
lines.push(`- [err] memory.search "${a.query}": project_id required (chat has no pinned project)`);
|
|
37194
|
+
results.push({ type: "memory.search", status: "error", error: "project_id required (chat has no pinned project)", label: a.query });
|
|
37195
|
+
tally(false);
|
|
37196
|
+
continue;
|
|
37197
|
+
}
|
|
37198
|
+
const limit = a.limit ?? 10;
|
|
37199
|
+
const qs = new URLSearchParams({ project_id, q: a.query, limit: String(limit) });
|
|
37200
|
+
const res = await apiClient.get(`${ctx.apiUrl}/api/memory/search?${qs.toString()}`);
|
|
37201
|
+
if (!res.success) {
|
|
37202
|
+
lines.push(`- [err] memory.search "${a.query}": ${res.error || res.status}`);
|
|
37203
|
+
results.push({ type: "memory.search", status: "error", error: String(res.error || res.status), label: a.query });
|
|
37204
|
+
tally(false);
|
|
37205
|
+
continue;
|
|
37206
|
+
}
|
|
37207
|
+
const rows = Array.isArray(res.data?.rows) ? res.data.rows : [];
|
|
37208
|
+
lines.push(`- [ok] memory.search "${a.query}": ${rows.length} hit(s)`);
|
|
37209
|
+
for (const r of rows) {
|
|
37210
|
+
const summary5 = (r.summary || r.text || "").replace(/\s+/g, " ").slice(0, 200);
|
|
37211
|
+
lines.push(` - [${r.source_kind || "memory"}] ${summary5}`);
|
|
37212
|
+
}
|
|
37213
|
+
results.push({ type: "memory.search", status: "ok", count: rows.length, query: a.query });
|
|
37214
|
+
tally(true);
|
|
37215
|
+
} else if (a.type === "memory.write") {
|
|
37216
|
+
const project_id = a.project_id || ctx.projectId;
|
|
37217
|
+
if (!project_id) {
|
|
37218
|
+
lines.push(`- [err] memory.write: project_id required (chat has no pinned project)`);
|
|
37219
|
+
results.push({ type: "memory.write", status: "error", error: "project_id required (chat has no pinned project)" });
|
|
37220
|
+
tally(false);
|
|
37221
|
+
continue;
|
|
37222
|
+
}
|
|
37223
|
+
const body = {
|
|
37224
|
+
project_id,
|
|
37225
|
+
source_kind: a.kind || "agent_note",
|
|
37226
|
+
source_id: a.source_id || `chat:${ctx.chatId}:${Date.now()}`,
|
|
37227
|
+
agent_id: ctx.agentId || null,
|
|
37228
|
+
text: a.text,
|
|
37229
|
+
summary: a.summary || null
|
|
37230
|
+
};
|
|
37231
|
+
const res = await apiClient.post(`${ctx.apiUrl}/api/memory/write`, body);
|
|
37232
|
+
if (!res.success) {
|
|
37233
|
+
lines.push(`- [err] memory.write: ${res.error || res.status}`);
|
|
37234
|
+
results.push({ type: "memory.write", status: "error", error: String(res.error || res.status) });
|
|
37235
|
+
tally(false);
|
|
37236
|
+
continue;
|
|
37237
|
+
}
|
|
37238
|
+
const row = res.data?.row || {};
|
|
37239
|
+
lines.push(`- [ok] memory.write -> ${row.id || "(no id)"}${row.embedding_id ? " (embedded)" : ""}`);
|
|
37240
|
+
results.push({ type: "memory.write", status: "ok", memory_id: row.id, embedded: !!row.embedding_id });
|
|
37241
|
+
tally(true);
|
|
36840
37242
|
} else if (a.type === "agent.create") {
|
|
36841
37243
|
const res = await apiClient.post(agentsMutateUrl, {
|
|
36842
37244
|
action: "create",
|
|
@@ -36952,10 +37354,77 @@ var init_chat_plan_actions = __esm(() => {
|
|
|
36952
37354
|
"skill.detach": 5,
|
|
36953
37355
|
"agent.update": 5,
|
|
36954
37356
|
"session.create": 3,
|
|
36955
|
-
"issue.comment": 5
|
|
37357
|
+
"issue.comment": 5,
|
|
37358
|
+
"memory.search": 5,
|
|
37359
|
+
"memory.write": 5,
|
|
37360
|
+
"issue.get": 10,
|
|
37361
|
+
"issue.start": 3,
|
|
37362
|
+
"issue.stop": 3
|
|
36956
37363
|
};
|
|
36957
37364
|
});
|
|
36958
37365
|
|
|
37366
|
+
// src/_impl/chat-attachments.ts
|
|
37367
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync11, writeFileSync as writeFileSync9 } from "fs";
|
|
37368
|
+
import { dirname as dirname11, join as join15 } from "path";
|
|
37369
|
+
function safeName(n) {
|
|
37370
|
+
return n.replace(/[^A-Za-z0-9._-]+/g, "_").slice(0, 80) || "file";
|
|
37371
|
+
}
|
|
37372
|
+
async function downloadUserAttachments(opts) {
|
|
37373
|
+
const { apiUrl, authToken: authToken2, workspaceId, chatId, cwd, message, log: log4 } = opts;
|
|
37374
|
+
const atts = message.attachments || [];
|
|
37375
|
+
if (atts.length === 0)
|
|
37376
|
+
return [];
|
|
37377
|
+
const baseRel = join15(".multi", "attachments", message.id);
|
|
37378
|
+
const baseAbs = join15(cwd, baseRel);
|
|
37379
|
+
if (!existsSync15(baseAbs))
|
|
37380
|
+
mkdirSync11(baseAbs, { recursive: true });
|
|
37381
|
+
const out = [];
|
|
37382
|
+
for (const a of atts) {
|
|
37383
|
+
if (!a.url)
|
|
37384
|
+
continue;
|
|
37385
|
+
const fname = safeName(a.name);
|
|
37386
|
+
const relPath = join15(baseRel, fname);
|
|
37387
|
+
const absPath = join15(cwd, relPath);
|
|
37388
|
+
if (existsSync15(absPath)) {
|
|
37389
|
+
out.push({ attachment: a, absPath, relPath });
|
|
37390
|
+
continue;
|
|
37391
|
+
}
|
|
37392
|
+
try {
|
|
37393
|
+
const base = a.url.startsWith("http") ? a.url : `${apiUrl.replace(/\/$/, "")}${a.url}`;
|
|
37394
|
+
const sep = base.includes("?") ? "&" : "?";
|
|
37395
|
+
const url2 = `${base}${sep}token=${encodeURIComponent(authToken2)}`;
|
|
37396
|
+
const res = await fetch(url2);
|
|
37397
|
+
if (!res.ok) {
|
|
37398
|
+
log4(`[chat ${chatId}] attachment ${a.id} fetch ${res.status}`);
|
|
37399
|
+
continue;
|
|
37400
|
+
}
|
|
37401
|
+
const buf = new Uint8Array(await res.arrayBuffer());
|
|
37402
|
+
if (!existsSync15(dirname11(absPath)))
|
|
37403
|
+
mkdirSync11(dirname11(absPath), { recursive: true });
|
|
37404
|
+
writeFileSync9(absPath, buf);
|
|
37405
|
+
out.push({ attachment: a, absPath, relPath });
|
|
37406
|
+
} catch (e) {
|
|
37407
|
+
log4(`[chat ${chatId}] attachment ${a.id} err: ${e.message}`);
|
|
37408
|
+
}
|
|
37409
|
+
}
|
|
37410
|
+
return out;
|
|
37411
|
+
}
|
|
37412
|
+
function renderAttachmentPreamble(fetched) {
|
|
37413
|
+
if (fetched.length === 0)
|
|
37414
|
+
return "";
|
|
37415
|
+
const lines = fetched.map((f) => {
|
|
37416
|
+
const sizeBit = typeof f.attachment.size === "number" ? ` (${f.attachment.size}B)` : "";
|
|
37417
|
+
return `- ${f.relPath}${sizeBit}`;
|
|
37418
|
+
});
|
|
37419
|
+
return [
|
|
37420
|
+
`[multi system note — the user attached files; they are saved in the working directory:]`,
|
|
37421
|
+
...lines,
|
|
37422
|
+
``
|
|
37423
|
+
].join(`
|
|
37424
|
+
`);
|
|
37425
|
+
}
|
|
37426
|
+
var init_chat_attachments = () => {};
|
|
37427
|
+
|
|
36959
37428
|
// src/_impl/chat-supervisor.ts
|
|
36960
37429
|
async function runChatTurnWithPeer(peer, opts) {
|
|
36961
37430
|
const { apiUrl, authToken: authToken2, workspaceId, chatId, messageId, log: log4 } = opts;
|
|
@@ -36973,6 +37442,45 @@ async function runChatTurnWithPeer(peer, opts) {
|
|
|
36973
37442
|
}
|
|
36974
37443
|
await processUserMessage(chat2, msg, peer, { apiUrl, authToken: authToken2, workspaceId, log: log4 });
|
|
36975
37444
|
}
|
|
37445
|
+
function buildIssueContextMarkdown(issue2) {
|
|
37446
|
+
const key = issue2.key ?? issue2.id ?? "";
|
|
37447
|
+
const title = issue2.title ?? "";
|
|
37448
|
+
const description = typeof issue2.description === "string" ? issue2.description.trim() : "";
|
|
37449
|
+
const priority = issue2.priority ?? null;
|
|
37450
|
+
const status3 = issue2.status ?? null;
|
|
37451
|
+
const MAX_DESC = 8 * 1024;
|
|
37452
|
+
const desc = description.length > MAX_DESC ? description.slice(0, MAX_DESC) + `
|
|
37453
|
+
|
|
37454
|
+
[…truncated]` : description;
|
|
37455
|
+
const out = [];
|
|
37456
|
+
out.push(`## Issue ${key}: ${title}`);
|
|
37457
|
+
if (desc) {
|
|
37458
|
+
out.push("");
|
|
37459
|
+
out.push(desc);
|
|
37460
|
+
}
|
|
37461
|
+
const meta3 = [];
|
|
37462
|
+
if (priority)
|
|
37463
|
+
meta3.push(`priority: ${priority}`);
|
|
37464
|
+
if (status3)
|
|
37465
|
+
meta3.push(`status: ${status3}`);
|
|
37466
|
+
if (meta3.length) {
|
|
37467
|
+
out.push("");
|
|
37468
|
+
out.push(`_${meta3.join(" · ")}_`);
|
|
37469
|
+
}
|
|
37470
|
+
return out.join(`
|
|
37471
|
+
`);
|
|
37472
|
+
}
|
|
37473
|
+
async function fetchIssueContext(apiUrl, workspaceId, authToken2, issueId) {
|
|
37474
|
+
try {
|
|
37475
|
+
const r = await fetch(`${apiUrl}/api/workspaces/${workspaceId}/issues/${issueId}`, { headers: { authorization: `Bearer ${authToken2}` } });
|
|
37476
|
+
if (!r.ok)
|
|
37477
|
+
return null;
|
|
37478
|
+
const issue2 = await r.json();
|
|
37479
|
+
return buildIssueContextMarkdown(issue2);
|
|
37480
|
+
} catch {
|
|
37481
|
+
return null;
|
|
37482
|
+
}
|
|
37483
|
+
}
|
|
36976
37484
|
async function fetchAgents(apiUrl, workspaceId, authToken2) {
|
|
36977
37485
|
const headers = { authorization: `Bearer ${authToken2}` };
|
|
36978
37486
|
try {
|
|
@@ -36995,6 +37503,9 @@ function resolveAgentFromMention(agents, mentioned) {
|
|
|
36995
37503
|
async function resolveCwd(apiUrl, workspaceId, authToken2, chat2) {
|
|
36996
37504
|
if (!chat2.project_id || !chat2.device_id)
|
|
36997
37505
|
return;
|
|
37506
|
+
if (chat2.working_dir) {
|
|
37507
|
+
return chat2.working_dir;
|
|
37508
|
+
}
|
|
36998
37509
|
const headers = { authorization: `Bearer ${authToken2}` };
|
|
36999
37510
|
try {
|
|
37000
37511
|
const r = await fetch(`${apiUrl}/api/workspaces/${workspaceId}/projects/${chat2.project_id}/devices`, { headers });
|
|
@@ -37079,21 +37590,28 @@ async function processUserMessage(chat2, userMsg, peer, ctx) {
|
|
|
37079
37590
|
if (typeof r.filename === "string")
|
|
37080
37591
|
out.filename = r.filename;
|
|
37081
37592
|
if (typeof r.command === "string")
|
|
37082
|
-
out.command =
|
|
37593
|
+
out.command = r.command;
|
|
37083
37594
|
if (typeof r.pattern === "string")
|
|
37084
|
-
out.pattern =
|
|
37595
|
+
out.pattern = r.pattern;
|
|
37085
37596
|
if (typeof r.query === "string")
|
|
37086
|
-
out.query =
|
|
37597
|
+
out.query = r.query;
|
|
37087
37598
|
if (typeof r.url === "string")
|
|
37088
37599
|
out.url = r.url;
|
|
37600
|
+
if (typeof r.old_string === "string")
|
|
37601
|
+
out.old_string = r.old_string;
|
|
37602
|
+
if (typeof r.new_string === "string")
|
|
37603
|
+
out.new_string = r.new_string;
|
|
37604
|
+
if (typeof r.replace_all === "boolean")
|
|
37605
|
+
out.replace_all = r.replace_all;
|
|
37606
|
+
if (typeof r.content === "string")
|
|
37607
|
+
out.content = r.content;
|
|
37089
37608
|
return Object.keys(out).length ? out : undefined;
|
|
37090
37609
|
};
|
|
37091
37610
|
const summarizeContent = (raw) => {
|
|
37092
37611
|
const bytes = raw?.length ?? 0;
|
|
37093
37612
|
const lines = raw ? raw.split(`
|
|
37094
37613
|
`).length : 0;
|
|
37095
|
-
|
|
37096
|
-
return { content: trimmed, bytes, lines };
|
|
37614
|
+
return { content: raw || "", bytes, lines };
|
|
37097
37615
|
};
|
|
37098
37616
|
const mapToolKind = (raw) => {
|
|
37099
37617
|
const s = (raw || "").toLowerCase();
|
|
@@ -37231,7 +37749,42 @@ async function processUserMessage(chat2, userMsg, peer, ctx) {
|
|
|
37231
37749
|
return null;
|
|
37232
37750
|
}
|
|
37233
37751
|
};
|
|
37234
|
-
|
|
37752
|
+
let attachmentPrefix = "";
|
|
37753
|
+
if (cwd && userMsg.attachments && userMsg.attachments.length > 0) {
|
|
37754
|
+
try {
|
|
37755
|
+
const fetched = await downloadUserAttachments({
|
|
37756
|
+
apiUrl,
|
|
37757
|
+
authToken: authToken2,
|
|
37758
|
+
workspaceId,
|
|
37759
|
+
chatId: chat2.id,
|
|
37760
|
+
cwd,
|
|
37761
|
+
message: userMsg,
|
|
37762
|
+
log: log4
|
|
37763
|
+
});
|
|
37764
|
+
attachmentPrefix = renderAttachmentPreamble(fetched);
|
|
37765
|
+
if (fetched.length > 0)
|
|
37766
|
+
log4(`[chat ${chat2.id}] attachments: ${fetched.length} ready`);
|
|
37767
|
+
} catch (e) {
|
|
37768
|
+
log4(`[chat ${chat2.id}] attachment download err: ${e.message}`);
|
|
37769
|
+
}
|
|
37770
|
+
}
|
|
37771
|
+
let issueCtxBlock = "";
|
|
37772
|
+
if (chat2.issue_id && !issueContextSent.has(chat2.id)) {
|
|
37773
|
+
const ctx2 = await fetchIssueContext(apiUrl, workspaceId, authToken2, chat2.issue_id);
|
|
37774
|
+
if (ctx2) {
|
|
37775
|
+
issueCtxBlock = `${ctx2}
|
|
37776
|
+
|
|
37777
|
+
---
|
|
37778
|
+
|
|
37779
|
+
## New message
|
|
37780
|
+
|
|
37781
|
+
`;
|
|
37782
|
+
log4(`[chat ${chat2.id}] injected issue context (${ctx2.length} chars)`);
|
|
37783
|
+
}
|
|
37784
|
+
}
|
|
37785
|
+
await runOneTurn(`${issueCtxBlock}${attachmentPrefix}${userMsg.text}`, true);
|
|
37786
|
+
if (issueCtxBlock)
|
|
37787
|
+
issueContextSent.add(chat2.id);
|
|
37235
37788
|
const firstPlan = await execAndPostPlan();
|
|
37236
37789
|
if (firstPlan && firstPlan.ok > 0) {
|
|
37237
37790
|
const resultText = firstPlan.lines.join(`
|
|
@@ -37330,6 +37883,11 @@ Action vocabulary:
|
|
|
37330
37883
|
{"type":"issue.delete_where","status":"todo","limit":50},
|
|
37331
37884
|
{"type":"issue.list","status":"todo","limit":20},
|
|
37332
37885
|
{"type":"issue.search","query":"flaky tests","limit":10},
|
|
37886
|
+
{"type":"issue.get","id":"<issue id or key>"},
|
|
37887
|
+
{"type":"issue.start","id":"<issue id or key>"},
|
|
37888
|
+
{"type":"issue.stop","id":"<issue id or key>"},
|
|
37889
|
+
{"type":"memory.search","query":"deploy pipeline","limit":10},
|
|
37890
|
+
{"type":"memory.write","text":"durable note worth recalling next session","summary":"short hint","kind":"agent_note"},
|
|
37333
37891
|
{"type":"agent.create","name":"refactor-bot","prompt":"...","allowed_tools":["Read","Edit","Bash"]},
|
|
37334
37892
|
{"type":"agent.update","id":"ag_xxx","prompt":"..."},
|
|
37335
37893
|
{"type":"skill.create","name":"run-tests","description":"...","body":"---\\nname: run-tests\\n---\\n..."},
|
|
@@ -37341,7 +37899,9 @@ Action vocabulary:
|
|
|
37341
37899
|
Rules:
|
|
37342
37900
|
- Omit the block entirely if no actions are needed. Don't emit empty arrays.
|
|
37343
37901
|
- Status values for \`update\`: \`todo\` | \`in_progress\` | \`blocked\` | \`done\` | \`archived\` | \`failed\`. **Use \`blocked\` (NOT \`done\`) when the issue is paused waiting on a human decision** — confirmation, choice, or missing context. Marking such an issue \`done\` misrepresents finished work. Use \`archived\` to hide a completed/abandoned issue from default views.
|
|
37344
|
-
- Max 10 actions per turn. Sub-caps: agent.create=2, skill.create=3, agent.update=5, skill.attach/detach=5, session.create=3, issue.comment=5.
|
|
37902
|
+
- Max 10 actions per turn. Sub-caps: agent.create=2, skill.create=3, agent.update=5, skill.attach/detach=5, session.create=3, issue.comment=5, memory.search=5, memory.write=5, issue.get=10, issue.start=3, issue.stop=3.
|
|
37903
|
+
- Use \`issue.get\` to inspect an issue's current state (status, autonomy, assignee, live dispatch, bound chat) — the result lands in the next-turn summary. \`issue.start\` dispatches the assigned agent (equivalent to mentioning them); honors the autonomy gate. \`issue.stop\` gracefully stops a running dispatch; safe to call on a non-running issue (no-op).
|
|
37904
|
+
- Memory actions are project-scoped persistent notes (FTS5 + vector). \`memory.search\` returns hits in the action summary; the agent reads them next turn. \`memory.write\` records durable facts for future sessions — use sparingly, not for transient task state. Both require a pinned project.
|
|
37345
37905
|
- Chat-initiated agent.create / agent.update / skill.attach are auto-approved (the user is reading this reply right now). skill.create still queues for human review.
|
|
37346
37906
|
- Use \`issue.comment\` with \`@<agent name>\` mention to dispatch an agent on an existing issue. Plain comments without an @mention are recorded but do not trigger a run. Issues whose autonomy is \`manual\` will not dispatch.
|
|
37347
37907
|
- To create an issue AND assign/dispatch it to an agent in the same turn, set \`assignee_type\` + \`assignee_id\` directly on the \`create\` action. Do NOT emit a separate \`delegate\` or \`issue.comment\` action referring to a brand-new issue in the same plan block — actions execute as a flat list and the new issue's id/key is not available to later actions in the same block. \`delegate\` and \`issue.comment\` are for issues that already exist before this turn.
|
|
@@ -37364,11 +37924,14 @@ Rules:
|
|
|
37364
37924
|
|
|
37365
37925
|
${agentsBlock}`;
|
|
37366
37926
|
}
|
|
37927
|
+
var issueContextSent;
|
|
37367
37928
|
var init_chat_supervisor = __esm(() => {
|
|
37368
37929
|
init_chat_peer();
|
|
37369
37930
|
init_chat_turn();
|
|
37370
37931
|
init_chat_plan_actions();
|
|
37932
|
+
init_chat_attachments();
|
|
37371
37933
|
init_lib();
|
|
37934
|
+
issueContextSent = new Set;
|
|
37372
37935
|
});
|
|
37373
37936
|
|
|
37374
37937
|
// src/_impl/chat-session-registry.ts
|
|
@@ -37481,6 +38044,9 @@ var init_chat_session_registry = __esm(() => {
|
|
|
37481
38044
|
}
|
|
37482
38045
|
s.inFlight = runTurn(s, k, messageId, ctx, chatId);
|
|
37483
38046
|
},
|
|
38047
|
+
ensurePeer(wsId, chatId, ctx) {
|
|
38048
|
+
return ensureSession(wsId, chatId, ctx).peer;
|
|
38049
|
+
},
|
|
37484
38050
|
async closeAll() {
|
|
37485
38051
|
for (const [k, s] of sessions) {
|
|
37486
38052
|
clearIdle(s);
|
|
@@ -38115,7 +38681,7 @@ import { parseArgs } from "util";
|
|
|
38115
38681
|
// package.json
|
|
38116
38682
|
var package_default = {
|
|
38117
38683
|
name: "@shipers-dev/multi",
|
|
38118
|
-
version: "0.
|
|
38684
|
+
version: "0.63.0",
|
|
38119
38685
|
type: "module",
|
|
38120
38686
|
bin: {
|
|
38121
38687
|
"multi-agent": "./dist/index.js"
|
|
@@ -38495,8 +39061,6 @@ revision=${Date.now()}
|
|
|
38495
39061
|
}
|
|
38496
39062
|
}
|
|
38497
39063
|
function writeManagedAgent(slug, agent, skillsForAgent) {
|
|
38498
|
-
if (agent.type !== "claude-code")
|
|
38499
|
-
return;
|
|
38500
39064
|
mkdirSync4(CLAUDE_AGENTS, { recursive: true });
|
|
38501
39065
|
const out = join6(CLAUDE_AGENTS, `${slug}.md`);
|
|
38502
39066
|
safeRmManaged(out);
|
|
@@ -39030,7 +39594,7 @@ var killStaleConnects = () => {
|
|
|
39030
39594
|
const cmd = m[2];
|
|
39031
39595
|
if (!Number.isFinite(pid) || pid === self || pid === parent)
|
|
39032
39596
|
continue;
|
|
39033
|
-
if (!/@shipers-dev\/multi\/dist\/index\.js/.test(cmd) && !/multi-agent/.test(cmd))
|
|
39597
|
+
if (!/@shipers-dev\/multi\/dist\/index\.js/.test(cmd) && !/multi-agent/.test(cmd) && !/multi-daemon/.test(cmd))
|
|
39034
39598
|
continue;
|
|
39035
39599
|
if (!/\bconnect\b/.test(cmd))
|
|
39036
39600
|
continue;
|
|
@@ -39193,22 +39757,24 @@ init_client();
|
|
|
39193
39757
|
init_detect();
|
|
39194
39758
|
init_run_task();
|
|
39195
39759
|
import { Database as Database3 } from "bun:sqlite";
|
|
39196
|
-
import { existsSync as
|
|
39197
|
-
import { homedir as
|
|
39198
|
-
import { join as
|
|
39760
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync12, writeFileSync as writeFileSync10, unlinkSync as unlinkSync7 } from "fs";
|
|
39761
|
+
import { homedir as homedir4 } from "os";
|
|
39762
|
+
import { join as join16 } from "path";
|
|
39199
39763
|
init_adapter_pidfile();
|
|
39200
39764
|
init_errors();
|
|
39201
|
-
var MULTI_DIR5 =
|
|
39202
|
-
var PID_PATH2 =
|
|
39203
|
-
var PORT_PATH2 =
|
|
39204
|
-
var STOP_PATH2 =
|
|
39205
|
-
var TASKS_DB_PATH3 =
|
|
39765
|
+
var MULTI_DIR5 = join16(homedir4(), ".multi");
|
|
39766
|
+
var PID_PATH2 = join16(MULTI_DIR5, "agent.pid");
|
|
39767
|
+
var PORT_PATH2 = join16(MULTI_DIR5, "agent.port");
|
|
39768
|
+
var STOP_PATH2 = join16(MULTI_DIR5, "stop.flag");
|
|
39769
|
+
var TASKS_DB_PATH3 = join16(MULTI_DIR5, "tasks.db");
|
|
39770
|
+
var LOCAL_SERVER_DIR = process.env.MULTI_HOME || join16(homedir4(), ".multi");
|
|
39771
|
+
var LOCAL_SERVER_PATH = join16(LOCAL_SERVER_DIR, "local-server.json");
|
|
39206
39772
|
function ensureDirs2() {
|
|
39207
|
-
if (!
|
|
39208
|
-
|
|
39209
|
-
const logs =
|
|
39210
|
-
if (!
|
|
39211
|
-
|
|
39773
|
+
if (!existsSync16(MULTI_DIR5))
|
|
39774
|
+
mkdirSync12(MULTI_DIR5, { recursive: true });
|
|
39775
|
+
const logs = join16(MULTI_DIR5, "logs");
|
|
39776
|
+
if (!existsSync16(logs))
|
|
39777
|
+
mkdirSync12(logs, { recursive: true });
|
|
39212
39778
|
}
|
|
39213
39779
|
function isLocalApi(url2) {
|
|
39214
39780
|
try {
|
|
@@ -39500,15 +40066,18 @@ var daemonProgram = ({ cfg, apiUrl }) => exports_Effect.gen(function* () {
|
|
|
39500
40066
|
if (cfg.authToken)
|
|
39501
40067
|
setAuthToken(cfg.authToken);
|
|
39502
40068
|
ensureDirs2();
|
|
39503
|
-
if (
|
|
40069
|
+
if (existsSync16(STOP_PATH2))
|
|
39504
40070
|
unlinkSync7(STOP_PATH2);
|
|
39505
40071
|
const reaped = reapStaleAdapters((m) => log3(m));
|
|
39506
40072
|
if (reaped > 0)
|
|
39507
40073
|
log3(`reaped ${reaped} stale adapter${reaped === 1 ? "" : "s"}`);
|
|
39508
40074
|
const detected = yield* exports_Effect.promise(() => detectAgents());
|
|
39509
|
-
const
|
|
40075
|
+
const skipTunnel = process.env.MULTI_NO_TUNNEL === "1";
|
|
40076
|
+
const localMode = isLocalApi(apiUrl) || skipTunnel;
|
|
39510
40077
|
log3(`daemon device=${cfg.deviceId} pid=${process.pid}`);
|
|
39511
|
-
log3(` API: ${apiUrl}${localMode ? " (local — cloudflared skipped)" : ""}`);
|
|
40078
|
+
log3(` API: ${apiUrl}${localMode ? skipTunnel && !isLocalApi(apiUrl) ? " (no-tunnel mode)" : " (local — cloudflared skipped)" : ""}`);
|
|
40079
|
+
log3(` PATH=${(process.env.PATH || "").slice(0, 500)}`);
|
|
40080
|
+
log3(` detected: ${JSON.stringify(detected.map((d) => ({ type: d.type, path: d.path })))}`);
|
|
39512
40081
|
log3(` runtimes: ${detected.map((d) => d.type).join(", ") || "stub"}`);
|
|
39513
40082
|
const db2 = openTasksDb();
|
|
39514
40083
|
db2.run("UPDATE tasks SET status = 'queued' WHERE status = 'running'");
|
|
@@ -39520,128 +40089,573 @@ var daemonProgram = ({ cfg, apiUrl }) => exports_Effect.gen(function* () {
|
|
|
39520
40089
|
const stopDeferred = yield* exports_Deferred.make();
|
|
39521
40090
|
const port = yield* exports_Effect.promise(() => pickFreePort());
|
|
39522
40091
|
const expectedAuth = `Bearer ${cfg.dispatchSecret}`;
|
|
40092
|
+
const corsHeaders = (origin) => ({
|
|
40093
|
+
"Access-Control-Allow-Origin": origin || "*",
|
|
40094
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
40095
|
+
"Access-Control-Allow-Headers": "Authorization, Content-Type",
|
|
40096
|
+
"Access-Control-Max-Age": "600",
|
|
40097
|
+
Vary: "Origin"
|
|
40098
|
+
});
|
|
40099
|
+
const withCors = (res, origin) => {
|
|
40100
|
+
for (const [k, v] of Object.entries(corsHeaders(origin))) {
|
|
40101
|
+
res.headers.set(k, v);
|
|
40102
|
+
}
|
|
40103
|
+
return res;
|
|
40104
|
+
};
|
|
39523
40105
|
const server = Bun.serve({
|
|
39524
40106
|
port,
|
|
39525
40107
|
hostname: "127.0.0.1",
|
|
39526
|
-
fetch(req) {
|
|
40108
|
+
fetch(req, server2) {
|
|
39527
40109
|
const url2 = new URL(req.url);
|
|
39528
|
-
|
|
39529
|
-
|
|
39530
|
-
|
|
39531
|
-
|
|
39532
|
-
|
|
39533
|
-
|
|
39534
|
-
|
|
39535
|
-
|
|
39536
|
-
|
|
39537
|
-
|
|
39538
|
-
db2.run("INSERT INTO tasks (id, status, payload, agent_id, issue_id) VALUES (?, 'queued', ?, ?, ?)", [taskId, JSON.stringify(t), t.agent_id ?? null, t.issue_id ?? null]);
|
|
39539
|
-
const pos = db2.query("SELECT COUNT(*) AS c FROM tasks WHERE status = 'queued' AND created_at <= (SELECT created_at FROM tasks WHERE id = ?)").get(taskId)?.c ?? 1;
|
|
39540
|
-
if (t.issue_id)
|
|
39541
|
-
postStream(apiUrl, t.issue_id, "queued", { queue_position: pos });
|
|
39542
|
-
if (t.dispatch_id && cfg.workspaceId)
|
|
39543
|
-
ackDispatch(apiUrl, cfg.workspaceId, t.dispatch_id, cfg.authToken);
|
|
39544
|
-
try {
|
|
39545
|
-
exports_Queue.unsafeOffer(wakeQ, undefined);
|
|
39546
|
-
} catch {}
|
|
39547
|
-
return Response.json({ accepted: true, task_id: taskId }, { status: 202 });
|
|
39548
|
-
} catch (e) {
|
|
39549
|
-
return Response.json({ error: String(e) }, { status: 400 });
|
|
40110
|
+
const origin = req.headers.get("origin");
|
|
40111
|
+
if (req.method === "OPTIONS") {
|
|
40112
|
+
return new Response(null, { status: 204, headers: corsHeaders(origin) });
|
|
40113
|
+
}
|
|
40114
|
+
{
|
|
40115
|
+
const m = /^\/chat-sync\/([^/]+)$/.exec(url2.pathname);
|
|
40116
|
+
if (m && req.headers.get("upgrade") === "websocket") {
|
|
40117
|
+
const token = url2.searchParams.get("token") || "";
|
|
40118
|
+
if (token !== cfg.dispatchSecret) {
|
|
40119
|
+
return new Response("unauthorized", { status: 401 });
|
|
39550
40120
|
}
|
|
39551
|
-
|
|
40121
|
+
if (!cfg.workspaceId || !cfg.authToken || !cfg.deviceId) {
|
|
40122
|
+
return new Response("daemon not configured", { status: 503 });
|
|
40123
|
+
}
|
|
40124
|
+
const chatId = decodeURIComponent(m[1]);
|
|
40125
|
+
const ok = server2.upgrade(req, {
|
|
40126
|
+
data: { kind: "chat-sync", chatId, wsId: cfg.workspaceId }
|
|
40127
|
+
});
|
|
40128
|
+
if (!ok)
|
|
40129
|
+
return new Response("upgrade failed", { status: 500 });
|
|
40130
|
+
return;
|
|
40131
|
+
}
|
|
39552
40132
|
}
|
|
39553
|
-
|
|
39554
|
-
if (
|
|
39555
|
-
return
|
|
39556
|
-
|
|
39557
|
-
|
|
39558
|
-
|
|
39559
|
-
|
|
39560
|
-
|
|
40133
|
+
const route = () => {
|
|
40134
|
+
if (url2.pathname === "/health")
|
|
40135
|
+
return Response.json({ ok: true, device_id: cfg.deviceId });
|
|
40136
|
+
if (url2.pathname === "/files" && req.method === "GET") {
|
|
40137
|
+
if (req.headers.get("authorization") !== expectedAuth)
|
|
40138
|
+
return new Response("unauthorized", { status: 401 });
|
|
40139
|
+
return (async () => {
|
|
40140
|
+
try {
|
|
40141
|
+
const dir = url2.searchParams.get("dir") || "";
|
|
40142
|
+
if (!dir || !dir.startsWith("/")) {
|
|
40143
|
+
return Response.json({ error: "absolute dir required" }, { status: 400 });
|
|
40144
|
+
}
|
|
40145
|
+
const showHidden = url2.searchParams.get("hidden") === "1";
|
|
40146
|
+
const { readdir } = await import("fs/promises");
|
|
40147
|
+
const path = await import("path");
|
|
40148
|
+
const entries2 = await readdir(dir, { withFileTypes: true });
|
|
40149
|
+
const repoRoot = await (async () => {
|
|
40150
|
+
try {
|
|
40151
|
+
const p = Bun.spawn(["git", "rev-parse", "--show-toplevel"], {
|
|
40152
|
+
cwd: dir,
|
|
40153
|
+
stdout: "pipe",
|
|
40154
|
+
stderr: "ignore"
|
|
40155
|
+
});
|
|
40156
|
+
const out3 = (await new Response(p.stdout).text()).trim();
|
|
40157
|
+
await p.exited;
|
|
40158
|
+
return p.exitCode === 0 && out3 ? out3 : null;
|
|
40159
|
+
} catch {
|
|
40160
|
+
return null;
|
|
40161
|
+
}
|
|
40162
|
+
})();
|
|
40163
|
+
const status3 = new Map;
|
|
40164
|
+
if (repoRoot) {
|
|
40165
|
+
try {
|
|
40166
|
+
const p = Bun.spawn(["git", "status", "--porcelain=v1", "--ignored", "-z"], { cwd: repoRoot, stdout: "pipe", stderr: "ignore" });
|
|
40167
|
+
const out3 = await new Response(p.stdout).text();
|
|
40168
|
+
await p.exited;
|
|
40169
|
+
if (p.exitCode === 0) {
|
|
40170
|
+
const parts2 = out3.split("\x00").filter((s) => s.length > 0);
|
|
40171
|
+
for (let i = 0;i < parts2.length; i++) {
|
|
40172
|
+
const entry = parts2[i];
|
|
40173
|
+
const code = entry.slice(0, 2);
|
|
40174
|
+
const rel = entry.slice(3);
|
|
40175
|
+
if (code[0] === "R" || code[0] === "C")
|
|
40176
|
+
i++;
|
|
40177
|
+
const abs = path.join(repoRoot, rel);
|
|
40178
|
+
const kind = code === "!!" ? "ignored" : code === "??" ? "untracked" : code.includes("D") ? "deleted" : code.includes("A") ? "added" : code.includes("R") ? "renamed" : code.includes("M") ? "modified" : "tracked";
|
|
40179
|
+
status3.set(abs.replace(/\/$/, ""), kind);
|
|
40180
|
+
}
|
|
40181
|
+
}
|
|
40182
|
+
} catch {}
|
|
40183
|
+
}
|
|
40184
|
+
const lookupStatus = (abs, isDir) => {
|
|
40185
|
+
const trimmed = abs.replace(/\/$/, "");
|
|
40186
|
+
if (status3.has(trimmed))
|
|
40187
|
+
return status3.get(trimmed);
|
|
40188
|
+
if (isDir) {
|
|
40189
|
+
for (let p = trimmed;p && p !== repoRoot; p = p.replace(/\/[^/]*$/, "")) {
|
|
40190
|
+
const s = status3.get(p);
|
|
40191
|
+
if (s === "ignored" || s === "untracked")
|
|
40192
|
+
return s;
|
|
40193
|
+
if (!p.includes("/"))
|
|
40194
|
+
break;
|
|
40195
|
+
}
|
|
40196
|
+
}
|
|
40197
|
+
if (!repoRoot)
|
|
40198
|
+
return "tracked";
|
|
40199
|
+
return "tracked";
|
|
40200
|
+
};
|
|
40201
|
+
const out2 = entries2.filter((e) => showHidden || !e.name.startsWith(".")).map((e) => {
|
|
40202
|
+
const abs = path.join(dir, e.name);
|
|
40203
|
+
return {
|
|
40204
|
+
name: e.name,
|
|
40205
|
+
path: abs,
|
|
40206
|
+
is_dir: e.isDirectory(),
|
|
40207
|
+
status: lookupStatus(abs, e.isDirectory())
|
|
40208
|
+
};
|
|
40209
|
+
}).sort((a, b) => a.is_dir === b.is_dir ? a.name.localeCompare(b.name) : a.is_dir ? -1 : 1);
|
|
40210
|
+
return Response.json({ dir, repo_root: repoRoot, entries: out2 });
|
|
40211
|
+
} catch (e) {
|
|
40212
|
+
return Response.json({ error: String(e) }, { status: 400 });
|
|
39561
40213
|
}
|
|
39562
|
-
|
|
39563
|
-
|
|
40214
|
+
})();
|
|
40215
|
+
}
|
|
40216
|
+
if (url2.pathname === "/worktree/create" && req.method === "POST") {
|
|
40217
|
+
if (req.headers.get("authorization") !== expectedAuth)
|
|
40218
|
+
return new Response("unauthorized", { status: 401 });
|
|
40219
|
+
return (async () => {
|
|
40220
|
+
try {
|
|
40221
|
+
const body = await req.json();
|
|
40222
|
+
if (!body?.base_dir || !body.base_dir.startsWith("/")) {
|
|
40223
|
+
return Response.json({ error: "absolute base_dir required" }, { status: 400 });
|
|
40224
|
+
}
|
|
40225
|
+
const { codename: codename2, materialiseWorktree: materialiseWorktree2 } = await Promise.resolve().then(() => (init_worktree_create(), exports_worktree_create));
|
|
40226
|
+
const name = (body.name || codename2()).replace(/[^a-zA-Z0-9_-]/g, "-").slice(0, 40);
|
|
40227
|
+
const result = await materialiseWorktree2({
|
|
40228
|
+
baseDir: body.base_dir,
|
|
40229
|
+
baseBranch: body.base_branch || null,
|
|
40230
|
+
name,
|
|
40231
|
+
chatId: body.chat_id || null,
|
|
40232
|
+
log: (m) => log3(m)
|
|
40233
|
+
});
|
|
40234
|
+
return Response.json(result);
|
|
40235
|
+
} catch (e) {
|
|
40236
|
+
return Response.json({ error: String(e.message || e) }, { status: 400 });
|
|
39564
40237
|
}
|
|
39565
|
-
|
|
39566
|
-
|
|
39567
|
-
|
|
39568
|
-
|
|
39569
|
-
|
|
39570
|
-
|
|
39571
|
-
|
|
39572
|
-
|
|
39573
|
-
|
|
39574
|
-
|
|
39575
|
-
|
|
39576
|
-
|
|
39577
|
-
|
|
39578
|
-
|
|
39579
|
-
|
|
39580
|
-
|
|
39581
|
-
return new Response("unauthorized", { status: 401 });
|
|
39582
|
-
return (async () => {
|
|
39583
|
-
try {
|
|
39584
|
-
const body = await req.json();
|
|
39585
|
-
if (!body?.tick_id || !body?.callback_url || !body?.callback_token) {
|
|
39586
|
-
return Response.json({ error: "tick_id, callback_url, callback_token required" }, { status: 400 });
|
|
40238
|
+
})();
|
|
40239
|
+
}
|
|
40240
|
+
if (url2.pathname === "/worktree/delete" && req.method === "POST") {
|
|
40241
|
+
if (req.headers.get("authorization") !== expectedAuth)
|
|
40242
|
+
return new Response("unauthorized", { status: 401 });
|
|
40243
|
+
return (async () => {
|
|
40244
|
+
try {
|
|
40245
|
+
const body = await req.json();
|
|
40246
|
+
if (!body?.path || !body.path.startsWith("/")) {
|
|
40247
|
+
return Response.json({ error: "absolute path required" }, { status: 400 });
|
|
40248
|
+
}
|
|
40249
|
+
const { removeWorktree: removeWorktree3 } = await Promise.resolve().then(() => (init_worktree_create(), exports_worktree_create));
|
|
40250
|
+
const result = await removeWorktree3(body.path, (m) => log3(m));
|
|
40251
|
+
return Response.json(result);
|
|
40252
|
+
} catch (e) {
|
|
40253
|
+
return Response.json({ error: String(e.message || e) }, { status: 400 });
|
|
39587
40254
|
}
|
|
39588
|
-
|
|
39589
|
-
|
|
39590
|
-
|
|
39591
|
-
|
|
39592
|
-
|
|
39593
|
-
|
|
39594
|
-
|
|
39595
|
-
|
|
39596
|
-
|
|
39597
|
-
|
|
39598
|
-
|
|
39599
|
-
|
|
39600
|
-
|
|
39601
|
-
|
|
39602
|
-
|
|
39603
|
-
|
|
39604
|
-
|
|
39605
|
-
|
|
39606
|
-
|
|
39607
|
-
|
|
39608
|
-
|
|
39609
|
-
|
|
39610
|
-
|
|
39611
|
-
|
|
39612
|
-
|
|
39613
|
-
|
|
39614
|
-
|
|
39615
|
-
|
|
39616
|
-
|
|
40255
|
+
})();
|
|
40256
|
+
}
|
|
40257
|
+
if (url2.pathname === "/branch" && req.method === "GET") {
|
|
40258
|
+
if (req.headers.get("authorization") !== expectedAuth)
|
|
40259
|
+
return new Response("unauthorized", { status: 401 });
|
|
40260
|
+
return (async () => {
|
|
40261
|
+
try {
|
|
40262
|
+
const dir = url2.searchParams.get("dir") || "";
|
|
40263
|
+
if (!dir || !dir.startsWith("/")) {
|
|
40264
|
+
return Response.json({ error: "absolute dir required" }, { status: 400 });
|
|
40265
|
+
}
|
|
40266
|
+
const runGit = async (args2) => {
|
|
40267
|
+
const proc = Bun.spawn(["git", ...args2], {
|
|
40268
|
+
cwd: dir,
|
|
40269
|
+
stdout: "pipe",
|
|
40270
|
+
stderr: "ignore"
|
|
40271
|
+
});
|
|
40272
|
+
const out2 = (await new Response(proc.stdout).text()).trim();
|
|
40273
|
+
await proc.exited;
|
|
40274
|
+
return { ok: proc.exitCode === 0, out: out2 };
|
|
40275
|
+
};
|
|
40276
|
+
const branchR = await runGit(["branch", "--show-current"]);
|
|
40277
|
+
const branch = branchR.ok ? branchR.out || null : null;
|
|
40278
|
+
const upstreamR = await runGit(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]);
|
|
40279
|
+
const upstream = upstreamR.ok ? upstreamR.out || null : null;
|
|
40280
|
+
let ahead = 0;
|
|
40281
|
+
let behind = 0;
|
|
40282
|
+
const unpushed = [];
|
|
40283
|
+
if (upstream) {
|
|
40284
|
+
const countR = await runGit(["rev-list", "--left-right", "--count", `${upstream}...HEAD`]);
|
|
40285
|
+
if (countR.ok) {
|
|
40286
|
+
const [b, a] = countR.out.split(/\s+/);
|
|
40287
|
+
behind = Number(b) || 0;
|
|
40288
|
+
ahead = Number(a) || 0;
|
|
40289
|
+
}
|
|
40290
|
+
if (ahead > 0) {
|
|
40291
|
+
const logR = await runGit(["log", `${upstream}..HEAD`, "--pretty=format:%h\x1F%s", "-n", "50"]);
|
|
40292
|
+
if (logR.ok && logR.out) {
|
|
40293
|
+
for (const line of logR.out.split(`
|
|
40294
|
+
`)) {
|
|
40295
|
+
const idx = line.indexOf("\x1F");
|
|
40296
|
+
if (idx === -1)
|
|
40297
|
+
continue;
|
|
40298
|
+
unpushed.push({ sha: line.slice(0, idx), subject: line.slice(idx + 1) });
|
|
40299
|
+
}
|
|
40300
|
+
}
|
|
40301
|
+
}
|
|
40302
|
+
} else if (branch) {
|
|
40303
|
+
const logR = await runGit(["log", "HEAD", "--not", "--remotes", "--pretty=format:%h\x1F%s", "-n", "50"]);
|
|
40304
|
+
if (logR.ok && logR.out) {
|
|
40305
|
+
for (const line of logR.out.split(`
|
|
40306
|
+
`)) {
|
|
40307
|
+
const idx = line.indexOf("\x1F");
|
|
40308
|
+
if (idx === -1)
|
|
40309
|
+
continue;
|
|
40310
|
+
unpushed.push({ sha: line.slice(0, idx), subject: line.slice(idx + 1) });
|
|
40311
|
+
}
|
|
40312
|
+
ahead = unpushed.length;
|
|
40313
|
+
}
|
|
40314
|
+
}
|
|
40315
|
+
return Response.json({ dir, branch, upstream, ahead, behind, unpushed });
|
|
40316
|
+
} catch (e) {
|
|
40317
|
+
return Response.json({ error: String(e) }, { status: 400 });
|
|
40318
|
+
}
|
|
40319
|
+
})();
|
|
40320
|
+
}
|
|
40321
|
+
if (url2.pathname === "/changes" && req.method === "GET") {
|
|
40322
|
+
if (req.headers.get("authorization") !== expectedAuth)
|
|
40323
|
+
return new Response("unauthorized", { status: 401 });
|
|
40324
|
+
return (async () => {
|
|
40325
|
+
try {
|
|
40326
|
+
const dir = url2.searchParams.get("dir") || "";
|
|
40327
|
+
if (!dir || !dir.startsWith("/")) {
|
|
40328
|
+
return Response.json({ error: "absolute dir required" }, { status: 400 });
|
|
40329
|
+
}
|
|
40330
|
+
const rootP = Bun.spawn(["git", "rev-parse", "--show-toplevel"], {
|
|
40331
|
+
cwd: dir,
|
|
40332
|
+
stdout: "pipe",
|
|
40333
|
+
stderr: "ignore"
|
|
40334
|
+
});
|
|
40335
|
+
const repoRoot = (await new Response(rootP.stdout).text()).trim();
|
|
40336
|
+
await rootP.exited;
|
|
40337
|
+
const proc = Bun.spawn(["git", "status", "--porcelain=v1", "-z"], {
|
|
40338
|
+
cwd: dir,
|
|
40339
|
+
stdout: "pipe",
|
|
40340
|
+
stderr: "pipe"
|
|
40341
|
+
});
|
|
40342
|
+
const out2 = await new Response(proc.stdout).text();
|
|
40343
|
+
const errOut = await new Response(proc.stderr).text();
|
|
40344
|
+
await proc.exited;
|
|
40345
|
+
if (proc.exitCode !== 0) {
|
|
40346
|
+
return Response.json({ dir, changes: [], error: errOut.trim() || "git failed" });
|
|
40347
|
+
}
|
|
40348
|
+
const changes = [];
|
|
40349
|
+
const parts2 = out2.split("\x00").filter((p) => p.length > 0);
|
|
40350
|
+
for (let i = 0;i < parts2.length; i++) {
|
|
40351
|
+
const entry = parts2[i];
|
|
40352
|
+
const code = entry.slice(0, 2);
|
|
40353
|
+
let p = entry.slice(3);
|
|
40354
|
+
if (code[0] === "R" || code[0] === "C") {
|
|
40355
|
+
i++;
|
|
40356
|
+
}
|
|
40357
|
+
changes.push({ code, path: p });
|
|
40358
|
+
}
|
|
40359
|
+
return Response.json({ dir, repo_root: repoRoot || null, changes });
|
|
40360
|
+
} catch (e) {
|
|
40361
|
+
return Response.json({ error: String(e) }, { status: 400 });
|
|
40362
|
+
}
|
|
40363
|
+
})();
|
|
40364
|
+
}
|
|
40365
|
+
if (url2.pathname === "/read-file" && req.method === "GET") {
|
|
40366
|
+
if (req.headers.get("authorization") !== expectedAuth)
|
|
40367
|
+
return new Response("unauthorized", { status: 401 });
|
|
40368
|
+
return (async () => {
|
|
40369
|
+
try {
|
|
40370
|
+
const p = url2.searchParams.get("path") || "";
|
|
40371
|
+
if (!p || !p.startsWith("/")) {
|
|
40372
|
+
return Response.json({ error: "absolute path required" }, { status: 400 });
|
|
40373
|
+
}
|
|
40374
|
+
const fs3 = await import("fs/promises");
|
|
40375
|
+
const stat = await fs3.stat(p);
|
|
40376
|
+
if (stat.isDirectory()) {
|
|
40377
|
+
return Response.json({ error: "is a directory" }, { status: 400 });
|
|
40378
|
+
}
|
|
40379
|
+
const MAX = 2097152;
|
|
40380
|
+
if (stat.size > MAX) {
|
|
40381
|
+
return Response.json({ path: p, size: stat.size, truncated: true, error: `file too large (${stat.size} bytes)` }, { status: 413 });
|
|
40382
|
+
}
|
|
40383
|
+
const buf = await fs3.readFile(p);
|
|
40384
|
+
const head5 = buf.subarray(0, Math.min(8192, buf.length));
|
|
40385
|
+
let nonPrint = 0;
|
|
40386
|
+
for (let i = 0;i < head5.length; i++) {
|
|
40387
|
+
const b = head5[i];
|
|
40388
|
+
if (b === 0) {
|
|
40389
|
+
nonPrint = head5.length;
|
|
40390
|
+
break;
|
|
40391
|
+
}
|
|
40392
|
+
if (b < 9 || b > 13 && b < 32)
|
|
40393
|
+
nonPrint++;
|
|
40394
|
+
}
|
|
40395
|
+
const isBinary = head5.length > 0 && nonPrint / head5.length > 0.3;
|
|
40396
|
+
if (isBinary) {
|
|
40397
|
+
return Response.json({ path: p, size: stat.size, binary: true });
|
|
40398
|
+
}
|
|
40399
|
+
return Response.json({ path: p, size: stat.size, content: buf.toString("utf8") });
|
|
40400
|
+
} catch (e) {
|
|
40401
|
+
return Response.json({ error: String(e.message || e) }, { status: 400 });
|
|
40402
|
+
}
|
|
40403
|
+
})();
|
|
40404
|
+
}
|
|
40405
|
+
if (url2.pathname === "/diff" && req.method === "GET") {
|
|
40406
|
+
if (req.headers.get("authorization") !== expectedAuth)
|
|
40407
|
+
return new Response("unauthorized", { status: 401 });
|
|
40408
|
+
return (async () => {
|
|
40409
|
+
try {
|
|
40410
|
+
const p = url2.searchParams.get("path") || "";
|
|
40411
|
+
if (!p || !p.startsWith("/")) {
|
|
40412
|
+
return Response.json({ error: "absolute path required" }, { status: 400 });
|
|
40413
|
+
}
|
|
40414
|
+
const path = await import("path");
|
|
40415
|
+
const repoDir = path.dirname(p);
|
|
40416
|
+
const rootP = Bun.spawn(["git", "rev-parse", "--show-toplevel"], {
|
|
40417
|
+
cwd: repoDir,
|
|
40418
|
+
stdout: "pipe",
|
|
40419
|
+
stderr: "ignore"
|
|
40420
|
+
});
|
|
40421
|
+
const rootOut = (await new Response(rootP.stdout).text()).trim();
|
|
40422
|
+
await rootP.exited;
|
|
40423
|
+
if (rootP.exitCode !== 0 || !rootOut) {
|
|
40424
|
+
return Response.json({ path: p, diff: "", error: "not a git repo" });
|
|
40425
|
+
}
|
|
40426
|
+
const proc = Bun.spawn(["git", "diff", "HEAD", "--", p], { cwd: rootOut, stdout: "pipe", stderr: "pipe" });
|
|
40427
|
+
const out2 = await new Response(proc.stdout).text();
|
|
40428
|
+
const err = await new Response(proc.stderr).text();
|
|
40429
|
+
await proc.exited;
|
|
40430
|
+
if (proc.exitCode !== 0) {
|
|
40431
|
+
return Response.json({ path: p, diff: "", error: err.trim() || "git diff failed" });
|
|
40432
|
+
}
|
|
40433
|
+
return Response.json({ path: p, diff: out2 });
|
|
40434
|
+
} catch (e) {
|
|
40435
|
+
return Response.json({ error: String(e.message || e) }, { status: 400 });
|
|
39617
40436
|
}
|
|
39618
|
-
|
|
39619
|
-
|
|
39620
|
-
|
|
40437
|
+
})();
|
|
40438
|
+
}
|
|
40439
|
+
if (url2.pathname === "/run" && req.method === "POST") {
|
|
40440
|
+
if (req.headers.get("authorization") !== expectedAuth)
|
|
40441
|
+
return new Response("unauthorized", { status: 401 });
|
|
40442
|
+
return (async () => {
|
|
40443
|
+
try {
|
|
40444
|
+
const body = await req.json();
|
|
40445
|
+
const t = body?.task || {};
|
|
40446
|
+
const taskId = t.issue_id ? `${t.issue_id}-${Date.now()}` : crypto.randomUUID();
|
|
40447
|
+
db2.run("INSERT INTO tasks (id, status, payload, agent_id, issue_id) VALUES (?, 'queued', ?, ?, ?)", [taskId, JSON.stringify(t), t.agent_id ?? null, t.issue_id ?? null]);
|
|
40448
|
+
const pos = db2.query("SELECT COUNT(*) AS c FROM tasks WHERE status = 'queued' AND created_at <= (SELECT created_at FROM tasks WHERE id = ?)").get(taskId)?.c ?? 1;
|
|
40449
|
+
if (t.issue_id)
|
|
40450
|
+
postStream(apiUrl, t.issue_id, "queued", { queue_position: pos });
|
|
40451
|
+
if (t.dispatch_id && cfg.workspaceId)
|
|
40452
|
+
ackDispatch(apiUrl, cfg.workspaceId, t.dispatch_id, cfg.authToken);
|
|
39621
40453
|
try {
|
|
39622
|
-
|
|
40454
|
+
exports_Queue.unsafeOffer(wakeQ, undefined);
|
|
39623
40455
|
} catch {}
|
|
39624
|
-
|
|
40456
|
+
return Response.json({ accepted: true, task_id: taskId }, { status: 202 });
|
|
40457
|
+
} catch (e) {
|
|
40458
|
+
return Response.json({ error: String(e) }, { status: 400 });
|
|
40459
|
+
}
|
|
40460
|
+
})();
|
|
40461
|
+
}
|
|
40462
|
+
{
|
|
40463
|
+
const m = /^\/chat-snapshot\/([^/]+)$/.exec(url2.pathname);
|
|
40464
|
+
if (m && req.method === "GET") {
|
|
40465
|
+
const token = url2.searchParams.get("token") || "";
|
|
40466
|
+
const headerToken = req.headers.get("authorization") === expectedAuth;
|
|
40467
|
+
if (!headerToken && token !== cfg.dispatchSecret) {
|
|
40468
|
+
return new Response("unauthorized", { status: 401 });
|
|
40469
|
+
}
|
|
40470
|
+
if (!cfg.workspaceId || !cfg.authToken || !cfg.deviceId) {
|
|
40471
|
+
return new Response("daemon not configured", { status: 503 });
|
|
40472
|
+
}
|
|
40473
|
+
const chatId = decodeURIComponent(m[1]);
|
|
40474
|
+
return (async () => {
|
|
40475
|
+
const { chatSessionRegistry: chatSessionRegistry2 } = await Promise.resolve().then(() => (init_chat_session_registry(), exports_chat_session_registry));
|
|
40476
|
+
const peer = chatSessionRegistry2.ensurePeer(cfg.workspaceId, chatId, {
|
|
40477
|
+
apiUrl,
|
|
40478
|
+
authToken: cfg.authToken,
|
|
40479
|
+
workspaceId: cfg.workspaceId,
|
|
40480
|
+
deviceId: cfg.deviceId,
|
|
40481
|
+
log: log3
|
|
40482
|
+
});
|
|
40483
|
+
const greet = peer.buildSnapshotGreet();
|
|
40484
|
+
return new Response(greet.subarray(1), {
|
|
40485
|
+
headers: { "content-type": "application/octet-stream" }
|
|
40486
|
+
});
|
|
40487
|
+
})();
|
|
40488
|
+
}
|
|
40489
|
+
}
|
|
40490
|
+
if (url2.pathname === "/run-chat-turn" && req.method === "POST") {
|
|
40491
|
+
if (req.headers.get("authorization") !== expectedAuth)
|
|
40492
|
+
return new Response("unauthorized", { status: 401 });
|
|
40493
|
+
return (async () => {
|
|
40494
|
+
try {
|
|
40495
|
+
const body = await req.json();
|
|
40496
|
+
if (!body?.chat_id || !body?.message_id) {
|
|
40497
|
+
return Response.json({ error: "chat_id and message_id required" }, { status: 400 });
|
|
40498
|
+
}
|
|
40499
|
+
if (!cfg.workspaceId || !cfg.authToken || !cfg.deviceId) {
|
|
40500
|
+
return Response.json({ error: "daemon not configured" }, { status: 503 });
|
|
40501
|
+
}
|
|
40502
|
+
const { chatSessionRegistry: chatSessionRegistry2 } = await Promise.resolve().then(() => (init_chat_session_registry(), exports_chat_session_registry));
|
|
40503
|
+
chatSessionRegistry2.ensureAndProcess(cfg.workspaceId, body.chat_id, body.message_id, {
|
|
40504
|
+
apiUrl,
|
|
40505
|
+
authToken: cfg.authToken,
|
|
40506
|
+
workspaceId: cfg.workspaceId,
|
|
40507
|
+
deviceId: cfg.deviceId,
|
|
40508
|
+
log: log3
|
|
40509
|
+
}).catch((e) => log3(`[chat ${body.chat_id}] runChatTurn error: ${e.message}`));
|
|
40510
|
+
return Response.json({ accepted: true }, { status: 202 });
|
|
40511
|
+
} catch (e) {
|
|
40512
|
+
return Response.json({ error: String(e) }, { status: 400 });
|
|
40513
|
+
}
|
|
40514
|
+
})();
|
|
40515
|
+
}
|
|
40516
|
+
if (url2.pathname === "/run-supervisor-tick" && req.method === "POST") {
|
|
40517
|
+
if (req.headers.get("authorization") !== expectedAuth)
|
|
40518
|
+
return new Response("unauthorized", { status: 401 });
|
|
40519
|
+
return (async () => {
|
|
40520
|
+
try {
|
|
40521
|
+
const body = await req.json();
|
|
40522
|
+
if (!body?.tick_id || !body?.callback_url || !body?.callback_token) {
|
|
40523
|
+
return Response.json({ error: "tick_id, callback_url, callback_token required" }, { status: 400 });
|
|
40524
|
+
}
|
|
40525
|
+
const { runSupervisorTick: runSupervisorTick2 } = await Promise.resolve().then(() => (init_supervisor_tick(), exports_supervisor_tick));
|
|
40526
|
+
runSupervisorTick2({
|
|
40527
|
+
apiUrl,
|
|
40528
|
+
tickId: body.tick_id,
|
|
40529
|
+
projectId: body.project_id,
|
|
40530
|
+
system: body.system,
|
|
40531
|
+
user: body.user,
|
|
40532
|
+
callbackUrl: body.callback_url,
|
|
40533
|
+
callbackToken: body.callback_token,
|
|
40534
|
+
log: log3
|
|
40535
|
+
}).catch((e) => log3(`[sup ${body.tick_id}] runSupervisorTick error: ${e.message}`));
|
|
40536
|
+
return Response.json({ accepted: true }, { status: 202 });
|
|
40537
|
+
} catch (e) {
|
|
40538
|
+
return Response.json({ error: String(e) }, { status: 400 });
|
|
40539
|
+
}
|
|
40540
|
+
})();
|
|
40541
|
+
}
|
|
40542
|
+
if (url2.pathname === "/stop" && req.method === "POST") {
|
|
40543
|
+
if (req.headers.get("authorization") !== expectedAuth)
|
|
40544
|
+
return new Response("unauthorized", { status: 401 });
|
|
40545
|
+
return (async () => {
|
|
40546
|
+
try {
|
|
40547
|
+
const { issue_id } = await req.json();
|
|
40548
|
+
if (!issue_id)
|
|
40549
|
+
return Response.json({ error: "issue_id required" }, { status: 400 });
|
|
40550
|
+
const entries2 = Array.from(running3.values()).filter((e) => e.issueId === issue_id);
|
|
40551
|
+
if (!entries2.length) {
|
|
40552
|
+
db2.run("UPDATE tasks SET status = 'failed', error = 'stopped before start', finished_at = unixepoch() WHERE issue_id = ? AND status = 'queued'", [issue_id]);
|
|
40553
|
+
return Response.json({ ok: true, state: "queued-cancelled" });
|
|
40554
|
+
}
|
|
40555
|
+
for (const entry of entries2) {
|
|
40556
|
+
entry.stopped = true;
|
|
40557
|
+
entry.stopReason = "user requested";
|
|
39625
40558
|
try {
|
|
39626
|
-
entry.child?.kill("
|
|
40559
|
+
entry.child?.kill("SIGTERM");
|
|
39627
40560
|
} catch {}
|
|
39628
|
-
|
|
40561
|
+
setTimeout(() => {
|
|
40562
|
+
try {
|
|
40563
|
+
entry.child?.kill("SIGKILL");
|
|
40564
|
+
} catch {}
|
|
40565
|
+
}, 1500);
|
|
40566
|
+
}
|
|
40567
|
+
return Response.json({ ok: true, state: "running-signalled" });
|
|
40568
|
+
} catch (e) {
|
|
40569
|
+
return Response.json({ error: String(e) }, { status: 400 });
|
|
39629
40570
|
}
|
|
39630
|
-
|
|
39631
|
-
|
|
39632
|
-
|
|
39633
|
-
|
|
39634
|
-
|
|
40571
|
+
})();
|
|
40572
|
+
}
|
|
40573
|
+
return new Response("not found", { status: 404 });
|
|
40574
|
+
};
|
|
40575
|
+
const out = route();
|
|
40576
|
+
return out instanceof Promise ? out.then((r) => withCors(r, origin)) : withCors(out, origin);
|
|
40577
|
+
},
|
|
40578
|
+
websocket: {
|
|
40579
|
+
async open(ws) {
|
|
40580
|
+
const data = ws.data;
|
|
40581
|
+
if (!data || data.kind !== "chat-sync")
|
|
40582
|
+
return;
|
|
40583
|
+
if (!cfg.workspaceId || !cfg.authToken || !cfg.deviceId) {
|
|
40584
|
+
try {
|
|
40585
|
+
ws.close(1011, "daemon not configured");
|
|
40586
|
+
} catch {}
|
|
40587
|
+
return;
|
|
40588
|
+
}
|
|
40589
|
+
const { chatSessionRegistry: chatSessionRegistry2 } = await Promise.resolve().then(() => (init_chat_session_registry(), exports_chat_session_registry));
|
|
40590
|
+
const peer = chatSessionRegistry2.ensurePeer(data.wsId, data.chatId, {
|
|
40591
|
+
apiUrl,
|
|
40592
|
+
authToken: cfg.authToken,
|
|
40593
|
+
workspaceId: cfg.workspaceId,
|
|
40594
|
+
deviceId: cfg.deviceId,
|
|
40595
|
+
log: log3
|
|
40596
|
+
});
|
|
40597
|
+
const sub = peer.addLocalSubscriber((frame) => {
|
|
40598
|
+
try {
|
|
40599
|
+
ws.sendBinary(frame);
|
|
40600
|
+
} catch {}
|
|
40601
|
+
});
|
|
40602
|
+
ws.data.peerToken = sub.token;
|
|
40603
|
+
ws.data.unsubscribe = sub.close;
|
|
40604
|
+
try {
|
|
40605
|
+
ws.sendBinary(peer.buildSnapshotGreet());
|
|
40606
|
+
} catch {}
|
|
40607
|
+
},
|
|
40608
|
+
async message(ws, message) {
|
|
40609
|
+
const data = ws.data;
|
|
40610
|
+
if (!data || data.kind !== "chat-sync" || !data.peerToken)
|
|
40611
|
+
return;
|
|
40612
|
+
if (typeof message === "string")
|
|
40613
|
+
return;
|
|
40614
|
+
const buf = message instanceof Uint8Array ? message : new Uint8Array(message);
|
|
40615
|
+
if (buf.byteLength < 1)
|
|
40616
|
+
return;
|
|
40617
|
+
const tag3 = buf[0];
|
|
40618
|
+
if (tag3 === 1) {
|
|
40619
|
+
const { chatSessionRegistry: chatSessionRegistry2 } = await Promise.resolve().then(() => (init_chat_session_registry(), exports_chat_session_registry));
|
|
40620
|
+
const peer = chatSessionRegistry2.ensurePeer(data.wsId, data.chatId, {
|
|
40621
|
+
apiUrl,
|
|
40622
|
+
authToken: cfg.authToken,
|
|
40623
|
+
workspaceId: cfg.workspaceId,
|
|
40624
|
+
deviceId: cfg.deviceId,
|
|
40625
|
+
log: log3
|
|
40626
|
+
});
|
|
40627
|
+
peer.ingestLocalUpdate(buf.subarray(1), data.peerToken);
|
|
40628
|
+
}
|
|
40629
|
+
},
|
|
40630
|
+
close(ws) {
|
|
40631
|
+
const data = ws.data;
|
|
40632
|
+
if (data?.unsubscribe) {
|
|
40633
|
+
try {
|
|
40634
|
+
data.unsubscribe();
|
|
40635
|
+
} catch {}
|
|
40636
|
+
}
|
|
39635
40637
|
}
|
|
39636
|
-
return new Response("not found", { status: 404 });
|
|
39637
40638
|
}
|
|
39638
40639
|
});
|
|
39639
40640
|
log3(`Local server: http://127.0.0.1:${port}`);
|
|
39640
40641
|
try {
|
|
39641
|
-
|
|
40642
|
+
writeFileSync10(PORT_PATH2, String(port));
|
|
40643
|
+
} catch {}
|
|
40644
|
+
try {
|
|
40645
|
+
writeFileSync10(PID_PATH2, String(process.pid));
|
|
39642
40646
|
} catch {}
|
|
39643
40647
|
try {
|
|
39644
|
-
|
|
40648
|
+
if (!existsSync16(LOCAL_SERVER_DIR))
|
|
40649
|
+
mkdirSync12(LOCAL_SERVER_DIR, { recursive: true });
|
|
40650
|
+
writeFileSync10(LOCAL_SERVER_PATH, JSON.stringify({
|
|
40651
|
+
url: `http://127.0.0.1:${port}`,
|
|
40652
|
+
port,
|
|
40653
|
+
token: cfg.dispatchSecret,
|
|
40654
|
+
device_id: cfg.deviceId,
|
|
40655
|
+
workspace_id: cfg.workspaceId,
|
|
40656
|
+
api_url: apiUrl,
|
|
40657
|
+
pid: process.pid
|
|
40658
|
+
}, null, 2));
|
|
39645
40659
|
} catch {}
|
|
39646
40660
|
let tunnel;
|
|
39647
40661
|
if (localMode) {
|
|
@@ -39705,7 +40719,7 @@ var daemonProgram = ({ cfg, apiUrl }) => exports_Effect.gen(function* () {
|
|
|
39705
40719
|
let probeFailures = 0;
|
|
39706
40720
|
while (true) {
|
|
39707
40721
|
yield* exports_Effect.sleep(exports_Duration.seconds(120));
|
|
39708
|
-
if (
|
|
40722
|
+
if (existsSync16(STOP_PATH2)) {
|
|
39709
40723
|
yield* exports_Deferred.succeed(stopDeferred, "stop flag");
|
|
39710
40724
|
return;
|
|
39711
40725
|
}
|
|
@@ -39730,7 +40744,7 @@ var daemonProgram = ({ cfg, apiUrl }) => exports_Effect.gen(function* () {
|
|
|
39730
40744
|
yield* exports_Effect.forkIn(daemonScope)(exports_Effect.gen(function* () {
|
|
39731
40745
|
while (true) {
|
|
39732
40746
|
yield* exports_Effect.sleep(exports_Duration.seconds(5));
|
|
39733
|
-
if (
|
|
40747
|
+
if (existsSync16(STOP_PATH2)) {
|
|
39734
40748
|
yield* exports_Deferred.succeed(stopDeferred, "stop flag");
|
|
39735
40749
|
return;
|
|
39736
40750
|
}
|
|
@@ -39769,13 +40783,22 @@ var daemonProgram = ({ cfg, apiUrl }) => exports_Effect.gen(function* () {
|
|
|
39769
40783
|
if (inFlight.length) {
|
|
39770
40784
|
yield* exports_Effect.race(exports_Fiber.interruptAll(inFlight), exports_Effect.sleep(exports_Duration.seconds(10)));
|
|
39771
40785
|
}
|
|
40786
|
+
try {
|
|
40787
|
+
const killed = killDaemonChildren(process.pid, (m) => log3(m));
|
|
40788
|
+
if (killed > 0)
|
|
40789
|
+
log3(`killed ${killed} adapter child${killed === 1 ? "" : "ren"} on shutdown`);
|
|
40790
|
+
} catch (e) {
|
|
40791
|
+
log3(`adapter shutdown sweep failed: ${String(e)}`);
|
|
40792
|
+
}
|
|
39772
40793
|
yield* exports_Scope.close(daemonScope, exports_Exit.void).pipe(exports_Effect.catchAll(() => exports_Effect.void));
|
|
39773
|
-
if (
|
|
40794
|
+
if (existsSync16(PID_PATH2))
|
|
39774
40795
|
unlinkSync7(PID_PATH2);
|
|
39775
|
-
if (
|
|
40796
|
+
if (existsSync16(STOP_PATH2))
|
|
39776
40797
|
unlinkSync7(STOP_PATH2);
|
|
39777
|
-
if (
|
|
40798
|
+
if (existsSync16(PORT_PATH2))
|
|
39778
40799
|
unlinkSync7(PORT_PATH2);
|
|
40800
|
+
if (existsSync16(LOCAL_SERVER_PATH))
|
|
40801
|
+
unlinkSync7(LOCAL_SERVER_PATH);
|
|
39779
40802
|
db2.close();
|
|
39780
40803
|
log3("disconnected");
|
|
39781
40804
|
});
|
|
@@ -39788,7 +40811,81 @@ async function daemonMain(opts) {
|
|
|
39788
40811
|
}
|
|
39789
40812
|
|
|
39790
40813
|
// src/commands/connect.ts
|
|
40814
|
+
init_detect();
|
|
39791
40815
|
init_outbox();
|
|
40816
|
+
|
|
40817
|
+
// src/_impl/pair-flow.ts
|
|
40818
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync13, writeFileSync as writeFileSync11 } from "fs";
|
|
40819
|
+
import { homedir as homedir5 } from "os";
|
|
40820
|
+
import { join as join17 } from "path";
|
|
40821
|
+
var LOCAL_SERVER_DIR2 = process.env.MULTI_HOME || join17(homedir5(), ".multi");
|
|
40822
|
+
var LOCAL_SERVER_PATH2 = join17(LOCAL_SERVER_DIR2, "local-server.json");
|
|
40823
|
+
function writePairingInfo(payload) {
|
|
40824
|
+
try {
|
|
40825
|
+
if (!existsSync17(LOCAL_SERVER_DIR2))
|
|
40826
|
+
mkdirSync13(LOCAL_SERVER_DIR2, { recursive: true });
|
|
40827
|
+
writeFileSync11(LOCAL_SERVER_PATH2, JSON.stringify(payload, null, 2));
|
|
40828
|
+
} catch {}
|
|
40829
|
+
}
|
|
40830
|
+
async function awaitPairing(apiUrl, deviceName, opts = {}) {
|
|
40831
|
+
const log4 = opts.log ?? ((m) => console.log(m));
|
|
40832
|
+
const timeoutMs = opts.timeoutMs ?? 10 * 60 * 1000;
|
|
40833
|
+
const startRes = await fetch(`${apiUrl}/api/pair/start`, {
|
|
40834
|
+
method: "POST",
|
|
40835
|
+
headers: { "content-type": "application/json" },
|
|
40836
|
+
body: JSON.stringify({
|
|
40837
|
+
name: deviceName,
|
|
40838
|
+
platform: opts.platform ?? process.platform,
|
|
40839
|
+
arch: opts.arch ?? process.arch,
|
|
40840
|
+
os_version: opts.osVersion ?? process.version,
|
|
40841
|
+
detected_runtimes: opts.detectedRuntimes ?? []
|
|
40842
|
+
})
|
|
40843
|
+
});
|
|
40844
|
+
if (!startRes.ok) {
|
|
40845
|
+
throw new Error(`pair/start failed: ${startRes.status} ${await startRes.text()}`);
|
|
40846
|
+
}
|
|
40847
|
+
const start3 = await startRes.json();
|
|
40848
|
+
const code = start3.code;
|
|
40849
|
+
const pairUrl = `${apiUrl}/pair/${code}`;
|
|
40850
|
+
writePairingInfo({
|
|
40851
|
+
status: "pairing",
|
|
40852
|
+
pair_code: code,
|
|
40853
|
+
pair_url: pairUrl,
|
|
40854
|
+
device_name: deviceName,
|
|
40855
|
+
api_url: apiUrl,
|
|
40856
|
+
expires_at: start3.expires_at
|
|
40857
|
+
});
|
|
40858
|
+
log4(`pair: code=${code} url=${pairUrl}`);
|
|
40859
|
+
const deadline = Date.now() + timeoutMs;
|
|
40860
|
+
while (Date.now() < deadline) {
|
|
40861
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
40862
|
+
let res;
|
|
40863
|
+
try {
|
|
40864
|
+
res = await fetch(`${apiUrl}/api/pair/poll/${code}`);
|
|
40865
|
+
} catch {
|
|
40866
|
+
continue;
|
|
40867
|
+
}
|
|
40868
|
+
if (res.status === 410)
|
|
40869
|
+
throw new Error("Pairing expired.");
|
|
40870
|
+
if (!res.ok)
|
|
40871
|
+
continue;
|
|
40872
|
+
const j = await res.json();
|
|
40873
|
+
if (j.status === "approved" && j.device_id && j.token) {
|
|
40874
|
+
return {
|
|
40875
|
+
device_id: j.device_id,
|
|
40876
|
+
token: j.token,
|
|
40877
|
+
dispatch_secret: j.dispatch_secret ?? null,
|
|
40878
|
+
workspace_id: j.workspace_id ?? null
|
|
40879
|
+
};
|
|
40880
|
+
}
|
|
40881
|
+
}
|
|
40882
|
+
throw new Error("Pairing timed out.");
|
|
40883
|
+
}
|
|
40884
|
+
function deviceNameFromEnv() {
|
|
40885
|
+
return process.env.MULTI_DEVICE_NAME || process.env.HOSTNAME || process.env.COMPUTERNAME || "Multi Desktop";
|
|
40886
|
+
}
|
|
40887
|
+
|
|
40888
|
+
// src/commands/connect.ts
|
|
39792
40889
|
init_paths();
|
|
39793
40890
|
init_errors();
|
|
39794
40891
|
var connectCmd = exports_Effect.fn("connectCmd")(function* () {
|
|
@@ -39796,9 +40893,28 @@ var connectCmd = exports_Effect.fn("connectCmd")(function* () {
|
|
|
39796
40893
|
const logger = yield* Logger4;
|
|
39797
40894
|
const api2 = yield* Api2;
|
|
39798
40895
|
yield* config2.ensureDirs;
|
|
39799
|
-
|
|
39800
|
-
if (!cfg.deviceId) {
|
|
39801
|
-
|
|
40896
|
+
let cfg = yield* config2.load;
|
|
40897
|
+
if (!cfg.deviceId || !cfg.authToken) {
|
|
40898
|
+
const apiUrl = cfg.apiUrl || process.env.MULTI_API || process.env.MULTI_API_URL || "https://multi-api.adnb3r.workers.dev";
|
|
40899
|
+
const name = deviceNameFromEnv();
|
|
40900
|
+
const detected = yield* exports_Effect.promise(() => detectAgents());
|
|
40901
|
+
yield* logger.log(`connect: no device registered — starting pair flow for "${name}" ` + `(runtimes: ${detected.map((d) => d.type).join(", ") || "none"})`);
|
|
40902
|
+
const bundle = yield* exports_Effect.tryPromise({
|
|
40903
|
+
try: () => awaitPairing(apiUrl, name, {
|
|
40904
|
+
log: (m) => console.log(m),
|
|
40905
|
+
detectedRuntimes: detected.map((d) => d.type)
|
|
40906
|
+
}),
|
|
40907
|
+
catch: (cause3) => new ApiError({ url: `${apiUrl}/api/pair/start`, status: 0, message: cause3.message })
|
|
40908
|
+
});
|
|
40909
|
+
yield* config2.save({
|
|
40910
|
+
apiUrl,
|
|
40911
|
+
deviceId: bundle.device_id,
|
|
40912
|
+
authToken: bundle.token,
|
|
40913
|
+
dispatchSecret: bundle.dispatch_secret ?? undefined,
|
|
40914
|
+
workspaceId: bundle.workspace_id ?? undefined
|
|
40915
|
+
});
|
|
40916
|
+
cfg = yield* config2.load;
|
|
40917
|
+
yield* logger.log(`connect: paired device=${cfg.deviceId} workspace=${cfg.workspaceId}`);
|
|
39802
40918
|
}
|
|
39803
40919
|
if (!cfg.dispatchSecret) {
|
|
39804
40920
|
return yield* exports_Effect.fail(new UsageError({ message: "Missing dispatch secret. Re-pair via 'multi-agent setup'." }));
|
|
@@ -39835,6 +40951,250 @@ ${err?.stack ?? ""}`);
|
|
|
39835
40951
|
}).pipe(exports_Effect.ensuring(exports_Effect.sync(() => stopFlusher())));
|
|
39836
40952
|
});
|
|
39837
40953
|
|
|
40954
|
+
// src/commands/service.ts
|
|
40955
|
+
init_esm();
|
|
40956
|
+
import { existsSync as existsSync18, readFileSync as readFileSync12 } from "fs";
|
|
40957
|
+
import { homedir as homedir6, platform } from "os";
|
|
40958
|
+
import { join as join18 } from "path";
|
|
40959
|
+
init_errors();
|
|
40960
|
+
init_paths();
|
|
40961
|
+
var LABEL = "dev.shipers.multi.daemon";
|
|
40962
|
+
var HOME5 = homedir6();
|
|
40963
|
+
var PLIST_PATH = join18(HOME5, "Library", "LaunchAgents", `${LABEL}.plist`);
|
|
40964
|
+
var SYSTEMD_UNIT_PATH = join18(HOME5, ".config", "systemd", "user", "multi-daemon.service");
|
|
40965
|
+
var isMac = () => platform() === "darwin";
|
|
40966
|
+
var isLinux = () => platform() === "linux";
|
|
40967
|
+
var isRunningPid = (pid) => {
|
|
40968
|
+
try {
|
|
40969
|
+
process.kill(pid, 0);
|
|
40970
|
+
return true;
|
|
40971
|
+
} catch {
|
|
40972
|
+
return false;
|
|
40973
|
+
}
|
|
40974
|
+
};
|
|
40975
|
+
var readPid = () => {
|
|
40976
|
+
try {
|
|
40977
|
+
if (!existsSync18(PID_PATH))
|
|
40978
|
+
return null;
|
|
40979
|
+
const n = Number(readFileSync12(PID_PATH, "utf8").trim());
|
|
40980
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
40981
|
+
} catch {
|
|
40982
|
+
return null;
|
|
40983
|
+
}
|
|
40984
|
+
};
|
|
40985
|
+
var resolveBinary = exports_Effect.fn("service.resolveBinary")(function* () {
|
|
40986
|
+
const proc = Bun.spawn(["which", "multi-agent"], { stdout: "pipe", stderr: "pipe" });
|
|
40987
|
+
yield* exports_Effect.promise(() => proc.exited);
|
|
40988
|
+
const out = yield* exports_Effect.promise(() => new Response(proc.stdout).text());
|
|
40989
|
+
const path = out.trim();
|
|
40990
|
+
if (!path || !existsSync18(path)) {
|
|
40991
|
+
return yield* exports_Effect.fail(new DaemonError({
|
|
40992
|
+
message: "Could not find `multi-agent` on PATH. Install with: bun install -g @shipers-dev/multi"
|
|
40993
|
+
}));
|
|
40994
|
+
}
|
|
40995
|
+
return path;
|
|
40996
|
+
});
|
|
40997
|
+
var run6 = exports_Effect.fn("service.run")(function* (cmd, args2) {
|
|
40998
|
+
const proc = Bun.spawn([cmd, ...args2], { stdout: "pipe", stderr: "pipe" });
|
|
40999
|
+
yield* exports_Effect.promise(() => proc.exited);
|
|
41000
|
+
const stdout = yield* exports_Effect.promise(() => new Response(proc.stdout).text());
|
|
41001
|
+
const stderr = yield* exports_Effect.promise(() => new Response(proc.stderr).text());
|
|
41002
|
+
if (proc.exitCode !== 0) {
|
|
41003
|
+
return yield* exports_Effect.fail(new SpawnError({
|
|
41004
|
+
cmd: `${cmd} ${args2.join(" ")}`,
|
|
41005
|
+
cause: stderr || stdout || `exit ${proc.exitCode}`
|
|
41006
|
+
}));
|
|
41007
|
+
}
|
|
41008
|
+
return { stdout, stderr };
|
|
41009
|
+
});
|
|
41010
|
+
var buildPlist = (binary, env) => {
|
|
41011
|
+
const envEntries = Object.entries(env).map(([k, v]) => ` <key>${k}</key>
|
|
41012
|
+
<string>${escapeXml(v)}</string>`).join(`
|
|
41013
|
+
`);
|
|
41014
|
+
const logDir = join18(MULTI_DIR, "logs");
|
|
41015
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
41016
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
41017
|
+
<plist version="1.0">
|
|
41018
|
+
<dict>
|
|
41019
|
+
<key>Label</key>
|
|
41020
|
+
<string>${LABEL}</string>
|
|
41021
|
+
<key>ProgramArguments</key>
|
|
41022
|
+
<array>
|
|
41023
|
+
<string>${escapeXml(binary)}</string>
|
|
41024
|
+
<string>connect</string>
|
|
41025
|
+
</array>
|
|
41026
|
+
<key>RunAtLoad</key>
|
|
41027
|
+
<true/>
|
|
41028
|
+
<key>KeepAlive</key>
|
|
41029
|
+
<true/>
|
|
41030
|
+
<key>WorkingDirectory</key>
|
|
41031
|
+
<string>${escapeXml(HOME5)}</string>
|
|
41032
|
+
<key>StandardOutPath</key>
|
|
41033
|
+
<string>${escapeXml(join18(logDir, "launchd.out.log"))}</string>
|
|
41034
|
+
<key>StandardErrorPath</key>
|
|
41035
|
+
<string>${escapeXml(join18(logDir, "launchd.err.log"))}</string>
|
|
41036
|
+
<key>EnvironmentVariables</key>
|
|
41037
|
+
<dict>
|
|
41038
|
+
${envEntries}
|
|
41039
|
+
</dict>
|
|
41040
|
+
<key>ProcessType</key>
|
|
41041
|
+
<string>Interactive</string>
|
|
41042
|
+
</dict>
|
|
41043
|
+
</plist>
|
|
41044
|
+
`;
|
|
41045
|
+
};
|
|
41046
|
+
var escapeXml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
41047
|
+
var buildSystemdUnit = (binary, env) => {
|
|
41048
|
+
const envLines = Object.entries(env).map(([k, v]) => `Environment=${k}=${v}`).join(`
|
|
41049
|
+
`);
|
|
41050
|
+
return `[Unit]
|
|
41051
|
+
Description=Multi agent daemon
|
|
41052
|
+
After=network-online.target
|
|
41053
|
+
|
|
41054
|
+
[Service]
|
|
41055
|
+
Type=simple
|
|
41056
|
+
ExecStart=${binary} connect
|
|
41057
|
+
Restart=always
|
|
41058
|
+
RestartSec=3
|
|
41059
|
+
WorkingDirectory=${HOME5}
|
|
41060
|
+
${envLines}
|
|
41061
|
+
|
|
41062
|
+
[Install]
|
|
41063
|
+
WantedBy=default.target
|
|
41064
|
+
`;
|
|
41065
|
+
};
|
|
41066
|
+
var collectServiceEnv = () => {
|
|
41067
|
+
const env = {};
|
|
41068
|
+
if (process.env.PATH)
|
|
41069
|
+
env.PATH = process.env.PATH;
|
|
41070
|
+
if (process.env.HOME)
|
|
41071
|
+
env.HOME = process.env.HOME;
|
|
41072
|
+
if (process.env.MULTI_HOME)
|
|
41073
|
+
env.MULTI_HOME = process.env.MULTI_HOME;
|
|
41074
|
+
if (process.env.MULTI_API)
|
|
41075
|
+
env.MULTI_API = process.env.MULTI_API;
|
|
41076
|
+
if (process.env.SHELL)
|
|
41077
|
+
env.SHELL = process.env.SHELL;
|
|
41078
|
+
if (process.env.LANG)
|
|
41079
|
+
env.LANG = process.env.LANG;
|
|
41080
|
+
return env;
|
|
41081
|
+
};
|
|
41082
|
+
var ensureSupported = exports_Effect.fn("service.ensureSupported")(function* () {
|
|
41083
|
+
if (!isMac() && !isLinux()) {
|
|
41084
|
+
return yield* exports_Effect.fail(new UsageError({
|
|
41085
|
+
message: `service commands are not supported on ${platform()} yet (macOS + Linux only).`
|
|
41086
|
+
}));
|
|
41087
|
+
}
|
|
41088
|
+
});
|
|
41089
|
+
var installMac = exports_Effect.fn("service.installMac")(function* (binary) {
|
|
41090
|
+
const fs3 = yield* FileSystem;
|
|
41091
|
+
const env = collectServiceEnv();
|
|
41092
|
+
const plist = buildPlist(binary, env);
|
|
41093
|
+
yield* fs3.mkdirp(join18(MULTI_DIR, "logs"));
|
|
41094
|
+
yield* fs3.writeText(PLIST_PATH, plist);
|
|
41095
|
+
yield* run6("launchctl", ["unload", PLIST_PATH]).pipe(exports_Effect.ignore);
|
|
41096
|
+
yield* run6("launchctl", ["load", "-w", PLIST_PATH]);
|
|
41097
|
+
console.log(`✅ Installed launchd agent at ${PLIST_PATH}`);
|
|
41098
|
+
console.log(` The daemon will start now and on every login.`);
|
|
41099
|
+
});
|
|
41100
|
+
var installLinux = exports_Effect.fn("service.installLinux")(function* (binary) {
|
|
41101
|
+
const fs3 = yield* FileSystem;
|
|
41102
|
+
const env = collectServiceEnv();
|
|
41103
|
+
const unit = buildSystemdUnit(binary, env);
|
|
41104
|
+
yield* fs3.mkdirp(join18(MULTI_DIR, "logs"));
|
|
41105
|
+
yield* fs3.writeText(SYSTEMD_UNIT_PATH, unit);
|
|
41106
|
+
yield* run6("systemctl", ["--user", "daemon-reload"]);
|
|
41107
|
+
yield* run6("systemctl", ["--user", "enable", "--now", "multi-daemon.service"]);
|
|
41108
|
+
console.log(`✅ Installed systemd user unit at ${SYSTEMD_UNIT_PATH}`);
|
|
41109
|
+
console.log(` The daemon will start now and on every login.`);
|
|
41110
|
+
console.log(` To keep it running after logout: loginctl enable-linger ${process.env.USER || ""}`.trimEnd());
|
|
41111
|
+
});
|
|
41112
|
+
var uninstallMac = exports_Effect.fn("service.uninstallMac")(function* () {
|
|
41113
|
+
const fs3 = yield* FileSystem;
|
|
41114
|
+
if (existsSync18(PLIST_PATH)) {
|
|
41115
|
+
yield* run6("launchctl", ["unload", PLIST_PATH]).pipe(exports_Effect.ignore);
|
|
41116
|
+
yield* fs3.remove(PLIST_PATH);
|
|
41117
|
+
console.log(`\uD83D\uDDD1 Removed ${PLIST_PATH}`);
|
|
41118
|
+
} else {
|
|
41119
|
+
console.log("Nothing to uninstall (no plist found).");
|
|
41120
|
+
}
|
|
41121
|
+
});
|
|
41122
|
+
var uninstallLinux = exports_Effect.fn("service.uninstallLinux")(function* () {
|
|
41123
|
+
const fs3 = yield* FileSystem;
|
|
41124
|
+
if (existsSync18(SYSTEMD_UNIT_PATH)) {
|
|
41125
|
+
yield* run6("systemctl", ["--user", "disable", "--now", "multi-daemon.service"]).pipe(exports_Effect.ignore);
|
|
41126
|
+
yield* fs3.remove(SYSTEMD_UNIT_PATH);
|
|
41127
|
+
yield* run6("systemctl", ["--user", "daemon-reload"]).pipe(exports_Effect.ignore);
|
|
41128
|
+
console.log(`\uD83D\uDDD1 Removed ${SYSTEMD_UNIT_PATH}`);
|
|
41129
|
+
} else {
|
|
41130
|
+
console.log("Nothing to uninstall (no unit found).");
|
|
41131
|
+
}
|
|
41132
|
+
});
|
|
41133
|
+
var serviceCmd = exports_Effect.fn("serviceCmd")(function* (sub) {
|
|
41134
|
+
yield* ensureSupported();
|
|
41135
|
+
switch (sub) {
|
|
41136
|
+
case "install": {
|
|
41137
|
+
const binary = yield* resolveBinary();
|
|
41138
|
+
if (isMac())
|
|
41139
|
+
yield* installMac(binary);
|
|
41140
|
+
else
|
|
41141
|
+
yield* installLinux(binary);
|
|
41142
|
+
return;
|
|
41143
|
+
}
|
|
41144
|
+
case "uninstall": {
|
|
41145
|
+
if (isMac())
|
|
41146
|
+
yield* uninstallMac();
|
|
41147
|
+
else
|
|
41148
|
+
yield* uninstallLinux();
|
|
41149
|
+
return;
|
|
41150
|
+
}
|
|
41151
|
+
case "start": {
|
|
41152
|
+
if (isMac()) {
|
|
41153
|
+
if (!existsSync18(PLIST_PATH)) {
|
|
41154
|
+
return yield* exports_Effect.fail(new DaemonError({
|
|
41155
|
+
message: "Not installed. Run: multi-agent service install"
|
|
41156
|
+
}));
|
|
41157
|
+
}
|
|
41158
|
+
yield* run6("launchctl", ["kickstart", "-k", `gui/${process.getuid?.() ?? ""}/${LABEL}`]).pipe(exports_Effect.ignore);
|
|
41159
|
+
yield* run6("launchctl", ["load", "-w", PLIST_PATH]).pipe(exports_Effect.ignore);
|
|
41160
|
+
console.log("▶ Started.");
|
|
41161
|
+
} else {
|
|
41162
|
+
yield* run6("systemctl", ["--user", "start", "multi-daemon.service"]);
|
|
41163
|
+
console.log("▶ Started.");
|
|
41164
|
+
}
|
|
41165
|
+
return;
|
|
41166
|
+
}
|
|
41167
|
+
case "stop": {
|
|
41168
|
+
if (isMac()) {
|
|
41169
|
+
yield* run6("launchctl", ["unload", PLIST_PATH]).pipe(exports_Effect.ignore);
|
|
41170
|
+
console.log("⏹ Stopped.");
|
|
41171
|
+
} else {
|
|
41172
|
+
yield* run6("systemctl", ["--user", "stop", "multi-daemon.service"]);
|
|
41173
|
+
console.log("⏹ Stopped.");
|
|
41174
|
+
}
|
|
41175
|
+
return;
|
|
41176
|
+
}
|
|
41177
|
+
case "status":
|
|
41178
|
+
case undefined: {
|
|
41179
|
+
const installed = isMac() ? existsSync18(PLIST_PATH) : existsSync18(SYSTEMD_UNIT_PATH);
|
|
41180
|
+
const pid = readPid();
|
|
41181
|
+
const running3 = pid !== null && isRunningPid(pid);
|
|
41182
|
+
console.log(`Service: ${isMac() ? "launchd" : "systemd --user"}`);
|
|
41183
|
+
console.log(`Installed: ${installed ? "yes" : "no"}`);
|
|
41184
|
+
console.log(`Unit path: ${isMac() ? PLIST_PATH : SYSTEMD_UNIT_PATH}`);
|
|
41185
|
+
console.log(`Daemon: ${running3 ? `running (pid ${pid})` : "stopped"}`);
|
|
41186
|
+
if (!installed)
|
|
41187
|
+
console.log(`
|
|
41188
|
+
Not installed. Run: multi-agent service install`);
|
|
41189
|
+
return;
|
|
41190
|
+
}
|
|
41191
|
+
default:
|
|
41192
|
+
return yield* exports_Effect.fail(new UsageError({
|
|
41193
|
+
message: `Unknown service subcommand: ${sub}. Use: install | uninstall | start | stop | status`
|
|
41194
|
+
}));
|
|
41195
|
+
}
|
|
41196
|
+
});
|
|
41197
|
+
|
|
39838
41198
|
// src/index.ts
|
|
39839
41199
|
var VERSION = package_default.version;
|
|
39840
41200
|
var COMMANDS = {
|
|
@@ -39847,7 +41207,8 @@ var COMMANDS = {
|
|
|
39847
41207
|
restart: "Stop and relaunch the daemon in background",
|
|
39848
41208
|
logs: "View execution logs",
|
|
39849
41209
|
reset: "Reset acpx session for an issue (--issue <id>)",
|
|
39850
|
-
worktree: "Manage per-issue worktrees (subcommands: gc, list, ...)"
|
|
41210
|
+
worktree: "Manage per-issue worktrees (subcommands: gc, list, ...)",
|
|
41211
|
+
service: "Manage the always-on daemon (subcommands: install, uninstall, start, stop, status)"
|
|
39851
41212
|
};
|
|
39852
41213
|
function printUsage() {
|
|
39853
41214
|
console.log(`multi-agent v${VERSION}
|
|
@@ -39902,7 +41263,7 @@ var program = exports_Effect.gen(function* () {
|
|
|
39902
41263
|
force: !!values3.force,
|
|
39903
41264
|
noPush: !!values3["no-push"],
|
|
39904
41265
|
key: values3.key
|
|
39905
|
-
})), exports_Match.exhaustive);
|
|
41266
|
+
})), exports_Match.when("service", () => serviceCmd(positionals[1])), exports_Match.exhaustive);
|
|
39906
41267
|
});
|
|
39907
41268
|
var main = program.pipe(exports_Effect.provide(AppLayer), exports_Effect.tapErrorCause((cause3) => exports_Effect.sync(() => {
|
|
39908
41269
|
console.error(exports_Cause.pretty(cause3));
|