@prismer/runtime 2.0.0 → 2.0.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/CHANGELOG.md +50 -0
- package/dist/cli.cjs +560 -31
- package/dist/cli.js +568 -39
- package/dist/index.cjs +562 -31
- package/dist/index.d.cts +142 -65
- package/dist/index.d.ts +142 -65
- package/dist/index.js +573 -43
- package/package.json +2 -1
package/dist/cli.cjs
CHANGED
|
@@ -465,7 +465,7 @@ var require_package = __commonJS({
|
|
|
465
465
|
"package.json"(exports2, module2) {
|
|
466
466
|
module2.exports = {
|
|
467
467
|
name: "@prismer/runtime",
|
|
468
|
-
version: "2.0.
|
|
468
|
+
version: "2.0.1",
|
|
469
469
|
description: "Prismer Cloud daemon runtime \u2014 TS-only adapter host for hosted IM agents",
|
|
470
470
|
type: "module",
|
|
471
471
|
main: "dist/index.js",
|
|
@@ -502,6 +502,7 @@ var require_package = __commonJS({
|
|
|
502
502
|
"better-sqlite3": "^11.8.0",
|
|
503
503
|
commander: "^12.1.0",
|
|
504
504
|
qrcode: "^1.5.4",
|
|
505
|
+
undici: "^7.24.0",
|
|
505
506
|
ws: "^8.19.0",
|
|
506
507
|
yaml: "^2.8.2",
|
|
507
508
|
zod: "^3.23.8"
|
|
@@ -3928,8 +3929,11 @@ var import_node_path11 = require("path");
|
|
|
3928
3929
|
|
|
3929
3930
|
// src/asset-cache.ts
|
|
3930
3931
|
var import_node_crypto = require("crypto");
|
|
3932
|
+
var import_node_dns = require("dns");
|
|
3931
3933
|
var import_node_fs7 = require("fs");
|
|
3934
|
+
var import_node_net = require("net");
|
|
3932
3935
|
var import_node_path8 = require("path");
|
|
3936
|
+
var import_undici = require("undici");
|
|
3933
3937
|
function rowToCached(row) {
|
|
3934
3938
|
return {
|
|
3935
3939
|
contentHash: row.content_hash,
|
|
@@ -4050,6 +4054,73 @@ var AssetCache = class {
|
|
|
4050
4054
|
pin: false
|
|
4051
4055
|
};
|
|
4052
4056
|
}
|
|
4057
|
+
/**
|
|
4058
|
+
* Fetch a public http(s) URL, content-hash dedup, cache locally.
|
|
4059
|
+
*
|
|
4060
|
+
* Mirrors `getOrFetch` (cache key = sha256 of body bytes), but the input
|
|
4061
|
+
* is a URL whose hash we don't know up front. The fetcher applies:
|
|
4062
|
+
*
|
|
4063
|
+
* - SSRF guard (resolve hostname, reject loopback / RFC1918 / link-local
|
|
4064
|
+
* / cloud-metadata / IPv6 ULA + link-local). Cross-host redirects
|
|
4065
|
+
* re-validate the new host before each hop.
|
|
4066
|
+
* - manual redirect handling (max 3 hops).
|
|
4067
|
+
* - hard timeout (default 15 s, override via env
|
|
4068
|
+
* `PRISMER_URL_FETCH_TIMEOUT_MS`).
|
|
4069
|
+
* - streaming body read with abort-at-limit (default 5 MiB, override
|
|
4070
|
+
* via env `PRISMER_URL_FETCH_MAX_BYTES`). Truncated bodies are NOT
|
|
4071
|
+
* cached.
|
|
4072
|
+
* - non-2xx → throws (caller should leave the URL in place and emit
|
|
4073
|
+
* an `error` observation).
|
|
4074
|
+
*
|
|
4075
|
+
* On success returns { cached, finalUrl, durationMs }; the caller can
|
|
4076
|
+
* pin / unpin the hash like any other asset.
|
|
4077
|
+
*/
|
|
4078
|
+
async getOrFetchUrl(url, opts) {
|
|
4079
|
+
const started = Date.now();
|
|
4080
|
+
const maxBytes = opts?.maxBytes ?? defaultMaxBytes();
|
|
4081
|
+
const timeoutMs = opts?.timeoutMs ?? defaultTimeoutMs();
|
|
4082
|
+
const maxRedirects = opts?.maxRedirects ?? 3;
|
|
4083
|
+
const userAgent = opts?.userAgent ?? "prismer-daemon";
|
|
4084
|
+
const { body, finalUrl, mime } = await fetchUrlWithGuards(url, {
|
|
4085
|
+
signal: opts?.signal,
|
|
4086
|
+
maxBytes,
|
|
4087
|
+
timeoutMs,
|
|
4088
|
+
maxRedirects,
|
|
4089
|
+
userAgent
|
|
4090
|
+
});
|
|
4091
|
+
const hash = sha256(body);
|
|
4092
|
+
const existing = this.get(hash);
|
|
4093
|
+
if (existing) {
|
|
4094
|
+
return { cached: existing, finalUrl, durationMs: Date.now() - started };
|
|
4095
|
+
}
|
|
4096
|
+
const localPath = this.pathFor(hash);
|
|
4097
|
+
if (!(0, import_node_fs7.existsSync)((0, import_node_path8.dirname)(localPath))) {
|
|
4098
|
+
(0, import_node_fs7.mkdirSync)((0, import_node_path8.dirname)(localPath), { recursive: true });
|
|
4099
|
+
}
|
|
4100
|
+
const tmpPath = `${localPath}.${process.pid}.${Date.now()}.tmp`;
|
|
4101
|
+
(0, import_node_fs7.writeFileSync)(tmpPath, body);
|
|
4102
|
+
(0, import_node_fs7.renameSync)(tmpPath, localPath);
|
|
4103
|
+
const now = Date.now();
|
|
4104
|
+
this.db.prepare(
|
|
4105
|
+
`INSERT OR REPLACE INTO cached_assets
|
|
4106
|
+
(content_hash, size_bytes, mime, local_path, fetched_at, last_used_at, pin)
|
|
4107
|
+
VALUES (?, ?, ?, ?, ?, ?, 0)`
|
|
4108
|
+
).run(hash, body.length, mime, localPath, now, now);
|
|
4109
|
+
this.evictIfOver();
|
|
4110
|
+
return {
|
|
4111
|
+
cached: {
|
|
4112
|
+
contentHash: hash,
|
|
4113
|
+
sizeBytes: body.length,
|
|
4114
|
+
mime,
|
|
4115
|
+
localPath,
|
|
4116
|
+
fetchedAt: now,
|
|
4117
|
+
lastUsedAt: now,
|
|
4118
|
+
pin: false
|
|
4119
|
+
},
|
|
4120
|
+
finalUrl,
|
|
4121
|
+
durationMs: now - started
|
|
4122
|
+
};
|
|
4123
|
+
}
|
|
4053
4124
|
/** Insert an already-on-disk asset into the cache (e.g., asset just produced by adapter). */
|
|
4054
4125
|
registerLocal(hash, localPath, mime) {
|
|
4055
4126
|
const actualHash = sha256(readFileBuffer(localPath));
|
|
@@ -4101,6 +4172,184 @@ function sha256(buf) {
|
|
|
4101
4172
|
function readFileBuffer(path9) {
|
|
4102
4173
|
return (0, import_node_fs7.readFileSync)(path9);
|
|
4103
4174
|
}
|
|
4175
|
+
var DEFAULT_URL_FETCH_MAX_BYTES = 5 * 1024 * 1024;
|
|
4176
|
+
var DEFAULT_URL_FETCH_TIMEOUT_MS = 15e3;
|
|
4177
|
+
function defaultMaxBytes() {
|
|
4178
|
+
const v = Number(process.env.PRISMER_URL_FETCH_MAX_BYTES);
|
|
4179
|
+
return Number.isFinite(v) && v > 0 ? v : DEFAULT_URL_FETCH_MAX_BYTES;
|
|
4180
|
+
}
|
|
4181
|
+
function defaultTimeoutMs() {
|
|
4182
|
+
const v = Number(process.env.PRISMER_URL_FETCH_TIMEOUT_MS);
|
|
4183
|
+
return Number.isFinite(v) && v > 0 ? v : DEFAULT_URL_FETCH_TIMEOUT_MS;
|
|
4184
|
+
}
|
|
4185
|
+
var urlFetchDeps = {};
|
|
4186
|
+
function activeFetch() {
|
|
4187
|
+
return urlFetchDeps.fetch ?? globalThis.fetch;
|
|
4188
|
+
}
|
|
4189
|
+
async function activeResolveHostname(host) {
|
|
4190
|
+
if (urlFetchDeps.resolveHostname) return urlFetchDeps.resolveHostname(host);
|
|
4191
|
+
if ((0, import_node_net.isIP)(host)) return [host];
|
|
4192
|
+
const res = await import_node_dns.promises.lookup(host, { all: true, verbatim: true });
|
|
4193
|
+
return res.map((r) => r.address);
|
|
4194
|
+
}
|
|
4195
|
+
function isForbiddenIp(ip) {
|
|
4196
|
+
if ((0, import_node_net.isIPv4)(ip)) {
|
|
4197
|
+
const parts = ip.split(".").map((n) => Number(n));
|
|
4198
|
+
if (parts.length !== 4 || parts.some((p) => !Number.isFinite(p) || p < 0 || p > 255)) {
|
|
4199
|
+
return true;
|
|
4200
|
+
}
|
|
4201
|
+
const [a = 0, b = 0] = parts;
|
|
4202
|
+
if (a === 127) return true;
|
|
4203
|
+
if (a === 10) return true;
|
|
4204
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
4205
|
+
if (a === 192 && b === 168) return true;
|
|
4206
|
+
if (a === 169 && b === 254) return true;
|
|
4207
|
+
if (a === 0) return true;
|
|
4208
|
+
return false;
|
|
4209
|
+
}
|
|
4210
|
+
if ((0, import_node_net.isIPv6)(ip)) {
|
|
4211
|
+
const lower = ip.toLowerCase();
|
|
4212
|
+
if (lower === "::1" || lower === "0:0:0:0:0:0:0:1") return true;
|
|
4213
|
+
if (lower === "::" || lower === "0:0:0:0:0:0:0:0") return true;
|
|
4214
|
+
if (/^fe[89ab][0-9a-f]?:/.test(lower)) return true;
|
|
4215
|
+
if (/^f[cd][0-9a-f]{2}:/.test(lower)) return true;
|
|
4216
|
+
const mapped = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/i.exec(lower);
|
|
4217
|
+
if (mapped) return isForbiddenIp(mapped[1]);
|
|
4218
|
+
return false;
|
|
4219
|
+
}
|
|
4220
|
+
return true;
|
|
4221
|
+
}
|
|
4222
|
+
async function assertHostAllowed(hostname5) {
|
|
4223
|
+
let ips;
|
|
4224
|
+
try {
|
|
4225
|
+
ips = await activeResolveHostname(hostname5);
|
|
4226
|
+
} catch (err) {
|
|
4227
|
+
throw new Error(`hostname resolution failed for ${hostname5}: ${err.message}`);
|
|
4228
|
+
}
|
|
4229
|
+
if (ips.length === 0) {
|
|
4230
|
+
throw new Error(`no IPs resolved for ${hostname5}`);
|
|
4231
|
+
}
|
|
4232
|
+
for (const ip of ips) {
|
|
4233
|
+
if (isForbiddenIp(ip)) {
|
|
4234
|
+
throw new Error(`SSRF guard: ${hostname5} \u2192 ${ip} is in a forbidden range`);
|
|
4235
|
+
}
|
|
4236
|
+
}
|
|
4237
|
+
return ips[0];
|
|
4238
|
+
}
|
|
4239
|
+
async function fetchUrlWithGuards(rawUrl, opts) {
|
|
4240
|
+
let currentUrl;
|
|
4241
|
+
try {
|
|
4242
|
+
currentUrl = new URL(rawUrl);
|
|
4243
|
+
} catch {
|
|
4244
|
+
throw new Error(`invalid URL: ${rawUrl}`);
|
|
4245
|
+
}
|
|
4246
|
+
for (let hop = 0; hop <= opts.maxRedirects; hop += 1) {
|
|
4247
|
+
if (currentUrl.protocol !== "http:" && currentUrl.protocol !== "https:") {
|
|
4248
|
+
throw new Error(`unsupported scheme: ${currentUrl.protocol}`);
|
|
4249
|
+
}
|
|
4250
|
+
const pinnedIp = await assertHostAllowed(currentUrl.hostname);
|
|
4251
|
+
const controller = new AbortController();
|
|
4252
|
+
const timer = setTimeout(() => controller.abort(new Error("timeout")), opts.timeoutMs);
|
|
4253
|
+
if (opts.signal) {
|
|
4254
|
+
if (opts.signal.aborted) controller.abort(opts.signal.reason);
|
|
4255
|
+
else opts.signal.addEventListener("abort", () => controller.abort(opts.signal.reason), { once: true });
|
|
4256
|
+
}
|
|
4257
|
+
const usingMockedFetch = urlFetchDeps.fetch !== void 0;
|
|
4258
|
+
const dispatcher = usingMockedFetch ? void 0 : new import_undici.Agent({
|
|
4259
|
+
connect: {
|
|
4260
|
+
lookup: (_hostname, _options, cb) => {
|
|
4261
|
+
cb(null, pinnedIp, (0, import_node_net.isIPv6)(pinnedIp) ? 6 : 4);
|
|
4262
|
+
}
|
|
4263
|
+
}
|
|
4264
|
+
});
|
|
4265
|
+
try {
|
|
4266
|
+
let res;
|
|
4267
|
+
try {
|
|
4268
|
+
res = await activeFetch()(currentUrl, {
|
|
4269
|
+
method: "GET",
|
|
4270
|
+
redirect: "manual",
|
|
4271
|
+
headers: { "User-Agent": opts.userAgent, Accept: "*/*" },
|
|
4272
|
+
signal: controller.signal,
|
|
4273
|
+
// `dispatcher` is undici-specific; Node's built-in fetch (which
|
|
4274
|
+
// IS undici under the hood) accepts it via this option.
|
|
4275
|
+
...dispatcher ? { dispatcher } : {}
|
|
4276
|
+
});
|
|
4277
|
+
} catch (err) {
|
|
4278
|
+
clearTimeout(timer);
|
|
4279
|
+
throw new Error(`fetch failed: ${err.message}`);
|
|
4280
|
+
}
|
|
4281
|
+
if (res.status >= 300 && res.status < 400) {
|
|
4282
|
+
clearTimeout(timer);
|
|
4283
|
+
const loc = res.headers.get("location");
|
|
4284
|
+
if (!loc) throw new Error(`redirect ${res.status} with no Location header`);
|
|
4285
|
+
try {
|
|
4286
|
+
currentUrl = new URL(loc, currentUrl);
|
|
4287
|
+
} catch {
|
|
4288
|
+
throw new Error(`invalid redirect Location: ${loc}`);
|
|
4289
|
+
}
|
|
4290
|
+
try {
|
|
4291
|
+
await res.arrayBuffer();
|
|
4292
|
+
} catch {
|
|
4293
|
+
}
|
|
4294
|
+
if (hop === opts.maxRedirects) {
|
|
4295
|
+
throw new Error(`too many redirects (>${opts.maxRedirects})`);
|
|
4296
|
+
}
|
|
4297
|
+
continue;
|
|
4298
|
+
}
|
|
4299
|
+
if (!res.ok) {
|
|
4300
|
+
clearTimeout(timer);
|
|
4301
|
+
throw new Error(`HTTP ${res.status}`);
|
|
4302
|
+
}
|
|
4303
|
+
const contentLength = Number(res.headers.get("content-length"));
|
|
4304
|
+
if (Number.isFinite(contentLength) && contentLength > opts.maxBytes) {
|
|
4305
|
+
clearTimeout(timer);
|
|
4306
|
+
controller.abort(new Error("content-length exceeds limit"));
|
|
4307
|
+
throw new Error(`body too large: content-length=${contentLength} > ${opts.maxBytes}`);
|
|
4308
|
+
}
|
|
4309
|
+
const chunks = [];
|
|
4310
|
+
let total = 0;
|
|
4311
|
+
const reader = res.body?.getReader();
|
|
4312
|
+
try {
|
|
4313
|
+
if (reader) {
|
|
4314
|
+
for (; ; ) {
|
|
4315
|
+
const { done, value } = await reader.read();
|
|
4316
|
+
if (done) break;
|
|
4317
|
+
if (value) {
|
|
4318
|
+
total += value.byteLength;
|
|
4319
|
+
if (total > opts.maxBytes) {
|
|
4320
|
+
controller.abort(new Error("body exceeds limit"));
|
|
4321
|
+
try {
|
|
4322
|
+
await reader.cancel();
|
|
4323
|
+
} catch {
|
|
4324
|
+
}
|
|
4325
|
+
throw new Error(`body too large: read ${total} > ${opts.maxBytes}`);
|
|
4326
|
+
}
|
|
4327
|
+
chunks.push(value);
|
|
4328
|
+
}
|
|
4329
|
+
}
|
|
4330
|
+
} else {
|
|
4331
|
+
const ab = await res.arrayBuffer();
|
|
4332
|
+
if (ab.byteLength > opts.maxBytes) {
|
|
4333
|
+
throw new Error(`body too large: ${ab.byteLength} > ${opts.maxBytes}`);
|
|
4334
|
+
}
|
|
4335
|
+
chunks.push(new Uint8Array(ab));
|
|
4336
|
+
total = ab.byteLength;
|
|
4337
|
+
}
|
|
4338
|
+
} finally {
|
|
4339
|
+
clearTimeout(timer);
|
|
4340
|
+
}
|
|
4341
|
+
const body = Buffer.concat(chunks.map((c) => Buffer.from(c.buffer, c.byteOffset, c.byteLength)), total);
|
|
4342
|
+
return {
|
|
4343
|
+
body,
|
|
4344
|
+
finalUrl: currentUrl.toString(),
|
|
4345
|
+
mime: res.headers.get("content-type")
|
|
4346
|
+
};
|
|
4347
|
+
} finally {
|
|
4348
|
+
await dispatcher?.close().catch(() => void 0);
|
|
4349
|
+
}
|
|
4350
|
+
}
|
|
4351
|
+
throw new Error(`too many redirects (>${opts.maxRedirects})`);
|
|
4352
|
+
}
|
|
4104
4353
|
|
|
4105
4354
|
// src/daemon/asset/metadata-index.ts
|
|
4106
4355
|
var import_node_fs8 = require("fs");
|
|
@@ -4462,11 +4711,11 @@ function buildAssetCommand() {
|
|
|
4462
4711
|
}
|
|
4463
4712
|
printAssetDetail("Asset", assetId, body);
|
|
4464
4713
|
}, { code: "asset_get_failed" }));
|
|
4465
|
-
cmd.command("by-hash <sha256>").description("Fetch asset metadata by content hash").option("--workspace-id <id>", "Workspace id (required by the cloud API)").option("--json", "Output machine-readable JSON").action(runAction(async (
|
|
4714
|
+
cmd.command("by-hash <sha256>").description("Fetch asset metadata by content hash").option("--workspace-id <id>", "Workspace id (required by the cloud API)").option("--json", "Output machine-readable JSON").action(runAction(async (sha2563, opts) => {
|
|
4466
4715
|
const cloud = mkCloud();
|
|
4467
4716
|
const query = new URLSearchParams();
|
|
4468
4717
|
if (opts.workspaceId) query.set("wsId", opts.workspaceId);
|
|
4469
|
-
const path9 = `/api/im/assets/by-hash/${encodeURIComponent(
|
|
4718
|
+
const path9 = `/api/im/assets/by-hash/${encodeURIComponent(sha2563)}${query.toString() ? `?${query.toString()}` : ""}`;
|
|
4470
4719
|
const res = await cloud.request("GET", path9);
|
|
4471
4720
|
if (!res.ok) exitWithError(`asset by-hash failed (${res.status}): ${res.error?.message ?? "request failed"}`, { code: res.error?.code ?? "asset_by_hash_failed" });
|
|
4472
4721
|
const body = res.data;
|
|
@@ -4477,7 +4726,7 @@ function buildAssetCommand() {
|
|
|
4477
4726
|
printJson(body ?? null);
|
|
4478
4727
|
return;
|
|
4479
4728
|
}
|
|
4480
|
-
printAssetDetail("Asset hash",
|
|
4729
|
+
printAssetDetail("Asset hash", sha2563, body);
|
|
4481
4730
|
}, { code: "asset_by_hash_failed" }));
|
|
4482
4731
|
return cmd;
|
|
4483
4732
|
}
|
|
@@ -5619,6 +5868,8 @@ var SyncWorker = class extends import_node_events.EventEmitter {
|
|
|
5619
5868
|
// src/uri-resolver.ts
|
|
5620
5869
|
var URI_REGEX_WORKSPACE = /prismer:\/\/workspace\/([^/\s]+)\/(asset|file)\/([^\s)\]]+)/g;
|
|
5621
5870
|
var URI_REGEX_LEGACY = /prismer:\/\/(?!workspace\/)([^/\s]+)\/(asset|file)\/([^\s)\]]+)/g;
|
|
5871
|
+
var HTTP_URL_REGEX = /https?:\/\/[^\s<>"'`]+/g;
|
|
5872
|
+
var URL_TRAILING_PUNCT_RE = /[,.;:!?)\]}'"]+$/;
|
|
5622
5873
|
function parseUris(text) {
|
|
5623
5874
|
if (!text) return [];
|
|
5624
5875
|
const out = [];
|
|
@@ -5681,12 +5932,20 @@ var UriResolver = class {
|
|
|
5681
5932
|
return cached.localPath;
|
|
5682
5933
|
}
|
|
5683
5934
|
/**
|
|
5684
|
-
* Walk a string, replace every `prismer://(asset|file)/...`
|
|
5685
|
-
*
|
|
5935
|
+
* Walk a string, replace every `prismer://(asset|file)/...` and
|
|
5936
|
+
* `https://…` / `http://…` URL with `file://<localPath>`.
|
|
5937
|
+
*
|
|
5938
|
+
* Unrecognized URIs pass through unchanged. Returns rewritten text, the
|
|
5939
|
+
* list of pinned hashes, and one observation per http(s) URL the resolver
|
|
5940
|
+
* attempted (success + error both surface) so the dispatch can include
|
|
5941
|
+
* them in `reply.assetObservability`.
|
|
5942
|
+
*
|
|
5943
|
+
* `urlCache` lets the caller dedupe URL fetches across multiple rewrite
|
|
5944
|
+
* calls within a single dispatch (e.g. prompt + each context entry).
|
|
5945
|
+
* Pass the same Map instance to every rewrite() / rewriteAll() call.
|
|
5686
5946
|
*/
|
|
5687
5947
|
async rewrite(text, opts) {
|
|
5688
5948
|
const uris = parseUris(text);
|
|
5689
|
-
if (uris.length === 0) return { text, resolvedHashes: [] };
|
|
5690
5949
|
const replacements = /* @__PURE__ */ new Map();
|
|
5691
5950
|
const resolvedHashes = [];
|
|
5692
5951
|
for (const u of uris) {
|
|
@@ -5709,6 +5968,46 @@ var UriResolver = class {
|
|
|
5709
5968
|
console.warn(`[uri-resolver] failed to resolve ${u.raw}: ${err.message}`);
|
|
5710
5969
|
}
|
|
5711
5970
|
}
|
|
5971
|
+
if (opts?.fetchUrls !== false) {
|
|
5972
|
+
const urlCache = opts?.urlCache;
|
|
5973
|
+
const urls = extractHttpUrls(text);
|
|
5974
|
+
for (const original of urls) {
|
|
5975
|
+
try {
|
|
5976
|
+
let resolution = urlCache?.get(original);
|
|
5977
|
+
if (!resolution) {
|
|
5978
|
+
const { cached, finalUrl, durationMs } = await this.assetCache.getOrFetchUrl(original, {
|
|
5979
|
+
signal: opts?.signal
|
|
5980
|
+
});
|
|
5981
|
+
resolution = { hash: cached.contentHash, localPath: cached.localPath, sizeBytes: cached.sizeBytes, mime: cached.mime, finalUrl, durationMs };
|
|
5982
|
+
urlCache?.set(original, resolution);
|
|
5983
|
+
opts?.urlObservations?.push({
|
|
5984
|
+
contentHash: cached.contentHash,
|
|
5985
|
+
mime: cached.mime,
|
|
5986
|
+
sizeBytes: cached.sizeBytes,
|
|
5987
|
+
strategy: "fetched-https",
|
|
5988
|
+
originalUrl: original,
|
|
5989
|
+
finalUrl,
|
|
5990
|
+
durationMs
|
|
5991
|
+
});
|
|
5992
|
+
}
|
|
5993
|
+
replacements.set(original, `file://${resolution.localPath}`);
|
|
5994
|
+
resolvedHashes.push(resolution.hash);
|
|
5995
|
+
if (opts?.pin) this.assetCache.pin(resolution.hash);
|
|
5996
|
+
} catch (err) {
|
|
5997
|
+
const message = err.message;
|
|
5998
|
+
console.warn(`[uri-resolver] failed to fetch ${original}: ${message}`);
|
|
5999
|
+
opts?.urlObservations?.push({
|
|
6000
|
+
contentHash: "",
|
|
6001
|
+
mime: null,
|
|
6002
|
+
sizeBytes: null,
|
|
6003
|
+
strategy: "error",
|
|
6004
|
+
originalUrl: original,
|
|
6005
|
+
error: message
|
|
6006
|
+
});
|
|
6007
|
+
}
|
|
6008
|
+
}
|
|
6009
|
+
}
|
|
6010
|
+
if (replacements.size === 0) return { text, resolvedHashes };
|
|
5712
6011
|
let rewritten = text;
|
|
5713
6012
|
for (const [raw, sub] of replacements) {
|
|
5714
6013
|
rewritten = rewritten.split(raw).join(sub);
|
|
@@ -5746,6 +6045,27 @@ var UriResolver = class {
|
|
|
5746
6045
|
function dedupe(xs) {
|
|
5747
6046
|
return Array.from(new Set(xs));
|
|
5748
6047
|
}
|
|
6048
|
+
function extractHttpUrls(text) {
|
|
6049
|
+
if (!text) return [];
|
|
6050
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6051
|
+
const out = [];
|
|
6052
|
+
for (const m of text.matchAll(HTTP_URL_REGEX)) {
|
|
6053
|
+
let url = m[0];
|
|
6054
|
+
const punct = URL_TRAILING_PUNCT_RE.exec(url);
|
|
6055
|
+
if (punct) url = url.slice(0, -punct[0].length);
|
|
6056
|
+
if (!url) continue;
|
|
6057
|
+
try {
|
|
6058
|
+
const parsed = new URL(url);
|
|
6059
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") continue;
|
|
6060
|
+
} catch {
|
|
6061
|
+
continue;
|
|
6062
|
+
}
|
|
6063
|
+
if (seen.has(url)) continue;
|
|
6064
|
+
seen.add(url);
|
|
6065
|
+
out.push(url);
|
|
6066
|
+
}
|
|
6067
|
+
return out;
|
|
6068
|
+
}
|
|
5749
6069
|
|
|
5750
6070
|
// src/lib/logger.ts
|
|
5751
6071
|
var LEVEL_ORDER = {
|
|
@@ -5834,29 +6154,141 @@ async function syncInstalledSkillsForDispatch(profile, agentImUserId, cloud, sig
|
|
|
5834
6154
|
for (const entry of entries) {
|
|
5835
6155
|
const slug = sanitizeSlug(trimmedStringFrom(entry.skill?.slug) ?? trimmedStringFrom(entry.slug));
|
|
5836
6156
|
const skillId = trimmedStringFrom(entry.skill?.id);
|
|
5837
|
-
|
|
5838
|
-
if (!slug || !content) {
|
|
6157
|
+
if (!slug) {
|
|
5839
6158
|
skipped++;
|
|
5840
|
-
|
|
5841
|
-
|
|
5842
|
-
|
|
5843
|
-
|
|
6159
|
+
continue;
|
|
6160
|
+
}
|
|
6161
|
+
let files = null;
|
|
6162
|
+
const manifestRaw = entry.skill?.contentManifest;
|
|
6163
|
+
if (typeof manifestRaw === "string" && manifestRaw.trim()) {
|
|
6164
|
+
files = parseManifest(manifestRaw, slug);
|
|
6165
|
+
}
|
|
6166
|
+
if (!files) {
|
|
6167
|
+
const legacyContent = contentStringFrom(entry.skill?.content) ?? contentStringFrom(entry.content);
|
|
6168
|
+
if (legacyContent) {
|
|
6169
|
+
const buf = Buffer.from(legacyContent, "utf8");
|
|
6170
|
+
files = [
|
|
6171
|
+
{
|
|
6172
|
+
path: "SKILL.md",
|
|
6173
|
+
size: buf.byteLength,
|
|
6174
|
+
sha256: sha256Buffer(buf),
|
|
6175
|
+
inline: true,
|
|
6176
|
+
content: buf.toString("base64")
|
|
6177
|
+
}
|
|
6178
|
+
];
|
|
5844
6179
|
}
|
|
6180
|
+
}
|
|
6181
|
+
if (!files || files.length === 0) {
|
|
6182
|
+
skipped++;
|
|
6183
|
+
process.stderr.write(`[daemon] skill sync skipped ${slug}: no manifest or content
|
|
6184
|
+
`);
|
|
6185
|
+
await ackSkillSync(cloud, agentImUserId, { skillId, slug, error: "missing content" }, signal);
|
|
5845
6186
|
continue;
|
|
5846
6187
|
}
|
|
5847
|
-
const revision = sha2562(content);
|
|
5848
6188
|
const skillDir = (0, import_node_path12.join)(skillsRoot, slug);
|
|
5849
6189
|
await import_node_fs11.promises.mkdir(skillDir, { recursive: true });
|
|
5850
|
-
const
|
|
5851
|
-
|
|
5852
|
-
|
|
5853
|
-
|
|
5854
|
-
|
|
6190
|
+
const localFiles = await walkLocalDir(skillDir);
|
|
6191
|
+
let dirty = false;
|
|
6192
|
+
let perFileFailures = 0;
|
|
6193
|
+
for (const file of files) {
|
|
6194
|
+
if (!isSafeRelativePath(file.path)) {
|
|
6195
|
+
process.stderr.write(`[daemon] skill sync ${slug}: skipping suspicious path "${file.path}"
|
|
6196
|
+
`);
|
|
6197
|
+
perFileFailures++;
|
|
6198
|
+
continue;
|
|
6199
|
+
}
|
|
6200
|
+
const targetPath = (0, import_node_path12.join)(skillDir, file.path);
|
|
6201
|
+
const normalizedTarget = targetPath + (targetPath.endsWith(import_node_path12.sep) ? "" : "");
|
|
6202
|
+
if (normalizedTarget !== skillDir && !normalizedTarget.startsWith(skillDir + import_node_path12.sep)) {
|
|
6203
|
+
process.stderr.write(`[daemon] skill sync ${slug}: path escapes skillDir "${file.path}"
|
|
6204
|
+
`);
|
|
6205
|
+
perFileFailures++;
|
|
6206
|
+
continue;
|
|
6207
|
+
}
|
|
6208
|
+
const existingHash = localFiles.get(file.path);
|
|
6209
|
+
if (existingHash === file.sha256) {
|
|
6210
|
+
localFiles.delete(file.path);
|
|
6211
|
+
continue;
|
|
6212
|
+
}
|
|
6213
|
+
let bytes = null;
|
|
6214
|
+
try {
|
|
6215
|
+
if (file.inline !== false && typeof file.content === "string") {
|
|
6216
|
+
bytes = Buffer.from(file.content, "base64");
|
|
6217
|
+
} else if (typeof file.url === "string" && file.url) {
|
|
6218
|
+
bytes = await downloadUrl(file.url, signal);
|
|
6219
|
+
} else {
|
|
6220
|
+
process.stderr.write(
|
|
6221
|
+
`[daemon] skill sync ${slug}: file ${file.path} has neither inline content nor url
|
|
6222
|
+
`
|
|
6223
|
+
);
|
|
6224
|
+
perFileFailures++;
|
|
6225
|
+
continue;
|
|
6226
|
+
}
|
|
6227
|
+
} catch (err) {
|
|
6228
|
+
process.stderr.write(
|
|
6229
|
+
`[daemon] skill sync ${slug}: failed to fetch ${file.path}: ${err.message}
|
|
6230
|
+
`
|
|
6231
|
+
);
|
|
6232
|
+
perFileFailures++;
|
|
6233
|
+
continue;
|
|
6234
|
+
}
|
|
6235
|
+
const downloadedHash = sha256Buffer(bytes);
|
|
6236
|
+
if (downloadedHash !== file.sha256) {
|
|
6237
|
+
process.stderr.write(
|
|
6238
|
+
`[daemon] skill sync ${slug}: hash mismatch for ${file.path} (expected ${file.sha256}, got ${downloadedHash})
|
|
6239
|
+
`
|
|
6240
|
+
);
|
|
6241
|
+
perFileFailures++;
|
|
6242
|
+
continue;
|
|
6243
|
+
}
|
|
6244
|
+
await import_node_fs11.promises.mkdir((0, import_node_path12.dirname)(targetPath), { recursive: true });
|
|
6245
|
+
await import_node_fs11.promises.writeFile(targetPath, bytes);
|
|
6246
|
+
localFiles.delete(file.path);
|
|
6247
|
+
dirty = true;
|
|
6248
|
+
}
|
|
6249
|
+
for (const orphan of localFiles.keys()) {
|
|
6250
|
+
try {
|
|
6251
|
+
await import_node_fs11.promises.unlink((0, import_node_path12.join)(skillDir, orphan));
|
|
6252
|
+
dirty = true;
|
|
6253
|
+
} catch (err) {
|
|
6254
|
+
if (err.code !== "ENOENT") {
|
|
6255
|
+
process.stderr.write(
|
|
6256
|
+
`[daemon] skill sync ${slug}: failed to remove orphan ${orphan}: ${err.message}
|
|
6257
|
+
`
|
|
6258
|
+
);
|
|
6259
|
+
}
|
|
6260
|
+
}
|
|
6261
|
+
}
|
|
6262
|
+
if (perFileFailures > 0) {
|
|
6263
|
+
skipped++;
|
|
6264
|
+
await ackSkillSync(
|
|
6265
|
+
cloud,
|
|
6266
|
+
agentImUserId,
|
|
6267
|
+
{ skillId, slug, error: `${perFileFailures} file(s) failed` },
|
|
6268
|
+
signal
|
|
6269
|
+
);
|
|
5855
6270
|
continue;
|
|
5856
6271
|
}
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
|
|
6272
|
+
const localMerkle = computeMerkle(files);
|
|
6273
|
+
const declaredRevision = trimmedStringFrom(entry.skill?.contentManifestRevision);
|
|
6274
|
+
const revision = declaredRevision ?? localMerkle;
|
|
6275
|
+
if (declaredRevision && declaredRevision !== localMerkle) {
|
|
6276
|
+
process.stderr.write(
|
|
6277
|
+
`[daemon] skill sync ${slug}: declared revision ${declaredRevision} != computed ${localMerkle}; using computed
|
|
6278
|
+
`
|
|
6279
|
+
);
|
|
6280
|
+
}
|
|
6281
|
+
if (dirty) {
|
|
6282
|
+
synced++;
|
|
6283
|
+
} else {
|
|
6284
|
+
unchanged++;
|
|
6285
|
+
}
|
|
6286
|
+
await ackSkillSync(
|
|
6287
|
+
cloud,
|
|
6288
|
+
agentImUserId,
|
|
6289
|
+
{ skillId, slug, revision: declaredRevision === localMerkle ? declaredRevision : localMerkle },
|
|
6290
|
+
signal
|
|
6291
|
+
);
|
|
5860
6292
|
}
|
|
5861
6293
|
return { synced, skipped, unchanged };
|
|
5862
6294
|
}
|
|
@@ -5874,9 +6306,96 @@ function resolveSkillsRoot(profile) {
|
|
|
5874
6306
|
}
|
|
5875
6307
|
return null;
|
|
5876
6308
|
}
|
|
5877
|
-
function
|
|
6309
|
+
function sha256Buffer(value) {
|
|
5878
6310
|
return (0, import_node_crypto3.createHash)("sha256").update(value).digest("hex");
|
|
5879
6311
|
}
|
|
6312
|
+
function computeMerkle(files) {
|
|
6313
|
+
const sorted = [...files].sort((a, b) => a.path.localeCompare(b.path));
|
|
6314
|
+
const lines = sorted.map((f) => `${f.path}:${f.sha256}`).join("\n");
|
|
6315
|
+
return (0, import_node_crypto3.createHash)("sha256").update(lines).digest("hex");
|
|
6316
|
+
}
|
|
6317
|
+
function parseManifest(raw, slug) {
|
|
6318
|
+
try {
|
|
6319
|
+
const parsed = JSON.parse(raw);
|
|
6320
|
+
const arr = Array.isArray(parsed) ? parsed : parsed && typeof parsed === "object" && Array.isArray(parsed.files) ? parsed.files : null;
|
|
6321
|
+
if (!Array.isArray(arr)) {
|
|
6322
|
+
process.stderr.write(`[daemon] skill sync ${slug}: contentManifest is not an array
|
|
6323
|
+
`);
|
|
6324
|
+
return null;
|
|
6325
|
+
}
|
|
6326
|
+
const files = [];
|
|
6327
|
+
for (const item of arr) {
|
|
6328
|
+
if (!item || typeof item !== "object") continue;
|
|
6329
|
+
const rec = item;
|
|
6330
|
+
const path9 = typeof rec.path === "string" ? rec.path : null;
|
|
6331
|
+
const sha2563 = typeof rec.sha256 === "string" ? rec.sha256 : null;
|
|
6332
|
+
const size = typeof rec.size === "number" && Number.isFinite(rec.size) && rec.size >= 0 ? Math.floor(rec.size) : null;
|
|
6333
|
+
if (!path9 || !sha2563 || size === null) continue;
|
|
6334
|
+
const content = typeof rec.content === "string" ? rec.content : void 0;
|
|
6335
|
+
const url = typeof rec.url === "string" ? rec.url : void 0;
|
|
6336
|
+
const inline = typeof rec.inline === "boolean" ? rec.inline : content !== void 0 ? true : void 0;
|
|
6337
|
+
files.push({ path: path9, size, sha256: sha2563, inline, content, url });
|
|
6338
|
+
}
|
|
6339
|
+
return files;
|
|
6340
|
+
} catch (err) {
|
|
6341
|
+
process.stderr.write(
|
|
6342
|
+
`[daemon] skill sync ${slug}: invalid contentManifest JSON: ${err.message}
|
|
6343
|
+
`
|
|
6344
|
+
);
|
|
6345
|
+
return null;
|
|
6346
|
+
}
|
|
6347
|
+
}
|
|
6348
|
+
function isSafeRelativePath(p) {
|
|
6349
|
+
if (typeof p !== "string" || !p) return false;
|
|
6350
|
+
if (p.length > 512) return false;
|
|
6351
|
+
if (p.includes("\0")) return false;
|
|
6352
|
+
if (p.startsWith("/") || p.startsWith("\\")) return false;
|
|
6353
|
+
if (/^[A-Za-z]:[\\/]/.test(p)) return false;
|
|
6354
|
+
const segs = p.replace(/\\/g, "/").split("/");
|
|
6355
|
+
for (const seg of segs) {
|
|
6356
|
+
if (seg === "" || seg === "." || seg === "..") return false;
|
|
6357
|
+
}
|
|
6358
|
+
return true;
|
|
6359
|
+
}
|
|
6360
|
+
async function walkLocalDir(root) {
|
|
6361
|
+
const out = /* @__PURE__ */ new Map();
|
|
6362
|
+
async function walk2(dir) {
|
|
6363
|
+
let entries;
|
|
6364
|
+
try {
|
|
6365
|
+
entries = await import_node_fs11.promises.readdir(dir, { withFileTypes: true });
|
|
6366
|
+
} catch (err) {
|
|
6367
|
+
if (err.code === "ENOENT") return;
|
|
6368
|
+
throw err;
|
|
6369
|
+
}
|
|
6370
|
+
for (const ent of entries) {
|
|
6371
|
+
const full = (0, import_node_path12.join)(dir, ent.name);
|
|
6372
|
+
if (ent.isDirectory()) {
|
|
6373
|
+
await walk2(full);
|
|
6374
|
+
} else if (ent.isFile()) {
|
|
6375
|
+
try {
|
|
6376
|
+
const buf = await import_node_fs11.promises.readFile(full);
|
|
6377
|
+
const rel = (0, import_node_path12.relative)(root, full).split(import_node_path12.sep).join("/");
|
|
6378
|
+
out.set(rel, sha256Buffer(buf));
|
|
6379
|
+
} catch {
|
|
6380
|
+
}
|
|
6381
|
+
}
|
|
6382
|
+
}
|
|
6383
|
+
}
|
|
6384
|
+
await walk2(root);
|
|
6385
|
+
return out;
|
|
6386
|
+
}
|
|
6387
|
+
async function downloadUrl(url, signal) {
|
|
6388
|
+
const lower = url.toLowerCase();
|
|
6389
|
+
if (!lower.startsWith("http://") && !lower.startsWith("https://")) {
|
|
6390
|
+
throw new Error(`unsupported url scheme: ${url.slice(0, 32)}`);
|
|
6391
|
+
}
|
|
6392
|
+
const res = await fetch(url, { signal });
|
|
6393
|
+
if (!res.ok) {
|
|
6394
|
+
throw new Error(`fetch ${url} failed: HTTP ${res.status}`);
|
|
6395
|
+
}
|
|
6396
|
+
const ab = await res.arrayBuffer();
|
|
6397
|
+
return Buffer.from(ab);
|
|
6398
|
+
}
|
|
5880
6399
|
function normalizeInstalledSkills(data) {
|
|
5881
6400
|
if (Array.isArray(data)) return data.filter(isInstalledSkillEntry);
|
|
5882
6401
|
if (!data || typeof data !== "object" || Array.isArray(data)) return [];
|
|
@@ -5996,12 +6515,22 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
5996
6515
|
payload.prompt = hashRefResult.text;
|
|
5997
6516
|
}
|
|
5998
6517
|
}
|
|
5999
|
-
const
|
|
6518
|
+
const urlCache = /* @__PURE__ */ new Map();
|
|
6519
|
+
const urlObservations = [];
|
|
6520
|
+
const rewrittenPrompt = await deps.uriResolver.rewrite(payload.prompt, {
|
|
6521
|
+
pin: true,
|
|
6522
|
+
urlCache,
|
|
6523
|
+
urlObservations
|
|
6524
|
+
});
|
|
6000
6525
|
resolvedHashes.push(...rewrittenPrompt.resolvedHashes);
|
|
6001
6526
|
let rewrittenContext = [];
|
|
6002
6527
|
if (payload.context && payload.context.length > 0) {
|
|
6003
6528
|
const contents = payload.context.map((e) => e.content);
|
|
6004
|
-
const r = await deps.uriResolver.rewriteAll(contents, {
|
|
6529
|
+
const r = await deps.uriResolver.rewriteAll(contents, {
|
|
6530
|
+
pin: true,
|
|
6531
|
+
urlCache,
|
|
6532
|
+
urlObservations
|
|
6533
|
+
});
|
|
6005
6534
|
resolvedHashes.push(...r.resolvedHashes);
|
|
6006
6535
|
rewrittenContext = payload.context.map((e, i) => ({ ...e, content: r.texts[i] }));
|
|
6007
6536
|
}
|
|
@@ -6121,7 +6650,7 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
6121
6650
|
} : result.error,
|
|
6122
6651
|
...collectedAssetIds.length > 0 ? { assetIds: collectedAssetIds } : {},
|
|
6123
6652
|
metrics: result.metrics,
|
|
6124
|
-
...assetResolution.observability.length > 0 ? { assetObservability: assetResolution.observability } : {}
|
|
6653
|
+
...assetResolution.observability.length > 0 || urlObservations.length > 0 ? { assetObservability: [...assetResolution.observability, ...urlObservations] } : {}
|
|
6125
6654
|
};
|
|
6126
6655
|
await writeBridgeMetadata(payload.taskId, deps.cloud, result.metadata, deps.signal);
|
|
6127
6656
|
await writeObservabilityMetadata(
|
|
@@ -7288,10 +7817,10 @@ async function walkAndDigest(root, current) {
|
|
|
7288
7817
|
}
|
|
7289
7818
|
if (!st.isFile()) continue;
|
|
7290
7819
|
const buf = await import_node_fs13.promises.readFile(full);
|
|
7291
|
-
const
|
|
7820
|
+
const sha2563 = (0, import_node_crypto4.createHash)("sha256").update(buf).digest("hex");
|
|
7292
7821
|
out.push({
|
|
7293
7822
|
path: rel,
|
|
7294
|
-
sha256:
|
|
7823
|
+
sha256: sha2563,
|
|
7295
7824
|
sizeBytes: st.size,
|
|
7296
7825
|
mtime: Math.floor(st.mtimeMs)
|
|
7297
7826
|
});
|
|
@@ -7498,7 +8027,7 @@ var MemoryStore = class {
|
|
|
7498
8027
|
}
|
|
7499
8028
|
const db = this.requireDb();
|
|
7500
8029
|
const now = Date.now();
|
|
7501
|
-
const contentHash =
|
|
8030
|
+
const contentHash = sha2562(input.content);
|
|
7502
8031
|
const payload = sealPlaintext(input.content);
|
|
7503
8032
|
if (payload.kind !== "inline") {
|
|
7504
8033
|
throw new Error("MemoryStore.write: non-inline payload not yet supported in phase-0");
|
|
@@ -7692,7 +8221,7 @@ var MemoryStore = class {
|
|
|
7692
8221
|
};
|
|
7693
8222
|
}
|
|
7694
8223
|
};
|
|
7695
|
-
function
|
|
8224
|
+
function sha2562(s) {
|
|
7696
8225
|
return (0, import_node_crypto5.createHash)("sha256").update(s, "utf8").digest("hex");
|
|
7697
8226
|
}
|
|
7698
8227
|
|
|
@@ -14178,7 +14707,7 @@ async function readResponseError(res) {
|
|
|
14178
14707
|
|
|
14179
14708
|
// src/cli/index.ts
|
|
14180
14709
|
init_ui();
|
|
14181
|
-
var VERSION = "2.0.
|
|
14710
|
+
var VERSION = "2.0.1";
|
|
14182
14711
|
function buildProgram() {
|
|
14183
14712
|
const program = new import_commander20.Command("prismer").description("Prismer Cloud daemon CLI (TS-only).").version(VERSION);
|
|
14184
14713
|
program.addCommand(buildBannerCommand());
|