@shipers-dev/multi 0.52.0 → 0.64.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 +1626 -303
- 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) {
|
|
@@ -17114,29 +17116,49 @@ async function ensureWorktree(workingDir, issueKey, issueId, opts) {
|
|
|
17114
17116
|
return { path: workingDir, branch: "", created: false };
|
|
17115
17117
|
}
|
|
17116
17118
|
ensureGitignoreEntry(workingDir, ".multi/");
|
|
17117
|
-
const
|
|
17118
|
-
const branch = `multi/${
|
|
17119
|
+
const slug = normalizeKey(opts?.worktreeName ?? issueKey);
|
|
17120
|
+
const branch = `multi/${slug}`;
|
|
17119
17121
|
const wtDir = join5(workingDir, ".multi", "worktrees");
|
|
17120
|
-
const wtPath = join5(wtDir,
|
|
17122
|
+
const wtPath = join5(wtDir, slug);
|
|
17123
|
+
let baseRef = "HEAD";
|
|
17124
|
+
const explicit = opts?.baseBranch || opts?.parentBranch || null;
|
|
17125
|
+
if (explicit) {
|
|
17126
|
+
const localOk = await branchExists(workingDir, explicit);
|
|
17127
|
+
if (localOk) {
|
|
17128
|
+
baseRef = explicit;
|
|
17129
|
+
} else {
|
|
17130
|
+
const fetched = await run3(workingDir, "git", ["fetch", "origin", `${explicit}:${explicit}`]);
|
|
17131
|
+
if (fetched.code === 0)
|
|
17132
|
+
baseRef = explicit;
|
|
17133
|
+
}
|
|
17134
|
+
}
|
|
17121
17135
|
if (existsSync3(wtPath)) {
|
|
17136
|
+
if (await isDirty(wtPath)) {
|
|
17137
|
+
throw new WorktreeDirtyError({ path: wtPath, branch, reason: "dirty" });
|
|
17138
|
+
}
|
|
17139
|
+
if (opts?.baseBranch) {
|
|
17140
|
+
const mergeBase = await run3(wtPath, "git", ["merge-base", branch, opts.baseBranch]);
|
|
17141
|
+
if (mergeBase.code === 0 && mergeBase.stdout) {
|
|
17142
|
+
const forkOnBase = await run3(workingDir, "git", ["merge-base", "--is-ancestor", mergeBase.stdout, opts.baseBranch]);
|
|
17143
|
+
if (forkOnBase.code === 1) {
|
|
17144
|
+
const baseHead = await run3(workingDir, "git", ["rev-parse", "--verify", `${opts.baseBranch}^{commit}`]);
|
|
17145
|
+
throw new WorktreeDirtyError({
|
|
17146
|
+
path: wtPath,
|
|
17147
|
+
branch,
|
|
17148
|
+
reason: "base_mismatch",
|
|
17149
|
+
expectedBase: opts.baseBranch,
|
|
17150
|
+
actualBase: baseHead.code === 0 ? baseHead.stdout : mergeBase.stdout
|
|
17151
|
+
});
|
|
17152
|
+
}
|
|
17153
|
+
}
|
|
17154
|
+
}
|
|
17122
17155
|
if (issueId)
|
|
17123
|
-
setWorktreeIndexEntry(workingDir,
|
|
17156
|
+
setWorktreeIndexEntry(workingDir, slug, issueId);
|
|
17124
17157
|
return { path: wtPath, branch, created: false };
|
|
17125
17158
|
}
|
|
17126
17159
|
try {
|
|
17127
17160
|
mkdirSync3(wtDir, { recursive: true });
|
|
17128
17161
|
} catch {}
|
|
17129
|
-
let baseRef = "HEAD";
|
|
17130
|
-
if (opts?.parentBranch) {
|
|
17131
|
-
const localOk = await branchExists(workingDir, opts.parentBranch);
|
|
17132
|
-
if (localOk) {
|
|
17133
|
-
baseRef = opts.parentBranch;
|
|
17134
|
-
} else {
|
|
17135
|
-
const fetched = await run3(workingDir, "git", ["fetch", "origin", `${opts.parentBranch}:${opts.parentBranch}`]);
|
|
17136
|
-
if (fetched.code === 0)
|
|
17137
|
-
baseRef = opts.parentBranch;
|
|
17138
|
-
}
|
|
17139
|
-
}
|
|
17140
17162
|
const exists4 = await branchExists(workingDir, branch);
|
|
17141
17163
|
const args2 = exists4 ? ["worktree", "add", wtPath, branch] : ["worktree", "add", "-b", branch, wtPath, baseRef];
|
|
17142
17164
|
const r = await run3(workingDir, "git", args2);
|
|
@@ -17145,7 +17167,7 @@ async function ensureWorktree(workingDir, issueKey, issueId, opts) {
|
|
|
17145
17167
|
}
|
|
17146
17168
|
await linkIgnoredFiles(workingDir, wtPath);
|
|
17147
17169
|
if (issueId)
|
|
17148
|
-
setWorktreeIndexEntry(workingDir,
|
|
17170
|
+
setWorktreeIndexEntry(workingDir, slug, issueId);
|
|
17149
17171
|
return { path: wtPath, branch, created: true };
|
|
17150
17172
|
}
|
|
17151
17173
|
async function linkIgnoredFiles(workingDir, wtPath) {
|
|
@@ -17272,7 +17294,17 @@ async function squashMergeChild(wtPath, childBranch, childKey) {
|
|
|
17272
17294
|
result.message = `${childKey} squash-merged (${stagedFiles.length} file${stagedFiles.length === 1 ? "" : "s"})`;
|
|
17273
17295
|
return result;
|
|
17274
17296
|
}
|
|
17275
|
-
var
|
|
17297
|
+
var WorktreeDirtyError;
|
|
17298
|
+
var init_worktree = __esm(() => {
|
|
17299
|
+
WorktreeDirtyError = class WorktreeDirtyError extends Error {
|
|
17300
|
+
details;
|
|
17301
|
+
code = "E_WORKTREE_DIRTY";
|
|
17302
|
+
constructor(details) {
|
|
17303
|
+
super(`worktree dirty or base-mismatched at ${details.path} (${details.reason})`);
|
|
17304
|
+
this.details = details;
|
|
17305
|
+
}
|
|
17306
|
+
};
|
|
17307
|
+
});
|
|
17276
17308
|
|
|
17277
17309
|
// ../../node_modules/zod/v4/core/core.js
|
|
17278
17310
|
function $constructor(name, initializer, params) {
|
|
@@ -27970,7 +28002,7 @@ function finalize(ctx, schema) {
|
|
|
27970
28002
|
result.$schema = "http://json-schema.org/draft-07/schema#";
|
|
27971
28003
|
} else if (ctx.target === "draft-04") {
|
|
27972
28004
|
result.$schema = "http://json-schema.org/draft-04/schema#";
|
|
27973
|
-
} else if (ctx.target === "openapi-3.0") {}
|
|
28005
|
+
} else if (ctx.target === "openapi-3.0") {}
|
|
27974
28006
|
if (ctx.external?.uri) {
|
|
27975
28007
|
const id3 = ctx.external.registry.get(schema)?.id;
|
|
27976
28008
|
if (!id3)
|
|
@@ -28235,7 +28267,7 @@ var formatMap, stringProcessor = (schema, ctx, _json, _params) => {
|
|
|
28235
28267
|
if (val === undefined) {
|
|
28236
28268
|
if (ctx.unrepresentable === "throw") {
|
|
28237
28269
|
throw new Error("Literal `undefined` cannot be represented in JSON Schema");
|
|
28238
|
-
}
|
|
28270
|
+
}
|
|
28239
28271
|
} else if (typeof val === "bigint") {
|
|
28240
28272
|
if (ctx.unrepresentable === "throw") {
|
|
28241
28273
|
throw new Error("BigInt literals cannot be represented in JSON Schema");
|
|
@@ -33196,6 +33228,35 @@ function isPidAlive(pid) {
|
|
|
33196
33228
|
return false;
|
|
33197
33229
|
}
|
|
33198
33230
|
}
|
|
33231
|
+
function listAdapterProcs() {
|
|
33232
|
+
try {
|
|
33233
|
+
const r = spawnSync("ps", ["-axo", "pid=,ppid=,command="], { encoding: "utf8" });
|
|
33234
|
+
if (r.status !== 0)
|
|
33235
|
+
return [];
|
|
33236
|
+
const out = [];
|
|
33237
|
+
for (const line of (r.stdout || "").split(`
|
|
33238
|
+
`)) {
|
|
33239
|
+
const m = line.match(/^\s*(\d+)\s+(\d+)\s+(.*)$/);
|
|
33240
|
+
if (!m)
|
|
33241
|
+
continue;
|
|
33242
|
+
const command = m[3];
|
|
33243
|
+
if (!command.includes("claude-code-acp") && !command.includes("acpx"))
|
|
33244
|
+
continue;
|
|
33245
|
+
out.push({ pid: parseInt(m[1], 10), ppid: parseInt(m[2], 10), command });
|
|
33246
|
+
}
|
|
33247
|
+
return out;
|
|
33248
|
+
} catch {
|
|
33249
|
+
return [];
|
|
33250
|
+
}
|
|
33251
|
+
}
|
|
33252
|
+
function killOne(pid, log3, label, command) {
|
|
33253
|
+
log3(`[reap] ${label} pid=${pid} cmd=${command.slice(0, 120)}`);
|
|
33254
|
+
tryKill(pid, "SIGTERM");
|
|
33255
|
+
setTimeout(() => {
|
|
33256
|
+
if (isPidAlive(pid))
|
|
33257
|
+
tryKill(pid, "SIGKILL");
|
|
33258
|
+
}, 1500).unref?.();
|
|
33259
|
+
}
|
|
33199
33260
|
function reapStaleAdapters(log3 = () => {}) {
|
|
33200
33261
|
ensureDir();
|
|
33201
33262
|
let entries2 = [];
|
|
@@ -33247,8 +33308,37 @@ function reapStaleAdapters(log3 = () => {}) {
|
|
|
33247
33308
|
} catch {}
|
|
33248
33309
|
reaped++;
|
|
33249
33310
|
}
|
|
33311
|
+
const accountedFor = new Set;
|
|
33312
|
+
try {
|
|
33313
|
+
for (const name of readdirSync2(ADAPTERS_DIR)) {
|
|
33314
|
+
const m = name.match(/^(\d+)\.json$/);
|
|
33315
|
+
if (m)
|
|
33316
|
+
accountedFor.add(parseInt(m[1], 10));
|
|
33317
|
+
}
|
|
33318
|
+
} catch {}
|
|
33319
|
+
for (const p of listAdapterProcs()) {
|
|
33320
|
+
if (p.ppid !== 1)
|
|
33321
|
+
continue;
|
|
33322
|
+
if (accountedFor.has(p.pid))
|
|
33323
|
+
continue;
|
|
33324
|
+
killOne(p.pid, log3, "killing pidfile-less orphan", p.command);
|
|
33325
|
+
reaped++;
|
|
33326
|
+
}
|
|
33250
33327
|
return reaped;
|
|
33251
33328
|
}
|
|
33329
|
+
function killDaemonChildren(daemonPid, log3 = () => {}) {
|
|
33330
|
+
let killed = 0;
|
|
33331
|
+
for (const p of listAdapterProcs()) {
|
|
33332
|
+
if (p.ppid !== daemonPid)
|
|
33333
|
+
continue;
|
|
33334
|
+
killOne(p.pid, log3, "daemon-exit killing", p.command);
|
|
33335
|
+
try {
|
|
33336
|
+
unlinkSync3(pidfilePath(p.pid));
|
|
33337
|
+
} catch {}
|
|
33338
|
+
killed++;
|
|
33339
|
+
}
|
|
33340
|
+
return killed;
|
|
33341
|
+
}
|
|
33252
33342
|
var ADAPTERS_DIR;
|
|
33253
33343
|
var init_adapter_pidfile = __esm(() => {
|
|
33254
33344
|
ADAPTERS_DIR = join7(homedir(), ".multi", "adapters");
|
|
@@ -33456,8 +33546,20 @@ async function runAcp(opts) {
|
|
|
33456
33546
|
return { stopReason, sessionId: activeSessionId, summaryText };
|
|
33457
33547
|
} finally {
|
|
33458
33548
|
try {
|
|
33459
|
-
child.kill();
|
|
33549
|
+
child.kill("SIGTERM");
|
|
33550
|
+
} catch {}
|
|
33551
|
+
const killTimer = setTimeout(() => {
|
|
33552
|
+
try {
|
|
33553
|
+
child.kill("SIGKILL");
|
|
33554
|
+
} catch {}
|
|
33555
|
+
}, 3000);
|
|
33556
|
+
try {
|
|
33557
|
+
killTimer.unref?.();
|
|
33460
33558
|
} catch {}
|
|
33559
|
+
try {
|
|
33560
|
+
await child.exited;
|
|
33561
|
+
} catch {}
|
|
33562
|
+
clearTimeout(killTimer);
|
|
33461
33563
|
if (typeof child.pid === "number")
|
|
33462
33564
|
removeAdapterPidfile(child.pid);
|
|
33463
33565
|
}
|
|
@@ -33950,7 +34052,8 @@ var init_streams = __esm(() => {
|
|
|
33950
34052
|
"stopped",
|
|
33951
34053
|
"queued",
|
|
33952
34054
|
"worktree_created",
|
|
33953
|
-
"worktree_error"
|
|
34055
|
+
"worktree_error",
|
|
34056
|
+
"worktree_dirty"
|
|
33954
34057
|
];
|
|
33955
34058
|
StreamEventTypeSchema = exports_external.enum(STREAM_EVENT_TYPES);
|
|
33956
34059
|
StreamEventInputSchema = exports_external.object({
|
|
@@ -34148,12 +34251,12 @@ function parsePlanBlocks(text) {
|
|
|
34148
34251
|
}
|
|
34149
34252
|
return { actions, errors: errors3 };
|
|
34150
34253
|
}
|
|
34151
|
-
var PLAN_SCHEMA_VERSION =
|
|
34254
|
+
var PLAN_SCHEMA_VERSION = 11, Priority, AssigneeType, IssueStatus, SessionRole, SkillFile, EvalPolicy, PlanActionSchema, PlanEnvelopeSchema, UiBlockSchema, UI_FENCE_RE, FENCE_RE;
|
|
34152
34255
|
var init_plans = __esm(() => {
|
|
34153
34256
|
init_zod();
|
|
34154
34257
|
Priority = exports_external.enum(["low", "medium", "high"]);
|
|
34155
34258
|
AssigneeType = exports_external.enum(["human", "agent"]);
|
|
34156
|
-
IssueStatus = exports_external.enum(["todo", "in_progress", "blocked", "done", "archived", "failed", "stopped"
|
|
34259
|
+
IssueStatus = exports_external.enum(["todo", "in_progress", "blocked", "done", "archived", "failed", "stopped"]);
|
|
34157
34260
|
SessionRole = exports_external.enum(["implementer", "reviewer", "test-fixer"]);
|
|
34158
34261
|
SkillFile = exports_external.object({ path: exports_external.string().min(1), content: exports_external.string() });
|
|
34159
34262
|
EvalPolicy = exports_external.object({
|
|
@@ -34171,6 +34274,8 @@ var init_plans = __esm(() => {
|
|
|
34171
34274
|
priority: Priority.optional(),
|
|
34172
34275
|
assignee_type: AssigneeType.optional(),
|
|
34173
34276
|
assignee_id: exports_external.string().optional(),
|
|
34277
|
+
base_ref: exports_external.string().min(1).max(200).optional(),
|
|
34278
|
+
worktree: exports_external.string().min(1).max(60).regex(/^[a-z0-9][a-z0-9_\-\/]*$/i).optional(),
|
|
34174
34279
|
parent_id: exports_external.string().optional(),
|
|
34175
34280
|
blocked_by: exports_external.array(exports_external.string().min(1)).optional(),
|
|
34176
34281
|
await_children: exports_external.boolean().optional(),
|
|
@@ -34193,6 +34298,14 @@ var init_plans = __esm(() => {
|
|
|
34193
34298
|
id: exports_external.string().min(1),
|
|
34194
34299
|
assignee_id: exports_external.string().min(1)
|
|
34195
34300
|
}),
|
|
34301
|
+
exports_external.object({
|
|
34302
|
+
type: exports_external.literal("handoff"),
|
|
34303
|
+
target_agent_id: exports_external.string().min(1),
|
|
34304
|
+
prompt: exports_external.string().min(1).max(8000),
|
|
34305
|
+
expect: exports_external.enum(["summary", "patch"]).optional(),
|
|
34306
|
+
return_to: exports_external.string().min(1).optional(),
|
|
34307
|
+
title: exports_external.string().min(1).max(200).optional()
|
|
34308
|
+
}),
|
|
34196
34309
|
exports_external.object({
|
|
34197
34310
|
type: exports_external.literal("issue.comment"),
|
|
34198
34311
|
id: exports_external.string().min(1),
|
|
@@ -34222,6 +34335,18 @@ var init_plans = __esm(() => {
|
|
|
34222
34335
|
query: exports_external.string().min(1).max(500),
|
|
34223
34336
|
limit: exports_external.number().int().min(1).max(100).optional()
|
|
34224
34337
|
}),
|
|
34338
|
+
exports_external.object({
|
|
34339
|
+
type: exports_external.literal("issue.get"),
|
|
34340
|
+
id: exports_external.string().min(1)
|
|
34341
|
+
}),
|
|
34342
|
+
exports_external.object({
|
|
34343
|
+
type: exports_external.literal("issue.start"),
|
|
34344
|
+
id: exports_external.string().min(1)
|
|
34345
|
+
}),
|
|
34346
|
+
exports_external.object({
|
|
34347
|
+
type: exports_external.literal("issue.stop"),
|
|
34348
|
+
id: exports_external.string().min(1)
|
|
34349
|
+
}),
|
|
34225
34350
|
exports_external.object({
|
|
34226
34351
|
type: exports_external.literal("agent.create"),
|
|
34227
34352
|
name: exports_external.string().min(1).max(120),
|
|
@@ -34359,13 +34484,13 @@ var init_chat = __esm(() => {
|
|
|
34359
34484
|
title: exports_external.string().min(1).max(200),
|
|
34360
34485
|
primary_agent_id: exports_external.string().nullable().optional(),
|
|
34361
34486
|
device_id: exports_external.string().nullable().optional(),
|
|
34362
|
-
runtime: exports_external.string().nullable().optional()
|
|
34487
|
+
runtime: exports_external.string().nullable().optional(),
|
|
34488
|
+
working_dir: exports_external.string().nullable().optional(),
|
|
34489
|
+
branch: exports_external.string().nullable().optional()
|
|
34363
34490
|
});
|
|
34364
34491
|
UpdateChatBodySchema = exports_external.object({
|
|
34365
34492
|
title: exports_external.string().min(1).max(200).optional(),
|
|
34366
|
-
primary_agent_id: exports_external.string().nullable().optional()
|
|
34367
|
-
device_id: exports_external.string().nullable().optional(),
|
|
34368
|
-
runtime: exports_external.string().nullable().optional()
|
|
34493
|
+
primary_agent_id: exports_external.string().nullable().optional()
|
|
34369
34494
|
});
|
|
34370
34495
|
});
|
|
34371
34496
|
// ../lib/index.ts
|
|
@@ -34679,14 +34804,28 @@ function log3(msg) {
|
|
|
34679
34804
|
appendFileSync4(LOG_PATH3, line);
|
|
34680
34805
|
process.stdout.write(line);
|
|
34681
34806
|
}
|
|
34682
|
-
function nestedIssueBase(apiUrl, wsId, projectId, issueId) {
|
|
34683
|
-
return `${apiUrl}/api/workspaces/${wsId}/projects/${projectId}/issues/${issueId}`;
|
|
34684
|
-
}
|
|
34685
34807
|
async function patchIssueStatus(apiUrl, wsId, issueId, status3) {
|
|
34808
|
+
if (status3 === "todo") {
|
|
34809
|
+
throw new Error("patchIssueStatus: cannot return an issue to todo (Phase 4 one-way invariant)");
|
|
34810
|
+
}
|
|
34686
34811
|
if (!wsId)
|
|
34687
34812
|
return { success: false, error: "no workspace id" };
|
|
34688
34813
|
return apiClient.post(`${apiUrl}/api/workspaces/${wsId}/agent/issues/mutate`, { action: "update", id: issueId, status: status3 });
|
|
34689
34814
|
}
|
|
34815
|
+
async function postBoundChatMessage(apiUrl, task, body, authorKind = "agent") {
|
|
34816
|
+
const wsId = task?.tenant_workspace_id ?? null;
|
|
34817
|
+
if (!wsId)
|
|
34818
|
+
return;
|
|
34819
|
+
try {
|
|
34820
|
+
await apiClient.post(`${apiUrl}/api/workspaces/${wsId}/agent/issues/mutate`, {
|
|
34821
|
+
action: "comment",
|
|
34822
|
+
id: task.issue_id,
|
|
34823
|
+
body
|
|
34824
|
+
});
|
|
34825
|
+
} catch (e) {
|
|
34826
|
+
log3(`[bound-chat] post failed: ${String(e).slice(0, 200)}`);
|
|
34827
|
+
}
|
|
34828
|
+
}
|
|
34690
34829
|
async function markStopped(apiUrl, task, reason) {
|
|
34691
34830
|
const wsId = task?.tenant_workspace_id ?? null;
|
|
34692
34831
|
const pid = task?.project_id ?? null;
|
|
@@ -34696,14 +34835,7 @@ async function markStopped(apiUrl, task, reason) {
|
|
|
34696
34835
|
await patchIssueStatus(apiUrl, wsId, issueId, "stopped");
|
|
34697
34836
|
} catch {}
|
|
34698
34837
|
try {
|
|
34699
|
-
|
|
34700
|
-
await apiClient.post(`${nestedIssueBase(apiUrl, wsId, pid, issueId)}/comments`, {
|
|
34701
|
-
author_type: "agent",
|
|
34702
|
-
author_id: "daemon",
|
|
34703
|
-
author_name: "daemon",
|
|
34704
|
-
body: `⏹ Stopped: ${reason}`
|
|
34705
|
-
});
|
|
34706
|
-
}
|
|
34838
|
+
await postBoundChatMessage(apiUrl, task, `⏹ Stopped: ${reason}`);
|
|
34707
34839
|
} catch {}
|
|
34708
34840
|
await postStream(apiUrl, issueId, "stopped", { reason });
|
|
34709
34841
|
}
|
|
@@ -34754,9 +34886,54 @@ async function handleRunTask(apiUrl, deviceId, task, detected, ctx) {
|
|
|
34754
34886
|
const ISSUE_BASE = tenantWsId && projectId ? `${apiUrl}/api/workspaces/${tenantWsId}/projects/${projectId}/issues/${issueId}` : null;
|
|
34755
34887
|
let workingDir = baseWorkingDir;
|
|
34756
34888
|
let worktreeBranch = "";
|
|
34757
|
-
|
|
34889
|
+
const inlineMode = task.inline_mode === true;
|
|
34890
|
+
const baseBranch = task.base_branch ?? null;
|
|
34891
|
+
const worktreeName = task.worktree_name ?? null;
|
|
34892
|
+
let inlineRestoreHead = null;
|
|
34893
|
+
const blockOnDirty = async (reason, extra = {}) => {
|
|
34894
|
+
await postStream(apiUrl, issueId, "worktree_dirty", { reason, ...extra });
|
|
34895
|
+
if (tenantWsId) {
|
|
34896
|
+
try {
|
|
34897
|
+
await patchIssueStatus(apiUrl, tenantWsId, issueId, "blocked");
|
|
34898
|
+
} catch {}
|
|
34899
|
+
}
|
|
34900
|
+
await postStream(apiUrl, issueId, "run_finished", { stopReason: "blocked", duration_ms: 0 });
|
|
34901
|
+
};
|
|
34902
|
+
if (baseWorkingDir && inlineMode) {
|
|
34758
34903
|
try {
|
|
34759
|
-
const
|
|
34904
|
+
const status3 = Bun.spawnSync(["git", "status", "--porcelain"], { cwd: baseWorkingDir, stdout: "pipe" });
|
|
34905
|
+
const dirty = (status3.stdout?.toString() || "").trim().length > 0;
|
|
34906
|
+
if (dirty) {
|
|
34907
|
+
await blockOnDirty("inline_dirty", { path: baseWorkingDir });
|
|
34908
|
+
return;
|
|
34909
|
+
}
|
|
34910
|
+
if (baseBranch) {
|
|
34911
|
+
const origSymRef = Bun.spawnSync(["git", "symbolic-ref", "--quiet", "HEAD"], { cwd: baseWorkingDir, stdout: "pipe" });
|
|
34912
|
+
if (origSymRef.exitCode === 0) {
|
|
34913
|
+
inlineRestoreHead = (origSymRef.stdout?.toString() || "").trim().replace(/^refs\/heads\//, "") || null;
|
|
34914
|
+
} else {
|
|
34915
|
+
const origSha = Bun.spawnSync(["git", "rev-parse", "HEAD"], { cwd: baseWorkingDir, stdout: "pipe" });
|
|
34916
|
+
inlineRestoreHead = (origSha.stdout?.toString() || "").trim() || null;
|
|
34917
|
+
}
|
|
34918
|
+
const co = Bun.spawnSync(["git", "checkout", baseBranch], { cwd: baseWorkingDir, stdout: "pipe", stderr: "pipe" });
|
|
34919
|
+
if (co.exitCode !== 0) {
|
|
34920
|
+
await postStream(apiUrl, issueId, "worktree_error", { mode: "inline", message: co.stderr?.toString() || "checkout failed" });
|
|
34921
|
+
await blockOnDirty("inline_checkout_failed", { baseBranch });
|
|
34922
|
+
return;
|
|
34923
|
+
}
|
|
34924
|
+
worktreeBranch = baseBranch;
|
|
34925
|
+
}
|
|
34926
|
+
await postStream(apiUrl, issueId, "worktree_created", { path: baseWorkingDir, branch: worktreeBranch, reused: true, inline: true });
|
|
34927
|
+
} catch (e) {
|
|
34928
|
+
await postStream(apiUrl, issueId, "worktree_error", { mode: "inline", message: fmtError(e) });
|
|
34929
|
+
}
|
|
34930
|
+
} else if (baseWorkingDir) {
|
|
34931
|
+
try {
|
|
34932
|
+
const wt = await ensureWorktree(baseWorkingDir, task.key || issueId, issueId, {
|
|
34933
|
+
baseBranch: baseBranch ?? undefined,
|
|
34934
|
+
parentBranch: task.parent_branch ?? undefined,
|
|
34935
|
+
worktreeName: worktreeName ?? undefined
|
|
34936
|
+
});
|
|
34760
34937
|
workingDir = wt.path;
|
|
34761
34938
|
worktreeBranch = wt.branch;
|
|
34762
34939
|
await postStream(apiUrl, issueId, "worktree_created", { path: wt.path, branch: wt.branch, reused: !wt.created });
|
|
@@ -34768,6 +34945,10 @@ async function handleRunTask(apiUrl, deviceId, task, detected, ctx) {
|
|
|
34768
34945
|
}
|
|
34769
34946
|
}
|
|
34770
34947
|
} catch (e) {
|
|
34948
|
+
if (e?.code === "E_WORKTREE_DIRTY") {
|
|
34949
|
+
await blockOnDirty("worktree_dirty", { details: e.details });
|
|
34950
|
+
return;
|
|
34951
|
+
}
|
|
34771
34952
|
await postStream(apiUrl, issueId, "worktree_error", { message: fmtError(e) });
|
|
34772
34953
|
}
|
|
34773
34954
|
}
|
|
@@ -34817,48 +34998,18 @@ async function handleRunTask(apiUrl, deviceId, task, detected, ctx) {
|
|
|
34817
34998
|
if (task.from_comment_id && task.tenant_workspace_id && task.project_id) {
|
|
34818
34999
|
const baseDir = workingDir || join12(MULTI_DIR4, "tmp", issueId);
|
|
34819
35000
|
const inDir = join12(baseDir, ".multi-in", task.from_comment_id);
|
|
34820
|
-
|
|
34821
|
-
|
|
34822
|
-
|
|
35001
|
+
try {
|
|
35002
|
+
attachmentRefs = await downloadCommentAttachments(apiUrl, task.tenant_workspace_id, task.project_id, issueId, task.from_comment_id, inDir);
|
|
35003
|
+
if (attachmentRefs.length)
|
|
35004
|
+
log3(` fetched ${attachmentRefs.length} attachment(s) → ${inDir}`);
|
|
35005
|
+
} catch (e) {
|
|
35006
|
+
log3(`[from_comment_id] attachment fetch failed — comments table may be gone (Phase 6): ${String(e).slice(0, 200)}`);
|
|
35007
|
+
}
|
|
34823
35008
|
}
|
|
34824
35009
|
const outDir = join12(workingDir || join12(MULTI_DIR4, "tmp", issueId), ".multi-out");
|
|
34825
|
-
let liveCommentId;
|
|
34826
|
-
let liveBody = "";
|
|
34827
35010
|
let hadError = false;
|
|
34828
35011
|
let hasAssistantText = false;
|
|
34829
|
-
let liveCommentPromise = null;
|
|
34830
35012
|
let memorySummary = null;
|
|
34831
|
-
const ensureLiveComment = () => {
|
|
34832
|
-
if (liveCommentId)
|
|
34833
|
-
return Promise.resolve();
|
|
34834
|
-
if (!ISSUE_BASE)
|
|
34835
|
-
return Promise.resolve();
|
|
34836
|
-
if (!liveCommentPromise) {
|
|
34837
|
-
liveCommentPromise = apiClient.post(`${ISSUE_BASE}/comments`, {
|
|
34838
|
-
author_type: "agent",
|
|
34839
|
-
author_id: task.agent_id,
|
|
34840
|
-
author_name: "agent",
|
|
34841
|
-
body: "…"
|
|
34842
|
-
}).then((res) => {
|
|
34843
|
-
liveCommentId = res.data?.id;
|
|
34844
|
-
});
|
|
34845
|
-
}
|
|
34846
|
-
return liveCommentPromise;
|
|
34847
|
-
};
|
|
34848
|
-
const patchLive = async (body) => {
|
|
34849
|
-
if (!liveCommentId || !ISSUE_BASE)
|
|
34850
|
-
return;
|
|
34851
|
-
try {
|
|
34852
|
-
await apiClient.patch(`${ISSUE_BASE}/comments/${liveCommentId}`, { body: body || "…" });
|
|
34853
|
-
} catch {}
|
|
34854
|
-
};
|
|
34855
|
-
const postComment = async (body) => {
|
|
34856
|
-
if (!ISSUE_BASE)
|
|
34857
|
-
return;
|
|
34858
|
-
try {
|
|
34859
|
-
await apiClient.post(`${ISSUE_BASE}/comments`, { author_type: "agent", author_id: task.agent_id, author_name: "agent", body });
|
|
34860
|
-
} catch {}
|
|
34861
|
-
};
|
|
34862
35013
|
const turn = {
|
|
34863
35014
|
blocks: [],
|
|
34864
35015
|
tools: new Map,
|
|
@@ -34961,18 +35112,7 @@ _${bits.join(" · ")}_`);
|
|
|
34961
35112
|
|
|
34962
35113
|
`) || "…";
|
|
34963
35114
|
};
|
|
34964
|
-
|
|
34965
|
-
const schedulePatch = () => {
|
|
34966
|
-
if (renderScheduled)
|
|
34967
|
-
return;
|
|
34968
|
-
renderScheduled = true;
|
|
34969
|
-
queueMicrotask(async () => {
|
|
34970
|
-
renderScheduled = false;
|
|
34971
|
-
try {
|
|
34972
|
-
await patchLive(render2());
|
|
34973
|
-
} catch {}
|
|
34974
|
-
});
|
|
34975
|
-
};
|
|
35115
|
+
const schedulePatch = () => {};
|
|
34976
35116
|
const eventHandler = async (event) => {
|
|
34977
35117
|
if (event.event_type === "error")
|
|
34978
35118
|
hadError = true;
|
|
@@ -34985,7 +35125,6 @@ _${bits.join(" · ")}_`);
|
|
|
34985
35125
|
break;
|
|
34986
35126
|
}
|
|
34987
35127
|
case "assistant_text": {
|
|
34988
|
-
await ensureLiveComment();
|
|
34989
35128
|
appendText(p.text);
|
|
34990
35129
|
hasAssistantText = true;
|
|
34991
35130
|
schedulePatch();
|
|
@@ -34993,7 +35132,6 @@ _${bits.join(" · ")}_`);
|
|
|
34993
35132
|
}
|
|
34994
35133
|
case "stdout": {
|
|
34995
35134
|
if (p.line) {
|
|
34996
|
-
await ensureLiveComment();
|
|
34997
35135
|
appendText((turn.blocks.length ? `
|
|
34998
35136
|
` : "") + p.line);
|
|
34999
35137
|
hasAssistantText = true;
|
|
@@ -35002,14 +35140,12 @@ _${bits.join(" · ")}_`);
|
|
|
35002
35140
|
break;
|
|
35003
35141
|
}
|
|
35004
35142
|
case "tool_call": {
|
|
35005
|
-
await ensureLiveComment();
|
|
35006
35143
|
const id3 = p.id || `anon-${turn.tools.size}`;
|
|
35007
35144
|
upsertTool(id3, { tool: p.tool, kind: p.kind, status: p.status, input: p.input });
|
|
35008
35145
|
schedulePatch();
|
|
35009
35146
|
break;
|
|
35010
35147
|
}
|
|
35011
35148
|
case "tool_result": {
|
|
35012
|
-
await ensureLiveComment();
|
|
35013
35149
|
const lastToolId = [...turn.blocks].reverse().find((b) => b.kind === "tool")?.id;
|
|
35014
35150
|
const id3 = p.tool_use_id || lastToolId;
|
|
35015
35151
|
const entry = id3 ? turn.tools.get(id3) : undefined;
|
|
@@ -35026,7 +35162,6 @@ _${bits.join(" · ")}_`);
|
|
|
35026
35162
|
break;
|
|
35027
35163
|
}
|
|
35028
35164
|
case "result": {
|
|
35029
|
-
await ensureLiveComment();
|
|
35030
35165
|
if (p.is_error)
|
|
35031
35166
|
hadError = true;
|
|
35032
35167
|
if (!hasAssistantText && p.result) {
|
|
@@ -35038,7 +35173,6 @@ _${bits.join(" · ")}_`);
|
|
|
35038
35173
|
break;
|
|
35039
35174
|
}
|
|
35040
35175
|
case "error": {
|
|
35041
|
-
await ensureLiveComment();
|
|
35042
35176
|
turn.error = p.message || "error";
|
|
35043
35177
|
schedulePatch();
|
|
35044
35178
|
break;
|
|
@@ -35265,10 +35399,33 @@ ${userPart}` : userPart;
|
|
|
35265
35399
|
for await (const event of runner(task))
|
|
35266
35400
|
await eventHandler(event);
|
|
35267
35401
|
}
|
|
35268
|
-
|
|
35269
|
-
const
|
|
35270
|
-
|
|
35271
|
-
|
|
35402
|
+
const finalBody = (() => {
|
|
35403
|
+
const parts2 = [];
|
|
35404
|
+
for (const b of turn.blocks) {
|
|
35405
|
+
if (b.kind === "text") {
|
|
35406
|
+
if (b.text)
|
|
35407
|
+
parts2.push(b.text);
|
|
35408
|
+
} else if (b.kind === "tool") {
|
|
35409
|
+
const t = turn.tools.get(b.id);
|
|
35410
|
+
if (t)
|
|
35411
|
+
parts2.push(`- \uD83D\uDD27 ${t.tool}${t.status === "completed" ? " ✓" : t.status === "failed" ? " ✗" : ""}`);
|
|
35412
|
+
}
|
|
35413
|
+
}
|
|
35414
|
+
return parts2.join(`
|
|
35415
|
+
`).trim();
|
|
35416
|
+
})();
|
|
35417
|
+
if (finalBody)
|
|
35418
|
+
await postBoundChatMessage(apiUrl, task, finalBody);
|
|
35419
|
+
if (existsSync12(outDir)) {
|
|
35420
|
+
const files = readdirSync3(outDir).filter((f) => {
|
|
35421
|
+
try {
|
|
35422
|
+
return statSync2(join12(outDir, f)).isFile();
|
|
35423
|
+
} catch {
|
|
35424
|
+
return false;
|
|
35425
|
+
}
|
|
35426
|
+
});
|
|
35427
|
+
if (files.length > 0)
|
|
35428
|
+
log3(` [Phase 4.5] agent wrote ${files.length} file(s) to .multi-out; attachments via chat upload not wired in this path`);
|
|
35272
35429
|
}
|
|
35273
35430
|
if (ctx) {
|
|
35274
35431
|
const fullText = turn.blocks.filter((b) => b.kind === "text").map((b) => b.text).join(`
|
|
@@ -35278,15 +35435,14 @@ ${userPart}` : userPart;
|
|
|
35278
35435
|
const summary5 = await executePlanActions(apiUrl, task, actions, ctx, parseErrors);
|
|
35279
35436
|
if (summary5) {
|
|
35280
35437
|
try {
|
|
35281
|
-
|
|
35282
|
-
await apiClient.post(`${ISSUE_BASE}/comments`, { author_type: "agent", author_id: task.agent_id, author_name: "agent", body: summary5 });
|
|
35438
|
+
await postBoundChatMessage(apiUrl, task, summary5);
|
|
35283
35439
|
} catch {}
|
|
35284
35440
|
}
|
|
35285
35441
|
}
|
|
35286
35442
|
}
|
|
35287
35443
|
if (!hasAssistantText && !hadError) {
|
|
35288
35444
|
const stopReason = turn.result?.stopReason || "unknown";
|
|
35289
|
-
await
|
|
35445
|
+
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.`);
|
|
35290
35446
|
log3(` ⚠ ${task.key} produced no assistant output (stopReason=${stopReason})`);
|
|
35291
35447
|
}
|
|
35292
35448
|
if (ctx?.runEntry?.stopped) {
|
|
@@ -35319,11 +35475,17 @@ ${userPart}` : userPart;
|
|
|
35319
35475
|
log3(` ⏹ ${task.key} stopped (${msg})`);
|
|
35320
35476
|
} else {
|
|
35321
35477
|
await postStream(apiUrl, issueId, "error", { message: msg });
|
|
35322
|
-
await
|
|
35478
|
+
await postBoundChatMessage(apiUrl, task, `❌ spawn error: ${msg}`);
|
|
35323
35479
|
if (ISSUE_BASE)
|
|
35324
35480
|
await apiClient.post(`${ISSUE_BASE}/fail`, {});
|
|
35325
35481
|
log3(` ✗ ${task.key} failed: ${msg}`);
|
|
35326
35482
|
}
|
|
35483
|
+
} finally {
|
|
35484
|
+
if (inlineMode && inlineRestoreHead && baseWorkingDir) {
|
|
35485
|
+
try {
|
|
35486
|
+
Bun.spawnSync(["git", "checkout", inlineRestoreHead], { cwd: baseWorkingDir, stdout: "pipe", stderr: "pipe" });
|
|
35487
|
+
} catch {}
|
|
35488
|
+
}
|
|
35327
35489
|
}
|
|
35328
35490
|
}
|
|
35329
35491
|
async function buildPlanningPreamble(apiUrl, task, _wsId) {
|
|
@@ -35381,6 +35543,9 @@ Issue actions:
|
|
|
35381
35543
|
{"type":"issue.delete_where","status":"todo"},
|
|
35382
35544
|
{"type":"issue.list","status":"todo","assignee_id":"<agent id>","limit":20},
|
|
35383
35545
|
{"type":"issue.search","query":"flaky tests","limit":10},
|
|
35546
|
+
{"type":"issue.get","id":"<issue id or key>"},
|
|
35547
|
+
{"type":"issue.start","id":"<issue id or key>"},
|
|
35548
|
+
{"type":"issue.stop","id":"<issue id or key>"},
|
|
35384
35549
|
{"type":"memory.search","query":"how does the deploy pipeline work","limit":10},
|
|
35385
35550
|
{"type":"memory.write","text":"Long-form note worth remembering across sessions...","summary":"short index hint","kind":"agent_note"}
|
|
35386
35551
|
]}
|
|
@@ -35388,9 +35553,11 @@ Issue actions:
|
|
|
35388
35553
|
|
|
35389
35554
|
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.
|
|
35390
35555
|
|
|
35556
|
+
**\`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.
|
|
35557
|
+
|
|
35391
35558
|
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.
|
|
35392
35559
|
|
|
35393
|
-
Read actions (\`issue.list\`, \`issue.search\`, \`memory.search\`) return the matched rows in the action summary
|
|
35560
|
+
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.
|
|
35394
35561
|
|
|
35395
35562
|
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.
|
|
35396
35563
|
|
|
@@ -35442,7 +35609,7 @@ async function executePlanActions(apiUrl, parentTask, actions, ctx, parseErrors
|
|
|
35442
35609
|
results.push({ type: "note", status: "note", message: `${blocked3} non-update action(s) blocked (planning depth limit ${PLANNING_DEPTH_LIMIT})` });
|
|
35443
35610
|
}
|
|
35444
35611
|
}
|
|
35445
|
-
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 };
|
|
35612
|
+
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 };
|
|
35446
35613
|
const counts = {};
|
|
35447
35614
|
actions = actions.filter((a) => {
|
|
35448
35615
|
const cap = SUBCAPS[a.type];
|
|
@@ -35624,6 +35791,42 @@ async function executePlanActions(apiUrl, parentTask, actions, ctx, parseErrors
|
|
|
35624
35791
|
lines.push(` - **${r.key}** [${r.status}] ${r.title}`);
|
|
35625
35792
|
}
|
|
35626
35793
|
results.push({ type: "issue.search", status: "ok", count: rows.length, query: a.query });
|
|
35794
|
+
} else if (a.type === "issue.get") {
|
|
35795
|
+
const res = await apiClient.post(queryUrl, { kind: "get", id: a.id }, { headers });
|
|
35796
|
+
if (!res.success) {
|
|
35797
|
+
lines.push(`- [err] issue.get ${a.id}: ${res.error || res.status}`);
|
|
35798
|
+
results.push({ type: "issue.get", status: "error", error: String(res.error || res.status), label: a.id });
|
|
35799
|
+
continue;
|
|
35800
|
+
}
|
|
35801
|
+
const r = res.data || {};
|
|
35802
|
+
const dispatchLine = r.live_dispatch ? ` dispatch=${r.live_dispatch.status}` : " dispatch=none";
|
|
35803
|
+
lines.push(`- [ok] issue.get **${r.key}** [${r.status}]${r.assignee_id ? ` @${r.assignee_id}` : ""} autonomy=${r.autonomy_level}${dispatchLine}`);
|
|
35804
|
+
lines.push(` - ${r.title}`);
|
|
35805
|
+
if (r.bound_chat_id)
|
|
35806
|
+
lines.push(` - bound chat: ${r.bound_chat_id}`);
|
|
35807
|
+
if (r.live_dispatch?.last_error)
|
|
35808
|
+
lines.push(` - last error: ${r.live_dispatch.last_error}`);
|
|
35809
|
+
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 });
|
|
35810
|
+
} else if (a.type === "issue.start") {
|
|
35811
|
+
const res = await apiClient.post(mutateUrl, { action: "start", id: a.id }, { headers });
|
|
35812
|
+
if (!res.success) {
|
|
35813
|
+
lines.push(`- [err] issue.start ${a.id}: ${res.error || res.status}`);
|
|
35814
|
+
results.push({ type: "issue.start", status: "error", error: String(res.error || res.status), label: a.id });
|
|
35815
|
+
continue;
|
|
35816
|
+
}
|
|
35817
|
+
const r = res.data || {};
|
|
35818
|
+
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)`);
|
|
35819
|
+
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 });
|
|
35820
|
+
} else if (a.type === "issue.stop") {
|
|
35821
|
+
const res = await apiClient.post(mutateUrl, { action: "stop", id: a.id }, { headers });
|
|
35822
|
+
if (!res.success) {
|
|
35823
|
+
lines.push(`- [err] issue.stop ${a.id}: ${res.error || res.status}`);
|
|
35824
|
+
results.push({ type: "issue.stop", status: "error", error: String(res.error || res.status), label: a.id });
|
|
35825
|
+
continue;
|
|
35826
|
+
}
|
|
35827
|
+
const r = res.data || {};
|
|
35828
|
+
lines.push(r.was_running ? `- [ok] issue.stop ${r.key || a.id} -> stopped` : `- [ok] issue.stop ${r.key || a.id} (nothing was running)`);
|
|
35829
|
+
results.push({ type: "issue.stop", status: "ok", issue_id: r.id || a.id, key: r.key, was_running: !!r.was_running });
|
|
35627
35830
|
} else if (a.type === "memory.search") {
|
|
35628
35831
|
const projectId = a.project_id || parentProjectId;
|
|
35629
35832
|
if (!projectId) {
|
|
@@ -35916,56 +36119,6 @@ function authTokenHeader() {
|
|
|
35916
36119
|
return null;
|
|
35917
36120
|
}
|
|
35918
36121
|
}
|
|
35919
|
-
async function uploadOutputDir(apiUrl, wsId, projectId, issueId, commentId, dir) {
|
|
35920
|
-
if (!existsSync12(dir))
|
|
35921
|
-
return 0;
|
|
35922
|
-
const files = [];
|
|
35923
|
-
const walk = (d, depth = 0) => {
|
|
35924
|
-
if (depth > 3)
|
|
35925
|
-
return;
|
|
35926
|
-
for (const name of readdirSync3(d)) {
|
|
35927
|
-
const p = join12(d, name);
|
|
35928
|
-
try {
|
|
35929
|
-
const st = statSync2(p);
|
|
35930
|
-
if (st.isDirectory())
|
|
35931
|
-
walk(p, depth + 1);
|
|
35932
|
-
else if (st.isFile())
|
|
35933
|
-
files.push(p);
|
|
35934
|
-
} catch {}
|
|
35935
|
-
}
|
|
35936
|
-
};
|
|
35937
|
-
walk(dir);
|
|
35938
|
-
if (!files.length)
|
|
35939
|
-
return 0;
|
|
35940
|
-
const token = authTokenHeader();
|
|
35941
|
-
const issueBase = `${apiUrl}/api/workspaces/${wsId}/projects/${projectId}/issues/${issueId}`;
|
|
35942
|
-
let uploaded = 0;
|
|
35943
|
-
for (const f of files) {
|
|
35944
|
-
try {
|
|
35945
|
-
const data = readFileSync10(f);
|
|
35946
|
-
const form = new FormData;
|
|
35947
|
-
const blob = new Blob([data]);
|
|
35948
|
-
form.append("file", blob, f.split("/").pop() || "file");
|
|
35949
|
-
const headers = {};
|
|
35950
|
-
if (token)
|
|
35951
|
-
headers.Authorization = token;
|
|
35952
|
-
const res = await fetch(`${issueBase}/comments/${commentId}/attachments`, {
|
|
35953
|
-
method: "POST",
|
|
35954
|
-
body: form,
|
|
35955
|
-
headers
|
|
35956
|
-
});
|
|
35957
|
-
if (res.ok) {
|
|
35958
|
-
uploaded++;
|
|
35959
|
-
try {
|
|
35960
|
-
unlinkSync6(f);
|
|
35961
|
-
} catch {}
|
|
35962
|
-
}
|
|
35963
|
-
} catch (e) {
|
|
35964
|
-
log3(`upload failed for ${f}: ${String(e)}`);
|
|
35965
|
-
}
|
|
35966
|
-
}
|
|
35967
|
-
return uploaded;
|
|
35968
|
-
}
|
|
35969
36122
|
function pickRunner(detected, preferType) {
|
|
35970
36123
|
const forceStub = process.env.MULTI_STUB === "1";
|
|
35971
36124
|
if (forceStub || !detected.length)
|
|
@@ -36239,6 +36392,116 @@ var init_run_task = __esm(() => {
|
|
|
36239
36392
|
SKILLS_DIR2 = join12(MULTI_DIR4, "skills");
|
|
36240
36393
|
});
|
|
36241
36394
|
|
|
36395
|
+
// src/_impl/worktree-create.ts
|
|
36396
|
+
var exports_worktree_create = {};
|
|
36397
|
+
__export(exports_worktree_create, {
|
|
36398
|
+
removeWorktree: () => removeWorktree2,
|
|
36399
|
+
materialiseWorktree: () => materialiseWorktree,
|
|
36400
|
+
codename: () => codename
|
|
36401
|
+
});
|
|
36402
|
+
import { homedir as homedir3 } from "os";
|
|
36403
|
+
import { existsSync as existsSync13 } from "fs";
|
|
36404
|
+
import { join as join13 } from "path";
|
|
36405
|
+
function codename() {
|
|
36406
|
+
const c = CITY_CODENAMES[Math.floor(Math.random() * CITY_CODENAMES.length)];
|
|
36407
|
+
const suffix = Math.random().toString(36).slice(2, 6);
|
|
36408
|
+
return `${c}-${suffix}`;
|
|
36409
|
+
}
|
|
36410
|
+
async function run5(cmd, cwd) {
|
|
36411
|
+
const proc = Bun.spawn(cmd, { cwd, stdout: "pipe", stderr: "pipe" });
|
|
36412
|
+
const [stdout, stderr] = await Promise.all([
|
|
36413
|
+
new Response(proc.stdout).text(),
|
|
36414
|
+
new Response(proc.stderr).text()
|
|
36415
|
+
]);
|
|
36416
|
+
await proc.exited;
|
|
36417
|
+
return { ok: proc.exitCode === 0, stdout: stdout.trim(), stderr: stderr.trim() };
|
|
36418
|
+
}
|
|
36419
|
+
async function materialiseWorktree(opts) {
|
|
36420
|
+
const { baseDir, name, log: log4 } = opts;
|
|
36421
|
+
if (!existsSync13(baseDir)) {
|
|
36422
|
+
throw new Error(`base_dir does not exist: ${baseDir}`);
|
|
36423
|
+
}
|
|
36424
|
+
const top = await run5(["git", "rev-parse", "--show-toplevel"], baseDir);
|
|
36425
|
+
if (!top.ok || !top.stdout) {
|
|
36426
|
+
throw new Error(`base_dir is not in a git repo: ${baseDir}`);
|
|
36427
|
+
}
|
|
36428
|
+
const repoRoot = top.stdout;
|
|
36429
|
+
let baseBranch = opts.baseBranch;
|
|
36430
|
+
if (!baseBranch) {
|
|
36431
|
+
const head5 = await run5(["git", "rev-parse", "--abbrev-ref", "HEAD"], repoRoot);
|
|
36432
|
+
baseBranch = head5.ok ? head5.stdout : "HEAD";
|
|
36433
|
+
}
|
|
36434
|
+
const worktreesDir = join13(homedir3(), ".multi", "worktrees");
|
|
36435
|
+
const path = join13(worktreesDir, name);
|
|
36436
|
+
if (existsSync13(path)) {
|
|
36437
|
+
throw new Error(`worktree path already exists: ${path}`);
|
|
36438
|
+
}
|
|
36439
|
+
const branch = `multi/${name}`;
|
|
36440
|
+
log4(`worktree: creating ${path} (branch ${branch} off ${baseBranch})`);
|
|
36441
|
+
const create = await run5(["git", "worktree", "add", "-b", branch, path, baseBranch], repoRoot);
|
|
36442
|
+
if (!create.ok) {
|
|
36443
|
+
throw new Error(`git worktree add failed: ${create.stderr || create.stdout}`);
|
|
36444
|
+
}
|
|
36445
|
+
return { path, branch, base_branch: baseBranch, name };
|
|
36446
|
+
}
|
|
36447
|
+
async function removeWorktree2(path, log4) {
|
|
36448
|
+
if (!existsSync13(path)) {
|
|
36449
|
+
return { ok: true };
|
|
36450
|
+
}
|
|
36451
|
+
const r = await run5(["git", "worktree", "remove", "--force", path], path);
|
|
36452
|
+
if (!r.ok) {
|
|
36453
|
+
log4(`worktree remove failed: ${r.stderr || r.stdout}`);
|
|
36454
|
+
return { ok: false, error: r.stderr || r.stdout };
|
|
36455
|
+
}
|
|
36456
|
+
return { ok: true };
|
|
36457
|
+
}
|
|
36458
|
+
var CITY_CODENAMES;
|
|
36459
|
+
var init_worktree_create = __esm(() => {
|
|
36460
|
+
CITY_CODENAMES = [
|
|
36461
|
+
"raleigh",
|
|
36462
|
+
"yokohama",
|
|
36463
|
+
"sydney",
|
|
36464
|
+
"porto",
|
|
36465
|
+
"kyoto",
|
|
36466
|
+
"bristol",
|
|
36467
|
+
"oslo",
|
|
36468
|
+
"havana",
|
|
36469
|
+
"lisbon",
|
|
36470
|
+
"tallinn",
|
|
36471
|
+
"salem",
|
|
36472
|
+
"marrakech",
|
|
36473
|
+
"perth",
|
|
36474
|
+
"boise",
|
|
36475
|
+
"nantes",
|
|
36476
|
+
"munich",
|
|
36477
|
+
"asheville",
|
|
36478
|
+
"stockholm",
|
|
36479
|
+
"halifax",
|
|
36480
|
+
"kolkata",
|
|
36481
|
+
"tucson",
|
|
36482
|
+
"valencia",
|
|
36483
|
+
"kazan",
|
|
36484
|
+
"savannah",
|
|
36485
|
+
"lyon",
|
|
36486
|
+
"iquitos",
|
|
36487
|
+
"wuhan",
|
|
36488
|
+
"leeds",
|
|
36489
|
+
"ankara",
|
|
36490
|
+
"auckland",
|
|
36491
|
+
"geneva",
|
|
36492
|
+
"izmir",
|
|
36493
|
+
"anchorage",
|
|
36494
|
+
"split",
|
|
36495
|
+
"kigali",
|
|
36496
|
+
"miami",
|
|
36497
|
+
"kazan",
|
|
36498
|
+
"krakow",
|
|
36499
|
+
"bilbao",
|
|
36500
|
+
"calgary",
|
|
36501
|
+
"dakar"
|
|
36502
|
+
];
|
|
36503
|
+
});
|
|
36504
|
+
|
|
36242
36505
|
// ../lib/chat-doc.ts
|
|
36243
36506
|
import { LoroDoc, LoroMap, LoroList } from "loro-crdt";
|
|
36244
36507
|
function appendMessage(doc2, msg) {
|
|
@@ -36328,8 +36591,8 @@ var init_chat_doc = __esm(() => {
|
|
|
36328
36591
|
});
|
|
36329
36592
|
|
|
36330
36593
|
// src/_impl/chat-peer.ts
|
|
36331
|
-
import { existsSync as
|
|
36332
|
-
import { dirname as dirname10, join as
|
|
36594
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync10, readFileSync as readFileSync11, writeFileSync as writeFileSync8 } from "fs";
|
|
36595
|
+
import { dirname as dirname10, join as join14 } from "path";
|
|
36333
36596
|
import { LoroDoc as LoroDoc2 } from "loro-crdt";
|
|
36334
36597
|
|
|
36335
36598
|
class ChatPeer {
|
|
@@ -36341,19 +36604,20 @@ class ChatPeer {
|
|
|
36341
36604
|
dirtySinceWrite = 0;
|
|
36342
36605
|
seenIds = new Set;
|
|
36343
36606
|
closed = false;
|
|
36607
|
+
localSubs = new Map;
|
|
36344
36608
|
firstFrameSinceConnect = true;
|
|
36345
36609
|
reconnectTimer = null;
|
|
36346
36610
|
constructor(opts) {
|
|
36347
36611
|
this.opts = opts;
|
|
36348
36612
|
this.chatId = opts.chatId;
|
|
36349
|
-
this.snapshotPath =
|
|
36613
|
+
this.snapshotPath = join14(MULTI_DIR, "chats", `${opts.chatId}.loro`);
|
|
36350
36614
|
this.doc = this.loadFromDisk();
|
|
36351
36615
|
for (const m of listMessages(this.doc))
|
|
36352
36616
|
this.seenIds.add(m.id);
|
|
36353
36617
|
}
|
|
36354
36618
|
loadFromDisk() {
|
|
36355
36619
|
try {
|
|
36356
|
-
if (
|
|
36620
|
+
if (existsSync14(this.snapshotPath)) {
|
|
36357
36621
|
const bytes = readFileSync11(this.snapshotPath);
|
|
36358
36622
|
return importSnapshot(new Uint8Array(bytes));
|
|
36359
36623
|
}
|
|
@@ -36361,7 +36625,7 @@ class ChatPeer {
|
|
|
36361
36625
|
return new LoroDoc2;
|
|
36362
36626
|
}
|
|
36363
36627
|
persist() {
|
|
36364
|
-
if (!
|
|
36628
|
+
if (!existsSync14(dirname10(this.snapshotPath)))
|
|
36365
36629
|
mkdirSync10(dirname10(this.snapshotPath), { recursive: true });
|
|
36366
36630
|
writeFileSync8(this.snapshotPath, exportSnapshot(this.doc));
|
|
36367
36631
|
this.dirtySinceWrite = 0;
|
|
@@ -36517,14 +36781,70 @@ class ChatPeer {
|
|
|
36517
36781
|
}, 3000);
|
|
36518
36782
|
}
|
|
36519
36783
|
sendLoro(update5) {
|
|
36520
|
-
if (!this.ws || this.ws.readyState !== 1)
|
|
36521
|
-
return;
|
|
36522
36784
|
const frame = new Uint8Array(1 + update5.byteLength);
|
|
36523
36785
|
frame[0] = FRAME_LORO;
|
|
36524
36786
|
frame.set(update5, 1);
|
|
36787
|
+
if (this.ws && this.ws.readyState === 1) {
|
|
36788
|
+
try {
|
|
36789
|
+
this.ws.send(frame);
|
|
36790
|
+
} catch {}
|
|
36791
|
+
}
|
|
36792
|
+
this.broadcastLocal(frame, null);
|
|
36793
|
+
}
|
|
36794
|
+
broadcastLocal(frame, except) {
|
|
36795
|
+
for (const [token, send] of this.localSubs) {
|
|
36796
|
+
if (token === except)
|
|
36797
|
+
continue;
|
|
36798
|
+
try {
|
|
36799
|
+
send(frame);
|
|
36800
|
+
} catch {}
|
|
36801
|
+
}
|
|
36802
|
+
}
|
|
36803
|
+
addLocalSubscriber(send) {
|
|
36804
|
+
const token = Symbol("local-chat-sub");
|
|
36805
|
+
this.localSubs.set(token, send);
|
|
36806
|
+
return {
|
|
36807
|
+
token,
|
|
36808
|
+
close: () => {
|
|
36809
|
+
this.localSubs.delete(token);
|
|
36810
|
+
}
|
|
36811
|
+
};
|
|
36812
|
+
}
|
|
36813
|
+
buildSnapshotGreet() {
|
|
36814
|
+
const snap = exportSnapshot(this.doc);
|
|
36815
|
+
const frame = new Uint8Array(1 + snap.byteLength);
|
|
36816
|
+
frame[0] = FRAME_LORO;
|
|
36817
|
+
frame.set(snap, 1);
|
|
36818
|
+
return frame;
|
|
36819
|
+
}
|
|
36820
|
+
ingestLocalUpdate(update5, fromToken) {
|
|
36821
|
+
const before2 = new Set(this.seenIds);
|
|
36525
36822
|
try {
|
|
36526
|
-
this.
|
|
36527
|
-
} catch {
|
|
36823
|
+
applyUpdate(this.doc, update5);
|
|
36824
|
+
} catch {
|
|
36825
|
+
return;
|
|
36826
|
+
}
|
|
36827
|
+
const frame = new Uint8Array(1 + update5.byteLength);
|
|
36828
|
+
frame[0] = FRAME_LORO;
|
|
36829
|
+
frame.set(update5, 1);
|
|
36830
|
+
if (this.ws && this.ws.readyState === 1) {
|
|
36831
|
+
try {
|
|
36832
|
+
this.ws.send(frame);
|
|
36833
|
+
} catch {}
|
|
36834
|
+
}
|
|
36835
|
+
this.broadcastLocal(frame, fromToken);
|
|
36836
|
+
for (const m of listMessages(this.doc)) {
|
|
36837
|
+
if (before2.has(m.id))
|
|
36838
|
+
continue;
|
|
36839
|
+
this.seenIds.add(m.id);
|
|
36840
|
+
if (m.author?.kind === "user" && !m.partial && this.opts.onUserMessage) {
|
|
36841
|
+
const cb = this.opts.onUserMessage;
|
|
36842
|
+
Promise.resolve().then(() => cb(m, this)).catch((e) => this.opts.log(`[chat ${this.chatId}] onUserMessage error: ${e.message}`));
|
|
36843
|
+
}
|
|
36844
|
+
}
|
|
36845
|
+
this.dirtySinceWrite++;
|
|
36846
|
+
if (this.dirtySinceWrite >= 8)
|
|
36847
|
+
this.persist();
|
|
36528
36848
|
}
|
|
36529
36849
|
sendJson(obj) {
|
|
36530
36850
|
if (!this.ws || this.ws.readyState !== 1)
|
|
@@ -36544,13 +36864,20 @@ class ChatPeer {
|
|
|
36544
36864
|
const tag3 = buf[0];
|
|
36545
36865
|
if (tag3 === FRAME_LORO) {
|
|
36546
36866
|
const before2 = new Set(this.seenIds);
|
|
36867
|
+
const updateBytes = buf.subarray(1);
|
|
36547
36868
|
try {
|
|
36548
|
-
applyUpdate(this.doc,
|
|
36869
|
+
applyUpdate(this.doc, updateBytes);
|
|
36549
36870
|
} catch {
|
|
36550
36871
|
return;
|
|
36551
36872
|
}
|
|
36552
36873
|
const isFirstFrame = this.firstFrameSinceConnect;
|
|
36553
36874
|
this.firstFrameSinceConnect = false;
|
|
36875
|
+
if (!isFirstFrame && this.localSubs.size > 0) {
|
|
36876
|
+
const frame = new Uint8Array(1 + updateBytes.byteLength);
|
|
36877
|
+
frame[0] = FRAME_LORO;
|
|
36878
|
+
frame.set(updateBytes, 1);
|
|
36879
|
+
this.broadcastLocal(frame, null);
|
|
36880
|
+
}
|
|
36554
36881
|
for (const m of listMessages(this.doc)) {
|
|
36555
36882
|
if (before2.has(m.id))
|
|
36556
36883
|
continue;
|
|
@@ -36906,6 +37233,48 @@ async function executeChatPlanActions(actionsIn, parseErrors, ctx) {
|
|
|
36906
37233
|
lines.push(` - **${r.key}** [${r.status}] ${r.title}`);
|
|
36907
37234
|
results.push({ type: "issue.search", status: "ok", count: rows.length, query: a.query });
|
|
36908
37235
|
tally(true);
|
|
37236
|
+
} else if (a.type === "issue.get") {
|
|
37237
|
+
const res = await apiClient.post(queryUrl, { kind: "get", id: a.id }, { headers });
|
|
37238
|
+
if (!res.success) {
|
|
37239
|
+
lines.push(`- [err] issue.get ${a.id}: ${res.error || res.status}`);
|
|
37240
|
+
results.push({ type: "issue.get", status: "error", error: String(res.error || res.status), label: a.id });
|
|
37241
|
+
tally(false);
|
|
37242
|
+
continue;
|
|
37243
|
+
}
|
|
37244
|
+
const r = res.data || {};
|
|
37245
|
+
const dispatchLine = r.live_dispatch ? ` dispatch=${r.live_dispatch.status}` : " dispatch=none";
|
|
37246
|
+
lines.push(`- [ok] issue.get **${r.key}** [${r.status}]${r.assignee_id ? ` @${r.assignee_id}` : ""} autonomy=${r.autonomy_level}${dispatchLine}`);
|
|
37247
|
+
lines.push(` - ${r.title}`);
|
|
37248
|
+
if (r.bound_chat_id)
|
|
37249
|
+
lines.push(` - bound chat: ${r.bound_chat_id}`);
|
|
37250
|
+
if (r.live_dispatch?.last_error)
|
|
37251
|
+
lines.push(` - last error: ${r.live_dispatch.last_error}`);
|
|
37252
|
+
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 });
|
|
37253
|
+
tally(true);
|
|
37254
|
+
} else if (a.type === "issue.start") {
|
|
37255
|
+
const res = await apiClient.post(mutateUrl, { action: "start", id: a.id }, { headers });
|
|
37256
|
+
if (!res.success) {
|
|
37257
|
+
lines.push(`- [err] issue.start ${a.id}: ${res.error || res.status}`);
|
|
37258
|
+
results.push({ type: "issue.start", status: "error", error: String(res.error || res.status), label: a.id });
|
|
37259
|
+
tally(false);
|
|
37260
|
+
continue;
|
|
37261
|
+
}
|
|
37262
|
+
const r = res.data || {};
|
|
37263
|
+
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)`);
|
|
37264
|
+
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 });
|
|
37265
|
+
tally(true);
|
|
37266
|
+
} else if (a.type === "issue.stop") {
|
|
37267
|
+
const res = await apiClient.post(mutateUrl, { action: "stop", id: a.id }, { headers });
|
|
37268
|
+
if (!res.success) {
|
|
37269
|
+
lines.push(`- [err] issue.stop ${a.id}: ${res.error || res.status}`);
|
|
37270
|
+
results.push({ type: "issue.stop", status: "error", error: String(res.error || res.status), label: a.id });
|
|
37271
|
+
tally(false);
|
|
37272
|
+
continue;
|
|
37273
|
+
}
|
|
37274
|
+
const r = res.data || {};
|
|
37275
|
+
lines.push(r.was_running ? `- [ok] issue.stop ${r.key || a.id} -> stopped` : `- [ok] issue.stop ${r.key || a.id} (nothing was running)`);
|
|
37276
|
+
results.push({ type: "issue.stop", status: "ok", issue_id: r.id || a.id, key: r.key, was_running: !!r.was_running });
|
|
37277
|
+
tally(true);
|
|
36909
37278
|
} else if (a.type === "memory.search") {
|
|
36910
37279
|
const project_id = a.project_id || ctx.projectId;
|
|
36911
37280
|
if (!project_id) {
|
|
@@ -37075,10 +37444,75 @@ var init_chat_plan_actions = __esm(() => {
|
|
|
37075
37444
|
"session.create": 3,
|
|
37076
37445
|
"issue.comment": 5,
|
|
37077
37446
|
"memory.search": 5,
|
|
37078
|
-
"memory.write": 5
|
|
37447
|
+
"memory.write": 5,
|
|
37448
|
+
"issue.get": 10,
|
|
37449
|
+
"issue.start": 3,
|
|
37450
|
+
"issue.stop": 3
|
|
37079
37451
|
};
|
|
37080
37452
|
});
|
|
37081
37453
|
|
|
37454
|
+
// src/_impl/chat-attachments.ts
|
|
37455
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync11, writeFileSync as writeFileSync9 } from "fs";
|
|
37456
|
+
import { dirname as dirname11, join as join15 } from "path";
|
|
37457
|
+
function safeName(n) {
|
|
37458
|
+
return n.replace(/[^A-Za-z0-9._-]+/g, "_").slice(0, 80) || "file";
|
|
37459
|
+
}
|
|
37460
|
+
async function downloadUserAttachments(opts) {
|
|
37461
|
+
const { apiUrl, authToken: authToken2, workspaceId, chatId, cwd, message, log: log4 } = opts;
|
|
37462
|
+
const atts = message.attachments || [];
|
|
37463
|
+
if (atts.length === 0)
|
|
37464
|
+
return [];
|
|
37465
|
+
const baseRel = join15(".multi", "attachments", message.id);
|
|
37466
|
+
const baseAbs = join15(cwd, baseRel);
|
|
37467
|
+
if (!existsSync15(baseAbs))
|
|
37468
|
+
mkdirSync11(baseAbs, { recursive: true });
|
|
37469
|
+
const out = [];
|
|
37470
|
+
for (const a of atts) {
|
|
37471
|
+
if (!a.url)
|
|
37472
|
+
continue;
|
|
37473
|
+
const fname = safeName(a.name);
|
|
37474
|
+
const relPath = join15(baseRel, fname);
|
|
37475
|
+
const absPath = join15(cwd, relPath);
|
|
37476
|
+
if (existsSync15(absPath)) {
|
|
37477
|
+
out.push({ attachment: a, absPath, relPath });
|
|
37478
|
+
continue;
|
|
37479
|
+
}
|
|
37480
|
+
try {
|
|
37481
|
+
const base = a.url.startsWith("http") ? a.url : `${apiUrl.replace(/\/$/, "")}${a.url}`;
|
|
37482
|
+
const sep = base.includes("?") ? "&" : "?";
|
|
37483
|
+
const url2 = `${base}${sep}token=${encodeURIComponent(authToken2)}`;
|
|
37484
|
+
const res = await fetch(url2);
|
|
37485
|
+
if (!res.ok) {
|
|
37486
|
+
log4(`[chat ${chatId}] attachment ${a.id} fetch ${res.status}`);
|
|
37487
|
+
continue;
|
|
37488
|
+
}
|
|
37489
|
+
const buf = new Uint8Array(await res.arrayBuffer());
|
|
37490
|
+
if (!existsSync15(dirname11(absPath)))
|
|
37491
|
+
mkdirSync11(dirname11(absPath), { recursive: true });
|
|
37492
|
+
writeFileSync9(absPath, buf);
|
|
37493
|
+
out.push({ attachment: a, absPath, relPath });
|
|
37494
|
+
} catch (e) {
|
|
37495
|
+
log4(`[chat ${chatId}] attachment ${a.id} err: ${e.message}`);
|
|
37496
|
+
}
|
|
37497
|
+
}
|
|
37498
|
+
return out;
|
|
37499
|
+
}
|
|
37500
|
+
function renderAttachmentPreamble(fetched) {
|
|
37501
|
+
if (fetched.length === 0)
|
|
37502
|
+
return "";
|
|
37503
|
+
const lines = fetched.map((f) => {
|
|
37504
|
+
const sizeBit = typeof f.attachment.size === "number" ? ` (${f.attachment.size}B)` : "";
|
|
37505
|
+
return `- ${f.relPath}${sizeBit}`;
|
|
37506
|
+
});
|
|
37507
|
+
return [
|
|
37508
|
+
`[multi system note — the user attached files; they are saved in the working directory:]`,
|
|
37509
|
+
...lines,
|
|
37510
|
+
``
|
|
37511
|
+
].join(`
|
|
37512
|
+
`);
|
|
37513
|
+
}
|
|
37514
|
+
var init_chat_attachments = () => {};
|
|
37515
|
+
|
|
37082
37516
|
// src/_impl/chat-supervisor.ts
|
|
37083
37517
|
async function runChatTurnWithPeer(peer, opts) {
|
|
37084
37518
|
const { apiUrl, authToken: authToken2, workspaceId, chatId, messageId, log: log4 } = opts;
|
|
@@ -37096,6 +37530,45 @@ async function runChatTurnWithPeer(peer, opts) {
|
|
|
37096
37530
|
}
|
|
37097
37531
|
await processUserMessage(chat2, msg, peer, { apiUrl, authToken: authToken2, workspaceId, log: log4 });
|
|
37098
37532
|
}
|
|
37533
|
+
function buildIssueContextMarkdown(issue2) {
|
|
37534
|
+
const key = issue2.key ?? issue2.id ?? "";
|
|
37535
|
+
const title = issue2.title ?? "";
|
|
37536
|
+
const description = typeof issue2.description === "string" ? issue2.description.trim() : "";
|
|
37537
|
+
const priority = issue2.priority ?? null;
|
|
37538
|
+
const status3 = issue2.status ?? null;
|
|
37539
|
+
const MAX_DESC = 8 * 1024;
|
|
37540
|
+
const desc = description.length > MAX_DESC ? description.slice(0, MAX_DESC) + `
|
|
37541
|
+
|
|
37542
|
+
[…truncated]` : description;
|
|
37543
|
+
const out = [];
|
|
37544
|
+
out.push(`## Issue ${key}: ${title}`);
|
|
37545
|
+
if (desc) {
|
|
37546
|
+
out.push("");
|
|
37547
|
+
out.push(desc);
|
|
37548
|
+
}
|
|
37549
|
+
const meta3 = [];
|
|
37550
|
+
if (priority)
|
|
37551
|
+
meta3.push(`priority: ${priority}`);
|
|
37552
|
+
if (status3)
|
|
37553
|
+
meta3.push(`status: ${status3}`);
|
|
37554
|
+
if (meta3.length) {
|
|
37555
|
+
out.push("");
|
|
37556
|
+
out.push(`_${meta3.join(" · ")}_`);
|
|
37557
|
+
}
|
|
37558
|
+
return out.join(`
|
|
37559
|
+
`);
|
|
37560
|
+
}
|
|
37561
|
+
async function fetchIssueContext(apiUrl, workspaceId, authToken2, issueId) {
|
|
37562
|
+
try {
|
|
37563
|
+
const r = await fetch(`${apiUrl}/api/workspaces/${workspaceId}/issues/${issueId}`, { headers: { authorization: `Bearer ${authToken2}` } });
|
|
37564
|
+
if (!r.ok)
|
|
37565
|
+
return null;
|
|
37566
|
+
const issue2 = await r.json();
|
|
37567
|
+
return buildIssueContextMarkdown(issue2);
|
|
37568
|
+
} catch {
|
|
37569
|
+
return null;
|
|
37570
|
+
}
|
|
37571
|
+
}
|
|
37099
37572
|
async function fetchAgents(apiUrl, workspaceId, authToken2) {
|
|
37100
37573
|
const headers = { authorization: `Bearer ${authToken2}` };
|
|
37101
37574
|
try {
|
|
@@ -37118,6 +37591,9 @@ function resolveAgentFromMention(agents, mentioned) {
|
|
|
37118
37591
|
async function resolveCwd(apiUrl, workspaceId, authToken2, chat2) {
|
|
37119
37592
|
if (!chat2.project_id || !chat2.device_id)
|
|
37120
37593
|
return;
|
|
37594
|
+
if (chat2.working_dir) {
|
|
37595
|
+
return chat2.working_dir;
|
|
37596
|
+
}
|
|
37121
37597
|
const headers = { authorization: `Bearer ${authToken2}` };
|
|
37122
37598
|
try {
|
|
37123
37599
|
const r = await fetch(`${apiUrl}/api/workspaces/${workspaceId}/projects/${chat2.project_id}/devices`, { headers });
|
|
@@ -37202,21 +37678,28 @@ async function processUserMessage(chat2, userMsg, peer, ctx) {
|
|
|
37202
37678
|
if (typeof r.filename === "string")
|
|
37203
37679
|
out.filename = r.filename;
|
|
37204
37680
|
if (typeof r.command === "string")
|
|
37205
|
-
out.command =
|
|
37681
|
+
out.command = r.command;
|
|
37206
37682
|
if (typeof r.pattern === "string")
|
|
37207
|
-
out.pattern =
|
|
37683
|
+
out.pattern = r.pattern;
|
|
37208
37684
|
if (typeof r.query === "string")
|
|
37209
|
-
out.query =
|
|
37685
|
+
out.query = r.query;
|
|
37210
37686
|
if (typeof r.url === "string")
|
|
37211
37687
|
out.url = r.url;
|
|
37688
|
+
if (typeof r.old_string === "string")
|
|
37689
|
+
out.old_string = r.old_string;
|
|
37690
|
+
if (typeof r.new_string === "string")
|
|
37691
|
+
out.new_string = r.new_string;
|
|
37692
|
+
if (typeof r.replace_all === "boolean")
|
|
37693
|
+
out.replace_all = r.replace_all;
|
|
37694
|
+
if (typeof r.content === "string")
|
|
37695
|
+
out.content = r.content;
|
|
37212
37696
|
return Object.keys(out).length ? out : undefined;
|
|
37213
37697
|
};
|
|
37214
37698
|
const summarizeContent = (raw) => {
|
|
37215
37699
|
const bytes = raw?.length ?? 0;
|
|
37216
37700
|
const lines = raw ? raw.split(`
|
|
37217
37701
|
`).length : 0;
|
|
37218
|
-
|
|
37219
|
-
return { content: trimmed, bytes, lines };
|
|
37702
|
+
return { content: raw || "", bytes, lines };
|
|
37220
37703
|
};
|
|
37221
37704
|
const mapToolKind = (raw) => {
|
|
37222
37705
|
const s = (raw || "").toLowerCase();
|
|
@@ -37354,7 +37837,42 @@ async function processUserMessage(chat2, userMsg, peer, ctx) {
|
|
|
37354
37837
|
return null;
|
|
37355
37838
|
}
|
|
37356
37839
|
};
|
|
37357
|
-
|
|
37840
|
+
let attachmentPrefix = "";
|
|
37841
|
+
if (cwd && userMsg.attachments && userMsg.attachments.length > 0) {
|
|
37842
|
+
try {
|
|
37843
|
+
const fetched = await downloadUserAttachments({
|
|
37844
|
+
apiUrl,
|
|
37845
|
+
authToken: authToken2,
|
|
37846
|
+
workspaceId,
|
|
37847
|
+
chatId: chat2.id,
|
|
37848
|
+
cwd,
|
|
37849
|
+
message: userMsg,
|
|
37850
|
+
log: log4
|
|
37851
|
+
});
|
|
37852
|
+
attachmentPrefix = renderAttachmentPreamble(fetched);
|
|
37853
|
+
if (fetched.length > 0)
|
|
37854
|
+
log4(`[chat ${chat2.id}] attachments: ${fetched.length} ready`);
|
|
37855
|
+
} catch (e) {
|
|
37856
|
+
log4(`[chat ${chat2.id}] attachment download err: ${e.message}`);
|
|
37857
|
+
}
|
|
37858
|
+
}
|
|
37859
|
+
let issueCtxBlock = "";
|
|
37860
|
+
if (chat2.issue_id && !issueContextSent.has(chat2.id)) {
|
|
37861
|
+
const ctx2 = await fetchIssueContext(apiUrl, workspaceId, authToken2, chat2.issue_id);
|
|
37862
|
+
if (ctx2) {
|
|
37863
|
+
issueCtxBlock = `${ctx2}
|
|
37864
|
+
|
|
37865
|
+
---
|
|
37866
|
+
|
|
37867
|
+
## New message
|
|
37868
|
+
|
|
37869
|
+
`;
|
|
37870
|
+
log4(`[chat ${chat2.id}] injected issue context (${ctx2.length} chars)`);
|
|
37871
|
+
}
|
|
37872
|
+
}
|
|
37873
|
+
await runOneTurn(`${issueCtxBlock}${attachmentPrefix}${userMsg.text}`, true);
|
|
37874
|
+
if (issueCtxBlock)
|
|
37875
|
+
issueContextSent.add(chat2.id);
|
|
37358
37876
|
const firstPlan = await execAndPostPlan();
|
|
37359
37877
|
if (firstPlan && firstPlan.ok > 0) {
|
|
37360
37878
|
const resultText = firstPlan.lines.join(`
|
|
@@ -37453,6 +37971,9 @@ Action vocabulary:
|
|
|
37453
37971
|
{"type":"issue.delete_where","status":"todo","limit":50},
|
|
37454
37972
|
{"type":"issue.list","status":"todo","limit":20},
|
|
37455
37973
|
{"type":"issue.search","query":"flaky tests","limit":10},
|
|
37974
|
+
{"type":"issue.get","id":"<issue id or key>"},
|
|
37975
|
+
{"type":"issue.start","id":"<issue id or key>"},
|
|
37976
|
+
{"type":"issue.stop","id":"<issue id or key>"},
|
|
37456
37977
|
{"type":"memory.search","query":"deploy pipeline","limit":10},
|
|
37457
37978
|
{"type":"memory.write","text":"durable note worth recalling next session","summary":"short hint","kind":"agent_note"},
|
|
37458
37979
|
{"type":"agent.create","name":"refactor-bot","prompt":"...","allowed_tools":["Read","Edit","Bash"]},
|
|
@@ -37466,7 +37987,8 @@ Action vocabulary:
|
|
|
37466
37987
|
Rules:
|
|
37467
37988
|
- Omit the block entirely if no actions are needed. Don't emit empty arrays.
|
|
37468
37989
|
- 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.
|
|
37469
|
-
- 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.
|
|
37990
|
+
- 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.
|
|
37991
|
+
- 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).
|
|
37470
37992
|
- 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.
|
|
37471
37993
|
- 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.
|
|
37472
37994
|
- 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.
|
|
@@ -37490,11 +38012,14 @@ Rules:
|
|
|
37490
38012
|
|
|
37491
38013
|
${agentsBlock}`;
|
|
37492
38014
|
}
|
|
38015
|
+
var issueContextSent;
|
|
37493
38016
|
var init_chat_supervisor = __esm(() => {
|
|
37494
38017
|
init_chat_peer();
|
|
37495
38018
|
init_chat_turn();
|
|
37496
38019
|
init_chat_plan_actions();
|
|
38020
|
+
init_chat_attachments();
|
|
37497
38021
|
init_lib();
|
|
38022
|
+
issueContextSent = new Set;
|
|
37498
38023
|
});
|
|
37499
38024
|
|
|
37500
38025
|
// src/_impl/chat-session-registry.ts
|
|
@@ -37607,6 +38132,9 @@ var init_chat_session_registry = __esm(() => {
|
|
|
37607
38132
|
}
|
|
37608
38133
|
s.inFlight = runTurn(s, k, messageId, ctx, chatId);
|
|
37609
38134
|
},
|
|
38135
|
+
ensurePeer(wsId, chatId, ctx) {
|
|
38136
|
+
return ensureSession(wsId, chatId, ctx).peer;
|
|
38137
|
+
},
|
|
37610
38138
|
async closeAll() {
|
|
37611
38139
|
for (const [k, s] of sessions) {
|
|
37612
38140
|
clearIdle(s);
|
|
@@ -38241,7 +38769,7 @@ import { parseArgs } from "util";
|
|
|
38241
38769
|
// package.json
|
|
38242
38770
|
var package_default = {
|
|
38243
38771
|
name: "@shipers-dev/multi",
|
|
38244
|
-
version: "0.
|
|
38772
|
+
version: "0.64.0",
|
|
38245
38773
|
type: "module",
|
|
38246
38774
|
bin: {
|
|
38247
38775
|
"multi-agent": "./dist/index.js"
|
|
@@ -38621,8 +39149,6 @@ revision=${Date.now()}
|
|
|
38621
39149
|
}
|
|
38622
39150
|
}
|
|
38623
39151
|
function writeManagedAgent(slug, agent, skillsForAgent) {
|
|
38624
|
-
if (agent.type !== "claude-code")
|
|
38625
|
-
return;
|
|
38626
39152
|
mkdirSync4(CLAUDE_AGENTS, { recursive: true });
|
|
38627
39153
|
const out = join6(CLAUDE_AGENTS, `${slug}.md`);
|
|
38628
39154
|
safeRmManaged(out);
|
|
@@ -39156,7 +39682,7 @@ var killStaleConnects = () => {
|
|
|
39156
39682
|
const cmd = m[2];
|
|
39157
39683
|
if (!Number.isFinite(pid) || pid === self || pid === parent)
|
|
39158
39684
|
continue;
|
|
39159
|
-
if (!/@shipers-dev\/multi\/dist\/index\.js/.test(cmd) && !/multi-agent/.test(cmd))
|
|
39685
|
+
if (!/@shipers-dev\/multi\/dist\/index\.js/.test(cmd) && !/multi-agent/.test(cmd) && !/multi-daemon/.test(cmd))
|
|
39160
39686
|
continue;
|
|
39161
39687
|
if (!/\bconnect\b/.test(cmd))
|
|
39162
39688
|
continue;
|
|
@@ -39319,22 +39845,24 @@ init_client();
|
|
|
39319
39845
|
init_detect();
|
|
39320
39846
|
init_run_task();
|
|
39321
39847
|
import { Database as Database3 } from "bun:sqlite";
|
|
39322
|
-
import { existsSync as
|
|
39323
|
-
import { homedir as
|
|
39324
|
-
import { join as
|
|
39848
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync12, writeFileSync as writeFileSync10, unlinkSync as unlinkSync7 } from "fs";
|
|
39849
|
+
import { homedir as homedir4 } from "os";
|
|
39850
|
+
import { join as join16 } from "path";
|
|
39325
39851
|
init_adapter_pidfile();
|
|
39326
39852
|
init_errors();
|
|
39327
|
-
var MULTI_DIR5 =
|
|
39328
|
-
var PID_PATH2 =
|
|
39329
|
-
var PORT_PATH2 =
|
|
39330
|
-
var STOP_PATH2 =
|
|
39331
|
-
var TASKS_DB_PATH3 =
|
|
39853
|
+
var MULTI_DIR5 = join16(homedir4(), ".multi");
|
|
39854
|
+
var PID_PATH2 = join16(MULTI_DIR5, "agent.pid");
|
|
39855
|
+
var PORT_PATH2 = join16(MULTI_DIR5, "agent.port");
|
|
39856
|
+
var STOP_PATH2 = join16(MULTI_DIR5, "stop.flag");
|
|
39857
|
+
var TASKS_DB_PATH3 = join16(MULTI_DIR5, "tasks.db");
|
|
39858
|
+
var LOCAL_SERVER_DIR = process.env.MULTI_HOME || join16(homedir4(), ".multi");
|
|
39859
|
+
var LOCAL_SERVER_PATH = join16(LOCAL_SERVER_DIR, "local-server.json");
|
|
39332
39860
|
function ensureDirs2() {
|
|
39333
|
-
if (!
|
|
39334
|
-
|
|
39335
|
-
const logs =
|
|
39336
|
-
if (!
|
|
39337
|
-
|
|
39861
|
+
if (!existsSync16(MULTI_DIR5))
|
|
39862
|
+
mkdirSync12(MULTI_DIR5, { recursive: true });
|
|
39863
|
+
const logs = join16(MULTI_DIR5, "logs");
|
|
39864
|
+
if (!existsSync16(logs))
|
|
39865
|
+
mkdirSync12(logs, { recursive: true });
|
|
39338
39866
|
}
|
|
39339
39867
|
function isLocalApi(url2) {
|
|
39340
39868
|
try {
|
|
@@ -39626,15 +40154,18 @@ var daemonProgram = ({ cfg, apiUrl }) => exports_Effect.gen(function* () {
|
|
|
39626
40154
|
if (cfg.authToken)
|
|
39627
40155
|
setAuthToken(cfg.authToken);
|
|
39628
40156
|
ensureDirs2();
|
|
39629
|
-
if (
|
|
40157
|
+
if (existsSync16(STOP_PATH2))
|
|
39630
40158
|
unlinkSync7(STOP_PATH2);
|
|
39631
40159
|
const reaped = reapStaleAdapters((m) => log3(m));
|
|
39632
40160
|
if (reaped > 0)
|
|
39633
40161
|
log3(`reaped ${reaped} stale adapter${reaped === 1 ? "" : "s"}`);
|
|
39634
40162
|
const detected = yield* exports_Effect.promise(() => detectAgents());
|
|
39635
|
-
const
|
|
40163
|
+
const skipTunnel = process.env.MULTI_NO_TUNNEL === "1";
|
|
40164
|
+
const localMode = isLocalApi(apiUrl) || skipTunnel;
|
|
39636
40165
|
log3(`daemon device=${cfg.deviceId} pid=${process.pid}`);
|
|
39637
|
-
log3(` API: ${apiUrl}${localMode ? " (local — cloudflared skipped)" : ""}`);
|
|
40166
|
+
log3(` API: ${apiUrl}${localMode ? skipTunnel && !isLocalApi(apiUrl) ? " (no-tunnel mode)" : " (local — cloudflared skipped)" : ""}`);
|
|
40167
|
+
log3(` PATH=${(process.env.PATH || "").slice(0, 500)}`);
|
|
40168
|
+
log3(` detected: ${JSON.stringify(detected.map((d) => ({ type: d.type, path: d.path })))}`);
|
|
39638
40169
|
log3(` runtimes: ${detected.map((d) => d.type).join(", ") || "stub"}`);
|
|
39639
40170
|
const db2 = openTasksDb();
|
|
39640
40171
|
db2.run("UPDATE tasks SET status = 'queued' WHERE status = 'running'");
|
|
@@ -39646,128 +40177,573 @@ var daemonProgram = ({ cfg, apiUrl }) => exports_Effect.gen(function* () {
|
|
|
39646
40177
|
const stopDeferred = yield* exports_Deferred.make();
|
|
39647
40178
|
const port = yield* exports_Effect.promise(() => pickFreePort());
|
|
39648
40179
|
const expectedAuth = `Bearer ${cfg.dispatchSecret}`;
|
|
40180
|
+
const corsHeaders = (origin) => ({
|
|
40181
|
+
"Access-Control-Allow-Origin": origin || "*",
|
|
40182
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
40183
|
+
"Access-Control-Allow-Headers": "Authorization, Content-Type",
|
|
40184
|
+
"Access-Control-Max-Age": "600",
|
|
40185
|
+
Vary: "Origin"
|
|
40186
|
+
});
|
|
40187
|
+
const withCors = (res, origin) => {
|
|
40188
|
+
for (const [k, v] of Object.entries(corsHeaders(origin))) {
|
|
40189
|
+
res.headers.set(k, v);
|
|
40190
|
+
}
|
|
40191
|
+
return res;
|
|
40192
|
+
};
|
|
39649
40193
|
const server = Bun.serve({
|
|
39650
40194
|
port,
|
|
39651
40195
|
hostname: "127.0.0.1",
|
|
39652
|
-
fetch(req) {
|
|
40196
|
+
fetch(req, server2) {
|
|
39653
40197
|
const url2 = new URL(req.url);
|
|
39654
|
-
|
|
39655
|
-
|
|
39656
|
-
|
|
39657
|
-
|
|
39658
|
-
|
|
39659
|
-
|
|
39660
|
-
|
|
39661
|
-
|
|
39662
|
-
|
|
39663
|
-
|
|
39664
|
-
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]);
|
|
39665
|
-
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;
|
|
39666
|
-
if (t.issue_id)
|
|
39667
|
-
postStream(apiUrl, t.issue_id, "queued", { queue_position: pos });
|
|
39668
|
-
if (t.dispatch_id && cfg.workspaceId)
|
|
39669
|
-
ackDispatch(apiUrl, cfg.workspaceId, t.dispatch_id, cfg.authToken);
|
|
39670
|
-
try {
|
|
39671
|
-
exports_Queue.unsafeOffer(wakeQ, undefined);
|
|
39672
|
-
} catch {}
|
|
39673
|
-
return Response.json({ accepted: true, task_id: taskId }, { status: 202 });
|
|
39674
|
-
} catch (e) {
|
|
39675
|
-
return Response.json({ error: String(e) }, { status: 400 });
|
|
40198
|
+
const origin = req.headers.get("origin");
|
|
40199
|
+
if (req.method === "OPTIONS") {
|
|
40200
|
+
return new Response(null, { status: 204, headers: corsHeaders(origin) });
|
|
40201
|
+
}
|
|
40202
|
+
{
|
|
40203
|
+
const m = /^\/chat-sync\/([^/]+)$/.exec(url2.pathname);
|
|
40204
|
+
if (m && req.headers.get("upgrade") === "websocket") {
|
|
40205
|
+
const token = url2.searchParams.get("token") || "";
|
|
40206
|
+
if (token !== cfg.dispatchSecret) {
|
|
40207
|
+
return new Response("unauthorized", { status: 401 });
|
|
39676
40208
|
}
|
|
39677
|
-
|
|
40209
|
+
if (!cfg.workspaceId || !cfg.authToken || !cfg.deviceId) {
|
|
40210
|
+
return new Response("daemon not configured", { status: 503 });
|
|
40211
|
+
}
|
|
40212
|
+
const chatId = decodeURIComponent(m[1]);
|
|
40213
|
+
const ok = server2.upgrade(req, {
|
|
40214
|
+
data: { kind: "chat-sync", chatId, wsId: cfg.workspaceId }
|
|
40215
|
+
});
|
|
40216
|
+
if (!ok)
|
|
40217
|
+
return new Response("upgrade failed", { status: 500 });
|
|
40218
|
+
return;
|
|
40219
|
+
}
|
|
39678
40220
|
}
|
|
39679
|
-
|
|
39680
|
-
if (
|
|
39681
|
-
return
|
|
39682
|
-
|
|
39683
|
-
|
|
39684
|
-
|
|
39685
|
-
|
|
39686
|
-
|
|
40221
|
+
const route = () => {
|
|
40222
|
+
if (url2.pathname === "/health")
|
|
40223
|
+
return Response.json({ ok: true, device_id: cfg.deviceId });
|
|
40224
|
+
if (url2.pathname === "/files" && req.method === "GET") {
|
|
40225
|
+
if (req.headers.get("authorization") !== expectedAuth)
|
|
40226
|
+
return new Response("unauthorized", { status: 401 });
|
|
40227
|
+
return (async () => {
|
|
40228
|
+
try {
|
|
40229
|
+
const dir = url2.searchParams.get("dir") || "";
|
|
40230
|
+
if (!dir || !dir.startsWith("/")) {
|
|
40231
|
+
return Response.json({ error: "absolute dir required" }, { status: 400 });
|
|
40232
|
+
}
|
|
40233
|
+
const showHidden = url2.searchParams.get("hidden") === "1";
|
|
40234
|
+
const { readdir } = await import("fs/promises");
|
|
40235
|
+
const path = await import("path");
|
|
40236
|
+
const entries2 = await readdir(dir, { withFileTypes: true });
|
|
40237
|
+
const repoRoot = await (async () => {
|
|
40238
|
+
try {
|
|
40239
|
+
const p = Bun.spawn(["git", "rev-parse", "--show-toplevel"], {
|
|
40240
|
+
cwd: dir,
|
|
40241
|
+
stdout: "pipe",
|
|
40242
|
+
stderr: "ignore"
|
|
40243
|
+
});
|
|
40244
|
+
const out3 = (await new Response(p.stdout).text()).trim();
|
|
40245
|
+
await p.exited;
|
|
40246
|
+
return p.exitCode === 0 && out3 ? out3 : null;
|
|
40247
|
+
} catch {
|
|
40248
|
+
return null;
|
|
40249
|
+
}
|
|
40250
|
+
})();
|
|
40251
|
+
const status3 = new Map;
|
|
40252
|
+
if (repoRoot) {
|
|
40253
|
+
try {
|
|
40254
|
+
const p = Bun.spawn(["git", "status", "--porcelain=v1", "--ignored", "-z"], { cwd: repoRoot, stdout: "pipe", stderr: "ignore" });
|
|
40255
|
+
const out3 = await new Response(p.stdout).text();
|
|
40256
|
+
await p.exited;
|
|
40257
|
+
if (p.exitCode === 0) {
|
|
40258
|
+
const parts2 = out3.split("\x00").filter((s) => s.length > 0);
|
|
40259
|
+
for (let i = 0;i < parts2.length; i++) {
|
|
40260
|
+
const entry = parts2[i];
|
|
40261
|
+
const code = entry.slice(0, 2);
|
|
40262
|
+
const rel = entry.slice(3);
|
|
40263
|
+
if (code[0] === "R" || code[0] === "C")
|
|
40264
|
+
i++;
|
|
40265
|
+
const abs = path.join(repoRoot, rel);
|
|
40266
|
+
const kind = code === "!!" ? "ignored" : code === "??" ? "untracked" : code.includes("D") ? "deleted" : code.includes("A") ? "added" : code.includes("R") ? "renamed" : code.includes("M") ? "modified" : "tracked";
|
|
40267
|
+
status3.set(abs.replace(/\/$/, ""), kind);
|
|
40268
|
+
}
|
|
40269
|
+
}
|
|
40270
|
+
} catch {}
|
|
40271
|
+
}
|
|
40272
|
+
const lookupStatus = (abs, isDir) => {
|
|
40273
|
+
const trimmed = abs.replace(/\/$/, "");
|
|
40274
|
+
if (status3.has(trimmed))
|
|
40275
|
+
return status3.get(trimmed);
|
|
40276
|
+
if (isDir) {
|
|
40277
|
+
for (let p = trimmed;p && p !== repoRoot; p = p.replace(/\/[^/]*$/, "")) {
|
|
40278
|
+
const s = status3.get(p);
|
|
40279
|
+
if (s === "ignored" || s === "untracked")
|
|
40280
|
+
return s;
|
|
40281
|
+
if (!p.includes("/"))
|
|
40282
|
+
break;
|
|
40283
|
+
}
|
|
40284
|
+
}
|
|
40285
|
+
if (!repoRoot)
|
|
40286
|
+
return "tracked";
|
|
40287
|
+
return "tracked";
|
|
40288
|
+
};
|
|
40289
|
+
const out2 = entries2.filter((e) => showHidden || !e.name.startsWith(".")).map((e) => {
|
|
40290
|
+
const abs = path.join(dir, e.name);
|
|
40291
|
+
return {
|
|
40292
|
+
name: e.name,
|
|
40293
|
+
path: abs,
|
|
40294
|
+
is_dir: e.isDirectory(),
|
|
40295
|
+
status: lookupStatus(abs, e.isDirectory())
|
|
40296
|
+
};
|
|
40297
|
+
}).sort((a, b) => a.is_dir === b.is_dir ? a.name.localeCompare(b.name) : a.is_dir ? -1 : 1);
|
|
40298
|
+
return Response.json({ dir, repo_root: repoRoot, entries: out2 });
|
|
40299
|
+
} catch (e) {
|
|
40300
|
+
return Response.json({ error: String(e) }, { status: 400 });
|
|
39687
40301
|
}
|
|
39688
|
-
|
|
39689
|
-
|
|
40302
|
+
})();
|
|
40303
|
+
}
|
|
40304
|
+
if (url2.pathname === "/worktree/create" && req.method === "POST") {
|
|
40305
|
+
if (req.headers.get("authorization") !== expectedAuth)
|
|
40306
|
+
return new Response("unauthorized", { status: 401 });
|
|
40307
|
+
return (async () => {
|
|
40308
|
+
try {
|
|
40309
|
+
const body = await req.json();
|
|
40310
|
+
if (!body?.base_dir || !body.base_dir.startsWith("/")) {
|
|
40311
|
+
return Response.json({ error: "absolute base_dir required" }, { status: 400 });
|
|
40312
|
+
}
|
|
40313
|
+
const { codename: codename2, materialiseWorktree: materialiseWorktree2 } = await Promise.resolve().then(() => (init_worktree_create(), exports_worktree_create));
|
|
40314
|
+
const name = (body.name || codename2()).replace(/[^a-zA-Z0-9_-]/g, "-").slice(0, 40);
|
|
40315
|
+
const result = await materialiseWorktree2({
|
|
40316
|
+
baseDir: body.base_dir,
|
|
40317
|
+
baseBranch: body.base_branch || null,
|
|
40318
|
+
name,
|
|
40319
|
+
chatId: body.chat_id || null,
|
|
40320
|
+
log: (m) => log3(m)
|
|
40321
|
+
});
|
|
40322
|
+
return Response.json(result);
|
|
40323
|
+
} catch (e) {
|
|
40324
|
+
return Response.json({ error: String(e.message || e) }, { status: 400 });
|
|
39690
40325
|
}
|
|
39691
|
-
|
|
39692
|
-
|
|
39693
|
-
|
|
39694
|
-
|
|
39695
|
-
|
|
39696
|
-
|
|
39697
|
-
|
|
39698
|
-
|
|
39699
|
-
|
|
39700
|
-
|
|
39701
|
-
|
|
39702
|
-
|
|
39703
|
-
|
|
39704
|
-
|
|
39705
|
-
|
|
39706
|
-
|
|
39707
|
-
return new Response("unauthorized", { status: 401 });
|
|
39708
|
-
return (async () => {
|
|
39709
|
-
try {
|
|
39710
|
-
const body = await req.json();
|
|
39711
|
-
if (!body?.tick_id || !body?.callback_url || !body?.callback_token) {
|
|
39712
|
-
return Response.json({ error: "tick_id, callback_url, callback_token required" }, { status: 400 });
|
|
40326
|
+
})();
|
|
40327
|
+
}
|
|
40328
|
+
if (url2.pathname === "/worktree/delete" && req.method === "POST") {
|
|
40329
|
+
if (req.headers.get("authorization") !== expectedAuth)
|
|
40330
|
+
return new Response("unauthorized", { status: 401 });
|
|
40331
|
+
return (async () => {
|
|
40332
|
+
try {
|
|
40333
|
+
const body = await req.json();
|
|
40334
|
+
if (!body?.path || !body.path.startsWith("/")) {
|
|
40335
|
+
return Response.json({ error: "absolute path required" }, { status: 400 });
|
|
40336
|
+
}
|
|
40337
|
+
const { removeWorktree: removeWorktree3 } = await Promise.resolve().then(() => (init_worktree_create(), exports_worktree_create));
|
|
40338
|
+
const result = await removeWorktree3(body.path, (m) => log3(m));
|
|
40339
|
+
return Response.json(result);
|
|
40340
|
+
} catch (e) {
|
|
40341
|
+
return Response.json({ error: String(e.message || e) }, { status: 400 });
|
|
39713
40342
|
}
|
|
39714
|
-
|
|
39715
|
-
|
|
39716
|
-
|
|
39717
|
-
|
|
39718
|
-
|
|
39719
|
-
|
|
39720
|
-
|
|
39721
|
-
|
|
39722
|
-
|
|
39723
|
-
|
|
39724
|
-
|
|
39725
|
-
|
|
39726
|
-
|
|
39727
|
-
|
|
39728
|
-
|
|
39729
|
-
|
|
39730
|
-
|
|
39731
|
-
|
|
39732
|
-
|
|
39733
|
-
|
|
39734
|
-
|
|
39735
|
-
|
|
39736
|
-
|
|
39737
|
-
|
|
39738
|
-
|
|
39739
|
-
|
|
39740
|
-
|
|
39741
|
-
|
|
39742
|
-
|
|
40343
|
+
})();
|
|
40344
|
+
}
|
|
40345
|
+
if (url2.pathname === "/branch" && req.method === "GET") {
|
|
40346
|
+
if (req.headers.get("authorization") !== expectedAuth)
|
|
40347
|
+
return new Response("unauthorized", { status: 401 });
|
|
40348
|
+
return (async () => {
|
|
40349
|
+
try {
|
|
40350
|
+
const dir = url2.searchParams.get("dir") || "";
|
|
40351
|
+
if (!dir || !dir.startsWith("/")) {
|
|
40352
|
+
return Response.json({ error: "absolute dir required" }, { status: 400 });
|
|
40353
|
+
}
|
|
40354
|
+
const runGit = async (args2) => {
|
|
40355
|
+
const proc = Bun.spawn(["git", ...args2], {
|
|
40356
|
+
cwd: dir,
|
|
40357
|
+
stdout: "pipe",
|
|
40358
|
+
stderr: "ignore"
|
|
40359
|
+
});
|
|
40360
|
+
const out2 = (await new Response(proc.stdout).text()).trim();
|
|
40361
|
+
await proc.exited;
|
|
40362
|
+
return { ok: proc.exitCode === 0, out: out2 };
|
|
40363
|
+
};
|
|
40364
|
+
const branchR = await runGit(["branch", "--show-current"]);
|
|
40365
|
+
const branch = branchR.ok ? branchR.out || null : null;
|
|
40366
|
+
const upstreamR = await runGit(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]);
|
|
40367
|
+
const upstream = upstreamR.ok ? upstreamR.out || null : null;
|
|
40368
|
+
let ahead = 0;
|
|
40369
|
+
let behind = 0;
|
|
40370
|
+
const unpushed = [];
|
|
40371
|
+
if (upstream) {
|
|
40372
|
+
const countR = await runGit(["rev-list", "--left-right", "--count", `${upstream}...HEAD`]);
|
|
40373
|
+
if (countR.ok) {
|
|
40374
|
+
const [b, a] = countR.out.split(/\s+/);
|
|
40375
|
+
behind = Number(b) || 0;
|
|
40376
|
+
ahead = Number(a) || 0;
|
|
40377
|
+
}
|
|
40378
|
+
if (ahead > 0) {
|
|
40379
|
+
const logR = await runGit(["log", `${upstream}..HEAD`, "--pretty=format:%h\x1F%s", "-n", "50"]);
|
|
40380
|
+
if (logR.ok && logR.out) {
|
|
40381
|
+
for (const line of logR.out.split(`
|
|
40382
|
+
`)) {
|
|
40383
|
+
const idx = line.indexOf("\x1F");
|
|
40384
|
+
if (idx === -1)
|
|
40385
|
+
continue;
|
|
40386
|
+
unpushed.push({ sha: line.slice(0, idx), subject: line.slice(idx + 1) });
|
|
40387
|
+
}
|
|
40388
|
+
}
|
|
40389
|
+
}
|
|
40390
|
+
} else if (branch) {
|
|
40391
|
+
const logR = await runGit(["log", "HEAD", "--not", "--remotes", "--pretty=format:%h\x1F%s", "-n", "50"]);
|
|
40392
|
+
if (logR.ok && logR.out) {
|
|
40393
|
+
for (const line of logR.out.split(`
|
|
40394
|
+
`)) {
|
|
40395
|
+
const idx = line.indexOf("\x1F");
|
|
40396
|
+
if (idx === -1)
|
|
40397
|
+
continue;
|
|
40398
|
+
unpushed.push({ sha: line.slice(0, idx), subject: line.slice(idx + 1) });
|
|
40399
|
+
}
|
|
40400
|
+
ahead = unpushed.length;
|
|
40401
|
+
}
|
|
40402
|
+
}
|
|
40403
|
+
return Response.json({ dir, branch, upstream, ahead, behind, unpushed });
|
|
40404
|
+
} catch (e) {
|
|
40405
|
+
return Response.json({ error: String(e) }, { status: 400 });
|
|
39743
40406
|
}
|
|
39744
|
-
|
|
39745
|
-
|
|
39746
|
-
|
|
40407
|
+
})();
|
|
40408
|
+
}
|
|
40409
|
+
if (url2.pathname === "/changes" && req.method === "GET") {
|
|
40410
|
+
if (req.headers.get("authorization") !== expectedAuth)
|
|
40411
|
+
return new Response("unauthorized", { status: 401 });
|
|
40412
|
+
return (async () => {
|
|
40413
|
+
try {
|
|
40414
|
+
const dir = url2.searchParams.get("dir") || "";
|
|
40415
|
+
if (!dir || !dir.startsWith("/")) {
|
|
40416
|
+
return Response.json({ error: "absolute dir required" }, { status: 400 });
|
|
40417
|
+
}
|
|
40418
|
+
const rootP = Bun.spawn(["git", "rev-parse", "--show-toplevel"], {
|
|
40419
|
+
cwd: dir,
|
|
40420
|
+
stdout: "pipe",
|
|
40421
|
+
stderr: "ignore"
|
|
40422
|
+
});
|
|
40423
|
+
const repoRoot = (await new Response(rootP.stdout).text()).trim();
|
|
40424
|
+
await rootP.exited;
|
|
40425
|
+
const proc = Bun.spawn(["git", "status", "--porcelain=v1", "-z"], {
|
|
40426
|
+
cwd: dir,
|
|
40427
|
+
stdout: "pipe",
|
|
40428
|
+
stderr: "pipe"
|
|
40429
|
+
});
|
|
40430
|
+
const out2 = await new Response(proc.stdout).text();
|
|
40431
|
+
const errOut = await new Response(proc.stderr).text();
|
|
40432
|
+
await proc.exited;
|
|
40433
|
+
if (proc.exitCode !== 0) {
|
|
40434
|
+
return Response.json({ dir, changes: [], error: errOut.trim() || "git failed" });
|
|
40435
|
+
}
|
|
40436
|
+
const changes = [];
|
|
40437
|
+
const parts2 = out2.split("\x00").filter((p) => p.length > 0);
|
|
40438
|
+
for (let i = 0;i < parts2.length; i++) {
|
|
40439
|
+
const entry = parts2[i];
|
|
40440
|
+
const code = entry.slice(0, 2);
|
|
40441
|
+
let p = entry.slice(3);
|
|
40442
|
+
if (code[0] === "R" || code[0] === "C") {
|
|
40443
|
+
i++;
|
|
40444
|
+
}
|
|
40445
|
+
changes.push({ code, path: p });
|
|
40446
|
+
}
|
|
40447
|
+
return Response.json({ dir, repo_root: repoRoot || null, changes });
|
|
40448
|
+
} catch (e) {
|
|
40449
|
+
return Response.json({ error: String(e) }, { status: 400 });
|
|
40450
|
+
}
|
|
40451
|
+
})();
|
|
40452
|
+
}
|
|
40453
|
+
if (url2.pathname === "/read-file" && req.method === "GET") {
|
|
40454
|
+
if (req.headers.get("authorization") !== expectedAuth)
|
|
40455
|
+
return new Response("unauthorized", { status: 401 });
|
|
40456
|
+
return (async () => {
|
|
40457
|
+
try {
|
|
40458
|
+
const p = url2.searchParams.get("path") || "";
|
|
40459
|
+
if (!p || !p.startsWith("/")) {
|
|
40460
|
+
return Response.json({ error: "absolute path required" }, { status: 400 });
|
|
40461
|
+
}
|
|
40462
|
+
const fs3 = await import("fs/promises");
|
|
40463
|
+
const stat = await fs3.stat(p);
|
|
40464
|
+
if (stat.isDirectory()) {
|
|
40465
|
+
return Response.json({ error: "is a directory" }, { status: 400 });
|
|
40466
|
+
}
|
|
40467
|
+
const MAX = 2097152;
|
|
40468
|
+
if (stat.size > MAX) {
|
|
40469
|
+
return Response.json({ path: p, size: stat.size, truncated: true, error: `file too large (${stat.size} bytes)` }, { status: 413 });
|
|
40470
|
+
}
|
|
40471
|
+
const buf = await fs3.readFile(p);
|
|
40472
|
+
const head5 = buf.subarray(0, Math.min(8192, buf.length));
|
|
40473
|
+
let nonPrint = 0;
|
|
40474
|
+
for (let i = 0;i < head5.length; i++) {
|
|
40475
|
+
const b = head5[i];
|
|
40476
|
+
if (b === 0) {
|
|
40477
|
+
nonPrint = head5.length;
|
|
40478
|
+
break;
|
|
40479
|
+
}
|
|
40480
|
+
if (b < 9 || b > 13 && b < 32)
|
|
40481
|
+
nonPrint++;
|
|
40482
|
+
}
|
|
40483
|
+
const isBinary = head5.length > 0 && nonPrint / head5.length > 0.3;
|
|
40484
|
+
if (isBinary) {
|
|
40485
|
+
return Response.json({ path: p, size: stat.size, binary: true });
|
|
40486
|
+
}
|
|
40487
|
+
return Response.json({ path: p, size: stat.size, content: buf.toString("utf8") });
|
|
40488
|
+
} catch (e) {
|
|
40489
|
+
return Response.json({ error: String(e.message || e) }, { status: 400 });
|
|
40490
|
+
}
|
|
40491
|
+
})();
|
|
40492
|
+
}
|
|
40493
|
+
if (url2.pathname === "/diff" && req.method === "GET") {
|
|
40494
|
+
if (req.headers.get("authorization") !== expectedAuth)
|
|
40495
|
+
return new Response("unauthorized", { status: 401 });
|
|
40496
|
+
return (async () => {
|
|
40497
|
+
try {
|
|
40498
|
+
const p = url2.searchParams.get("path") || "";
|
|
40499
|
+
if (!p || !p.startsWith("/")) {
|
|
40500
|
+
return Response.json({ error: "absolute path required" }, { status: 400 });
|
|
40501
|
+
}
|
|
40502
|
+
const path = await import("path");
|
|
40503
|
+
const repoDir = path.dirname(p);
|
|
40504
|
+
const rootP = Bun.spawn(["git", "rev-parse", "--show-toplevel"], {
|
|
40505
|
+
cwd: repoDir,
|
|
40506
|
+
stdout: "pipe",
|
|
40507
|
+
stderr: "ignore"
|
|
40508
|
+
});
|
|
40509
|
+
const rootOut = (await new Response(rootP.stdout).text()).trim();
|
|
40510
|
+
await rootP.exited;
|
|
40511
|
+
if (rootP.exitCode !== 0 || !rootOut) {
|
|
40512
|
+
return Response.json({ path: p, diff: "", error: "not a git repo" });
|
|
40513
|
+
}
|
|
40514
|
+
const proc = Bun.spawn(["git", "diff", "HEAD", "--", p], { cwd: rootOut, stdout: "pipe", stderr: "pipe" });
|
|
40515
|
+
const out2 = await new Response(proc.stdout).text();
|
|
40516
|
+
const err = await new Response(proc.stderr).text();
|
|
40517
|
+
await proc.exited;
|
|
40518
|
+
if (proc.exitCode !== 0) {
|
|
40519
|
+
return Response.json({ path: p, diff: "", error: err.trim() || "git diff failed" });
|
|
40520
|
+
}
|
|
40521
|
+
return Response.json({ path: p, diff: out2 });
|
|
40522
|
+
} catch (e) {
|
|
40523
|
+
return Response.json({ error: String(e.message || e) }, { status: 400 });
|
|
40524
|
+
}
|
|
40525
|
+
})();
|
|
40526
|
+
}
|
|
40527
|
+
if (url2.pathname === "/run" && req.method === "POST") {
|
|
40528
|
+
if (req.headers.get("authorization") !== expectedAuth)
|
|
40529
|
+
return new Response("unauthorized", { status: 401 });
|
|
40530
|
+
return (async () => {
|
|
40531
|
+
try {
|
|
40532
|
+
const body = await req.json();
|
|
40533
|
+
const t = body?.task || {};
|
|
40534
|
+
const taskId = t.issue_id ? `${t.issue_id}-${Date.now()}` : crypto.randomUUID();
|
|
40535
|
+
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]);
|
|
40536
|
+
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;
|
|
40537
|
+
if (t.issue_id)
|
|
40538
|
+
postStream(apiUrl, t.issue_id, "queued", { queue_position: pos });
|
|
40539
|
+
if (t.dispatch_id && cfg.workspaceId)
|
|
40540
|
+
ackDispatch(apiUrl, cfg.workspaceId, t.dispatch_id, cfg.authToken);
|
|
39747
40541
|
try {
|
|
39748
|
-
|
|
40542
|
+
exports_Queue.unsafeOffer(wakeQ, undefined);
|
|
39749
40543
|
} catch {}
|
|
39750
|
-
|
|
40544
|
+
return Response.json({ accepted: true, task_id: taskId }, { status: 202 });
|
|
40545
|
+
} catch (e) {
|
|
40546
|
+
return Response.json({ error: String(e) }, { status: 400 });
|
|
40547
|
+
}
|
|
40548
|
+
})();
|
|
40549
|
+
}
|
|
40550
|
+
{
|
|
40551
|
+
const m = /^\/chat-snapshot\/([^/]+)$/.exec(url2.pathname);
|
|
40552
|
+
if (m && req.method === "GET") {
|
|
40553
|
+
const token = url2.searchParams.get("token") || "";
|
|
40554
|
+
const headerToken = req.headers.get("authorization") === expectedAuth;
|
|
40555
|
+
if (!headerToken && token !== cfg.dispatchSecret) {
|
|
40556
|
+
return new Response("unauthorized", { status: 401 });
|
|
40557
|
+
}
|
|
40558
|
+
if (!cfg.workspaceId || !cfg.authToken || !cfg.deviceId) {
|
|
40559
|
+
return new Response("daemon not configured", { status: 503 });
|
|
40560
|
+
}
|
|
40561
|
+
const chatId = decodeURIComponent(m[1]);
|
|
40562
|
+
return (async () => {
|
|
40563
|
+
const { chatSessionRegistry: chatSessionRegistry2 } = await Promise.resolve().then(() => (init_chat_session_registry(), exports_chat_session_registry));
|
|
40564
|
+
const peer = chatSessionRegistry2.ensurePeer(cfg.workspaceId, chatId, {
|
|
40565
|
+
apiUrl,
|
|
40566
|
+
authToken: cfg.authToken,
|
|
40567
|
+
workspaceId: cfg.workspaceId,
|
|
40568
|
+
deviceId: cfg.deviceId,
|
|
40569
|
+
log: log3
|
|
40570
|
+
});
|
|
40571
|
+
const greet = peer.buildSnapshotGreet();
|
|
40572
|
+
return new Response(greet.subarray(1), {
|
|
40573
|
+
headers: { "content-type": "application/octet-stream" }
|
|
40574
|
+
});
|
|
40575
|
+
})();
|
|
40576
|
+
}
|
|
40577
|
+
}
|
|
40578
|
+
if (url2.pathname === "/run-chat-turn" && req.method === "POST") {
|
|
40579
|
+
if (req.headers.get("authorization") !== expectedAuth)
|
|
40580
|
+
return new Response("unauthorized", { status: 401 });
|
|
40581
|
+
return (async () => {
|
|
40582
|
+
try {
|
|
40583
|
+
const body = await req.json();
|
|
40584
|
+
if (!body?.chat_id || !body?.message_id) {
|
|
40585
|
+
return Response.json({ error: "chat_id and message_id required" }, { status: 400 });
|
|
40586
|
+
}
|
|
40587
|
+
if (!cfg.workspaceId || !cfg.authToken || !cfg.deviceId) {
|
|
40588
|
+
return Response.json({ error: "daemon not configured" }, { status: 503 });
|
|
40589
|
+
}
|
|
40590
|
+
const { chatSessionRegistry: chatSessionRegistry2 } = await Promise.resolve().then(() => (init_chat_session_registry(), exports_chat_session_registry));
|
|
40591
|
+
chatSessionRegistry2.ensureAndProcess(cfg.workspaceId, body.chat_id, body.message_id, {
|
|
40592
|
+
apiUrl,
|
|
40593
|
+
authToken: cfg.authToken,
|
|
40594
|
+
workspaceId: cfg.workspaceId,
|
|
40595
|
+
deviceId: cfg.deviceId,
|
|
40596
|
+
log: log3
|
|
40597
|
+
}).catch((e) => log3(`[chat ${body.chat_id}] runChatTurn error: ${e.message}`));
|
|
40598
|
+
return Response.json({ accepted: true }, { status: 202 });
|
|
40599
|
+
} catch (e) {
|
|
40600
|
+
return Response.json({ error: String(e) }, { status: 400 });
|
|
40601
|
+
}
|
|
40602
|
+
})();
|
|
40603
|
+
}
|
|
40604
|
+
if (url2.pathname === "/run-supervisor-tick" && req.method === "POST") {
|
|
40605
|
+
if (req.headers.get("authorization") !== expectedAuth)
|
|
40606
|
+
return new Response("unauthorized", { status: 401 });
|
|
40607
|
+
return (async () => {
|
|
40608
|
+
try {
|
|
40609
|
+
const body = await req.json();
|
|
40610
|
+
if (!body?.tick_id || !body?.callback_url || !body?.callback_token) {
|
|
40611
|
+
return Response.json({ error: "tick_id, callback_url, callback_token required" }, { status: 400 });
|
|
40612
|
+
}
|
|
40613
|
+
const { runSupervisorTick: runSupervisorTick2 } = await Promise.resolve().then(() => (init_supervisor_tick(), exports_supervisor_tick));
|
|
40614
|
+
runSupervisorTick2({
|
|
40615
|
+
apiUrl,
|
|
40616
|
+
tickId: body.tick_id,
|
|
40617
|
+
projectId: body.project_id,
|
|
40618
|
+
system: body.system,
|
|
40619
|
+
user: body.user,
|
|
40620
|
+
callbackUrl: body.callback_url,
|
|
40621
|
+
callbackToken: body.callback_token,
|
|
40622
|
+
log: log3
|
|
40623
|
+
}).catch((e) => log3(`[sup ${body.tick_id}] runSupervisorTick error: ${e.message}`));
|
|
40624
|
+
return Response.json({ accepted: true }, { status: 202 });
|
|
40625
|
+
} catch (e) {
|
|
40626
|
+
return Response.json({ error: String(e) }, { status: 400 });
|
|
40627
|
+
}
|
|
40628
|
+
})();
|
|
40629
|
+
}
|
|
40630
|
+
if (url2.pathname === "/stop" && req.method === "POST") {
|
|
40631
|
+
if (req.headers.get("authorization") !== expectedAuth)
|
|
40632
|
+
return new Response("unauthorized", { status: 401 });
|
|
40633
|
+
return (async () => {
|
|
40634
|
+
try {
|
|
40635
|
+
const { issue_id } = await req.json();
|
|
40636
|
+
if (!issue_id)
|
|
40637
|
+
return Response.json({ error: "issue_id required" }, { status: 400 });
|
|
40638
|
+
const entries2 = Array.from(running3.values()).filter((e) => e.issueId === issue_id);
|
|
40639
|
+
if (!entries2.length) {
|
|
40640
|
+
db2.run("UPDATE tasks SET status = 'failed', error = 'stopped before start', finished_at = unixepoch() WHERE issue_id = ? AND status = 'queued'", [issue_id]);
|
|
40641
|
+
return Response.json({ ok: true, state: "queued-cancelled" });
|
|
40642
|
+
}
|
|
40643
|
+
for (const entry of entries2) {
|
|
40644
|
+
entry.stopped = true;
|
|
40645
|
+
entry.stopReason = "user requested";
|
|
39751
40646
|
try {
|
|
39752
|
-
entry.child?.kill("
|
|
40647
|
+
entry.child?.kill("SIGTERM");
|
|
39753
40648
|
} catch {}
|
|
39754
|
-
|
|
40649
|
+
setTimeout(() => {
|
|
40650
|
+
try {
|
|
40651
|
+
entry.child?.kill("SIGKILL");
|
|
40652
|
+
} catch {}
|
|
40653
|
+
}, 1500);
|
|
40654
|
+
}
|
|
40655
|
+
return Response.json({ ok: true, state: "running-signalled" });
|
|
40656
|
+
} catch (e) {
|
|
40657
|
+
return Response.json({ error: String(e) }, { status: 400 });
|
|
39755
40658
|
}
|
|
39756
|
-
|
|
39757
|
-
|
|
39758
|
-
|
|
39759
|
-
|
|
39760
|
-
|
|
40659
|
+
})();
|
|
40660
|
+
}
|
|
40661
|
+
return new Response("not found", { status: 404 });
|
|
40662
|
+
};
|
|
40663
|
+
const out = route();
|
|
40664
|
+
return out instanceof Promise ? out.then((r) => withCors(r, origin)) : withCors(out, origin);
|
|
40665
|
+
},
|
|
40666
|
+
websocket: {
|
|
40667
|
+
async open(ws) {
|
|
40668
|
+
const data = ws.data;
|
|
40669
|
+
if (!data || data.kind !== "chat-sync")
|
|
40670
|
+
return;
|
|
40671
|
+
if (!cfg.workspaceId || !cfg.authToken || !cfg.deviceId) {
|
|
40672
|
+
try {
|
|
40673
|
+
ws.close(1011, "daemon not configured");
|
|
40674
|
+
} catch {}
|
|
40675
|
+
return;
|
|
40676
|
+
}
|
|
40677
|
+
const { chatSessionRegistry: chatSessionRegistry2 } = await Promise.resolve().then(() => (init_chat_session_registry(), exports_chat_session_registry));
|
|
40678
|
+
const peer = chatSessionRegistry2.ensurePeer(data.wsId, data.chatId, {
|
|
40679
|
+
apiUrl,
|
|
40680
|
+
authToken: cfg.authToken,
|
|
40681
|
+
workspaceId: cfg.workspaceId,
|
|
40682
|
+
deviceId: cfg.deviceId,
|
|
40683
|
+
log: log3
|
|
40684
|
+
});
|
|
40685
|
+
const sub = peer.addLocalSubscriber((frame) => {
|
|
40686
|
+
try {
|
|
40687
|
+
ws.sendBinary(frame);
|
|
40688
|
+
} catch {}
|
|
40689
|
+
});
|
|
40690
|
+
ws.data.peerToken = sub.token;
|
|
40691
|
+
ws.data.unsubscribe = sub.close;
|
|
40692
|
+
try {
|
|
40693
|
+
ws.sendBinary(peer.buildSnapshotGreet());
|
|
40694
|
+
} catch {}
|
|
40695
|
+
},
|
|
40696
|
+
async message(ws, message) {
|
|
40697
|
+
const data = ws.data;
|
|
40698
|
+
if (!data || data.kind !== "chat-sync" || !data.peerToken)
|
|
40699
|
+
return;
|
|
40700
|
+
if (typeof message === "string")
|
|
40701
|
+
return;
|
|
40702
|
+
const buf = message instanceof Uint8Array ? message : new Uint8Array(message);
|
|
40703
|
+
if (buf.byteLength < 1)
|
|
40704
|
+
return;
|
|
40705
|
+
const tag3 = buf[0];
|
|
40706
|
+
if (tag3 === 1) {
|
|
40707
|
+
const { chatSessionRegistry: chatSessionRegistry2 } = await Promise.resolve().then(() => (init_chat_session_registry(), exports_chat_session_registry));
|
|
40708
|
+
const peer = chatSessionRegistry2.ensurePeer(data.wsId, data.chatId, {
|
|
40709
|
+
apiUrl,
|
|
40710
|
+
authToken: cfg.authToken,
|
|
40711
|
+
workspaceId: cfg.workspaceId,
|
|
40712
|
+
deviceId: cfg.deviceId,
|
|
40713
|
+
log: log3
|
|
40714
|
+
});
|
|
40715
|
+
peer.ingestLocalUpdate(buf.subarray(1), data.peerToken);
|
|
40716
|
+
}
|
|
40717
|
+
},
|
|
40718
|
+
close(ws) {
|
|
40719
|
+
const data = ws.data;
|
|
40720
|
+
if (data?.unsubscribe) {
|
|
40721
|
+
try {
|
|
40722
|
+
data.unsubscribe();
|
|
40723
|
+
} catch {}
|
|
40724
|
+
}
|
|
39761
40725
|
}
|
|
39762
|
-
return new Response("not found", { status: 404 });
|
|
39763
40726
|
}
|
|
39764
40727
|
});
|
|
39765
40728
|
log3(`Local server: http://127.0.0.1:${port}`);
|
|
39766
40729
|
try {
|
|
39767
|
-
|
|
40730
|
+
writeFileSync10(PORT_PATH2, String(port));
|
|
39768
40731
|
} catch {}
|
|
39769
40732
|
try {
|
|
39770
|
-
|
|
40733
|
+
writeFileSync10(PID_PATH2, String(process.pid));
|
|
40734
|
+
} catch {}
|
|
40735
|
+
try {
|
|
40736
|
+
if (!existsSync16(LOCAL_SERVER_DIR))
|
|
40737
|
+
mkdirSync12(LOCAL_SERVER_DIR, { recursive: true });
|
|
40738
|
+
writeFileSync10(LOCAL_SERVER_PATH, JSON.stringify({
|
|
40739
|
+
url: `http://127.0.0.1:${port}`,
|
|
40740
|
+
port,
|
|
40741
|
+
token: cfg.dispatchSecret,
|
|
40742
|
+
device_id: cfg.deviceId,
|
|
40743
|
+
workspace_id: cfg.workspaceId,
|
|
40744
|
+
api_url: apiUrl,
|
|
40745
|
+
pid: process.pid
|
|
40746
|
+
}, null, 2));
|
|
39771
40747
|
} catch {}
|
|
39772
40748
|
let tunnel;
|
|
39773
40749
|
if (localMode) {
|
|
@@ -39831,7 +40807,7 @@ var daemonProgram = ({ cfg, apiUrl }) => exports_Effect.gen(function* () {
|
|
|
39831
40807
|
let probeFailures = 0;
|
|
39832
40808
|
while (true) {
|
|
39833
40809
|
yield* exports_Effect.sleep(exports_Duration.seconds(120));
|
|
39834
|
-
if (
|
|
40810
|
+
if (existsSync16(STOP_PATH2)) {
|
|
39835
40811
|
yield* exports_Deferred.succeed(stopDeferred, "stop flag");
|
|
39836
40812
|
return;
|
|
39837
40813
|
}
|
|
@@ -39856,7 +40832,7 @@ var daemonProgram = ({ cfg, apiUrl }) => exports_Effect.gen(function* () {
|
|
|
39856
40832
|
yield* exports_Effect.forkIn(daemonScope)(exports_Effect.gen(function* () {
|
|
39857
40833
|
while (true) {
|
|
39858
40834
|
yield* exports_Effect.sleep(exports_Duration.seconds(5));
|
|
39859
|
-
if (
|
|
40835
|
+
if (existsSync16(STOP_PATH2)) {
|
|
39860
40836
|
yield* exports_Deferred.succeed(stopDeferred, "stop flag");
|
|
39861
40837
|
return;
|
|
39862
40838
|
}
|
|
@@ -39895,13 +40871,22 @@ var daemonProgram = ({ cfg, apiUrl }) => exports_Effect.gen(function* () {
|
|
|
39895
40871
|
if (inFlight.length) {
|
|
39896
40872
|
yield* exports_Effect.race(exports_Fiber.interruptAll(inFlight), exports_Effect.sleep(exports_Duration.seconds(10)));
|
|
39897
40873
|
}
|
|
40874
|
+
try {
|
|
40875
|
+
const killed = killDaemonChildren(process.pid, (m) => log3(m));
|
|
40876
|
+
if (killed > 0)
|
|
40877
|
+
log3(`killed ${killed} adapter child${killed === 1 ? "" : "ren"} on shutdown`);
|
|
40878
|
+
} catch (e) {
|
|
40879
|
+
log3(`adapter shutdown sweep failed: ${String(e)}`);
|
|
40880
|
+
}
|
|
39898
40881
|
yield* exports_Scope.close(daemonScope, exports_Exit.void).pipe(exports_Effect.catchAll(() => exports_Effect.void));
|
|
39899
|
-
if (
|
|
40882
|
+
if (existsSync16(PID_PATH2))
|
|
39900
40883
|
unlinkSync7(PID_PATH2);
|
|
39901
|
-
if (
|
|
40884
|
+
if (existsSync16(STOP_PATH2))
|
|
39902
40885
|
unlinkSync7(STOP_PATH2);
|
|
39903
|
-
if (
|
|
40886
|
+
if (existsSync16(PORT_PATH2))
|
|
39904
40887
|
unlinkSync7(PORT_PATH2);
|
|
40888
|
+
if (existsSync16(LOCAL_SERVER_PATH))
|
|
40889
|
+
unlinkSync7(LOCAL_SERVER_PATH);
|
|
39905
40890
|
db2.close();
|
|
39906
40891
|
log3("disconnected");
|
|
39907
40892
|
});
|
|
@@ -39914,7 +40899,81 @@ async function daemonMain(opts) {
|
|
|
39914
40899
|
}
|
|
39915
40900
|
|
|
39916
40901
|
// src/commands/connect.ts
|
|
40902
|
+
init_detect();
|
|
39917
40903
|
init_outbox();
|
|
40904
|
+
|
|
40905
|
+
// src/_impl/pair-flow.ts
|
|
40906
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync13, writeFileSync as writeFileSync11 } from "fs";
|
|
40907
|
+
import { homedir as homedir5 } from "os";
|
|
40908
|
+
import { join as join17 } from "path";
|
|
40909
|
+
var LOCAL_SERVER_DIR2 = process.env.MULTI_HOME || join17(homedir5(), ".multi");
|
|
40910
|
+
var LOCAL_SERVER_PATH2 = join17(LOCAL_SERVER_DIR2, "local-server.json");
|
|
40911
|
+
function writePairingInfo(payload) {
|
|
40912
|
+
try {
|
|
40913
|
+
if (!existsSync17(LOCAL_SERVER_DIR2))
|
|
40914
|
+
mkdirSync13(LOCAL_SERVER_DIR2, { recursive: true });
|
|
40915
|
+
writeFileSync11(LOCAL_SERVER_PATH2, JSON.stringify(payload, null, 2));
|
|
40916
|
+
} catch {}
|
|
40917
|
+
}
|
|
40918
|
+
async function awaitPairing(apiUrl, deviceName, opts = {}) {
|
|
40919
|
+
const log4 = opts.log ?? ((m) => console.log(m));
|
|
40920
|
+
const timeoutMs = opts.timeoutMs ?? 10 * 60 * 1000;
|
|
40921
|
+
const startRes = await fetch(`${apiUrl}/api/pair/start`, {
|
|
40922
|
+
method: "POST",
|
|
40923
|
+
headers: { "content-type": "application/json" },
|
|
40924
|
+
body: JSON.stringify({
|
|
40925
|
+
name: deviceName,
|
|
40926
|
+
platform: opts.platform ?? process.platform,
|
|
40927
|
+
arch: opts.arch ?? process.arch,
|
|
40928
|
+
os_version: opts.osVersion ?? process.version,
|
|
40929
|
+
detected_runtimes: opts.detectedRuntimes ?? []
|
|
40930
|
+
})
|
|
40931
|
+
});
|
|
40932
|
+
if (!startRes.ok) {
|
|
40933
|
+
throw new Error(`pair/start failed: ${startRes.status} ${await startRes.text()}`);
|
|
40934
|
+
}
|
|
40935
|
+
const start3 = await startRes.json();
|
|
40936
|
+
const code = start3.code;
|
|
40937
|
+
const pairUrl = `${apiUrl}/pair/${code}`;
|
|
40938
|
+
writePairingInfo({
|
|
40939
|
+
status: "pairing",
|
|
40940
|
+
pair_code: code,
|
|
40941
|
+
pair_url: pairUrl,
|
|
40942
|
+
device_name: deviceName,
|
|
40943
|
+
api_url: apiUrl,
|
|
40944
|
+
expires_at: start3.expires_at
|
|
40945
|
+
});
|
|
40946
|
+
log4(`pair: code=${code} url=${pairUrl}`);
|
|
40947
|
+
const deadline = Date.now() + timeoutMs;
|
|
40948
|
+
while (Date.now() < deadline) {
|
|
40949
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
40950
|
+
let res;
|
|
40951
|
+
try {
|
|
40952
|
+
res = await fetch(`${apiUrl}/api/pair/poll/${code}`);
|
|
40953
|
+
} catch {
|
|
40954
|
+
continue;
|
|
40955
|
+
}
|
|
40956
|
+
if (res.status === 410)
|
|
40957
|
+
throw new Error("Pairing expired.");
|
|
40958
|
+
if (!res.ok)
|
|
40959
|
+
continue;
|
|
40960
|
+
const j = await res.json();
|
|
40961
|
+
if (j.status === "approved" && j.device_id && j.token) {
|
|
40962
|
+
return {
|
|
40963
|
+
device_id: j.device_id,
|
|
40964
|
+
token: j.token,
|
|
40965
|
+
dispatch_secret: j.dispatch_secret ?? null,
|
|
40966
|
+
workspace_id: j.workspace_id ?? null
|
|
40967
|
+
};
|
|
40968
|
+
}
|
|
40969
|
+
}
|
|
40970
|
+
throw new Error("Pairing timed out.");
|
|
40971
|
+
}
|
|
40972
|
+
function deviceNameFromEnv() {
|
|
40973
|
+
return process.env.MULTI_DEVICE_NAME || process.env.HOSTNAME || process.env.COMPUTERNAME || "Multi Desktop";
|
|
40974
|
+
}
|
|
40975
|
+
|
|
40976
|
+
// src/commands/connect.ts
|
|
39918
40977
|
init_paths();
|
|
39919
40978
|
init_errors();
|
|
39920
40979
|
var connectCmd = exports_Effect.fn("connectCmd")(function* () {
|
|
@@ -39922,9 +40981,28 @@ var connectCmd = exports_Effect.fn("connectCmd")(function* () {
|
|
|
39922
40981
|
const logger = yield* Logger4;
|
|
39923
40982
|
const api2 = yield* Api2;
|
|
39924
40983
|
yield* config2.ensureDirs;
|
|
39925
|
-
|
|
39926
|
-
if (!cfg.deviceId) {
|
|
39927
|
-
|
|
40984
|
+
let cfg = yield* config2.load;
|
|
40985
|
+
if (!cfg.deviceId || !cfg.authToken) {
|
|
40986
|
+
const apiUrl = cfg.apiUrl || process.env.MULTI_API || process.env.MULTI_API_URL || "https://multi-api.adnb3r.workers.dev";
|
|
40987
|
+
const name = deviceNameFromEnv();
|
|
40988
|
+
const detected = yield* exports_Effect.promise(() => detectAgents());
|
|
40989
|
+
yield* logger.log(`connect: no device registered — starting pair flow for "${name}" ` + `(runtimes: ${detected.map((d) => d.type).join(", ") || "none"})`);
|
|
40990
|
+
const bundle = yield* exports_Effect.tryPromise({
|
|
40991
|
+
try: () => awaitPairing(apiUrl, name, {
|
|
40992
|
+
log: (m) => console.log(m),
|
|
40993
|
+
detectedRuntimes: detected.map((d) => d.type)
|
|
40994
|
+
}),
|
|
40995
|
+
catch: (cause3) => new ApiError({ url: `${apiUrl}/api/pair/start`, status: 0, message: cause3.message })
|
|
40996
|
+
});
|
|
40997
|
+
yield* config2.save({
|
|
40998
|
+
apiUrl,
|
|
40999
|
+
deviceId: bundle.device_id,
|
|
41000
|
+
authToken: bundle.token,
|
|
41001
|
+
dispatchSecret: bundle.dispatch_secret ?? undefined,
|
|
41002
|
+
workspaceId: bundle.workspace_id ?? undefined
|
|
41003
|
+
});
|
|
41004
|
+
cfg = yield* config2.load;
|
|
41005
|
+
yield* logger.log(`connect: paired device=${cfg.deviceId} workspace=${cfg.workspaceId}`);
|
|
39928
41006
|
}
|
|
39929
41007
|
if (!cfg.dispatchSecret) {
|
|
39930
41008
|
return yield* exports_Effect.fail(new UsageError({ message: "Missing dispatch secret. Re-pair via 'multi-agent setup'." }));
|
|
@@ -39961,6 +41039,250 @@ ${err?.stack ?? ""}`);
|
|
|
39961
41039
|
}).pipe(exports_Effect.ensuring(exports_Effect.sync(() => stopFlusher())));
|
|
39962
41040
|
});
|
|
39963
41041
|
|
|
41042
|
+
// src/commands/service.ts
|
|
41043
|
+
init_esm();
|
|
41044
|
+
import { existsSync as existsSync18, readFileSync as readFileSync12 } from "fs";
|
|
41045
|
+
import { homedir as homedir6, platform } from "os";
|
|
41046
|
+
import { join as join18 } from "path";
|
|
41047
|
+
init_errors();
|
|
41048
|
+
init_paths();
|
|
41049
|
+
var LABEL = "dev.shipers.multi.daemon";
|
|
41050
|
+
var HOME5 = homedir6();
|
|
41051
|
+
var PLIST_PATH = join18(HOME5, "Library", "LaunchAgents", `${LABEL}.plist`);
|
|
41052
|
+
var SYSTEMD_UNIT_PATH = join18(HOME5, ".config", "systemd", "user", "multi-daemon.service");
|
|
41053
|
+
var isMac = () => platform() === "darwin";
|
|
41054
|
+
var isLinux = () => platform() === "linux";
|
|
41055
|
+
var isRunningPid = (pid) => {
|
|
41056
|
+
try {
|
|
41057
|
+
process.kill(pid, 0);
|
|
41058
|
+
return true;
|
|
41059
|
+
} catch {
|
|
41060
|
+
return false;
|
|
41061
|
+
}
|
|
41062
|
+
};
|
|
41063
|
+
var readPid = () => {
|
|
41064
|
+
try {
|
|
41065
|
+
if (!existsSync18(PID_PATH))
|
|
41066
|
+
return null;
|
|
41067
|
+
const n = Number(readFileSync12(PID_PATH, "utf8").trim());
|
|
41068
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
41069
|
+
} catch {
|
|
41070
|
+
return null;
|
|
41071
|
+
}
|
|
41072
|
+
};
|
|
41073
|
+
var resolveBinary = exports_Effect.fn("service.resolveBinary")(function* () {
|
|
41074
|
+
const proc = Bun.spawn(["which", "multi-agent"], { stdout: "pipe", stderr: "pipe" });
|
|
41075
|
+
yield* exports_Effect.promise(() => proc.exited);
|
|
41076
|
+
const out = yield* exports_Effect.promise(() => new Response(proc.stdout).text());
|
|
41077
|
+
const path = out.trim();
|
|
41078
|
+
if (!path || !existsSync18(path)) {
|
|
41079
|
+
return yield* exports_Effect.fail(new DaemonError({
|
|
41080
|
+
message: "Could not find `multi-agent` on PATH. Install with: bun install -g @shipers-dev/multi"
|
|
41081
|
+
}));
|
|
41082
|
+
}
|
|
41083
|
+
return path;
|
|
41084
|
+
});
|
|
41085
|
+
var run6 = exports_Effect.fn("service.run")(function* (cmd, args2) {
|
|
41086
|
+
const proc = Bun.spawn([cmd, ...args2], { stdout: "pipe", stderr: "pipe" });
|
|
41087
|
+
yield* exports_Effect.promise(() => proc.exited);
|
|
41088
|
+
const stdout = yield* exports_Effect.promise(() => new Response(proc.stdout).text());
|
|
41089
|
+
const stderr = yield* exports_Effect.promise(() => new Response(proc.stderr).text());
|
|
41090
|
+
if (proc.exitCode !== 0) {
|
|
41091
|
+
return yield* exports_Effect.fail(new SpawnError({
|
|
41092
|
+
cmd: `${cmd} ${args2.join(" ")}`,
|
|
41093
|
+
cause: stderr || stdout || `exit ${proc.exitCode}`
|
|
41094
|
+
}));
|
|
41095
|
+
}
|
|
41096
|
+
return { stdout, stderr };
|
|
41097
|
+
});
|
|
41098
|
+
var buildPlist = (binary, env) => {
|
|
41099
|
+
const envEntries = Object.entries(env).map(([k, v]) => ` <key>${k}</key>
|
|
41100
|
+
<string>${escapeXml(v)}</string>`).join(`
|
|
41101
|
+
`);
|
|
41102
|
+
const logDir = join18(MULTI_DIR, "logs");
|
|
41103
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
41104
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
41105
|
+
<plist version="1.0">
|
|
41106
|
+
<dict>
|
|
41107
|
+
<key>Label</key>
|
|
41108
|
+
<string>${LABEL}</string>
|
|
41109
|
+
<key>ProgramArguments</key>
|
|
41110
|
+
<array>
|
|
41111
|
+
<string>${escapeXml(binary)}</string>
|
|
41112
|
+
<string>connect</string>
|
|
41113
|
+
</array>
|
|
41114
|
+
<key>RunAtLoad</key>
|
|
41115
|
+
<true/>
|
|
41116
|
+
<key>KeepAlive</key>
|
|
41117
|
+
<true/>
|
|
41118
|
+
<key>WorkingDirectory</key>
|
|
41119
|
+
<string>${escapeXml(HOME5)}</string>
|
|
41120
|
+
<key>StandardOutPath</key>
|
|
41121
|
+
<string>${escapeXml(join18(logDir, "launchd.out.log"))}</string>
|
|
41122
|
+
<key>StandardErrorPath</key>
|
|
41123
|
+
<string>${escapeXml(join18(logDir, "launchd.err.log"))}</string>
|
|
41124
|
+
<key>EnvironmentVariables</key>
|
|
41125
|
+
<dict>
|
|
41126
|
+
${envEntries}
|
|
41127
|
+
</dict>
|
|
41128
|
+
<key>ProcessType</key>
|
|
41129
|
+
<string>Interactive</string>
|
|
41130
|
+
</dict>
|
|
41131
|
+
</plist>
|
|
41132
|
+
`;
|
|
41133
|
+
};
|
|
41134
|
+
var escapeXml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
41135
|
+
var buildSystemdUnit = (binary, env) => {
|
|
41136
|
+
const envLines = Object.entries(env).map(([k, v]) => `Environment=${k}=${v}`).join(`
|
|
41137
|
+
`);
|
|
41138
|
+
return `[Unit]
|
|
41139
|
+
Description=Multi agent daemon
|
|
41140
|
+
After=network-online.target
|
|
41141
|
+
|
|
41142
|
+
[Service]
|
|
41143
|
+
Type=simple
|
|
41144
|
+
ExecStart=${binary} connect
|
|
41145
|
+
Restart=always
|
|
41146
|
+
RestartSec=3
|
|
41147
|
+
WorkingDirectory=${HOME5}
|
|
41148
|
+
${envLines}
|
|
41149
|
+
|
|
41150
|
+
[Install]
|
|
41151
|
+
WantedBy=default.target
|
|
41152
|
+
`;
|
|
41153
|
+
};
|
|
41154
|
+
var collectServiceEnv = () => {
|
|
41155
|
+
const env = {};
|
|
41156
|
+
if (process.env.PATH)
|
|
41157
|
+
env.PATH = process.env.PATH;
|
|
41158
|
+
if (process.env.HOME)
|
|
41159
|
+
env.HOME = process.env.HOME;
|
|
41160
|
+
if (process.env.MULTI_HOME)
|
|
41161
|
+
env.MULTI_HOME = process.env.MULTI_HOME;
|
|
41162
|
+
if (process.env.MULTI_API)
|
|
41163
|
+
env.MULTI_API = process.env.MULTI_API;
|
|
41164
|
+
if (process.env.SHELL)
|
|
41165
|
+
env.SHELL = process.env.SHELL;
|
|
41166
|
+
if (process.env.LANG)
|
|
41167
|
+
env.LANG = process.env.LANG;
|
|
41168
|
+
return env;
|
|
41169
|
+
};
|
|
41170
|
+
var ensureSupported = exports_Effect.fn("service.ensureSupported")(function* () {
|
|
41171
|
+
if (!isMac() && !isLinux()) {
|
|
41172
|
+
return yield* exports_Effect.fail(new UsageError({
|
|
41173
|
+
message: `service commands are not supported on ${platform()} yet (macOS + Linux only).`
|
|
41174
|
+
}));
|
|
41175
|
+
}
|
|
41176
|
+
});
|
|
41177
|
+
var installMac = exports_Effect.fn("service.installMac")(function* (binary) {
|
|
41178
|
+
const fs3 = yield* FileSystem;
|
|
41179
|
+
const env = collectServiceEnv();
|
|
41180
|
+
const plist = buildPlist(binary, env);
|
|
41181
|
+
yield* fs3.mkdirp(join18(MULTI_DIR, "logs"));
|
|
41182
|
+
yield* fs3.writeText(PLIST_PATH, plist);
|
|
41183
|
+
yield* run6("launchctl", ["unload", PLIST_PATH]).pipe(exports_Effect.ignore);
|
|
41184
|
+
yield* run6("launchctl", ["load", "-w", PLIST_PATH]);
|
|
41185
|
+
console.log(`✅ Installed launchd agent at ${PLIST_PATH}`);
|
|
41186
|
+
console.log(` The daemon will start now and on every login.`);
|
|
41187
|
+
});
|
|
41188
|
+
var installLinux = exports_Effect.fn("service.installLinux")(function* (binary) {
|
|
41189
|
+
const fs3 = yield* FileSystem;
|
|
41190
|
+
const env = collectServiceEnv();
|
|
41191
|
+
const unit = buildSystemdUnit(binary, env);
|
|
41192
|
+
yield* fs3.mkdirp(join18(MULTI_DIR, "logs"));
|
|
41193
|
+
yield* fs3.writeText(SYSTEMD_UNIT_PATH, unit);
|
|
41194
|
+
yield* run6("systemctl", ["--user", "daemon-reload"]);
|
|
41195
|
+
yield* run6("systemctl", ["--user", "enable", "--now", "multi-daemon.service"]);
|
|
41196
|
+
console.log(`✅ Installed systemd user unit at ${SYSTEMD_UNIT_PATH}`);
|
|
41197
|
+
console.log(` The daemon will start now and on every login.`);
|
|
41198
|
+
console.log(` To keep it running after logout: loginctl enable-linger ${process.env.USER || ""}`.trimEnd());
|
|
41199
|
+
});
|
|
41200
|
+
var uninstallMac = exports_Effect.fn("service.uninstallMac")(function* () {
|
|
41201
|
+
const fs3 = yield* FileSystem;
|
|
41202
|
+
if (existsSync18(PLIST_PATH)) {
|
|
41203
|
+
yield* run6("launchctl", ["unload", PLIST_PATH]).pipe(exports_Effect.ignore);
|
|
41204
|
+
yield* fs3.remove(PLIST_PATH);
|
|
41205
|
+
console.log(`\uD83D\uDDD1 Removed ${PLIST_PATH}`);
|
|
41206
|
+
} else {
|
|
41207
|
+
console.log("Nothing to uninstall (no plist found).");
|
|
41208
|
+
}
|
|
41209
|
+
});
|
|
41210
|
+
var uninstallLinux = exports_Effect.fn("service.uninstallLinux")(function* () {
|
|
41211
|
+
const fs3 = yield* FileSystem;
|
|
41212
|
+
if (existsSync18(SYSTEMD_UNIT_PATH)) {
|
|
41213
|
+
yield* run6("systemctl", ["--user", "disable", "--now", "multi-daemon.service"]).pipe(exports_Effect.ignore);
|
|
41214
|
+
yield* fs3.remove(SYSTEMD_UNIT_PATH);
|
|
41215
|
+
yield* run6("systemctl", ["--user", "daemon-reload"]).pipe(exports_Effect.ignore);
|
|
41216
|
+
console.log(`\uD83D\uDDD1 Removed ${SYSTEMD_UNIT_PATH}`);
|
|
41217
|
+
} else {
|
|
41218
|
+
console.log("Nothing to uninstall (no unit found).");
|
|
41219
|
+
}
|
|
41220
|
+
});
|
|
41221
|
+
var serviceCmd = exports_Effect.fn("serviceCmd")(function* (sub) {
|
|
41222
|
+
yield* ensureSupported();
|
|
41223
|
+
switch (sub) {
|
|
41224
|
+
case "install": {
|
|
41225
|
+
const binary = yield* resolveBinary();
|
|
41226
|
+
if (isMac())
|
|
41227
|
+
yield* installMac(binary);
|
|
41228
|
+
else
|
|
41229
|
+
yield* installLinux(binary);
|
|
41230
|
+
return;
|
|
41231
|
+
}
|
|
41232
|
+
case "uninstall": {
|
|
41233
|
+
if (isMac())
|
|
41234
|
+
yield* uninstallMac();
|
|
41235
|
+
else
|
|
41236
|
+
yield* uninstallLinux();
|
|
41237
|
+
return;
|
|
41238
|
+
}
|
|
41239
|
+
case "start": {
|
|
41240
|
+
if (isMac()) {
|
|
41241
|
+
if (!existsSync18(PLIST_PATH)) {
|
|
41242
|
+
return yield* exports_Effect.fail(new DaemonError({
|
|
41243
|
+
message: "Not installed. Run: multi-agent service install"
|
|
41244
|
+
}));
|
|
41245
|
+
}
|
|
41246
|
+
yield* run6("launchctl", ["kickstart", "-k", `gui/${process.getuid?.() ?? ""}/${LABEL}`]).pipe(exports_Effect.ignore);
|
|
41247
|
+
yield* run6("launchctl", ["load", "-w", PLIST_PATH]).pipe(exports_Effect.ignore);
|
|
41248
|
+
console.log("▶ Started.");
|
|
41249
|
+
} else {
|
|
41250
|
+
yield* run6("systemctl", ["--user", "start", "multi-daemon.service"]);
|
|
41251
|
+
console.log("▶ Started.");
|
|
41252
|
+
}
|
|
41253
|
+
return;
|
|
41254
|
+
}
|
|
41255
|
+
case "stop": {
|
|
41256
|
+
if (isMac()) {
|
|
41257
|
+
yield* run6("launchctl", ["unload", PLIST_PATH]).pipe(exports_Effect.ignore);
|
|
41258
|
+
console.log("⏹ Stopped.");
|
|
41259
|
+
} else {
|
|
41260
|
+
yield* run6("systemctl", ["--user", "stop", "multi-daemon.service"]);
|
|
41261
|
+
console.log("⏹ Stopped.");
|
|
41262
|
+
}
|
|
41263
|
+
return;
|
|
41264
|
+
}
|
|
41265
|
+
case "status":
|
|
41266
|
+
case undefined: {
|
|
41267
|
+
const installed = isMac() ? existsSync18(PLIST_PATH) : existsSync18(SYSTEMD_UNIT_PATH);
|
|
41268
|
+
const pid = readPid();
|
|
41269
|
+
const running3 = pid !== null && isRunningPid(pid);
|
|
41270
|
+
console.log(`Service: ${isMac() ? "launchd" : "systemd --user"}`);
|
|
41271
|
+
console.log(`Installed: ${installed ? "yes" : "no"}`);
|
|
41272
|
+
console.log(`Unit path: ${isMac() ? PLIST_PATH : SYSTEMD_UNIT_PATH}`);
|
|
41273
|
+
console.log(`Daemon: ${running3 ? `running (pid ${pid})` : "stopped"}`);
|
|
41274
|
+
if (!installed)
|
|
41275
|
+
console.log(`
|
|
41276
|
+
Not installed. Run: multi-agent service install`);
|
|
41277
|
+
return;
|
|
41278
|
+
}
|
|
41279
|
+
default:
|
|
41280
|
+
return yield* exports_Effect.fail(new UsageError({
|
|
41281
|
+
message: `Unknown service subcommand: ${sub}. Use: install | uninstall | start | stop | status`
|
|
41282
|
+
}));
|
|
41283
|
+
}
|
|
41284
|
+
});
|
|
41285
|
+
|
|
39964
41286
|
// src/index.ts
|
|
39965
41287
|
var VERSION = package_default.version;
|
|
39966
41288
|
var COMMANDS = {
|
|
@@ -39973,7 +41295,8 @@ var COMMANDS = {
|
|
|
39973
41295
|
restart: "Stop and relaunch the daemon in background",
|
|
39974
41296
|
logs: "View execution logs",
|
|
39975
41297
|
reset: "Reset acpx session for an issue (--issue <id>)",
|
|
39976
|
-
worktree: "Manage per-issue worktrees (subcommands: gc, list, ...)"
|
|
41298
|
+
worktree: "Manage per-issue worktrees (subcommands: gc, list, ...)",
|
|
41299
|
+
service: "Manage the always-on daemon (subcommands: install, uninstall, start, stop, status)"
|
|
39977
41300
|
};
|
|
39978
41301
|
function printUsage() {
|
|
39979
41302
|
console.log(`multi-agent v${VERSION}
|
|
@@ -40028,7 +41351,7 @@ var program = exports_Effect.gen(function* () {
|
|
|
40028
41351
|
force: !!values3.force,
|
|
40029
41352
|
noPush: !!values3["no-push"],
|
|
40030
41353
|
key: values3.key
|
|
40031
|
-
})), exports_Match.exhaustive);
|
|
41354
|
+
})), exports_Match.when("service", () => serviceCmd(positionals[1])), exports_Match.exhaustive);
|
|
40032
41355
|
});
|
|
40033
41356
|
var main = program.pipe(exports_Effect.provide(AppLayer), exports_Effect.tapErrorCause((cause3) => exports_Effect.sync(() => {
|
|
40034
41357
|
console.error(exports_Cause.pretty(cause3));
|