@shipers-dev/multi 0.18.0 → 0.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -0
- package/dist/index.js +201 -35
- package/package.json +3 -2
package/README.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# @shipers-dev/multi
|
|
2
|
+
|
|
3
|
+
CLI daemon (`multi-agent`) that pairs a device with a Multi workspace and runs assigned issues via ACP / acpx adapters.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add -g @shipers-dev/multi
|
|
9
|
+
# or
|
|
10
|
+
npm i -g @shipers-dev/multi
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
multi-agent setup # pair the device against prod
|
|
17
|
+
multi-agent start # foreground daemon
|
|
18
|
+
multi-agent start -d # detached
|
|
19
|
+
multi-agent status
|
|
20
|
+
multi-agent logs
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Pointing at a local API worker
|
|
24
|
+
|
|
25
|
+
For local development against a wrangler-dev API worker, set `MULTI_API` to its base URL:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
MULTI_API=http://localhost:8787 multi-agent setup
|
|
29
|
+
MULTI_API=http://localhost:8787 multi-agent start
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The resolved API base URL is printed at start. When the host is `localhost` / `127.0.0.1` / `[::1]`, the daemon:
|
|
33
|
+
|
|
34
|
+
- skips spawning `cloudflared` (no quick-tunnel),
|
|
35
|
+
- advertises `http://127.0.0.1:<port>` as its push endpoint, so the local worker can reach the daemon directly over loopback,
|
|
36
|
+
- skips the tunnel self-heal + DNS probe loops.
|
|
37
|
+
|
|
38
|
+
`MULTI_API_URL` is still accepted as a fallback alias. CLI flag `--api <url>` overrides both.
|
|
39
|
+
|
|
40
|
+
## Common env vars
|
|
41
|
+
|
|
42
|
+
| Var | Purpose |
|
|
43
|
+
| --- | --- |
|
|
44
|
+
| `MULTI_API` | API base URL (default `https://multi-api.adnb3r.workers.dev`) |
|
|
45
|
+
| `MULTI_MAX_CONCURRENT` | Max concurrent tasks per device (default 3) |
|
|
46
|
+
| `MULTI_TUNNEL_NAME` | Use a named cloudflared tunnel instead of quick tunnel |
|
|
47
|
+
| `MULTI_TUNNEL_HOSTNAME` | Public hostname routed to the named tunnel |
|
package/dist/index.js
CHANGED
|
@@ -16071,6 +16071,83 @@ var StreamEventInputSchema = exports_external.object({
|
|
|
16071
16071
|
event_type: StreamEventTypeSchema,
|
|
16072
16072
|
payload: exports_external.unknown().optional()
|
|
16073
16073
|
});
|
|
16074
|
+
// ../lib/memory.ts
|
|
16075
|
+
var MEMORY_ENTRY_KINDS = ["fact", "event", "instruction", "task"];
|
|
16076
|
+
var MemoryEntryKindSchema = exports_external.enum(MEMORY_ENTRY_KINDS);
|
|
16077
|
+
var MemoryEntrySchema = exports_external.object({
|
|
16078
|
+
id: exports_external.string(),
|
|
16079
|
+
project_id: exports_external.string(),
|
|
16080
|
+
issue_id: exports_external.string().nullable().optional(),
|
|
16081
|
+
kind: MemoryEntryKindSchema,
|
|
16082
|
+
text: exports_external.string(),
|
|
16083
|
+
source_event_id: exports_external.string().nullable().optional(),
|
|
16084
|
+
source_comment_id: exports_external.string().nullable().optional(),
|
|
16085
|
+
score: exports_external.number().optional(),
|
|
16086
|
+
created_at: exports_external.number()
|
|
16087
|
+
});
|
|
16088
|
+
var MemoryCitationSchema = exports_external.object({
|
|
16089
|
+
entry_id: exports_external.string(),
|
|
16090
|
+
kind: MemoryEntryKindSchema,
|
|
16091
|
+
issue_id: exports_external.string().nullable().optional(),
|
|
16092
|
+
comment_id: exports_external.string().nullable().optional(),
|
|
16093
|
+
snippet: exports_external.string(),
|
|
16094
|
+
score: exports_external.number().optional()
|
|
16095
|
+
});
|
|
16096
|
+
var MemoryRecallRequestSchema = exports_external.object({
|
|
16097
|
+
project_id: exports_external.string().min(1),
|
|
16098
|
+
issue_id: exports_external.string().optional(),
|
|
16099
|
+
query: exports_external.string().min(1),
|
|
16100
|
+
k: exports_external.number().int().min(1).max(50).optional()
|
|
16101
|
+
});
|
|
16102
|
+
var MemoryRecallResponseSchema = exports_external.object({
|
|
16103
|
+
synthesis: exports_external.string(),
|
|
16104
|
+
citations: exports_external.array(MemoryCitationSchema),
|
|
16105
|
+
entries: exports_external.array(MemoryEntrySchema)
|
|
16106
|
+
});
|
|
16107
|
+
var StreamSourceKind = exports_external.enum(["assistant_text", "result"]);
|
|
16108
|
+
var IngestBase = exports_external.object({
|
|
16109
|
+
project_id: exports_external.string().min(1),
|
|
16110
|
+
issue_id: exports_external.string().min(1),
|
|
16111
|
+
ts: exports_external.number().int()
|
|
16112
|
+
});
|
|
16113
|
+
var MemoryIngestAssistantTextSchema = IngestBase.extend({
|
|
16114
|
+
kind: exports_external.literal("assistant_text"),
|
|
16115
|
+
event_id: exports_external.string(),
|
|
16116
|
+
text: exports_external.string(),
|
|
16117
|
+
final: exports_external.literal(true)
|
|
16118
|
+
});
|
|
16119
|
+
var MemoryIngestResultSchema = IngestBase.extend({
|
|
16120
|
+
kind: exports_external.literal("result"),
|
|
16121
|
+
event_id: exports_external.string(),
|
|
16122
|
+
text: exports_external.string(),
|
|
16123
|
+
status: exports_external.enum(["success", "error", "stopped"]).optional()
|
|
16124
|
+
});
|
|
16125
|
+
var MemoryIngestHumanCommentSchema = IngestBase.extend({
|
|
16126
|
+
kind: exports_external.literal("human_comment"),
|
|
16127
|
+
comment_id: exports_external.string(),
|
|
16128
|
+
author_id: exports_external.string(),
|
|
16129
|
+
body: exports_external.string()
|
|
16130
|
+
});
|
|
16131
|
+
var MemoryIngestIssueChangedSchema = IngestBase.extend({
|
|
16132
|
+
kind: exports_external.literal("issue_changed"),
|
|
16133
|
+
field: exports_external.enum(["title", "description"]),
|
|
16134
|
+
before: exports_external.string().nullable(),
|
|
16135
|
+
after: exports_external.string().nullable(),
|
|
16136
|
+
actor_id: exports_external.string()
|
|
16137
|
+
});
|
|
16138
|
+
var MemoryIngestEventSchema = exports_external.discriminatedUnion("kind", [
|
|
16139
|
+
MemoryIngestAssistantTextSchema,
|
|
16140
|
+
MemoryIngestResultSchema,
|
|
16141
|
+
MemoryIngestHumanCommentSchema,
|
|
16142
|
+
MemoryIngestIssueChangedSchema
|
|
16143
|
+
]);
|
|
16144
|
+
var MemoryIngestRequestSchema = exports_external.object({
|
|
16145
|
+
events: exports_external.array(MemoryIngestEventSchema).min(1).max(100)
|
|
16146
|
+
});
|
|
16147
|
+
var MemoryIngestResponseSchema = exports_external.object({
|
|
16148
|
+
accepted: exports_external.number().int().nonnegative(),
|
|
16149
|
+
rejected: exports_external.number().int().nonnegative()
|
|
16150
|
+
});
|
|
16074
16151
|
// src/worktree.ts
|
|
16075
16152
|
import { spawn } from "child_process";
|
|
16076
16153
|
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, symlinkSync, rmSync } from "fs";
|
|
@@ -16426,7 +16503,7 @@ import { join as join5, dirname as dirname4 } from "path";
|
|
|
16426
16503
|
// package.json
|
|
16427
16504
|
var package_default = {
|
|
16428
16505
|
name: "@shipers-dev/multi",
|
|
16429
|
-
version: "0.
|
|
16506
|
+
version: "0.20.1",
|
|
16430
16507
|
type: "module",
|
|
16431
16508
|
bin: {
|
|
16432
16509
|
"multi-agent": "./dist/index.js"
|
|
@@ -16440,8 +16517,9 @@ var package_default = {
|
|
|
16440
16517
|
prepublishOnly: "bun run build"
|
|
16441
16518
|
},
|
|
16442
16519
|
dependencies: {
|
|
16520
|
+
"@agentclientprotocol/claude-agent-acp": "^0.31.0",
|
|
16443
16521
|
"@agentclientprotocol/sdk": "^0.20.0",
|
|
16444
|
-
|
|
16522
|
+
effect: "^3.21.2"
|
|
16445
16523
|
},
|
|
16446
16524
|
devDependencies: {
|
|
16447
16525
|
"@multi/lib": "workspace:*"
|
|
@@ -16476,6 +16554,14 @@ function ensureDirs() {
|
|
|
16476
16554
|
mkdirSync4(d, { recursive: true });
|
|
16477
16555
|
}
|
|
16478
16556
|
}
|
|
16557
|
+
function isLocalApi(url2) {
|
|
16558
|
+
try {
|
|
16559
|
+
const h = new URL(url2).hostname;
|
|
16560
|
+
return h === "localhost" || h === "127.0.0.1" || h === "0.0.0.0" || h === "[::1]" || h === "::1";
|
|
16561
|
+
} catch {
|
|
16562
|
+
return false;
|
|
16563
|
+
}
|
|
16564
|
+
}
|
|
16479
16565
|
function log(msg) {
|
|
16480
16566
|
ensureDirs();
|
|
16481
16567
|
const line = `[${new Date().toISOString()}] ${msg}
|
|
@@ -16512,7 +16598,7 @@ async function main() {
|
|
|
16512
16598
|
printHelp();
|
|
16513
16599
|
process.exit(0);
|
|
16514
16600
|
}
|
|
16515
|
-
const apiUrl = args.values.api || process.env.MULTI_API_URL || "https://multi-api.adnb3r.workers.dev";
|
|
16601
|
+
const apiUrl = args.values.api || process.env.MULTI_API || process.env.MULTI_API_URL || "https://multi-api.adnb3r.workers.dev";
|
|
16516
16602
|
const config2 = loadConfig();
|
|
16517
16603
|
if (config2.token)
|
|
16518
16604
|
setAuthToken(config2.token);
|
|
@@ -16700,12 +16786,15 @@ async function cmdConnect(apiUrl, config2) {
|
|
|
16700
16786
|
if (existsSync4(STOP_PATH))
|
|
16701
16787
|
unlinkSync(STOP_PATH);
|
|
16702
16788
|
const detected = await detectAgents();
|
|
16789
|
+
const localMode = isLocalApi(apiUrl);
|
|
16703
16790
|
log(`\uD83D\uDE80 Starting daemon for device ${config2.deviceId} (pid ${process.pid})`);
|
|
16791
|
+
log(` API: ${apiUrl}${localMode ? " (local \u2014 cloudflared skipped)" : ""}`);
|
|
16704
16792
|
log(` runtimes: ${detected.map((d) => d.type).join(", ") || "stub"}`);
|
|
16705
16793
|
const db = openTasksDb();
|
|
16706
16794
|
db.run("UPDATE tasks SET status = 'queued' WHERE status = 'running'");
|
|
16707
16795
|
const MAX_DEVICE = Math.max(1, parseInt(process.env.MULTI_MAX_CONCURRENT ?? "3", 10) || 3);
|
|
16708
16796
|
const running = new Map;
|
|
16797
|
+
let triggerHeartbeat = () => {};
|
|
16709
16798
|
function resolvePayloadIds(row) {
|
|
16710
16799
|
let agent_id = row.agent_id;
|
|
16711
16800
|
let issue_id = row.issue_id;
|
|
@@ -16748,6 +16837,7 @@ async function cmdConnect(apiUrl, config2) {
|
|
|
16748
16837
|
db.run("UPDATE tasks SET status = 'running', started_at = unixepoch(), attempts = attempts + 1 WHERE id = ?", [row.id]);
|
|
16749
16838
|
const entry = { agentId: ids.agent_id || "", issueId: ids.issue_id, startedAt: Date.now(), child: null, worktreePath: "" };
|
|
16750
16839
|
running.set(row.id, entry);
|
|
16840
|
+
triggerHeartbeat();
|
|
16751
16841
|
(async () => {
|
|
16752
16842
|
try {
|
|
16753
16843
|
const task = JSON.parse(row.payload);
|
|
@@ -16758,6 +16848,7 @@ async function cmdConnect(apiUrl, config2) {
|
|
|
16758
16848
|
db.run("UPDATE tasks SET status = 'failed', finished_at = unixepoch(), error = ? WHERE id = ?", [String(e), row.id]);
|
|
16759
16849
|
} finally {
|
|
16760
16850
|
running.delete(row.id);
|
|
16851
|
+
triggerHeartbeat();
|
|
16761
16852
|
queueMicrotask(() => schedule());
|
|
16762
16853
|
}
|
|
16763
16854
|
})();
|
|
@@ -16786,7 +16877,7 @@ async function cmdConnect(apiUrl, config2) {
|
|
|
16786
16877
|
postStream(apiUrl, t.issue_id, "queued", { queue_position: pos });
|
|
16787
16878
|
}
|
|
16788
16879
|
if (t.dispatch_id) {
|
|
16789
|
-
ackDispatch(apiUrl, t.dispatch_id, config2.
|
|
16880
|
+
ackDispatch(apiUrl, t.dispatch_id, config2.token);
|
|
16790
16881
|
}
|
|
16791
16882
|
queueMicrotask(() => schedule());
|
|
16792
16883
|
return Response.json({ accepted: true, task_id: taskId }, { status: 202 });
|
|
@@ -16881,17 +16972,28 @@ async function cmdConnect(apiUrl, config2) {
|
|
|
16881
16972
|
try {
|
|
16882
16973
|
writeFileSync4(PORT_PATH, String(port));
|
|
16883
16974
|
} catch {}
|
|
16884
|
-
let tunnel =
|
|
16885
|
-
if (
|
|
16886
|
-
|
|
16887
|
-
|
|
16888
|
-
|
|
16889
|
-
|
|
16890
|
-
|
|
16975
|
+
let tunnel = null;
|
|
16976
|
+
if (localMode) {
|
|
16977
|
+
tunnel = { child: null, url: `http://127.0.0.1:${port}` };
|
|
16978
|
+
log(`\uD83C\uDFE0 Local tunnel: ${tunnel.url}`);
|
|
16979
|
+
} else {
|
|
16980
|
+
tunnel = await startTunnel(port, log);
|
|
16981
|
+
if (!tunnel) {
|
|
16982
|
+
log("\u274C cloudflared did not emit a tunnel URL \u2014 is `cloudflared` installed? (`brew install cloudflared`)");
|
|
16983
|
+
try {
|
|
16984
|
+
server.stop();
|
|
16985
|
+
} catch {}
|
|
16986
|
+
process.exit(1);
|
|
16987
|
+
}
|
|
16988
|
+
log(`\u2601\uFE0F Tunnel up: ${tunnel.url}`);
|
|
16891
16989
|
}
|
|
16892
|
-
log(`\u2601\uFE0F Tunnel up: ${tunnel.url}`);
|
|
16893
16990
|
const heartbeat = async () => {
|
|
16894
|
-
const res = await apiClient.post(`${apiUrl}/api/devices/${config2.deviceId}/heartbeat`, {
|
|
16991
|
+
const res = await apiClient.post(`${apiUrl}/api/devices/${config2.deviceId}/heartbeat`, {
|
|
16992
|
+
status: "online",
|
|
16993
|
+
tunnel_url: tunnel?.url,
|
|
16994
|
+
running_count: running.size,
|
|
16995
|
+
max_concurrent: MAX_DEVICE
|
|
16996
|
+
});
|
|
16895
16997
|
if (res.success && res.data) {
|
|
16896
16998
|
const remoteRev = Number(res.data.agent_skill_revision ?? 0);
|
|
16897
16999
|
if (remoteRev > 0 && remoteRev !== lastMaterializedRevision()) {
|
|
@@ -16904,12 +17006,21 @@ async function cmdConnect(apiUrl, config2) {
|
|
|
16904
17006
|
}
|
|
16905
17007
|
return res.success && res.data?.pending_dispatches || 0;
|
|
16906
17008
|
};
|
|
17009
|
+
let beatTimer = null;
|
|
17010
|
+
triggerHeartbeat = () => {
|
|
17011
|
+
if (beatTimer)
|
|
17012
|
+
return;
|
|
17013
|
+
beatTimer = setTimeout(() => {
|
|
17014
|
+
beatTimer = null;
|
|
17015
|
+
heartbeat().catch(() => {});
|
|
17016
|
+
}, 200);
|
|
17017
|
+
};
|
|
16907
17018
|
{
|
|
16908
17019
|
const pending = await heartbeat();
|
|
16909
17020
|
if (pending > 0)
|
|
16910
|
-
drainOfflineDispatches(apiUrl, config2.deviceId, config2.
|
|
17021
|
+
drainOfflineDispatches(apiUrl, config2.deviceId, config2.token, db, () => schedule());
|
|
16911
17022
|
}
|
|
16912
|
-
drainOfflineDispatches(apiUrl, config2.deviceId, config2.
|
|
17023
|
+
drainOfflineDispatches(apiUrl, config2.deviceId, config2.token, db, () => schedule());
|
|
16913
17024
|
let alive = true;
|
|
16914
17025
|
const shutdown = async (reason) => {
|
|
16915
17026
|
if (!alive)
|
|
@@ -16940,7 +17051,7 @@ async function cmdConnect(apiUrl, config2) {
|
|
|
16940
17051
|
schedule();
|
|
16941
17052
|
let restarting = false;
|
|
16942
17053
|
const restartTunnel = async (reason) => {
|
|
16943
|
-
if (!alive || restarting)
|
|
17054
|
+
if (!alive || restarting || localMode)
|
|
16944
17055
|
return;
|
|
16945
17056
|
restarting = true;
|
|
16946
17057
|
try {
|
|
@@ -16958,7 +17069,7 @@ async function cmdConnect(apiUrl, config2) {
|
|
|
16958
17069
|
try {
|
|
16959
17070
|
const pending = await heartbeat();
|
|
16960
17071
|
if (pending > 0)
|
|
16961
|
-
drainOfflineDispatches(apiUrl, config2.deviceId, config2.
|
|
17072
|
+
drainOfflineDispatches(apiUrl, config2.deviceId, config2.token, db, () => schedule());
|
|
16962
17073
|
} catch (e) {
|
|
16963
17074
|
log(`heartbeat error after tunnel restart: ${String(e)}`);
|
|
16964
17075
|
}
|
|
@@ -16972,20 +17083,21 @@ async function cmdConnect(apiUrl, config2) {
|
|
|
16972
17083
|
restarting = false;
|
|
16973
17084
|
}
|
|
16974
17085
|
};
|
|
16975
|
-
|
|
16976
|
-
|
|
16977
|
-
|
|
16978
|
-
|
|
16979
|
-
|
|
16980
|
-
|
|
17086
|
+
if (!localMode)
|
|
17087
|
+
(async () => {
|
|
17088
|
+
while (alive) {
|
|
17089
|
+
const t = tunnel;
|
|
17090
|
+
if (!t || !t.child) {
|
|
17091
|
+
await sleep(1000);
|
|
17092
|
+
continue;
|
|
17093
|
+
}
|
|
17094
|
+
const code = await t.child.exited;
|
|
17095
|
+
if (!alive)
|
|
17096
|
+
return;
|
|
17097
|
+
if (tunnel === t)
|
|
17098
|
+
await restartTunnel(`cloudflared exited code=${code}`);
|
|
16981
17099
|
}
|
|
16982
|
-
|
|
16983
|
-
if (!alive)
|
|
16984
|
-
return;
|
|
16985
|
-
if (tunnel === t)
|
|
16986
|
-
await restartTunnel(`cloudflared exited code=${code}`);
|
|
16987
|
-
}
|
|
16988
|
-
})();
|
|
17100
|
+
})();
|
|
16989
17101
|
let probeFailures = 0;
|
|
16990
17102
|
let tick = 0;
|
|
16991
17103
|
const PROBE_EVERY = 6;
|
|
@@ -16997,7 +17109,7 @@ async function cmdConnect(apiUrl, config2) {
|
|
|
16997
17109
|
}
|
|
16998
17110
|
tick++;
|
|
16999
17111
|
const currentUrl = tunnel?.url;
|
|
17000
|
-
if (currentUrl && tick % PROBE_EVERY === 0) {
|
|
17112
|
+
if (!localMode && currentUrl && tick % PROBE_EVERY === 0) {
|
|
17001
17113
|
const ok = await probeTunnel(currentUrl);
|
|
17002
17114
|
if (!ok) {
|
|
17003
17115
|
probeFailures++;
|
|
@@ -17014,7 +17126,7 @@ async function cmdConnect(apiUrl, config2) {
|
|
|
17014
17126
|
try {
|
|
17015
17127
|
const pending = await heartbeat();
|
|
17016
17128
|
if (pending > 0)
|
|
17017
|
-
drainOfflineDispatches(apiUrl, config2.deviceId, config2.
|
|
17129
|
+
drainOfflineDispatches(apiUrl, config2.deviceId, config2.token, db, () => schedule());
|
|
17018
17130
|
} catch (e) {
|
|
17019
17131
|
log(`heartbeat error: ${String(e)}`);
|
|
17020
17132
|
}
|
|
@@ -17518,6 +17630,7 @@ ${body}
|
|
|
17518
17630
|
} catch (e) {
|
|
17519
17631
|
log(`preamble fetch failed: ${String(e)}`);
|
|
17520
17632
|
}
|
|
17633
|
+
preamble += await fetchMemoryPreamble(apiUrl, task);
|
|
17521
17634
|
preamble += await buildPlanningPreamble(apiUrl, task);
|
|
17522
17635
|
const prompt = preamble ? `${preamble}
|
|
17523
17636
|
---
|
|
@@ -17583,6 +17696,7 @@ ${body}
|
|
|
17583
17696
|
}
|
|
17584
17697
|
}
|
|
17585
17698
|
} catch {}
|
|
17699
|
+
preamble += await fetchMemoryPreamble(apiUrl, task);
|
|
17586
17700
|
preamble += await buildPlanningPreamble(apiUrl, task);
|
|
17587
17701
|
const issueContext = `## Issue ${task.key}: ${task.title}${task.description ? `
|
|
17588
17702
|
|
|
@@ -17643,7 +17757,7 @@ ${userPart}` : userPart;
|
|
|
17643
17757
|
await eventHandler(event);
|
|
17644
17758
|
}
|
|
17645
17759
|
if (liveCommentId) {
|
|
17646
|
-
const n = await uploadOutputDir(apiUrl, liveCommentId, outDir);
|
|
17760
|
+
const n = await uploadOutputDir(apiUrl, liveCommentId, outDir, task.issue_id);
|
|
17647
17761
|
if (n > 0)
|
|
17648
17762
|
log(` \uD83D\uDCCE uploaded ${n} output file(s)`);
|
|
17649
17763
|
}
|
|
@@ -17692,6 +17806,53 @@ ${userPart}` : userPart;
|
|
|
17692
17806
|
}
|
|
17693
17807
|
}
|
|
17694
17808
|
}
|
|
17809
|
+
async function fetchMemoryPreamble(apiUrl, task) {
|
|
17810
|
+
if (process.env.MULTI_MEMORY_ENABLED !== "1")
|
|
17811
|
+
return "";
|
|
17812
|
+
try {
|
|
17813
|
+
const issueRes = await apiClient.get(`${apiUrl}/api/issues/${task.issue_id}`);
|
|
17814
|
+
const projectId = issueRes.data?.project_id;
|
|
17815
|
+
if (!projectId)
|
|
17816
|
+
return "";
|
|
17817
|
+
const title = String(task.title || "").trim();
|
|
17818
|
+
const desc = String(task.description || "").trim();
|
|
17819
|
+
const followup = String(task.followup || "").trim();
|
|
17820
|
+
const query = [title, desc, followup].filter(Boolean).join(`
|
|
17821
|
+
`).slice(0, 4000);
|
|
17822
|
+
if (!query)
|
|
17823
|
+
return "";
|
|
17824
|
+
const res = await apiClient.post(`${apiUrl}/api/memory/recall`, {
|
|
17825
|
+
project_id: projectId,
|
|
17826
|
+
issue_id: task.issue_id,
|
|
17827
|
+
query,
|
|
17828
|
+
k: 10
|
|
17829
|
+
});
|
|
17830
|
+
if (!res.success)
|
|
17831
|
+
return "";
|
|
17832
|
+
const synthesis = String(res.data?.synthesis || "").trim();
|
|
17833
|
+
if (!synthesis)
|
|
17834
|
+
return "";
|
|
17835
|
+
const cites = Array.isArray(res.data?.citations) ? res.data.citations : [];
|
|
17836
|
+
let block = `## Project memory
|
|
17837
|
+
|
|
17838
|
+
${synthesis}
|
|
17839
|
+
`;
|
|
17840
|
+
if (cites.length) {
|
|
17841
|
+
block += `
|
|
17842
|
+
`;
|
|
17843
|
+
for (let i = 0;i < cites.length; i++) {
|
|
17844
|
+
const c = cites[i];
|
|
17845
|
+
block += `[${i + 1}] ${String(c.snippet || "").replace(/\s+/g, " ").trim()}
|
|
17846
|
+
`;
|
|
17847
|
+
}
|
|
17848
|
+
}
|
|
17849
|
+
return `${block}
|
|
17850
|
+
`;
|
|
17851
|
+
} catch (e) {
|
|
17852
|
+
log(`memory recall failed: ${String(e?.message || e)}`);
|
|
17853
|
+
return "";
|
|
17854
|
+
}
|
|
17855
|
+
}
|
|
17695
17856
|
async function buildPlanningPreamble(apiUrl, task) {
|
|
17696
17857
|
const depth = typeof task.planning_depth === "number" ? task.planning_depth : 0;
|
|
17697
17858
|
if (depth >= PLANNING_DEPTH_LIMIT) {
|
|
@@ -18058,7 +18219,7 @@ function authTokenHeader() {
|
|
|
18058
18219
|
const cfg = loadConfig();
|
|
18059
18220
|
return cfg.token ? `Bearer ${cfg.token}` : null;
|
|
18060
18221
|
}
|
|
18061
|
-
async function uploadOutputDir(apiUrl, commentId, dir) {
|
|
18222
|
+
async function uploadOutputDir(apiUrl, commentId, dir, issueId) {
|
|
18062
18223
|
if (!existsSync4(dir))
|
|
18063
18224
|
return 0;
|
|
18064
18225
|
const files = [];
|
|
@@ -18087,10 +18248,15 @@ async function uploadOutputDir(apiUrl, commentId, dir) {
|
|
|
18087
18248
|
const form = new FormData;
|
|
18088
18249
|
const blob = new Blob([data]);
|
|
18089
18250
|
form.append("file", blob, f.split("/").pop() || "file");
|
|
18251
|
+
const headers = {};
|
|
18252
|
+
if (token)
|
|
18253
|
+
headers.Authorization = token;
|
|
18254
|
+
if (issueId)
|
|
18255
|
+
headers["x-issue-id"] = issueId;
|
|
18090
18256
|
const res = await fetch(`${apiUrl}/api/attachments/comments/${commentId}`, {
|
|
18091
18257
|
method: "POST",
|
|
18092
18258
|
body: form,
|
|
18093
|
-
headers
|
|
18259
|
+
headers
|
|
18094
18260
|
});
|
|
18095
18261
|
if (res.ok) {
|
|
18096
18262
|
uploaded++;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipers-dev/multi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"multi-agent": "./dist/index.js"
|
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
"prepublishOnly": "bun run build"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
+
"@agentclientprotocol/claude-agent-acp": "^0.31.0",
|
|
17
18
|
"@agentclientprotocol/sdk": "^0.20.0",
|
|
18
|
-
"
|
|
19
|
+
"effect": "^3.21.2"
|
|
19
20
|
},
|
|
20
21
|
"devDependencies": {
|
|
21
22
|
"@multi/lib": "workspace:*"
|