@shipers-dev/multi 0.18.0 → 0.20.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/README.md +47 -0
- package/dist/index.js +194 -29
- package/package.json +1 -1
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.0",
|
|
16430
16507
|
type: "module",
|
|
16431
16508
|
bin: {
|
|
16432
16509
|
"multi-agent": "./dist/index.js"
|
|
@@ -16476,6 +16553,14 @@ function ensureDirs() {
|
|
|
16476
16553
|
mkdirSync4(d, { recursive: true });
|
|
16477
16554
|
}
|
|
16478
16555
|
}
|
|
16556
|
+
function isLocalApi(url2) {
|
|
16557
|
+
try {
|
|
16558
|
+
const h = new URL(url2).hostname;
|
|
16559
|
+
return h === "localhost" || h === "127.0.0.1" || h === "0.0.0.0" || h === "[::1]" || h === "::1";
|
|
16560
|
+
} catch {
|
|
16561
|
+
return false;
|
|
16562
|
+
}
|
|
16563
|
+
}
|
|
16479
16564
|
function log(msg) {
|
|
16480
16565
|
ensureDirs();
|
|
16481
16566
|
const line = `[${new Date().toISOString()}] ${msg}
|
|
@@ -16512,7 +16597,7 @@ async function main() {
|
|
|
16512
16597
|
printHelp();
|
|
16513
16598
|
process.exit(0);
|
|
16514
16599
|
}
|
|
16515
|
-
const apiUrl = args.values.api || process.env.MULTI_API_URL || "https://multi-api.adnb3r.workers.dev";
|
|
16600
|
+
const apiUrl = args.values.api || process.env.MULTI_API || process.env.MULTI_API_URL || "https://multi-api.adnb3r.workers.dev";
|
|
16516
16601
|
const config2 = loadConfig();
|
|
16517
16602
|
if (config2.token)
|
|
16518
16603
|
setAuthToken(config2.token);
|
|
@@ -16700,12 +16785,15 @@ async function cmdConnect(apiUrl, config2) {
|
|
|
16700
16785
|
if (existsSync4(STOP_PATH))
|
|
16701
16786
|
unlinkSync(STOP_PATH);
|
|
16702
16787
|
const detected = await detectAgents();
|
|
16788
|
+
const localMode = isLocalApi(apiUrl);
|
|
16703
16789
|
log(`\uD83D\uDE80 Starting daemon for device ${config2.deviceId} (pid ${process.pid})`);
|
|
16790
|
+
log(` API: ${apiUrl}${localMode ? " (local \u2014 cloudflared skipped)" : ""}`);
|
|
16704
16791
|
log(` runtimes: ${detected.map((d) => d.type).join(", ") || "stub"}`);
|
|
16705
16792
|
const db = openTasksDb();
|
|
16706
16793
|
db.run("UPDATE tasks SET status = 'queued' WHERE status = 'running'");
|
|
16707
16794
|
const MAX_DEVICE = Math.max(1, parseInt(process.env.MULTI_MAX_CONCURRENT ?? "3", 10) || 3);
|
|
16708
16795
|
const running = new Map;
|
|
16796
|
+
let triggerHeartbeat = () => {};
|
|
16709
16797
|
function resolvePayloadIds(row) {
|
|
16710
16798
|
let agent_id = row.agent_id;
|
|
16711
16799
|
let issue_id = row.issue_id;
|
|
@@ -16748,6 +16836,7 @@ async function cmdConnect(apiUrl, config2) {
|
|
|
16748
16836
|
db.run("UPDATE tasks SET status = 'running', started_at = unixepoch(), attempts = attempts + 1 WHERE id = ?", [row.id]);
|
|
16749
16837
|
const entry = { agentId: ids.agent_id || "", issueId: ids.issue_id, startedAt: Date.now(), child: null, worktreePath: "" };
|
|
16750
16838
|
running.set(row.id, entry);
|
|
16839
|
+
triggerHeartbeat();
|
|
16751
16840
|
(async () => {
|
|
16752
16841
|
try {
|
|
16753
16842
|
const task = JSON.parse(row.payload);
|
|
@@ -16758,6 +16847,7 @@ async function cmdConnect(apiUrl, config2) {
|
|
|
16758
16847
|
db.run("UPDATE tasks SET status = 'failed', finished_at = unixepoch(), error = ? WHERE id = ?", [String(e), row.id]);
|
|
16759
16848
|
} finally {
|
|
16760
16849
|
running.delete(row.id);
|
|
16850
|
+
triggerHeartbeat();
|
|
16761
16851
|
queueMicrotask(() => schedule());
|
|
16762
16852
|
}
|
|
16763
16853
|
})();
|
|
@@ -16881,17 +16971,28 @@ async function cmdConnect(apiUrl, config2) {
|
|
|
16881
16971
|
try {
|
|
16882
16972
|
writeFileSync4(PORT_PATH, String(port));
|
|
16883
16973
|
} catch {}
|
|
16884
|
-
let tunnel =
|
|
16885
|
-
if (
|
|
16886
|
-
|
|
16887
|
-
|
|
16888
|
-
|
|
16889
|
-
|
|
16890
|
-
|
|
16974
|
+
let tunnel = null;
|
|
16975
|
+
if (localMode) {
|
|
16976
|
+
tunnel = { child: null, url: `http://127.0.0.1:${port}` };
|
|
16977
|
+
log(`\uD83C\uDFE0 Local tunnel: ${tunnel.url}`);
|
|
16978
|
+
} else {
|
|
16979
|
+
tunnel = await startTunnel(port, log);
|
|
16980
|
+
if (!tunnel) {
|
|
16981
|
+
log("\u274C cloudflared did not emit a tunnel URL \u2014 is `cloudflared` installed? (`brew install cloudflared`)");
|
|
16982
|
+
try {
|
|
16983
|
+
server.stop();
|
|
16984
|
+
} catch {}
|
|
16985
|
+
process.exit(1);
|
|
16986
|
+
}
|
|
16987
|
+
log(`\u2601\uFE0F Tunnel up: ${tunnel.url}`);
|
|
16891
16988
|
}
|
|
16892
|
-
log(`\u2601\uFE0F Tunnel up: ${tunnel.url}`);
|
|
16893
16989
|
const heartbeat = async () => {
|
|
16894
|
-
const res = await apiClient.post(`${apiUrl}/api/devices/${config2.deviceId}/heartbeat`, {
|
|
16990
|
+
const res = await apiClient.post(`${apiUrl}/api/devices/${config2.deviceId}/heartbeat`, {
|
|
16991
|
+
status: "online",
|
|
16992
|
+
tunnel_url: tunnel?.url,
|
|
16993
|
+
running_count: running.size,
|
|
16994
|
+
max_concurrent: MAX_DEVICE
|
|
16995
|
+
});
|
|
16895
16996
|
if (res.success && res.data) {
|
|
16896
16997
|
const remoteRev = Number(res.data.agent_skill_revision ?? 0);
|
|
16897
16998
|
if (remoteRev > 0 && remoteRev !== lastMaterializedRevision()) {
|
|
@@ -16904,6 +17005,15 @@ async function cmdConnect(apiUrl, config2) {
|
|
|
16904
17005
|
}
|
|
16905
17006
|
return res.success && res.data?.pending_dispatches || 0;
|
|
16906
17007
|
};
|
|
17008
|
+
let beatTimer = null;
|
|
17009
|
+
triggerHeartbeat = () => {
|
|
17010
|
+
if (beatTimer)
|
|
17011
|
+
return;
|
|
17012
|
+
beatTimer = setTimeout(() => {
|
|
17013
|
+
beatTimer = null;
|
|
17014
|
+
heartbeat().catch(() => {});
|
|
17015
|
+
}, 200);
|
|
17016
|
+
};
|
|
16907
17017
|
{
|
|
16908
17018
|
const pending = await heartbeat();
|
|
16909
17019
|
if (pending > 0)
|
|
@@ -16940,7 +17050,7 @@ async function cmdConnect(apiUrl, config2) {
|
|
|
16940
17050
|
schedule();
|
|
16941
17051
|
let restarting = false;
|
|
16942
17052
|
const restartTunnel = async (reason) => {
|
|
16943
|
-
if (!alive || restarting)
|
|
17053
|
+
if (!alive || restarting || localMode)
|
|
16944
17054
|
return;
|
|
16945
17055
|
restarting = true;
|
|
16946
17056
|
try {
|
|
@@ -16972,20 +17082,21 @@ async function cmdConnect(apiUrl, config2) {
|
|
|
16972
17082
|
restarting = false;
|
|
16973
17083
|
}
|
|
16974
17084
|
};
|
|
16975
|
-
|
|
16976
|
-
|
|
16977
|
-
|
|
16978
|
-
|
|
16979
|
-
|
|
16980
|
-
|
|
17085
|
+
if (!localMode)
|
|
17086
|
+
(async () => {
|
|
17087
|
+
while (alive) {
|
|
17088
|
+
const t = tunnel;
|
|
17089
|
+
if (!t || !t.child) {
|
|
17090
|
+
await sleep(1000);
|
|
17091
|
+
continue;
|
|
17092
|
+
}
|
|
17093
|
+
const code = await t.child.exited;
|
|
17094
|
+
if (!alive)
|
|
17095
|
+
return;
|
|
17096
|
+
if (tunnel === t)
|
|
17097
|
+
await restartTunnel(`cloudflared exited code=${code}`);
|
|
16981
17098
|
}
|
|
16982
|
-
|
|
16983
|
-
if (!alive)
|
|
16984
|
-
return;
|
|
16985
|
-
if (tunnel === t)
|
|
16986
|
-
await restartTunnel(`cloudflared exited code=${code}`);
|
|
16987
|
-
}
|
|
16988
|
-
})();
|
|
17099
|
+
})();
|
|
16989
17100
|
let probeFailures = 0;
|
|
16990
17101
|
let tick = 0;
|
|
16991
17102
|
const PROBE_EVERY = 6;
|
|
@@ -16997,7 +17108,7 @@ async function cmdConnect(apiUrl, config2) {
|
|
|
16997
17108
|
}
|
|
16998
17109
|
tick++;
|
|
16999
17110
|
const currentUrl = tunnel?.url;
|
|
17000
|
-
if (currentUrl && tick % PROBE_EVERY === 0) {
|
|
17111
|
+
if (!localMode && currentUrl && tick % PROBE_EVERY === 0) {
|
|
17001
17112
|
const ok = await probeTunnel(currentUrl);
|
|
17002
17113
|
if (!ok) {
|
|
17003
17114
|
probeFailures++;
|
|
@@ -17518,6 +17629,7 @@ ${body}
|
|
|
17518
17629
|
} catch (e) {
|
|
17519
17630
|
log(`preamble fetch failed: ${String(e)}`);
|
|
17520
17631
|
}
|
|
17632
|
+
preamble += await fetchMemoryPreamble(apiUrl, task);
|
|
17521
17633
|
preamble += await buildPlanningPreamble(apiUrl, task);
|
|
17522
17634
|
const prompt = preamble ? `${preamble}
|
|
17523
17635
|
---
|
|
@@ -17583,6 +17695,7 @@ ${body}
|
|
|
17583
17695
|
}
|
|
17584
17696
|
}
|
|
17585
17697
|
} catch {}
|
|
17698
|
+
preamble += await fetchMemoryPreamble(apiUrl, task);
|
|
17586
17699
|
preamble += await buildPlanningPreamble(apiUrl, task);
|
|
17587
17700
|
const issueContext = `## Issue ${task.key}: ${task.title}${task.description ? `
|
|
17588
17701
|
|
|
@@ -17643,7 +17756,7 @@ ${userPart}` : userPart;
|
|
|
17643
17756
|
await eventHandler(event);
|
|
17644
17757
|
}
|
|
17645
17758
|
if (liveCommentId) {
|
|
17646
|
-
const n = await uploadOutputDir(apiUrl, liveCommentId, outDir);
|
|
17759
|
+
const n = await uploadOutputDir(apiUrl, liveCommentId, outDir, task.issue_id);
|
|
17647
17760
|
if (n > 0)
|
|
17648
17761
|
log(` \uD83D\uDCCE uploaded ${n} output file(s)`);
|
|
17649
17762
|
}
|
|
@@ -17692,6 +17805,53 @@ ${userPart}` : userPart;
|
|
|
17692
17805
|
}
|
|
17693
17806
|
}
|
|
17694
17807
|
}
|
|
17808
|
+
async function fetchMemoryPreamble(apiUrl, task) {
|
|
17809
|
+
if (process.env.MULTI_MEMORY_ENABLED !== "1")
|
|
17810
|
+
return "";
|
|
17811
|
+
try {
|
|
17812
|
+
const issueRes = await apiClient.get(`${apiUrl}/api/issues/${task.issue_id}`);
|
|
17813
|
+
const projectId = issueRes.data?.project_id;
|
|
17814
|
+
if (!projectId)
|
|
17815
|
+
return "";
|
|
17816
|
+
const title = String(task.title || "").trim();
|
|
17817
|
+
const desc = String(task.description || "").trim();
|
|
17818
|
+
const followup = String(task.followup || "").trim();
|
|
17819
|
+
const query = [title, desc, followup].filter(Boolean).join(`
|
|
17820
|
+
`).slice(0, 4000);
|
|
17821
|
+
if (!query)
|
|
17822
|
+
return "";
|
|
17823
|
+
const res = await apiClient.post(`${apiUrl}/api/memory/recall`, {
|
|
17824
|
+
project_id: projectId,
|
|
17825
|
+
issue_id: task.issue_id,
|
|
17826
|
+
query,
|
|
17827
|
+
k: 10
|
|
17828
|
+
});
|
|
17829
|
+
if (!res.success)
|
|
17830
|
+
return "";
|
|
17831
|
+
const synthesis = String(res.data?.synthesis || "").trim();
|
|
17832
|
+
if (!synthesis)
|
|
17833
|
+
return "";
|
|
17834
|
+
const cites = Array.isArray(res.data?.citations) ? res.data.citations : [];
|
|
17835
|
+
let block = `## Project memory
|
|
17836
|
+
|
|
17837
|
+
${synthesis}
|
|
17838
|
+
`;
|
|
17839
|
+
if (cites.length) {
|
|
17840
|
+
block += `
|
|
17841
|
+
`;
|
|
17842
|
+
for (let i = 0;i < cites.length; i++) {
|
|
17843
|
+
const c = cites[i];
|
|
17844
|
+
block += `[${i + 1}] ${String(c.snippet || "").replace(/\s+/g, " ").trim()}
|
|
17845
|
+
`;
|
|
17846
|
+
}
|
|
17847
|
+
}
|
|
17848
|
+
return `${block}
|
|
17849
|
+
`;
|
|
17850
|
+
} catch (e) {
|
|
17851
|
+
log(`memory recall failed: ${String(e?.message || e)}`);
|
|
17852
|
+
return "";
|
|
17853
|
+
}
|
|
17854
|
+
}
|
|
17695
17855
|
async function buildPlanningPreamble(apiUrl, task) {
|
|
17696
17856
|
const depth = typeof task.planning_depth === "number" ? task.planning_depth : 0;
|
|
17697
17857
|
if (depth >= PLANNING_DEPTH_LIMIT) {
|
|
@@ -18058,7 +18218,7 @@ function authTokenHeader() {
|
|
|
18058
18218
|
const cfg = loadConfig();
|
|
18059
18219
|
return cfg.token ? `Bearer ${cfg.token}` : null;
|
|
18060
18220
|
}
|
|
18061
|
-
async function uploadOutputDir(apiUrl, commentId, dir) {
|
|
18221
|
+
async function uploadOutputDir(apiUrl, commentId, dir, issueId) {
|
|
18062
18222
|
if (!existsSync4(dir))
|
|
18063
18223
|
return 0;
|
|
18064
18224
|
const files = [];
|
|
@@ -18087,10 +18247,15 @@ async function uploadOutputDir(apiUrl, commentId, dir) {
|
|
|
18087
18247
|
const form = new FormData;
|
|
18088
18248
|
const blob = new Blob([data]);
|
|
18089
18249
|
form.append("file", blob, f.split("/").pop() || "file");
|
|
18250
|
+
const headers = {};
|
|
18251
|
+
if (token)
|
|
18252
|
+
headers.Authorization = token;
|
|
18253
|
+
if (issueId)
|
|
18254
|
+
headers["x-issue-id"] = issueId;
|
|
18090
18255
|
const res = await fetch(`${apiUrl}/api/attachments/comments/${commentId}`, {
|
|
18091
18256
|
method: "POST",
|
|
18092
18257
|
body: form,
|
|
18093
|
-
headers
|
|
18258
|
+
headers
|
|
18094
18259
|
});
|
|
18095
18260
|
if (res.ok) {
|
|
18096
18261
|
uploaded++;
|