@snowyroad/arp 0.1.0 → 0.1.2
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/cli.js +311 -17
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -46,11 +46,11 @@ async function redeemInvite(relayHttpUrl, code) {
|
|
|
46
46
|
throw new Error(`Invite redemption failed (HTTP ${res.status}).`);
|
|
47
47
|
}
|
|
48
48
|
const data = await res.json();
|
|
49
|
-
if (!data.ok || !data.
|
|
49
|
+
if (!data.ok || !data.agentKey || !data.agentId || !data.agentUuid) {
|
|
50
50
|
throw new Error("Invite redemption returned an incomplete response.");
|
|
51
51
|
}
|
|
52
52
|
return {
|
|
53
|
-
|
|
53
|
+
agentKey: data.agentKey,
|
|
54
54
|
agentId: data.agentId,
|
|
55
55
|
agentName: data.agentName ?? data.agentId,
|
|
56
56
|
agentUuid: data.agentUuid,
|
|
@@ -58,6 +58,218 @@ async function redeemInvite(relayHttpUrl, code) {
|
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
// src/keystore.ts
|
|
62
|
+
import { mkdirSync, readFileSync, writeFileSync, renameSync, readdirSync, unlinkSync, chmodSync, openSync, closeSync, existsSync } from "fs";
|
|
63
|
+
import { homedir } from "os";
|
|
64
|
+
import { join, dirname } from "path";
|
|
65
|
+
function configDir(env = process.env) {
|
|
66
|
+
const override = env.ARP_CONFIG_DIR?.trim();
|
|
67
|
+
return override && override !== "" ? override : join(homedir(), ".arp");
|
|
68
|
+
}
|
|
69
|
+
function relayHostSegment(relayUrl) {
|
|
70
|
+
try {
|
|
71
|
+
const u = new URL(relayUrl);
|
|
72
|
+
return `${u.hostname}${u.port ? "_" + u.port : ""}`.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
73
|
+
} catch {
|
|
74
|
+
return relayUrl.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function agentFilePath(dir, relayUrl, agentName) {
|
|
78
|
+
const safeName = agentName.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
79
|
+
return join(dir, "agents", relayHostSegment(relayUrl), `${safeName}.json`);
|
|
80
|
+
}
|
|
81
|
+
function saveAgent(dir, agent) {
|
|
82
|
+
const file = agentFilePath(dir, agent.relayUrl, agent.agentName);
|
|
83
|
+
mkdirSync(dirname(file), { recursive: true, mode: 448 });
|
|
84
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
85
|
+
writeFileSync(tmp, JSON.stringify(agent, null, 2) + "\n", { mode: 384 });
|
|
86
|
+
renameSync(tmp, file);
|
|
87
|
+
try {
|
|
88
|
+
chmodSync(file, 384);
|
|
89
|
+
} catch {
|
|
90
|
+
}
|
|
91
|
+
return file;
|
|
92
|
+
}
|
|
93
|
+
function parseStoredAgent(file) {
|
|
94
|
+
let raw;
|
|
95
|
+
try {
|
|
96
|
+
raw = readFileSync(file, "utf8");
|
|
97
|
+
} catch {
|
|
98
|
+
throw new Error(`No saved credential at ${file}`);
|
|
99
|
+
}
|
|
100
|
+
let p;
|
|
101
|
+
try {
|
|
102
|
+
p = JSON.parse(raw);
|
|
103
|
+
} catch {
|
|
104
|
+
throw new Error(`Corrupt credential file at ${file}`);
|
|
105
|
+
}
|
|
106
|
+
const a = p;
|
|
107
|
+
for (const field of ["relayUrl", "agentId", "agentName", "agentUuid", "agentKey"]) {
|
|
108
|
+
if (typeof a[field] !== "string" || a[field].trim() === "") {
|
|
109
|
+
throw new Error(`Corrupt credential file at ${file} (missing "${field}")`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
relayUrl: a.relayUrl.trim(),
|
|
114
|
+
agentId: a.agentId.trim(),
|
|
115
|
+
agentName: a.agentName.trim(),
|
|
116
|
+
agentUuid: a.agentUuid.trim(),
|
|
117
|
+
agentKey: a.agentKey.trim()
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function listAgents(dir) {
|
|
121
|
+
const root = join(dir, "agents");
|
|
122
|
+
const out = [];
|
|
123
|
+
let hosts = [];
|
|
124
|
+
try {
|
|
125
|
+
hosts = readdirSync(root);
|
|
126
|
+
} catch {
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
for (const host of hosts) {
|
|
130
|
+
let files = [];
|
|
131
|
+
try {
|
|
132
|
+
files = readdirSync(join(root, host));
|
|
133
|
+
} catch {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
for (const f of files) {
|
|
137
|
+
if (!f.endsWith(".json")) continue;
|
|
138
|
+
const file = join(root, host, f);
|
|
139
|
+
try {
|
|
140
|
+
out.push({ file, agent: parseStoredAgent(file) });
|
|
141
|
+
} catch {
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
function loadAgent(dir, agentName) {
|
|
148
|
+
const all = listAgents(dir);
|
|
149
|
+
if (agentName && agentName.trim() !== "") {
|
|
150
|
+
const matches = all.filter((e) => e.agent.agentName === agentName.trim());
|
|
151
|
+
if (matches.length === 0) {
|
|
152
|
+
const safeName = agentName.trim().replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
153
|
+
const root = join(dir, "agents");
|
|
154
|
+
let hosts = [];
|
|
155
|
+
try {
|
|
156
|
+
hosts = readdirSync(root);
|
|
157
|
+
} catch {
|
|
158
|
+
}
|
|
159
|
+
for (const host of hosts) {
|
|
160
|
+
const candidate = join(root, host, `${safeName}.json`);
|
|
161
|
+
if (existsSync(candidate)) parseStoredAgent(candidate);
|
|
162
|
+
}
|
|
163
|
+
throw new Error(
|
|
164
|
+
`No saved credential for "${agentName}". Run: npx @snowyroad/arp join <code>`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
if (matches.length > 1) {
|
|
168
|
+
const relays = matches.map((m) => m.agent.relayUrl).join(", ");
|
|
169
|
+
throw new Error(
|
|
170
|
+
`Agent "${agentName}" has credentials for multiple relays (${relays}). Set ARP_CONFIG_DIR or remove the stale file.`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
return matches[0];
|
|
174
|
+
}
|
|
175
|
+
if (all.length === 0) {
|
|
176
|
+
throw new Error("No saved credentials. Run: npx @snowyroad/arp join <code>");
|
|
177
|
+
}
|
|
178
|
+
if (all.length > 1) {
|
|
179
|
+
const names = all.map((e) => e.agent.agentName).join(", ");
|
|
180
|
+
throw new Error(`Multiple saved agents (${names}). Run: arp start <name>`);
|
|
181
|
+
}
|
|
182
|
+
return all[0];
|
|
183
|
+
}
|
|
184
|
+
function acquireAgentLock(agentFile) {
|
|
185
|
+
const lockFile = `${agentFile}.lock`;
|
|
186
|
+
const tryAcquire = () => {
|
|
187
|
+
try {
|
|
188
|
+
const fd = openSync(lockFile, "wx", 384);
|
|
189
|
+
writeFileSync(lockFile, String(process.pid));
|
|
190
|
+
closeSync(fd);
|
|
191
|
+
return true;
|
|
192
|
+
} catch {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
if (!tryAcquire()) {
|
|
197
|
+
let holderPid = NaN;
|
|
198
|
+
try {
|
|
199
|
+
holderPid = Number(readFileSync(lockFile, "utf8").trim());
|
|
200
|
+
} catch {
|
|
201
|
+
}
|
|
202
|
+
if (holderPid === process.pid) {
|
|
203
|
+
return () => {
|
|
204
|
+
try {
|
|
205
|
+
unlinkSync(lockFile);
|
|
206
|
+
} catch {
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
let holderAlive = false;
|
|
211
|
+
if (Number.isFinite(holderPid) && holderPid > 0) {
|
|
212
|
+
try {
|
|
213
|
+
process.kill(holderPid, 0);
|
|
214
|
+
holderAlive = true;
|
|
215
|
+
} catch (err) {
|
|
216
|
+
holderAlive = err.code === "EPERM";
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (holderAlive) {
|
|
220
|
+
throw new Error(
|
|
221
|
+
`This agent is already running on this machine (pid ${holderPid}). Running it twice would trip credential reuse detection.`
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
unlinkSync(lockFile);
|
|
226
|
+
} catch {
|
|
227
|
+
}
|
|
228
|
+
if (!tryAcquire()) {
|
|
229
|
+
throw new Error("This agent is already running on this machine.");
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return () => {
|
|
233
|
+
try {
|
|
234
|
+
if (existsSync(lockFile) && readFileSync(lockFile, "utf8").trim() === String(process.pid)) {
|
|
235
|
+
unlinkSync(lockFile);
|
|
236
|
+
}
|
|
237
|
+
} catch {
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// src/token.ts
|
|
243
|
+
var RebootstrapError = class extends Error {
|
|
244
|
+
constructor() {
|
|
245
|
+
super(
|
|
246
|
+
"This agent's credential was revoked or invalidated. Re-issue a connection from the website."
|
|
247
|
+
);
|
|
248
|
+
this.name = "RebootstrapError";
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
async function mintAccessToken(relayHttpUrl, agentKey, fetchFn = fetch) {
|
|
252
|
+
const res = await fetchFn(`${relayHttpUrl.replace(/\/$/, "")}/agents/token`, {
|
|
253
|
+
method: "POST",
|
|
254
|
+
headers: { Authorization: `Bearer ${agentKey}` }
|
|
255
|
+
});
|
|
256
|
+
if (res.status === 401) {
|
|
257
|
+
throw new RebootstrapError();
|
|
258
|
+
}
|
|
259
|
+
if (!res.ok) {
|
|
260
|
+
throw new Error(`Token mint failed (HTTP ${res.status}).`);
|
|
261
|
+
}
|
|
262
|
+
const data = await res.json();
|
|
263
|
+
if (!data.ok || !data.accessToken || !data.agentKey) {
|
|
264
|
+
throw new Error("Token mint returned an incomplete response.");
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
accessToken: data.accessToken,
|
|
268
|
+
expiresIn: typeof data.expiresIn === "number" ? data.expiresIn : 3600,
|
|
269
|
+
agentKey: data.agentKey
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
61
273
|
// src/config.ts
|
|
62
274
|
var DEFAULT_MODEL = "claude-opus-4-8";
|
|
63
275
|
var DEFAULT_AGENT_MODE = "acp";
|
|
@@ -111,25 +323,71 @@ function loadConfig(env) {
|
|
|
111
323
|
catchUpMaxMentions: positiveIntEnv(env.ARP_CATCHUP_MAX_MENTIONS, 3)
|
|
112
324
|
};
|
|
113
325
|
}
|
|
114
|
-
|
|
326
|
+
function wsToHttp(relayWsUrl) {
|
|
327
|
+
return relayWsUrl.replace(/^wss:\/\//, "https://").replace(/^ws:\/\//, "http://");
|
|
328
|
+
}
|
|
329
|
+
async function buildFromStoredAgent(dir, stored, env) {
|
|
115
330
|
const { agentMode, agent } = resolveAgentSelection(env);
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
-
const
|
|
331
|
+
const relayWsUrl = stored.relayUrl;
|
|
332
|
+
const relayHttpUrl = wsToHttp(relayWsUrl);
|
|
333
|
+
const file = agentFilePath(dir, stored.relayUrl, stored.agentName);
|
|
334
|
+
const release = acquireAgentLock(file);
|
|
335
|
+
process.once("exit", release);
|
|
336
|
+
let current = stored;
|
|
337
|
+
let inflight = null;
|
|
338
|
+
const mintToken = () => {
|
|
339
|
+
if (inflight) return inflight;
|
|
340
|
+
inflight = (async () => {
|
|
341
|
+
try {
|
|
342
|
+
const r = await mintAccessToken(relayHttpUrl, current.agentKey);
|
|
343
|
+
current = { ...current, agentKey: r.agentKey };
|
|
344
|
+
saveAgent(dir, current);
|
|
345
|
+
return r.accessToken;
|
|
346
|
+
} finally {
|
|
347
|
+
inflight = null;
|
|
348
|
+
}
|
|
349
|
+
})();
|
|
350
|
+
return inflight;
|
|
351
|
+
};
|
|
352
|
+
const token = await mintToken();
|
|
120
353
|
return {
|
|
121
354
|
relayWsUrl,
|
|
122
355
|
relayHttpUrl,
|
|
123
|
-
token
|
|
124
|
-
agentId:
|
|
125
|
-
agentName:
|
|
126
|
-
agentUuid:
|
|
356
|
+
token,
|
|
357
|
+
agentId: stored.agentId,
|
|
358
|
+
agentName: stored.agentName,
|
|
359
|
+
agentUuid: stored.agentUuid,
|
|
127
360
|
agentMode,
|
|
128
361
|
agent,
|
|
129
362
|
model: env.ARP_MODEL?.trim() || DEFAULT_MODEL,
|
|
130
363
|
catchUpTtlMs: positiveIntEnv(env.ARP_CATCHUP_TTL_MS, 72e5),
|
|
131
|
-
catchUpMaxMentions: positiveIntEnv(env.ARP_CATCHUP_MAX_MENTIONS, 3)
|
|
364
|
+
catchUpMaxMentions: positiveIntEnv(env.ARP_CATCHUP_MAX_MENTIONS, 3),
|
|
365
|
+
mintToken,
|
|
366
|
+
agentFile: file
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
async function loadConfigFromInvite(code, env) {
|
|
370
|
+
resolveAgentSelection(env);
|
|
371
|
+
const inv = decodeInvite(code);
|
|
372
|
+
const relayWsUrl = inv.relayUrl;
|
|
373
|
+
const relayHttpUrl = wsToHttp(relayWsUrl);
|
|
374
|
+
const bundle = await redeemInvite(relayHttpUrl, inv.code);
|
|
375
|
+
const dir = configDir(env);
|
|
376
|
+
const stored = {
|
|
377
|
+
relayUrl: relayWsUrl,
|
|
378
|
+
agentId: bundle.agentId,
|
|
379
|
+
agentName: bundle.agentName,
|
|
380
|
+
agentUuid: bundle.agentUuid,
|
|
381
|
+
agentKey: bundle.agentKey
|
|
132
382
|
};
|
|
383
|
+
const file = saveAgent(dir, stored);
|
|
384
|
+
console.log(`[arp-bridge] credential saved to ${file} (restart later with: arp start ${bundle.agentName})`);
|
|
385
|
+
return buildFromStoredAgent(dir, stored, env);
|
|
386
|
+
}
|
|
387
|
+
async function loadConfigFromStore(agentName, env) {
|
|
388
|
+
const dir = configDir(env);
|
|
389
|
+
const entry = loadAgent(dir, agentName);
|
|
390
|
+
return buildFromStoredAgent(dir, entry.agent, env);
|
|
133
391
|
}
|
|
134
392
|
function getFlag(argv, name) {
|
|
135
393
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -145,13 +403,17 @@ async function resolveConfig(argv, env) {
|
|
|
145
403
|
if (!code || code.trim() === "") throw new Error("Missing value for join");
|
|
146
404
|
return loadConfigFromInvite(code.trim(), env);
|
|
147
405
|
}
|
|
406
|
+
if (argv[0] === "start") {
|
|
407
|
+
return loadConfigFromStore(argv[1]?.trim() || void 0, env);
|
|
408
|
+
}
|
|
148
409
|
const argInvite = getFlag(argv, "--invite");
|
|
149
410
|
if (argInvite !== void 0 && argInvite.trim() === "") {
|
|
150
411
|
throw new Error("Missing value for --invite");
|
|
151
412
|
}
|
|
152
413
|
const invite = (argInvite ?? env.ARP_INVITE)?.trim();
|
|
153
414
|
if (invite) return loadConfigFromInvite(invite, env);
|
|
154
|
-
return loadConfig(env);
|
|
415
|
+
if (env.ARP_TOKEN && env.ARP_TOKEN.trim() !== "") return loadConfig(env);
|
|
416
|
+
return loadConfigFromStore(void 0, env);
|
|
155
417
|
}
|
|
156
418
|
function redactConfig(cfg) {
|
|
157
419
|
const model = cfg.agentMode === "acp" ? `(provider default; ARP_MODEL ignored in acp mode)` : cfg.model;
|
|
@@ -314,12 +576,15 @@ var SEEN_CAP = 5e3;
|
|
|
314
576
|
var RESUME_MAX_PAGES = 200;
|
|
315
577
|
var FATAL_CLOSE_CODES = /* @__PURE__ */ new Set([
|
|
316
578
|
4001,
|
|
317
|
-
// auth failed (bad/expired/tampered token)
|
|
579
|
+
// auth failed (bad/expired/tampered token) — recoverable via key re-mint when cfg.mintToken exists
|
|
318
580
|
4002,
|
|
319
581
|
// agent not found
|
|
320
|
-
4003
|
|
582
|
+
4003,
|
|
321
583
|
// duplicate connection
|
|
584
|
+
4004
|
|
585
|
+
// credential revoked (family revoke) — operator must re-bootstrap
|
|
322
586
|
]);
|
|
587
|
+
var MAX_REMINT_ATTEMPTS = 3;
|
|
323
588
|
var RelayClient = class {
|
|
324
589
|
constructor(cfg, deps) {
|
|
325
590
|
this.cfg = cfg;
|
|
@@ -336,6 +601,7 @@ var RelayClient = class {
|
|
|
336
601
|
stableTimer = null;
|
|
337
602
|
reconnectTimer = null;
|
|
338
603
|
reconnectAttempts = 0;
|
|
604
|
+
remintAttempts = 0;
|
|
339
605
|
stopped = false;
|
|
340
606
|
seenByChannel = /* @__PURE__ */ new Map();
|
|
341
607
|
cursors = /* @__PURE__ */ new Map();
|
|
@@ -401,6 +667,7 @@ var RelayClient = class {
|
|
|
401
667
|
this.armWatchdog();
|
|
402
668
|
this.stableTimer = setTimeout(() => {
|
|
403
669
|
this.reconnectAttempts = 0;
|
|
670
|
+
this.remintAttempts = 0;
|
|
404
671
|
}, STABLE_RESET_MS);
|
|
405
672
|
if (this.graceTimer) clearTimeout(this.graceTimer);
|
|
406
673
|
this.graceTimer = setTimeout(() => this.confirmReady(), AUTH_GRACE_MS);
|
|
@@ -495,6 +762,18 @@ var RelayClient = class {
|
|
|
495
762
|
onClose(code, reason) {
|
|
496
763
|
this.clearTimers();
|
|
497
764
|
if (this.stopped) return;
|
|
765
|
+
if (code === 4001 && this.cfg.mintToken && this.remintAttempts < MAX_REMINT_ATTEMPTS) {
|
|
766
|
+
this.remintAttempts++;
|
|
767
|
+
console.log("[arp-bridge] access token rejected; re-minting from agent key");
|
|
768
|
+
void this.cfg.mintToken().then((token) => {
|
|
769
|
+
this.cfg.token = token;
|
|
770
|
+
if (!this.stopped) this.connect();
|
|
771
|
+
}).catch((err) => {
|
|
772
|
+
this.stopped = true;
|
|
773
|
+
this.fatalCb?.(code, err instanceof Error ? err.message : String(err));
|
|
774
|
+
});
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
498
777
|
if (FATAL_CLOSE_CODES.has(code)) {
|
|
499
778
|
this.stopped = true;
|
|
500
779
|
this.fatalCb?.(code, reason);
|
|
@@ -1865,14 +2144,29 @@ function installGracefulShutdown(bridge) {
|
|
|
1865
2144
|
}
|
|
1866
2145
|
|
|
1867
2146
|
// src/cli.ts
|
|
2147
|
+
function printList() {
|
|
2148
|
+
const entries = listAgents(configDir(process.env));
|
|
2149
|
+
if (entries.length === 0) {
|
|
2150
|
+
console.log("No saved agents. Run: npx @snowyroad/arp join <code>");
|
|
2151
|
+
return;
|
|
2152
|
+
}
|
|
2153
|
+
for (const e of entries) {
|
|
2154
|
+
console.log(`${e.agent.agentName} relay=${e.agent.relayUrl} uuid=${e.agent.agentUuid}`);
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
1868
2157
|
async function main() {
|
|
1869
|
-
const
|
|
2158
|
+
const argv = process.argv.slice(2);
|
|
2159
|
+
if (argv[0] === "list") {
|
|
2160
|
+
printList();
|
|
2161
|
+
return;
|
|
2162
|
+
}
|
|
2163
|
+
const cfg = await resolveConfig(argv, process.env);
|
|
1870
2164
|
console.log("[arp-bridge] starting", redactConfig(cfg));
|
|
1871
2165
|
const bridge = await createAndStartBridge(cfg);
|
|
1872
2166
|
installGracefulShutdown(bridge);
|
|
1873
2167
|
console.log("[arp-bridge] connected; routing per-channel sessions. Ctrl-C to stop.");
|
|
1874
2168
|
}
|
|
1875
2169
|
main().catch((err) => {
|
|
1876
|
-
console.error("[arp-bridge] fatal:", err);
|
|
2170
|
+
console.error("[arp-bridge] fatal:", err instanceof Error ? err.message : err);
|
|
1877
2171
|
process.exit(1);
|
|
1878
2172
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@snowyroad/arp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Connect your own coding agent (Claude Code, Codex, Gemini, Grok) to an Agent Relay Protocol channel and collaborate with other agents and humans.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"author": "SnowyRoad",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"node": ">=20"
|
|
22
22
|
},
|
|
23
23
|
"bin": {
|
|
24
|
-
"arp": "dist/cli.js"
|
|
24
|
+
"arp-bridge": "dist/cli.js"
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
27
27
|
"dist",
|