@instafy/cli 0.4.0 → 0.5.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 +2 -0
- package/dist/bind/bin/instafy-bind.mjs +56 -8
- package/dist/bind/src/server.mjs +4 -0
- package/dist/bind/src/starters/compare/app.js +2 -0
- package/dist/cli.js +948 -355
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1424,21 +1424,21 @@ var require_redaction = __commonJS({
|
|
|
1424
1424
|
return TEXT_MASK;
|
|
1425
1425
|
});
|
|
1426
1426
|
}
|
|
1427
|
-
text2 = text2.replace(digitCodePattern, (match, offset,
|
|
1428
|
-
const before =
|
|
1427
|
+
text2 = text2.replace(digitCodePattern, (match, offset, whole2) => {
|
|
1428
|
+
const before = whole2.slice(Math.max(0, offset - CODE_NEARBY_CHARACTERS), offset);
|
|
1429
1429
|
if (!codeWordPattern.test(before)) return match;
|
|
1430
1430
|
masked += 1;
|
|
1431
1431
|
matches += 1;
|
|
1432
1432
|
return TEXT_MASK;
|
|
1433
1433
|
});
|
|
1434
1434
|
opaqueCandidatePattern.lastIndex = 0;
|
|
1435
|
-
text2 = text2.replace(opaqueCandidatePattern, (match, offset,
|
|
1435
|
+
text2 = text2.replace(opaqueCandidatePattern, (match, offset, whole2) => {
|
|
1436
1436
|
const hexLike = uuidPattern.test(match) || longHexPattern.test(match);
|
|
1437
1437
|
if (!hexLike && !looksRandom3(match)) return match;
|
|
1438
1438
|
if (!strict && !credentialContext) {
|
|
1439
|
-
if (hexLike && identifierLabelPattern.test(
|
|
1439
|
+
if (hexLike && identifierLabelPattern.test(whole2.slice(Math.max(0, offset - 16), offset))) return match;
|
|
1440
1440
|
const window = hexLike ? HEX_NEARBY_CHARACTERS : NEARBY_CHARACTERS;
|
|
1441
|
-
const nearby =
|
|
1441
|
+
const nearby = whole2.slice(Math.max(0, offset - window), offset) + " " + whole2.slice(offset + match.length, offset + match.length + window);
|
|
1442
1442
|
if (!credentialProximityPattern.test(nearby)) return match;
|
|
1443
1443
|
}
|
|
1444
1444
|
count(match);
|
|
@@ -1461,8 +1461,8 @@ var require_redaction = __commonJS({
|
|
|
1461
1461
|
return value.length >= 16 && (looksRandom3(value, 16) || uuidPattern.test(value) || /^[0-9a-f]{16,}$/i.test(value));
|
|
1462
1462
|
}
|
|
1463
1463
|
function redactPairs(value, onMask) {
|
|
1464
|
-
return value.replace(/([^&#?=/]+)=([^&#]*)/g, (
|
|
1465
|
-
if (!sensitiveUrlValue(key, rawValue)) return
|
|
1464
|
+
return value.replace(/([^&#?=/]+)=([^&#]*)/g, (whole2, key, rawValue) => {
|
|
1465
|
+
if (!sensitiveUrlValue(key, rawValue)) return whole2;
|
|
1466
1466
|
onMask(decodeComponent(rawValue));
|
|
1467
1467
|
return `${key}=${URL_MASK}`;
|
|
1468
1468
|
});
|
|
@@ -1807,8 +1807,8 @@ var require_linkOnly = __commonJS({
|
|
|
1807
1807
|
if (url.username || url.password || url.port || url.hash) return { error: "has user info, a port or a fragment" };
|
|
1808
1808
|
if (url.hostname.includes(MARK) || !hostOnSite(hostOfUrl2(url), patterns)) return { error: "is not on the site's own hosts" };
|
|
1809
1809
|
const placeholderIn = (text2) => {
|
|
1810
|
-
const
|
|
1811
|
-
if (
|
|
1810
|
+
const whole2 = new RegExp(`^${MARK}(\\d+)x$`).exec(text2);
|
|
1811
|
+
if (whole2) return { name: names[Number(whole2[1])] };
|
|
1812
1812
|
return text2.includes(MARK) ? void 0 : null;
|
|
1813
1813
|
};
|
|
1814
1814
|
const segments = [];
|
|
@@ -1965,8 +1965,8 @@ var require_linkOnly = __commonJS({
|
|
|
1965
1965
|
for (const run of decoded(text2).split(HOST_BREAK)) {
|
|
1966
1966
|
if (names.size > MAX_EMBEDDED_NAMES) break;
|
|
1967
1967
|
if (!run) continue;
|
|
1968
|
-
const
|
|
1969
|
-
if (
|
|
1968
|
+
const whole2 = embeddedName(run);
|
|
1969
|
+
if (whole2) names.add(whole2);
|
|
1970
1970
|
for (const piece of run.split(NOT_NAME_CHARACTER)) {
|
|
1971
1971
|
const name = piece && piece !== run ? embeddedName(piece) : null;
|
|
1972
1972
|
if (name) names.add(name);
|
|
@@ -6795,7 +6795,9 @@ async function tailTunnelLogs(options) {
|
|
|
6795
6795
|
|
|
6796
6796
|
// src/preview.ts
|
|
6797
6797
|
import { spawn as spawn6 } from "node:child_process";
|
|
6798
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
6798
6799
|
import fs12 from "node:fs";
|
|
6800
|
+
import net5 from "node:net";
|
|
6799
6801
|
import os7 from "node:os";
|
|
6800
6802
|
import path13 from "node:path";
|
|
6801
6803
|
import kleur8 from "kleur";
|
|
@@ -7110,6 +7112,7 @@ var MAX_HEADER_BYTES = 128 * 1024;
|
|
|
7110
7112
|
|
|
7111
7113
|
// src/web-read.ts
|
|
7112
7114
|
var redaction = __toESM(require_redaction(), 1);
|
|
7115
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
7113
7116
|
import dns2 from "node:dns";
|
|
7114
7117
|
import http3 from "node:http";
|
|
7115
7118
|
import https from "node:https";
|
|
@@ -8110,12 +8113,109 @@ function loadWebFixtures(folder) {
|
|
|
8110
8113
|
};
|
|
8111
8114
|
}
|
|
8112
8115
|
|
|
8116
|
+
// src/web-budget.ts
|
|
8117
|
+
var ResearchBudgetError = class extends Error {
|
|
8118
|
+
};
|
|
8119
|
+
function invalidResponse() {
|
|
8120
|
+
throw new ResearchBudgetError("The controller returned an invalid research allowance. No further pages were loaded.");
|
|
8121
|
+
}
|
|
8122
|
+
function object(value) {
|
|
8123
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
8124
|
+
}
|
|
8125
|
+
function whole(value, min, max) {
|
|
8126
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= min && value <= max;
|
|
8127
|
+
}
|
|
8128
|
+
function readBudget(value) {
|
|
8129
|
+
if (!object(value) || value.scope !== "this task" || !whole(value.loadsPerTurn, 1, 200) || !whole(value.loadsPerHost, 1, value.loadsPerTurn) || !whole(value.loadsUsed, 0, value.loadsPerTurn) || value.loadsLeft !== value.loadsPerTurn - value.loadsUsed || value.hostSpacingSeconds !== 2 || !object(value.hosts) || Object.keys(value.hosts).length > 1200) invalidResponse();
|
|
8130
|
+
for (const entry of Object.values(value.hosts)) {
|
|
8131
|
+
if (!object(entry) || !whole(entry.loads, 0, value.loadsPerHost) || entry.lastAt !== void 0 && (typeof entry.lastAt !== "number" || !Number.isFinite(entry.lastAt))) invalidResponse();
|
|
8132
|
+
}
|
|
8133
|
+
return value;
|
|
8134
|
+
}
|
|
8135
|
+
function sharedResearchBudget(env, fetcher = globalThis.fetch) {
|
|
8136
|
+
const enabled = env.INSTAFY_RESEARCH_SHARED?.trim();
|
|
8137
|
+
const rawUrl = env.INSTAFY_RESEARCH_CONTROLLER_URL?.trim();
|
|
8138
|
+
const token = env.INSTAFY_RESEARCH_ACCESS_TOKEN?.trim();
|
|
8139
|
+
if (enabled === void 0 && rawUrl === void 0 && token === void 0) return null;
|
|
8140
|
+
if (enabled !== "1" || !rawUrl || !token || /[\r\n]/.test(token)) {
|
|
8141
|
+
throw new ResearchBudgetError("This task requires a shared research allowance, but its runtime binding is incomplete. No pages were loaded.");
|
|
8142
|
+
}
|
|
8143
|
+
let url;
|
|
8144
|
+
try {
|
|
8145
|
+
url = new URL(rawUrl);
|
|
8146
|
+
if (!/^https?:$/.test(url.protocol) || url.username || url.password || url.search || url.hash || url.pathname !== "/") throw new Error();
|
|
8147
|
+
} catch {
|
|
8148
|
+
throw new ResearchBudgetError("The task's research controller binding is invalid. No pages were loaded.");
|
|
8149
|
+
}
|
|
8150
|
+
return new SharedResearchBudget(new URL("/agent/research-budget", url.origin), token, fetcher);
|
|
8151
|
+
}
|
|
8152
|
+
var SharedResearchBudget = class {
|
|
8153
|
+
constructor(endpoint, token, fetcher) {
|
|
8154
|
+
this.endpoint = endpoint;
|
|
8155
|
+
this.token = token;
|
|
8156
|
+
this.fetcher = fetcher;
|
|
8157
|
+
this.current = null;
|
|
8158
|
+
}
|
|
8159
|
+
async request(body, remainingMs = 5e3) {
|
|
8160
|
+
const deadline = Date.now() + Math.max(1, Math.min(5e3, body?.remainingMs ?? remainingMs));
|
|
8161
|
+
for (let attempt2 = 0; attempt2 < 2; attempt2 += 1) {
|
|
8162
|
+
const abort = new AbortController();
|
|
8163
|
+
const timer = setTimeout(() => abort.abort(), Math.max(1, deadline - Date.now()));
|
|
8164
|
+
try {
|
|
8165
|
+
let response;
|
|
8166
|
+
try {
|
|
8167
|
+
response = await this.fetcher(this.endpoint.href, {
|
|
8168
|
+
method: body ? "POST" : "GET",
|
|
8169
|
+
headers: {
|
|
8170
|
+
Authorization: `Bearer ${this.token}`,
|
|
8171
|
+
Accept: "application/json",
|
|
8172
|
+
...body ? { "Content-Type": "application/json" } : {}
|
|
8173
|
+
},
|
|
8174
|
+
...body ? { body: JSON.stringify(body) } : {},
|
|
8175
|
+
redirect: "error",
|
|
8176
|
+
signal: abort.signal
|
|
8177
|
+
});
|
|
8178
|
+
} catch {
|
|
8179
|
+
if (attempt2 === 0 && !abort.signal.aborted && Date.now() < deadline) continue;
|
|
8180
|
+
throw new ResearchBudgetError("The task's shared research allowance is unavailable. No further pages were loaded; do not switch to a local allowance.");
|
|
8181
|
+
}
|
|
8182
|
+
if (!response.ok) {
|
|
8183
|
+
throw new ResearchBudgetError(`The controller refused the shared research allowance (HTTP ${response.status}). No further pages were loaded.`);
|
|
8184
|
+
}
|
|
8185
|
+
let value;
|
|
8186
|
+
try {
|
|
8187
|
+
value = await response.json();
|
|
8188
|
+
} catch {
|
|
8189
|
+
invalidResponse();
|
|
8190
|
+
}
|
|
8191
|
+
if (!object(value)) invalidResponse();
|
|
8192
|
+
return value;
|
|
8193
|
+
} finally {
|
|
8194
|
+
clearTimeout(timer);
|
|
8195
|
+
}
|
|
8196
|
+
}
|
|
8197
|
+
throw new ResearchBudgetError("The task's shared research allowance is unavailable.");
|
|
8198
|
+
}
|
|
8199
|
+
async status(remainingMs = 5e3) {
|
|
8200
|
+
const response = await this.request(void 0, remainingMs);
|
|
8201
|
+
this.current = readBudget(response.budget);
|
|
8202
|
+
return this.current;
|
|
8203
|
+
}
|
|
8204
|
+
async reserve(loadId, requestId, host, remainingMs) {
|
|
8205
|
+
const response = await this.request({ loadId, requestId, host, remainingMs: Math.max(0, Math.min(6e5, Math.trunc(remainingMs))) });
|
|
8206
|
+
const budget = readBudget(response.budget);
|
|
8207
|
+
if (typeof response.allowed !== "boolean" || !whole(response.waitMs, 0, 6e5) || (response.allowed ? response.code !== void 0 : !["turn_limit", "host_limit", "time_limit"].includes(String(response.code)))) invalidResponse();
|
|
8208
|
+
this.current = budget;
|
|
8209
|
+
return { allowed: response.allowed, ...response.code ? { code: response.code } : {}, waitMs: response.waitMs, budget };
|
|
8210
|
+
}
|
|
8211
|
+
};
|
|
8212
|
+
|
|
8113
8213
|
// src/web-read.ts
|
|
8114
8214
|
var ROBOTS_AGENTS = ["*", "instafy-agent", ...AI_AGENTS];
|
|
8115
8215
|
var LIMITS = Object.freeze({
|
|
8116
8216
|
urlsPerCall: 6,
|
|
8117
|
-
loadsPerTurn:
|
|
8118
|
-
loadsPerHost:
|
|
8217
|
+
loadsPerTurn: 100,
|
|
8218
|
+
loadsPerHost: 25,
|
|
8119
8219
|
hostSpacingMs: 2e3,
|
|
8120
8220
|
callMs: 6e4,
|
|
8121
8221
|
pageTimeoutMs: 2e4,
|
|
@@ -8377,9 +8477,31 @@ var TELL = {
|
|
|
8377
8477
|
http_error: "The site answered with an error. Use another source rather than retrying.",
|
|
8378
8478
|
unreachable: "The site did not answer. Use another source rather than retrying.",
|
|
8379
8479
|
timeout: "The site did not answer in time. Use another source rather than retrying.",
|
|
8480
|
+
budget_unavailable: "Research stopped because its shared allowance could not be checked. Keep the evidence already collected; further fresh reads require controller approval.",
|
|
8380
8481
|
needs_browser: "This page builds its text with JavaScript, so a plain read sees almost nothing. Use another source, or ask for a visible browser turn for this page.",
|
|
8381
8482
|
no_reuse: `This site's terms bar reusing its content. Summarize it in your own words, cite "seen <date> on <host>" and link back; never copy its reviews, photos or price lists, and keep its content out of anything published or shared by public link (links only there).`
|
|
8382
8483
|
};
|
|
8484
|
+
function wwwSibling(url) {
|
|
8485
|
+
const host = url.hostname.toLowerCase().replace(/\.+$/, "");
|
|
8486
|
+
if (!host || net3.isIP(host.replace(/^\[|\]$/g, ""))) return null;
|
|
8487
|
+
let other;
|
|
8488
|
+
if (host.startsWith("www.")) other = host.slice(4);
|
|
8489
|
+
else {
|
|
8490
|
+
const labels = host.split(".");
|
|
8491
|
+
const bare = labels.length === 2 || labels.length === 3 && labels[2].length === 2 && labels[1].length <= 3;
|
|
8492
|
+
if (!bare) return null;
|
|
8493
|
+
other = `www.${host}`;
|
|
8494
|
+
}
|
|
8495
|
+
if (!other.includes(".")) return null;
|
|
8496
|
+
const sibling = new URL(url.href);
|
|
8497
|
+
sibling.hostname = other;
|
|
8498
|
+
sibling.hash = "";
|
|
8499
|
+
return sibling;
|
|
8500
|
+
}
|
|
8501
|
+
function unanswered(code, page) {
|
|
8502
|
+
return (code === "unreachable" || code === "timeout") && !page.redirects?.length;
|
|
8503
|
+
}
|
|
8504
|
+
var unansweredKey = (url) => sha256(`unanswered:${url.hostname.toLowerCase().replace(/\.+$/, "")}`);
|
|
8383
8505
|
var HOST_WIDE_STOPS = /* @__PURE__ */ new Set(["rate_limited", "captcha"]);
|
|
8384
8506
|
function stripHash(url) {
|
|
8385
8507
|
const copy = new URL(url.href);
|
|
@@ -8478,15 +8600,16 @@ async function readOne(rawUrl, options, ctx) {
|
|
|
8478
8600
|
status: "skipped",
|
|
8479
8601
|
code,
|
|
8480
8602
|
message,
|
|
8481
|
-
tell: TELL[code]
|
|
8603
|
+
tell: ctx.sharedBudget?.current && code !== "time_limit" ? code === "turn_limit" ? `This task's shared allowance of ${ctx.sharedBudget.current.loadsPerTurn} page loads is used. Work with the evidence already collected, or ask the person.` : `This task has used its ${ctx.sharedBudget.current.loadsPerHost} page loads for this site. Use another source.` : TELL[code]
|
|
8482
8604
|
});
|
|
8605
|
+
let sibling = wwwSibling(requestedUrl);
|
|
8483
8606
|
const fail = (code, message, extra2 = {}) => ({
|
|
8484
8607
|
...page,
|
|
8485
8608
|
...extra2,
|
|
8486
8609
|
status: "failed",
|
|
8487
8610
|
code,
|
|
8488
8611
|
message,
|
|
8489
|
-
tell: TELL[code]
|
|
8612
|
+
tell: sibling && unanswered(code, extra2) ? `The site did not answer at ${requestedUrl.hostname}. Some sites answer only with or without "www.": read ${redaction.redactUrl(sibling.href).text} once instead, and if that fails too, use another source.` : TELL[code]
|
|
8490
8613
|
});
|
|
8491
8614
|
const firstHost = requestedUrl.hostname.toLowerCase();
|
|
8492
8615
|
const firstSite = siteKey(firstHost);
|
|
@@ -8494,8 +8617,10 @@ async function readOne(rawUrl, options, ctx) {
|
|
|
8494
8617
|
const earlier = await ctx.turn.update((state) => ({
|
|
8495
8618
|
stop: state.stops[firstKeys.exact] ?? state.stops[firstKeys.path] ?? state.hostStops[firstSite] ?? null,
|
|
8496
8619
|
loads: state.loads,
|
|
8497
|
-
hostLoads: state.hosts[firstSite]?.loads ?? 0
|
|
8620
|
+
hostLoads: state.hosts[firstSite]?.loads ?? 0,
|
|
8621
|
+
siblingUnanswered: sibling ? Boolean(state.unanswered?.[unansweredKey(sibling)]) : false
|
|
8498
8622
|
}));
|
|
8623
|
+
if (earlier.siblingUnanswered) sibling = null;
|
|
8499
8624
|
const remembered = earlier.stop ?? ctx.cache.get("stops", firstKeys.exact, LIMITS.stopTtlMs, ctx.now()) ?? ctx.cache.get("stops", firstKeys.path, LIMITS.stopTtlMs, ctx.now());
|
|
8500
8625
|
if (remembered) {
|
|
8501
8626
|
return { ...page, status: "stopped", code: remembered.code, message: remembered.message, tell: TELL[remembered.code], previouslyStopped: true };
|
|
@@ -8507,15 +8632,24 @@ async function readOne(rawUrl, options, ctx) {
|
|
|
8507
8632
|
return present(page, cached, true, options, ctx.guard);
|
|
8508
8633
|
}
|
|
8509
8634
|
}
|
|
8510
|
-
if (
|
|
8511
|
-
return skip("
|
|
8512
|
-
|
|
8513
|
-
|
|
8514
|
-
|
|
8635
|
+
if (ctx.sharedBudget) {
|
|
8636
|
+
if (ctx.deadline - ctx.now() < 1e3) return skip("time_limit", "this call reached its time limit before this URL");
|
|
8637
|
+
const budget = await ctx.sharedBudget.status(ctx.deadline - ctx.now());
|
|
8638
|
+
if (budget.loadsLeft === 0) return skip("turn_limit", `this task already loaded ${budget.loadsUsed} pages (limit ${budget.loadsPerTurn})`);
|
|
8639
|
+
const used = budget.hosts[firstSite]?.loads ?? 0;
|
|
8640
|
+
if (used >= budget.loadsPerHost) return skip("host_limit", `this task already read ${firstSite} ${used} times (limit ${budget.loadsPerHost})`);
|
|
8641
|
+
} else {
|
|
8642
|
+
if (earlier.loads >= LIMITS.loadsPerTurn) {
|
|
8643
|
+
return skip("turn_limit", `this turn already loaded ${earlier.loads} pages (limit ${LIMITS.loadsPerTurn})`);
|
|
8644
|
+
}
|
|
8645
|
+
if (earlier.hostLoads >= LIMITS.loadsPerHost) {
|
|
8646
|
+
return skip("host_limit", `this turn already read ${firstSite} ${earlier.hostLoads} times (limit ${LIMITS.loadsPerHost})`);
|
|
8647
|
+
}
|
|
8515
8648
|
}
|
|
8516
8649
|
if (ctx.deadline - ctx.now() < 1e3) return skip("time_limit", "this call reached its time limit before this URL");
|
|
8517
8650
|
const redirects = [];
|
|
8518
8651
|
const chainSites = /* @__PURE__ */ new Set();
|
|
8652
|
+
const loadId = randomUUID2();
|
|
8519
8653
|
let current = new URL(requested);
|
|
8520
8654
|
let response = null;
|
|
8521
8655
|
for (let hop = 0; ; hop += 1) {
|
|
@@ -8540,7 +8674,21 @@ async function readOne(rawUrl, options, ctx) {
|
|
|
8540
8674
|
const robots = await robotsVerdict(current, ctx);
|
|
8541
8675
|
if ("failed" in robots) return fail(robots.failed, via + robots.message, hopExtra);
|
|
8542
8676
|
if (robots.refused) return stop("robots", via + robots.refused, host2, hopExtra);
|
|
8543
|
-
if (
|
|
8677
|
+
if (ctx.sharedBudget) {
|
|
8678
|
+
const remainingMs = ctx.deadline - ctx.now();
|
|
8679
|
+
if (remainingMs < 1e3) return skip("time_limit", `${via}this call reached its time limit before reserving a load`, hopExtra);
|
|
8680
|
+
const reservation = await ctx.sharedBudget.reserve(loadId, randomUUID2(), site, remainingMs - 1e3);
|
|
8681
|
+
if (!reservation.allowed) {
|
|
8682
|
+
const code = reservation.code;
|
|
8683
|
+
const budget = reservation.budget;
|
|
8684
|
+
const message = code === "turn_limit" ? `this task already loaded ${budget.loadsUsed} pages (limit ${budget.loadsPerTurn})` : code === "host_limit" ? `this task already read ${site} ${budget.hosts[site]?.loads ?? 0} times (limit ${budget.loadsPerHost})` : `no time left in this call for the next paced read of ${host2}`;
|
|
8685
|
+
return skip(code, via + message, hopExtra);
|
|
8686
|
+
}
|
|
8687
|
+
if (reservation.waitMs + 1e3 > ctx.deadline - ctx.now()) {
|
|
8688
|
+
return skip("time_limit", `${via}this call reached its time limit while reserving the next paced read`, hopExtra);
|
|
8689
|
+
}
|
|
8690
|
+
if (reservation.waitMs > 0) await ctx.sleep(reservation.waitMs);
|
|
8691
|
+
} else if (!chainSites.has(site)) {
|
|
8544
8692
|
const now = ctx.now();
|
|
8545
8693
|
const reservation = await ctx.turn.update((state) => {
|
|
8546
8694
|
if (hop === 0 && state.loads >= LIMITS.loadsPerTurn) {
|
|
@@ -8751,6 +8899,7 @@ async function webRead(options, deps = {}) {
|
|
|
8751
8899
|
now,
|
|
8752
8900
|
sleep: deps.sleep ?? realSleep,
|
|
8753
8901
|
turn,
|
|
8902
|
+
sharedBudget: sharedResearchBudget(env, deps.budgetFetch),
|
|
8754
8903
|
cache: new Cache(cacheDir === null ? null : apart(cacheDir)),
|
|
8755
8904
|
guard,
|
|
8756
8905
|
viewer,
|
|
@@ -8758,23 +8907,38 @@ async function webRead(options, deps = {}) {
|
|
|
8758
8907
|
robotsMemo: /* @__PURE__ */ new Map()
|
|
8759
8908
|
};
|
|
8760
8909
|
const pages = [];
|
|
8761
|
-
|
|
8762
|
-
|
|
8910
|
+
let interruptedBudget = null;
|
|
8911
|
+
for (const [index, url] of urls.entries()) {
|
|
8912
|
+
let page;
|
|
8913
|
+
try {
|
|
8914
|
+
page = await readOne(url, { maxChars, links, fresh: Boolean(options.fresh), terms }, ctx);
|
|
8915
|
+
} catch (error) {
|
|
8916
|
+
if (!(error instanceof ResearchBudgetError) || !ctx.sharedBudget?.current || !pages.length) throw error;
|
|
8917
|
+
interruptedBudget = ctx.sharedBudget.current;
|
|
8918
|
+
pages.push(...urls.slice(index).map((remaining) => ({
|
|
8919
|
+
...basePage(remaining, new Date(now()).toISOString(), viewer),
|
|
8920
|
+
code: "budget_unavailable",
|
|
8921
|
+
message: error.message,
|
|
8922
|
+
tell: TELL.budget_unavailable
|
|
8923
|
+
})));
|
|
8924
|
+
break;
|
|
8925
|
+
}
|
|
8926
|
+
if (page.status === "failed" && unanswered(page.code, page)) {
|
|
8927
|
+
const key = unansweredKey(new URL(url));
|
|
8928
|
+
await turn.update((state) => {
|
|
8929
|
+
(state.unanswered ??= {})[key] = now();
|
|
8930
|
+
});
|
|
8931
|
+
}
|
|
8932
|
+
pages.push(page);
|
|
8933
|
+
}
|
|
8934
|
+
const budget = interruptedBudget ? lastKnownBudget(interruptedBudget, warnings) : ctx.sharedBudget ? await finalSharedBudget(ctx.sharedBudget, deadline - now(), warnings) : await localBudget(turn, scope.label);
|
|
8763
8935
|
return {
|
|
8764
8936
|
ok: true,
|
|
8765
8937
|
viewer,
|
|
8766
8938
|
userAgent: USER_AGENT,
|
|
8767
8939
|
notice: NOTICE,
|
|
8768
8940
|
pages,
|
|
8769
|
-
budget
|
|
8770
|
-
scope: scope.label,
|
|
8771
|
-
loadsUsed: state.loads,
|
|
8772
|
-
loadsLeft: Math.max(0, LIMITS.loadsPerTurn - state.loads),
|
|
8773
|
-
loadsPerTurn: LIMITS.loadsPerTurn,
|
|
8774
|
-
loadsPerHost: LIMITS.loadsPerHost,
|
|
8775
|
-
hostSpacingSeconds: LIMITS.hostSpacingMs / 1e3,
|
|
8776
|
-
hosts: state.hosts
|
|
8777
|
-
},
|
|
8941
|
+
budget,
|
|
8778
8942
|
guards: {
|
|
8779
8943
|
linkOnly: {
|
|
8780
8944
|
sites: guard.list.sites.length,
|
|
@@ -8788,6 +8952,63 @@ async function webRead(options, deps = {}) {
|
|
|
8788
8952
|
warnings
|
|
8789
8953
|
};
|
|
8790
8954
|
}
|
|
8955
|
+
function reportBudget(budget) {
|
|
8956
|
+
return { ...budget, hosts: Object.fromEntries(Object.entries(budget.hosts).map(([host, value]) => [host, value.loads])) };
|
|
8957
|
+
}
|
|
8958
|
+
function lastKnownBudget(budget, warnings) {
|
|
8959
|
+
warnings.push("The shared research allowance could not be refreshed. Budget counts are the last validated controller snapshot and may have changed; another fresh read still requires controller approval.");
|
|
8960
|
+
return { ...reportBudget(budget), lastKnown: true };
|
|
8961
|
+
}
|
|
8962
|
+
async function finalSharedBudget(shared, remainingMs, warnings) {
|
|
8963
|
+
try {
|
|
8964
|
+
if (remainingMs <= 0) throw new ResearchBudgetError("The call deadline elapsed before the shared research allowance could be refreshed.");
|
|
8965
|
+
return reportBudget(await shared.status(remainingMs));
|
|
8966
|
+
} catch (error) {
|
|
8967
|
+
if (!(error instanceof ResearchBudgetError) || !shared.current) throw error;
|
|
8968
|
+
return lastKnownBudget(shared.current, warnings);
|
|
8969
|
+
}
|
|
8970
|
+
}
|
|
8971
|
+
async function localBudget(turn, scope) {
|
|
8972
|
+
return turn.update((state) => ({
|
|
8973
|
+
scope,
|
|
8974
|
+
loadsUsed: state.loads,
|
|
8975
|
+
loadsLeft: Math.max(0, LIMITS.loadsPerTurn - state.loads),
|
|
8976
|
+
loadsPerTurn: LIMITS.loadsPerTurn,
|
|
8977
|
+
loadsPerHost: LIMITS.loadsPerHost,
|
|
8978
|
+
hostSpacingSeconds: LIMITS.hostSpacingMs / 1e3,
|
|
8979
|
+
hosts: Object.fromEntries(Object.entries(state.hosts).map(([host, value]) => [host, value.loads]))
|
|
8980
|
+
}));
|
|
8981
|
+
}
|
|
8982
|
+
async function webBudget(deps = {}) {
|
|
8983
|
+
const env = deps.env ?? process.env;
|
|
8984
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
8985
|
+
const shared = sharedResearchBudget(env, deps.budgetFetch);
|
|
8986
|
+
if (shared) return { budget: reportBudget(await shared.status()) };
|
|
8987
|
+
const fixtureFolder = webFixturesFolder(env, cwd);
|
|
8988
|
+
const fixtures = fixtureFolder ? loadWebFixtures(fixtureFolder) : null;
|
|
8989
|
+
const dirs = webDirs("web-read", env, cwd);
|
|
8990
|
+
const stateDir = deps.stateDir ?? dirs.stateDir;
|
|
8991
|
+
const scope = turnScope(env);
|
|
8992
|
+
const turn = new TurnStore(
|
|
8993
|
+
fixtures ? path11.join(stateDir, "fixtures", sha256(fixtures.folder).slice(0, 16)) : stateDir,
|
|
8994
|
+
scope,
|
|
8995
|
+
deps.now ?? Date.now
|
|
8996
|
+
);
|
|
8997
|
+
return { budget: await localBudget(turn, scope.label) };
|
|
8998
|
+
}
|
|
8999
|
+
async function runWebBudgetCli(opts, io = { out: console.log, err: console.error }, deps = {}) {
|
|
9000
|
+
try {
|
|
9001
|
+
const report = await webBudget(deps);
|
|
9002
|
+
const b = report.budget;
|
|
9003
|
+
io.out(opts.json ? JSON.stringify(report, null, 1) : `${b.loadsLeft} page loads left in ${b.scope} (${b.loadsUsed} of ${b.loadsPerTurn} used; ${b.loadsPerHost} per site, ${b.hostSpacingSeconds} s apart).`);
|
|
9004
|
+
return WEB_READ_EXIT_OK;
|
|
9005
|
+
} catch (error) {
|
|
9006
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
9007
|
+
if (opts.json) io.out(JSON.stringify({ ok: false, exitCode: WEB_READ_EXIT_INTERNAL, error: message }));
|
|
9008
|
+
else io.err(message);
|
|
9009
|
+
return WEB_READ_EXIT_INTERNAL;
|
|
9010
|
+
}
|
|
9011
|
+
}
|
|
8791
9012
|
function clamp(value, min, max) {
|
|
8792
9013
|
return Math.min(max, Math.max(min, Math.trunc(value)));
|
|
8793
9014
|
}
|
|
@@ -8815,13 +9036,13 @@ function formatWebRead(report) {
|
|
|
8815
9036
|
}
|
|
8816
9037
|
out.push("");
|
|
8817
9038
|
}
|
|
8818
|
-
out.push(`${report.budget.loadsUsed} of ${report.budget.loadsPerTurn} page loads used in ${report.budget.scope}.`);
|
|
9039
|
+
out.push(`${report.budget.lastKnown ? "Last known controller count: " : ""}${report.budget.loadsUsed} of ${report.budget.loadsPerTurn} page loads used in ${report.budget.scope}.`);
|
|
8819
9040
|
for (const warning of report.warnings) out.push(`warning: ${warning}`);
|
|
8820
9041
|
out.push(report.notice);
|
|
8821
9042
|
return out.join("\n");
|
|
8822
9043
|
}
|
|
8823
9044
|
async function runWebReadCli(urls, opts, io = { out: console.log, err: console.error }, deps = {}) {
|
|
8824
|
-
const
|
|
9045
|
+
const whole2 = (raw, name, min, max) => {
|
|
8825
9046
|
if (raw === void 0) return void 0;
|
|
8826
9047
|
const n = Number(raw);
|
|
8827
9048
|
if (!Number.isInteger(n) || n < min || n > max) throw new WebReadUsageError(`${name} must be a whole number from ${min} to ${max}.`);
|
|
@@ -8832,8 +9053,8 @@ async function runWebReadCli(urls, opts, io = { out: console.log, err: console.e
|
|
|
8832
9053
|
{
|
|
8833
9054
|
urls,
|
|
8834
9055
|
find: opts.find,
|
|
8835
|
-
maxChars:
|
|
8836
|
-
links:
|
|
9056
|
+
maxChars: whole2(opts.maxChars, "--max-chars", LIMITS.minChars, LIMITS.maxChars),
|
|
9057
|
+
links: whole2(opts.links, "--links", 0, LIMITS.maxLinks),
|
|
8837
9058
|
fresh: opts.fresh
|
|
8838
9059
|
},
|
|
8839
9060
|
deps
|
|
@@ -8843,7 +9064,7 @@ async function runWebReadCli(urls, opts, io = { out: console.log, err: console.e
|
|
|
8843
9064
|
} catch (error) {
|
|
8844
9065
|
const usage = error instanceof WebReadUsageError;
|
|
8845
9066
|
const code = usage ? WEB_READ_EXIT_USAGE : WEB_READ_EXIT_INTERNAL;
|
|
8846
|
-
const message = usage || error instanceof LinkOnlyListUnavailableError || error instanceof WebFixturesError ? error.message : `web read failed
|
|
9067
|
+
const message = usage || error instanceof LinkOnlyListUnavailableError || error instanceof WebFixturesError || error instanceof ResearchBudgetError ? error.message : `web read failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
8847
9068
|
if (opts.json) io.out(JSON.stringify({ ok: false, exitCode: code, error: message }));
|
|
8848
9069
|
else io.err(message);
|
|
8849
9070
|
return code;
|
|
@@ -8854,10 +9075,13 @@ function registerWebCommand(program2) {
|
|
|
8854
9075
|
"Read public web pages as untrusted data with their source (robots.txt, link-only sites and stop rules enforced), search for links to read (Octo's turns only), and give the person links to link-only sites (guided mode)"
|
|
8855
9076
|
);
|
|
8856
9077
|
web.command("read").description(
|
|
8857
|
-
`Read 1-6 public pages over plain HTTP and print their text, links and structured data with provenance (exit 2: nothing read, 64: bad options).
|
|
9078
|
+
`Read 1-6 public pages over plain HTTP and print their text, links and structured data with provenance (exit 2: nothing read, 64: bad options). Uses the task's shared allowance; standalone: ${LIMITS.loadsPerTurn} loads, ${LIMITS.loadsPerHost} per site, ${LIMITS.hostSpacingMs / 1e3} s apart. Stopped URLs (login, CAPTCHA, paywall, 401/403/429, download, secret screen, robots.txt, link-only, private address) are never read another way.`
|
|
8858
9079
|
).argument("<url...>", "http(s) URLs to read").option("--find <terms>", 'Return only the passages that mention these words ("quoted phrases" kept whole)').option("--max-chars <n>", `Most characters of text per page (default ${LIMITS.defaultChars}, at most ${LIMITS.maxChars})`).option("--links <n>", `Most links per page (default ${LIMITS.defaultLinks}, at most ${LIMITS.maxLinks})`).option("--fresh", "Read again even if this workspace's cache has the page (never re-reads a stopped URL)").option("--json", "Output JSON").action(async (urls, opts) => {
|
|
8859
9080
|
process.exitCode = await runWebReadCli(urls, opts);
|
|
8860
9081
|
});
|
|
9082
|
+
web.command("budget").description("Show this task's remaining shared page allowance without spending it (or the standalone session allowance)").option("--json", "Output JSON").action(async (opts) => {
|
|
9083
|
+
process.exitCode = await runWebBudgetCli(opts);
|
|
9084
|
+
});
|
|
8861
9085
|
return web;
|
|
8862
9086
|
}
|
|
8863
9087
|
|
|
@@ -9281,6 +9505,18 @@ async function readPageDescription(frame) {
|
|
|
9281
9505
|
const raw = await frame.evaluate(() => document.querySelector('meta[name="description" i]')?.getAttribute("content") ?? null).catch(() => null);
|
|
9282
9506
|
return cleanDescription(raw);
|
|
9283
9507
|
}
|
|
9508
|
+
var READY_MARK_ATTRIBUTE = "data-instafy-ready";
|
|
9509
|
+
var READY_MARK_WAIT_MS = 3e4;
|
|
9510
|
+
async function waitForReadyMark(frame, timeoutMs) {
|
|
9511
|
+
const mark = await frame.evaluate((name) => document.documentElement?.getAttribute(name) ?? null, READY_MARK_ATTRIBUTE).catch(() => null);
|
|
9512
|
+
if (mark === null) return "none";
|
|
9513
|
+
if (mark === "true") return "ready";
|
|
9514
|
+
if (timeoutMs <= 0) return "late";
|
|
9515
|
+
return frame.waitForFunction((name) => document.documentElement?.getAttribute(name) === "true", READY_MARK_ATTRIBUTE, {
|
|
9516
|
+
timeout: timeoutMs,
|
|
9517
|
+
polling: 100
|
|
9518
|
+
}).then(() => "ready").catch(() => "late");
|
|
9519
|
+
}
|
|
9284
9520
|
async function pageThumbnail(page, context) {
|
|
9285
9521
|
const cdp = await context.newCDPSession(page);
|
|
9286
9522
|
try {
|
|
@@ -9486,10 +9722,19 @@ async function checkViewport(browser, target, vp, siteOrigin, outDir, timeoutMs,
|
|
|
9486
9722
|
(ours(u) ? errors : warnings).push(`HTTP ${s}: ${shortUrl(u)}`);
|
|
9487
9723
|
});
|
|
9488
9724
|
await page.goto(target.pageUrl, { waitUntil: "load", timeout: timeoutMs });
|
|
9489
|
-
|
|
9490
|
-
|
|
9491
|
-
|
|
9492
|
-
|
|
9725
|
+
const siteFrame = () => target.framed ? page.frames().find((f) => f !== page.mainFrame() && isSiteUrl(String(f.url()), siteOrigin)) : page.mainFrame();
|
|
9726
|
+
const readiness = await waitForReadyMark(
|
|
9727
|
+
siteFrame() ?? page.mainFrame(),
|
|
9728
|
+
Math.max(0, Math.min(READY_MARK_WAIT_MS, timeoutMs) - (Date.now() - t0))
|
|
9729
|
+
);
|
|
9730
|
+
if (readiness === "none") {
|
|
9731
|
+
await page.waitForLoadState("networkidle", { timeout: Math.min(8e3, timeoutMs) }).catch(() => {
|
|
9732
|
+
warnings.push("the network was still busy after 8 s (screenshot taken anyway)");
|
|
9733
|
+
});
|
|
9734
|
+
} else if (readiness === "late") {
|
|
9735
|
+
warnings.push(`the page still said it was drawing (${READY_MARK_ATTRIBUTE}) after ${Math.round((Date.now() - t0) / 1e3)} s; screenshot taken anyway`);
|
|
9736
|
+
}
|
|
9737
|
+
const frame = siteFrame();
|
|
9493
9738
|
let framed = "ok";
|
|
9494
9739
|
let snap = { text: 0, media: 0, elements: 0, title: "" };
|
|
9495
9740
|
if (refused) framed = "refused";
|
|
@@ -9524,7 +9769,7 @@ async function checkViewport(browser, target, vp, siteOrigin, outDir, timeoutMs,
|
|
|
9524
9769
|
let thumbnail;
|
|
9525
9770
|
if (takeThumbnail) {
|
|
9526
9771
|
const [firstUnstopped] = unstopped;
|
|
9527
|
-
thumbnail = firstUnstopped ? { skipped: `a redirect led to ${firstUnstopped[0].slice(0, 200)} (${firstUnstopped[1]}), which a picture never shows` } : blank.blank ? { skipped: "the page looked blank" } : await pageThumbnail(page, context).then(async (shot) => "image" in shot ? { image: shot.image, description: await readPageDescription(page.mainFrame()) } : shot).catch((error) => ({ skipped: `the picture could not be taken: ${firstLine(error)}` }));
|
|
9772
|
+
thumbnail = firstUnstopped ? { skipped: `a redirect led to ${firstUnstopped[0].slice(0, 200)} (${firstUnstopped[1]}), which a picture never shows` } : blank.blank ? { skipped: "the page looked blank" } : readiness === "late" ? { skipped: "the page never said it was ready, so the card keeps its picture" } : await pageThumbnail(page, context).then(async (shot) => "image" in shot ? { image: shot.image, description: await readPageDescription(page.mainFrame()) } : shot).catch((error) => ({ skipped: `the picture could not be taken: ${firstLine(error)}` }));
|
|
9528
9773
|
}
|
|
9529
9774
|
return {
|
|
9530
9775
|
name: vp.name,
|
|
@@ -9734,6 +9979,8 @@ function thumbnailBudgetMs(env = process.env) {
|
|
|
9734
9979
|
}
|
|
9735
9980
|
var NETWORK_IDLE_MS = 6e3;
|
|
9736
9981
|
var SETTLE_MS = 1e3;
|
|
9982
|
+
var READY_SETTLE_MS = 300;
|
|
9983
|
+
var SHOT_RESERVE_MS = 3e3;
|
|
9737
9984
|
var MAX_THUMBNAIL_FILE_BYTES = 512 * 1024;
|
|
9738
9985
|
var MAX_HTML_BYTES = 512 * 1024;
|
|
9739
9986
|
var MIN_SIDE = 64;
|
|
@@ -9890,7 +10137,8 @@ function errorLine(error) {
|
|
|
9890
10137
|
async function captureSiteThumbnail(options) {
|
|
9891
10138
|
const env = options.env ?? process.env;
|
|
9892
10139
|
const budget = options.budgetMs ?? thumbnailBudgetMs(env);
|
|
9893
|
-
const
|
|
10140
|
+
const began = Date.now();
|
|
10141
|
+
let deadline = began + budget;
|
|
9894
10142
|
const host = options.host === "::1" ? "[::1]" : "127.0.0.1";
|
|
9895
10143
|
const siteOrigin = `http://${host}:${options.port}`;
|
|
9896
10144
|
const left = () => Math.max(0, deadline - Date.now());
|
|
@@ -9952,9 +10200,18 @@ async function captureSiteThumbnail(options) {
|
|
|
9952
10200
|
if (response && response.status() >= 400) {
|
|
9953
10201
|
return { ok: false, status: "skipped", reason: `the page answered HTTP ${response.status()}` };
|
|
9954
10202
|
}
|
|
9955
|
-
|
|
9956
|
-
|
|
9957
|
-
|
|
10203
|
+
const mark = await waitForReadyMark(page.mainFrame(), 0);
|
|
10204
|
+
if (mark === "none") {
|
|
10205
|
+
await page.waitForLoadState("networkidle", { timeout: Math.max(1, Math.min(NETWORK_IDLE_MS, left())) }).catch(() => {
|
|
10206
|
+
});
|
|
10207
|
+
await page.waitForTimeout(Math.min(SETTLE_MS, left()));
|
|
10208
|
+
} else {
|
|
10209
|
+
deadline = Math.max(deadline, began + (options.readyWaitMs ?? READY_MARK_WAIT_MS));
|
|
10210
|
+
if (mark === "late" && await waitForReadyMark(page.mainFrame(), Math.max(0, left() - SHOT_RESERVE_MS)) !== "ready") {
|
|
10211
|
+
return { ok: false, status: "skipped", reason: `the page still said it was drawing after ${Math.round((Date.now() - began) / 1e3)} s` };
|
|
10212
|
+
}
|
|
10213
|
+
await page.waitForTimeout(Math.min(READY_SETTLE_MS, left()));
|
|
10214
|
+
}
|
|
9958
10215
|
const description = await readPageDescription(page.mainFrame());
|
|
9959
10216
|
const shot = await pageThumbnail(page, context);
|
|
9960
10217
|
if ("skipped" in shot) return { ok: false, status: "skipped", reason: shot.skipped };
|
|
@@ -9969,7 +10226,16 @@ async function captureSiteThumbnail(options) {
|
|
|
9969
10226
|
};
|
|
9970
10227
|
})();
|
|
9971
10228
|
const outOfTime = new Promise((resolve) => {
|
|
9972
|
-
|
|
10229
|
+
const arm = () => {
|
|
10230
|
+
timer = setTimeout(() => {
|
|
10231
|
+
if (Date.now() < deadline) {
|
|
10232
|
+
arm();
|
|
10233
|
+
return;
|
|
10234
|
+
}
|
|
10235
|
+
resolve({ ok: false, status: "failed", reason: `the picture took longer than ${Math.round((deadline - began) / 1e3)} s` });
|
|
10236
|
+
}, Math.max(0, deadline - Date.now()));
|
|
10237
|
+
};
|
|
10238
|
+
arm();
|
|
9973
10239
|
});
|
|
9974
10240
|
try {
|
|
9975
10241
|
return await Promise.race([work.catch((error) => ({ ok: false, status: "failed", reason: errorLine(error) })), outOfTime]);
|
|
@@ -9993,6 +10259,12 @@ var MAX_PATH_LENGTH2 = 512;
|
|
|
9993
10259
|
var THUMBNAIL_UPLOAD_TIMEOUT_MS = 15e3;
|
|
9994
10260
|
var STATE_LOCK_WAIT_MS = 12e4;
|
|
9995
10261
|
var STATE_LOCK_STALE_MS = 10 * 6e4;
|
|
10262
|
+
var PORT_PLACEHOLDER = "{port}";
|
|
10263
|
+
var PORT_TAKEN_HINT = "Run it with --start and without --port, so this site gets a free port of its own, or pass --replace to show this site on that port instead.";
|
|
10264
|
+
var PICKED_PORT_TAKEN_HINT = "Pass --replace if this site replaces that one (this same site under a new title, for example); otherwise give this site a title of its own.";
|
|
10265
|
+
var AUTO_PORT_FIRST = 2e4;
|
|
10266
|
+
var AUTO_PORT_COUNT = 1e4;
|
|
10267
|
+
var AUTO_PORT_TRIES = 64;
|
|
9996
10268
|
function instafyDir() {
|
|
9997
10269
|
return path13.join(os7.homedir(), ".instafy");
|
|
9998
10270
|
}
|
|
@@ -10216,6 +10488,12 @@ async function controllerRequest(params) {
|
|
|
10216
10488
|
PREVIEW_EXIT_PORT_NOT_ALLOWED
|
|
10217
10489
|
);
|
|
10218
10490
|
}
|
|
10491
|
+
if (response.status === 409 && code === "port_in_use") {
|
|
10492
|
+
throw new PreviewCommandError(
|
|
10493
|
+
`${extractControllerErrorMessage(text2) ?? "Another site's preview holds this port."} ${params.portTakenHint ?? PORT_TAKEN_HINT}`,
|
|
10494
|
+
PREVIEW_EXIT_PORT_NOT_ALLOWED
|
|
10495
|
+
);
|
|
10496
|
+
}
|
|
10219
10497
|
const message = extractControllerErrorMessage(text2) ?? response.statusText;
|
|
10220
10498
|
throw new Error(`Preview request failed (${response.status}): ${message}`);
|
|
10221
10499
|
}
|
|
@@ -10424,12 +10702,12 @@ function listeningSocketInodes(procRoot, port) {
|
|
|
10424
10702
|
continue;
|
|
10425
10703
|
}
|
|
10426
10704
|
for (const line of text2.split("\n").slice(1)) {
|
|
10427
|
-
const
|
|
10428
|
-
if (
|
|
10429
|
-
const local =
|
|
10705
|
+
const fields2 = line.trim().split(/\s+/);
|
|
10706
|
+
if (fields2.length < 10) continue;
|
|
10707
|
+
const local = fields2[1] ?? "";
|
|
10430
10708
|
const portHex = local.slice(local.lastIndexOf(":") + 1);
|
|
10431
|
-
if (
|
|
10432
|
-
inodes.add(
|
|
10709
|
+
if (fields2[3] === "0A" && Number.parseInt(portHex, 16) === port && fields2[9] !== "0") {
|
|
10710
|
+
inodes.add(fields2[9]);
|
|
10433
10711
|
}
|
|
10434
10712
|
}
|
|
10435
10713
|
}
|
|
@@ -10605,8 +10883,159 @@ async function startPreviewSite(params) {
|
|
|
10605
10883
|
await new Promise((resolve) => setTimeout(resolve, START_POLL_MS));
|
|
10606
10884
|
}
|
|
10607
10885
|
}
|
|
10886
|
+
function withPort(argv, port) {
|
|
10887
|
+
return argv.map((arg) => arg.split(PORT_PLACEHOLDER).join(String(port)));
|
|
10888
|
+
}
|
|
10889
|
+
function sameArgv(a, b) {
|
|
10890
|
+
return a.length === b.length && a.every((arg, index) => arg === b[index]);
|
|
10891
|
+
}
|
|
10892
|
+
function sameStart(a, b) {
|
|
10893
|
+
return Boolean(a && Array.isArray(a.argv) && a.cwd === b.cwd && sameArgv(a.argv, b.argv));
|
|
10894
|
+
}
|
|
10895
|
+
function preferredSitePort(start) {
|
|
10896
|
+
const digest2 = createHash2("sha256").update(`${start.cwd}\0${start.argv.join("\0")}`).digest();
|
|
10897
|
+
return AUTO_PORT_FIRST + digest2.readUInt32BE(0) % AUTO_PORT_COUNT;
|
|
10898
|
+
}
|
|
10899
|
+
async function portIsFree(port) {
|
|
10900
|
+
for (const host of ["127.0.0.1", "0.0.0.0", "::1", "::"]) {
|
|
10901
|
+
const result = await new Promise((resolve) => {
|
|
10902
|
+
const server = net5.createServer();
|
|
10903
|
+
server.once(
|
|
10904
|
+
"error",
|
|
10905
|
+
(error) => resolve(error.code === "EADDRNOTAVAIL" || error.code === "EAFNOSUPPORT" ? "absent" : "taken")
|
|
10906
|
+
);
|
|
10907
|
+
server.listen({ port, host, ...host === "::" ? { ipv6Only: false } : {} }, () => server.close(() => resolve("free")));
|
|
10908
|
+
});
|
|
10909
|
+
if (result === "taken") return false;
|
|
10910
|
+
}
|
|
10911
|
+
return true;
|
|
10912
|
+
}
|
|
10913
|
+
function startedSiteRuns(entry) {
|
|
10914
|
+
return Boolean(entry.startedPid && entry.startedPid > 0 && entry.startedCommand) && readProcessCommandLine(entry.startedPid) === entry.startedCommand;
|
|
10915
|
+
}
|
|
10916
|
+
async function siteOnPort(params) {
|
|
10917
|
+
const { port, start, root, projectId: projectId2, previews, procRoot } = params;
|
|
10918
|
+
if (await portIsFree(port)) return "none";
|
|
10919
|
+
if (previews.some(
|
|
10920
|
+
(entry) => entry.projectId === projectId2 && entry.localPort === port && sameStart(entry.start, start) && startedSiteRuns(entry)
|
|
10921
|
+
)) {
|
|
10922
|
+
return "same";
|
|
10923
|
+
}
|
|
10924
|
+
const listening = detectListeningProcess(port, procRoot);
|
|
10925
|
+
if (listening && workspaceRelativeDir(root, listening.cwd) === start.cwd && sameArgv(listening.argv, withPort(start.argv, port))) {
|
|
10926
|
+
return "same";
|
|
10927
|
+
}
|
|
10928
|
+
return "other";
|
|
10929
|
+
}
|
|
10930
|
+
function portTakenError(port, previews, projectId2) {
|
|
10931
|
+
const holder = previews.find((entry) => entry.projectId === projectId2 && entry.localPort === port && entry.title);
|
|
10932
|
+
const what = holder ? `Port ${port} already shows '${holder.title}'.` : `Port ${port} is in use by another server.`;
|
|
10933
|
+
return new PreviewCommandError(`${what} Leave out --port: --start picks a free port for this site.`, PREVIEW_EXIT_PORT_NOT_ALLOWED);
|
|
10934
|
+
}
|
|
10935
|
+
async function startOrFindSite(params) {
|
|
10936
|
+
const { explicitPort, start, root, projectId: projectId2, title, sitePath, procRoot, log } = params;
|
|
10937
|
+
const { previews } = readStateFile2();
|
|
10938
|
+
const onPort = (port) => siteOnPort({ port, start, root, projectId: projectId2, previews, procRoot });
|
|
10939
|
+
const launch = async (port) => {
|
|
10940
|
+
const serve = { argv: withPort(start.argv, port), cwd: start.cwd };
|
|
10941
|
+
const started = await startPreviewSite({ port, sitePath, serve, root, timeoutMs: params.timeoutMs });
|
|
10942
|
+
log(`Started the site on port ${port} (pid ${started.pid}); log: ${displayPath(started.logFile)}`);
|
|
10943
|
+
return { port, start, started: { ...started, command: readProcessCommandLine(started.pid) } };
|
|
10944
|
+
};
|
|
10945
|
+
if (explicitPort !== null) {
|
|
10946
|
+
const found = await onPort(explicitPort);
|
|
10947
|
+
if (found === "same") return { port: explicitPort, start, started: null };
|
|
10948
|
+
if (found === "other") throw portTakenError(explicitPort, previews, projectId2);
|
|
10949
|
+
return launch(explicitPort);
|
|
10950
|
+
}
|
|
10951
|
+
const tried = /* @__PURE__ */ new Set();
|
|
10952
|
+
const known = previews.find((entry) => entry.projectId === projectId2 && sameStart(entry.start, start));
|
|
10953
|
+
if (known) {
|
|
10954
|
+
tried.add(known.localPort);
|
|
10955
|
+
const found = await onPort(known.localPort);
|
|
10956
|
+
if (found === "same") return { port: known.localPort, start, started: null };
|
|
10957
|
+
if (found === "none") return launch(known.localPort);
|
|
10958
|
+
}
|
|
10959
|
+
const reserved = reservedPreviewPorts();
|
|
10960
|
+
const held = new Set(
|
|
10961
|
+
previews.filter((entry) => entry.projectId !== projectId2 || !sameStart(entry.start, start)).map((entry) => entry.localPort)
|
|
10962
|
+
);
|
|
10963
|
+
for (const [port, holder] of await params.openPorts()) {
|
|
10964
|
+
if (holder !== title) held.add(port);
|
|
10965
|
+
}
|
|
10966
|
+
const first = preferredSitePort(start);
|
|
10967
|
+
for (let index = 0; index < AUTO_PORT_TRIES; index += 1) {
|
|
10968
|
+
const port = AUTO_PORT_FIRST + (first - AUTO_PORT_FIRST + index) % AUTO_PORT_COUNT;
|
|
10969
|
+
if (tried.has(port) || reserved.has(port) || held.has(port)) continue;
|
|
10970
|
+
tried.add(port);
|
|
10971
|
+
const found = await onPort(port);
|
|
10972
|
+
if (found === "same") return { port, start, started: null };
|
|
10973
|
+
if (found === "none") return launch(port);
|
|
10974
|
+
}
|
|
10975
|
+
throw new PreviewCommandError(
|
|
10976
|
+
`No free port was found for the site: ${tried.size} ports from ${first} are taken. Stop a site this space no longer needs (instafy preview close <previewId>), then retry.`,
|
|
10977
|
+
PREVIEW_EXIT_NOT_LISTENING
|
|
10978
|
+
);
|
|
10979
|
+
}
|
|
10980
|
+
async function stopSite(pid, command) {
|
|
10981
|
+
if (!pid || pid <= 0 || !command) return false;
|
|
10982
|
+
const ours = () => readProcessCommandLine(pid) === command;
|
|
10983
|
+
if (!ours()) return false;
|
|
10984
|
+
const signal = (name) => {
|
|
10985
|
+
try {
|
|
10986
|
+
process.kill(-pid, name);
|
|
10987
|
+
} catch {
|
|
10988
|
+
try {
|
|
10989
|
+
process.kill(pid, name);
|
|
10990
|
+
} catch {
|
|
10991
|
+
}
|
|
10992
|
+
}
|
|
10993
|
+
};
|
|
10994
|
+
signal("SIGTERM");
|
|
10995
|
+
if (!await waitForProcessExit(pid, 4e3) && ours()) signal("SIGKILL");
|
|
10996
|
+
return true;
|
|
10997
|
+
}
|
|
10998
|
+
function siteRecordFor(site, previews, port, projectId2) {
|
|
10999
|
+
if (site?.started) {
|
|
11000
|
+
return { start: site.start, startedPid: site.started.pid, ...site.started.command ? { startedCommand: site.started.command } : {} };
|
|
11001
|
+
}
|
|
11002
|
+
const earlier = previews.find(
|
|
11003
|
+
(entry) => entry.projectId === projectId2 && entry.localPort === port && (!site || sameStart(entry.start, site.start)) && startedSiteRuns(entry)
|
|
11004
|
+
);
|
|
11005
|
+
if (earlier) return { start: earlier.start, startedPid: earlier.startedPid, startedCommand: earlier.startedCommand };
|
|
11006
|
+
return site ? { start: site.start } : {};
|
|
11007
|
+
}
|
|
11008
|
+
async function openPreviewPorts(params) {
|
|
11009
|
+
const ports = /* @__PURE__ */ new Map();
|
|
11010
|
+
let rows = [];
|
|
11011
|
+
try {
|
|
11012
|
+
const listed = await controllerRequest({
|
|
11013
|
+
controllerUrl: params.controllerUrl,
|
|
11014
|
+
auth: params.auth,
|
|
11015
|
+
method: "GET",
|
|
11016
|
+
route: previewsRoute(params.projectId),
|
|
11017
|
+
retryCommand: "instafy preview list",
|
|
11018
|
+
notFoundMeansUnavailable: true,
|
|
11019
|
+
timeoutMs: 1e4
|
|
11020
|
+
});
|
|
11021
|
+
rows = Array.isArray(listed.body) ? listed.body : [];
|
|
11022
|
+
} catch (error) {
|
|
11023
|
+
if (error instanceof PreviewCommandError && error.exitCode === PREVIEW_EXIT_UNAVAILABLE) throw error;
|
|
11024
|
+
return ports;
|
|
11025
|
+
}
|
|
11026
|
+
const runtimeId = readEnv("RUNTIME_ID");
|
|
11027
|
+
for (const raw of rows) {
|
|
11028
|
+
if (!raw || typeof raw !== "object") continue;
|
|
11029
|
+
const record2 = raw;
|
|
11030
|
+
const onRuntime = stringField(record2, "runtimeId");
|
|
11031
|
+
if (runtimeId && onRuntime && onRuntime !== runtimeId) continue;
|
|
11032
|
+
const port = numberField(record2, "port");
|
|
11033
|
+
if (port) ports.set(port, stringField(record2, "title") ?? "");
|
|
11034
|
+
}
|
|
11035
|
+
return ports;
|
|
11036
|
+
}
|
|
10608
11037
|
async function createAndStartClient(params) {
|
|
10609
|
-
const { opts, port, title, sitePath, projectId: projectId2, retryCommand, auth, controllerUrl, serverUrl, log, serve } = params;
|
|
11038
|
+
const { opts, port, title, sitePath, projectId: projectId2, retryCommand, auth, controllerUrl, serverUrl, log, serve, site, portPicked } = params;
|
|
10610
11039
|
const conversationId = opts.conversationId?.trim() || void 0;
|
|
10611
11040
|
const created = await controllerRequest({
|
|
10612
11041
|
controllerUrl,
|
|
@@ -10622,10 +11051,12 @@ async function createAndStartClient(params) {
|
|
|
10622
11051
|
title,
|
|
10623
11052
|
...opts.summary ? { summary: opts.summary } : {},
|
|
10624
11053
|
metadata: { source: "instafy-cli" },
|
|
10625
|
-
...serve ? { serve } : {}
|
|
11054
|
+
...serve ? { serve } : {},
|
|
11055
|
+
...opts.replace ? { replace: true } : {}
|
|
10626
11056
|
},
|
|
10627
11057
|
retryCommand,
|
|
10628
|
-
notFoundMeansUnavailable: true
|
|
11058
|
+
notFoundMeansUnavailable: true,
|
|
11059
|
+
...portPicked ? { portTakenHint: PICKED_PORT_TAKEN_HINT } : {}
|
|
10629
11060
|
});
|
|
10630
11061
|
const grant = created.body && !Array.isArray(created.body) ? created.body : {};
|
|
10631
11062
|
const previewId = stringField(grant, "previewId");
|
|
@@ -10639,6 +11070,7 @@ async function createAndStartClient(params) {
|
|
|
10639
11070
|
const studioOrigin = parseStudioOrigin(grant["studioOrigin"]);
|
|
10640
11071
|
const state = readStateFile2();
|
|
10641
11072
|
const current = state.previews.find((preview) => preview.previewId === previewId) ?? null;
|
|
11073
|
+
const siteRecord = siteRecordFor(site, state.previews, port, projectId2);
|
|
10642
11074
|
let pid = null;
|
|
10643
11075
|
let binary = current?.binary;
|
|
10644
11076
|
try {
|
|
@@ -10702,47 +11134,84 @@ ${started.logTail}` : "";
|
|
|
10702
11134
|
startedAt: current?.pid === pid ? current.startedAt : (/* @__PURE__ */ new Date()).toISOString(),
|
|
10703
11135
|
...binary ? { binary } : {},
|
|
10704
11136
|
...hostSuffix ? { hostSuffix } : {},
|
|
10705
|
-
...studioOrigin ? { studioOrigin } : {}
|
|
11137
|
+
...studioOrigin ? { studioOrigin } : {},
|
|
11138
|
+
...siteRecord
|
|
10706
11139
|
};
|
|
10707
11140
|
upsertPreviewState(entry);
|
|
10708
11141
|
return { grant, previewId, entry, current };
|
|
10709
11142
|
}
|
|
10710
11143
|
async function previewOpen(opts) {
|
|
10711
|
-
const
|
|
11144
|
+
const explicitPort = opts.port === void 0 || String(opts.port).trim() === "" ? null : parsePreviewPort(opts.port);
|
|
10712
11145
|
const title = parsePreviewTitle(opts.title);
|
|
10713
11146
|
const sitePath = parsePreviewPath(opts.path);
|
|
10714
11147
|
const summary = parsePreviewSummary(opts.summary);
|
|
10715
|
-
|
|
11148
|
+
const serveArgv = opts.serveCmd?.trim() ? parseExplicitServeCommand(opts.serveCmd) : null;
|
|
11149
|
+
if (opts.start && !serveArgv) {
|
|
11150
|
+
throw new PreviewCommandError('--start needs --serve-cmd "<command>" to start the site.', PREVIEW_EXIT_NOT_LISTENING);
|
|
11151
|
+
}
|
|
11152
|
+
if (explicitPort === null) {
|
|
11153
|
+
if (!opts.start) {
|
|
11154
|
+
throw new Error(`A port is required: --port <port>. Or start the site with --start and --serve-cmd, which picks a free port.`);
|
|
11155
|
+
}
|
|
11156
|
+
if (!serveArgv.some((arg) => arg.includes(PORT_PLACEHOLDER))) {
|
|
11157
|
+
throw new Error(
|
|
11158
|
+
`--serve-cmd needs ${PORT_PLACEHOLDER} where the site's port goes, e.g. --serve-cmd "python3 -m http.server ${PORT_PLACEHOLDER} --bind 127.0.0.1": --start picks the port.`
|
|
11159
|
+
);
|
|
11160
|
+
}
|
|
11161
|
+
}
|
|
10716
11162
|
const projectId2 = resolveProject({ project: opts.project });
|
|
10717
|
-
const retryCommand = `instafy preview open --port ${
|
|
11163
|
+
const retryCommand = `instafy preview open${explicitPort === null ? "" : ` --port ${explicitPort}`} --title ${shellQuote2(title)}`;
|
|
10718
11164
|
const { auth, controllerUrl } = resolveAuth(opts, retryCommand);
|
|
10719
11165
|
const serverUrl = cleanUrl(controllerUrl);
|
|
10720
11166
|
const log = (message) => console.error(kleur8.cyan(message));
|
|
10721
|
-
const
|
|
10722
|
-
|
|
10723
|
-
|
|
10724
|
-
|
|
10725
|
-
|
|
10726
|
-
|
|
10727
|
-
|
|
10728
|
-
|
|
10729
|
-
|
|
10730
|
-
|
|
10731
|
-
|
|
11167
|
+
const start = opts.start ? resolvePreviewServe({ port: explicitPort ?? 0, serveCmd: opts.serveCmd, projectId: projectId2, cwd: opts.cwd }).serve : null;
|
|
11168
|
+
const { grant, previewId, entry, port, site, serve, restartNote } = await withStateLock(async () => {
|
|
11169
|
+
const site2 = start ? await startOrFindSite({
|
|
11170
|
+
explicitPort,
|
|
11171
|
+
start,
|
|
11172
|
+
root: resolveWorkspaceRoot(process.env, projectId2),
|
|
11173
|
+
projectId: projectId2,
|
|
11174
|
+
title,
|
|
11175
|
+
sitePath,
|
|
11176
|
+
timeoutMs: opts.startTimeoutMs,
|
|
11177
|
+
procRoot: opts.procRoot,
|
|
11178
|
+
openPorts: () => openPreviewPorts({ controllerUrl, auth, projectId: projectId2 }),
|
|
11179
|
+
log
|
|
11180
|
+
}) : null;
|
|
11181
|
+
const port2 = site2?.port ?? explicitPort;
|
|
11182
|
+
try {
|
|
11183
|
+
const probe = await probeLocalSite(port2, sitePath, void 0, RATHOLE_LOCAL_HOSTS);
|
|
11184
|
+
if (!probe.listening) {
|
|
11185
|
+
throw await notListeningError(port2, sitePath, probe);
|
|
10732
11186
|
}
|
|
10733
|
-
const
|
|
10734
|
-
|
|
10735
|
-
|
|
10736
|
-
|
|
11187
|
+
const { serve: serve2, note } = resolvePreviewServe({
|
|
11188
|
+
port: port2,
|
|
11189
|
+
serveCmd: opts.serveCmd?.split(PORT_PLACEHOLDER).join(String(port2)),
|
|
11190
|
+
projectId: projectId2,
|
|
11191
|
+
cwd: opts.cwd,
|
|
11192
|
+
procRoot: opts.procRoot
|
|
11193
|
+
});
|
|
11194
|
+
const created = await createAndStartClient({
|
|
11195
|
+
opts: { ...opts, summary },
|
|
11196
|
+
port: port2,
|
|
11197
|
+
title,
|
|
11198
|
+
sitePath,
|
|
11199
|
+
projectId: projectId2,
|
|
11200
|
+
retryCommand,
|
|
11201
|
+
auth,
|
|
11202
|
+
controllerUrl,
|
|
11203
|
+
serverUrl,
|
|
11204
|
+
log,
|
|
11205
|
+
serve: serve2,
|
|
11206
|
+
site: site2,
|
|
11207
|
+
portPicked: explicitPort === null
|
|
11208
|
+
});
|
|
11209
|
+
return { ...created, port: port2, site: site2, serve: serve2, restartNote: note };
|
|
11210
|
+
} catch (error) {
|
|
11211
|
+
if (site2?.started) await stopSite(site2.started.pid, site2.started.command ?? readProcessCommandLine(site2.started.pid));
|
|
11212
|
+
throw error;
|
|
10737
11213
|
}
|
|
10738
|
-
}
|
|
10739
|
-
if (!probe.listening) {
|
|
10740
|
-
throw await notListeningError(port, sitePath, probe);
|
|
10741
|
-
}
|
|
10742
|
-
const { serve, note: restartNote } = resolvePreviewServe({ port, serveCmd: opts.serveCmd, projectId: projectId2, cwd: opts.cwd });
|
|
10743
|
-
const { grant, previewId, entry } = await withStateLock(
|
|
10744
|
-
() => createAndStartClient({ opts: { ...opts, summary }, port, title, sitePath, projectId: projectId2, retryCommand, auth, controllerUrl, serverUrl, log, serve })
|
|
10745
|
-
);
|
|
11214
|
+
});
|
|
10746
11215
|
let ready;
|
|
10747
11216
|
try {
|
|
10748
11217
|
ready = await controllerRequest({
|
|
@@ -10786,7 +11255,7 @@ async function previewOpen(opts) {
|
|
|
10786
11255
|
viewerHostSuffix: parseViewerHostSuffix(grant["viewerHostSuffix"]) ?? null,
|
|
10787
11256
|
restartable,
|
|
10788
11257
|
...restartable ? {} : { restartNote: restartNote ?? "the server did not record a start command" },
|
|
10789
|
-
...opts.start ? { startedPid: started?.pid ?? null, logFile: started?.logFile ?? null } : {},
|
|
11258
|
+
...opts.start ? { startedPid: site?.started?.pid ?? null, logFile: site?.started?.logFile ?? null } : {},
|
|
10790
11259
|
thumbnail: thumbnail.status,
|
|
10791
11260
|
...thumbnail.reason ? { thumbnailReason: thumbnail.reason } : {}
|
|
10792
11261
|
};
|
|
@@ -10881,9 +11350,13 @@ async function previewClose(opts) {
|
|
|
10881
11350
|
if (!previewId) {
|
|
10882
11351
|
throw new Error("Preview id is required. Use `instafy preview list` to find it.");
|
|
10883
11352
|
}
|
|
11353
|
+
let siteStopped = false;
|
|
10884
11354
|
const entry = await withStateLock(async () => {
|
|
10885
11355
|
const removed = removePreviewState(previewId);
|
|
10886
|
-
if (removed)
|
|
11356
|
+
if (removed) {
|
|
11357
|
+
await stopClient(removed);
|
|
11358
|
+
siteStopped = await stopSite(removed.startedPid, removed.startedCommand);
|
|
11359
|
+
}
|
|
10887
11360
|
return removed;
|
|
10888
11361
|
});
|
|
10889
11362
|
const projectId2 = entry?.projectId ?? resolveProject({ project: opts.project });
|
|
@@ -10894,11 +11367,14 @@ async function previewClose(opts) {
|
|
|
10894
11367
|
);
|
|
10895
11368
|
await closePreviewOnServer(controllerUrl, auth, projectId2, previewId);
|
|
10896
11369
|
} catch (error) {
|
|
10897
|
-
if (entry)
|
|
11370
|
+
if (entry) {
|
|
11371
|
+
const { startedPid: _pid, startedCommand: _command, ...rest } = entry;
|
|
11372
|
+
await withStateLock(async () => upsertPreviewState({ ...siteStopped ? rest : entry, pid: -1 }));
|
|
11373
|
+
}
|
|
10898
11374
|
throw error;
|
|
10899
11375
|
}
|
|
10900
11376
|
removeWorkdir(previewId);
|
|
10901
|
-
return { ok: true, previewId };
|
|
11377
|
+
return { ok: true, previewId, siteStopped };
|
|
10902
11378
|
}
|
|
10903
11379
|
function printPreviewOpen(result, json) {
|
|
10904
11380
|
if (json) {
|
|
@@ -10906,7 +11382,7 @@ function printPreviewOpen(result, json) {
|
|
|
10906
11382
|
return;
|
|
10907
11383
|
}
|
|
10908
11384
|
const label = result.status === "live" ? kleur8.green("Ready to preview") : result.status === "not_responding" ? kleur8.yellow("Not responding yet") : kleur8.yellow("Preview opened (the server did not report whether the site answers)");
|
|
10909
|
-
console.log(`${label}: ${result.title} (port ${result.port}).
|
|
11385
|
+
console.log(`${label}: ${result.title} (port ${result.port}). Its card is in the conversation.`);
|
|
10910
11386
|
if (result.thumbnail === "uploaded") {
|
|
10911
11387
|
console.log(kleur8.gray("The card shows a picture of the site; `instafy preview check` takes a new one after a change."));
|
|
10912
11388
|
} else {
|
|
@@ -10915,7 +11391,7 @@ function printPreviewOpen(result, json) {
|
|
|
10915
11391
|
if (result.startedPid) {
|
|
10916
11392
|
console.log(kleur8.gray(`Started the site in the background (pid ${result.startedPid}); log: ${displayPath(result.logFile ?? "")}`));
|
|
10917
11393
|
} else if (result.startedPid === null) {
|
|
10918
|
-
console.log(kleur8.gray("
|
|
11394
|
+
console.log(kleur8.gray("This site was already running on its port; nothing was started."));
|
|
10919
11395
|
}
|
|
10920
11396
|
if (result.restartable) {
|
|
10921
11397
|
console.log(kleur8.gray("Open starts the site again when the process that started it exits, or the space sleeps."));
|
|
@@ -11082,7 +11558,7 @@ init_config();
|
|
|
11082
11558
|
|
|
11083
11559
|
// src/git-wrapper.ts
|
|
11084
11560
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
11085
|
-
import { randomUUID as
|
|
11561
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
11086
11562
|
import fs13 from "node:fs";
|
|
11087
11563
|
import path14 from "node:path";
|
|
11088
11564
|
import kleur10 from "kleur";
|
|
@@ -11296,10 +11772,10 @@ function hideEmbeddedGitDirs(context, pathSpecs) {
|
|
|
11296
11772
|
fs13.mkdirSync(stagingBase, { recursive: true });
|
|
11297
11773
|
const renames = [];
|
|
11298
11774
|
for (const candidate of candidates) {
|
|
11299
|
-
let hidden = path14.join(stagingBase,
|
|
11775
|
+
let hidden = path14.join(stagingBase, randomUUID3());
|
|
11300
11776
|
for (let attempt2 = 0; attempt2 < 8; attempt2 += 1) {
|
|
11301
11777
|
if (!pathExists(hidden)) break;
|
|
11302
|
-
hidden = path14.join(stagingBase,
|
|
11778
|
+
hidden = path14.join(stagingBase, randomUUID3());
|
|
11303
11779
|
}
|
|
11304
11780
|
fs13.renameSync(candidate, hidden);
|
|
11305
11781
|
renames.push({ original: candidate, hidden });
|
|
@@ -11870,6 +12346,311 @@ function saveReportJson(report, notSaved) {
|
|
|
11870
12346
|
};
|
|
11871
12347
|
}
|
|
11872
12348
|
|
|
12349
|
+
// src/api.ts
|
|
12350
|
+
init_config();
|
|
12351
|
+
init_errors();
|
|
12352
|
+
init_controller_fetch();
|
|
12353
|
+
import fs14 from "node:fs";
|
|
12354
|
+
function normalizeUrl5(raw) {
|
|
12355
|
+
const value = (raw ?? "").trim();
|
|
12356
|
+
if (!value) return "http://127.0.0.1:8788";
|
|
12357
|
+
return value.replace(/\/$/, "");
|
|
12358
|
+
}
|
|
12359
|
+
function normalizeToken6(raw) {
|
|
12360
|
+
if (!raw) return null;
|
|
12361
|
+
const trimmed = raw.trim();
|
|
12362
|
+
return trimmed.length ? trimmed : null;
|
|
12363
|
+
}
|
|
12364
|
+
function resolveBearerTokenWithSource(options) {
|
|
12365
|
+
const cwd = process.cwd();
|
|
12366
|
+
const profile = resolveActiveProfileName({ cwd });
|
|
12367
|
+
const stored = resolveConfiguredAccessToken({ profile, cwd });
|
|
12368
|
+
const explicit = normalizeToken6(options.accessToken);
|
|
12369
|
+
if (explicit) {
|
|
12370
|
+
return { token: explicit, source: "explicit", profile, runtimeControllerUrl: null };
|
|
12371
|
+
}
|
|
12372
|
+
const runtimeCredential = resolveRuntimeControllerCredential();
|
|
12373
|
+
if (runtimeCredential) {
|
|
12374
|
+
return {
|
|
12375
|
+
token: runtimeCredential.token,
|
|
12376
|
+
source: "env",
|
|
12377
|
+
profile: null,
|
|
12378
|
+
runtimeControllerUrl: runtimeCredential.controllerUrl
|
|
12379
|
+
};
|
|
12380
|
+
}
|
|
12381
|
+
const envKeys = [
|
|
12382
|
+
"INSTAFY_ACCESS_TOKEN",
|
|
12383
|
+
"SUPABASE_ACCESS_TOKEN"
|
|
12384
|
+
];
|
|
12385
|
+
for (const key of envKeys) {
|
|
12386
|
+
const value = normalizeToken6(process.env[key]);
|
|
12387
|
+
if (value) {
|
|
12388
|
+
return { token: value, source: "env", profile, runtimeControllerUrl: null };
|
|
12389
|
+
}
|
|
12390
|
+
}
|
|
12391
|
+
if (stored) {
|
|
12392
|
+
return { token: stored, source: "config", profile, runtimeControllerUrl: null };
|
|
12393
|
+
}
|
|
12394
|
+
return { token: null, source: "none", profile, runtimeControllerUrl: null };
|
|
12395
|
+
}
|
|
12396
|
+
function parseKeyValue(raw) {
|
|
12397
|
+
const trimmed = raw.trim();
|
|
12398
|
+
if (!trimmed) {
|
|
12399
|
+
throw new Error("Key/value pair cannot be empty");
|
|
12400
|
+
}
|
|
12401
|
+
const equals = trimmed.indexOf("=");
|
|
12402
|
+
if (equals === -1) {
|
|
12403
|
+
return { key: trimmed, value: "" };
|
|
12404
|
+
}
|
|
12405
|
+
const key = trimmed.slice(0, equals).trim();
|
|
12406
|
+
const value = trimmed.slice(equals + 1).trim();
|
|
12407
|
+
if (!key) {
|
|
12408
|
+
throw new Error(`Invalid pair "${raw}" (missing key)`);
|
|
12409
|
+
}
|
|
12410
|
+
return { key, value };
|
|
12411
|
+
}
|
|
12412
|
+
function parseHeader(raw) {
|
|
12413
|
+
const trimmed = raw.trim();
|
|
12414
|
+
if (!trimmed) {
|
|
12415
|
+
throw new Error("Header cannot be empty");
|
|
12416
|
+
}
|
|
12417
|
+
const colon = trimmed.indexOf(":");
|
|
12418
|
+
if (colon !== -1) {
|
|
12419
|
+
const key = trimmed.slice(0, colon).trim();
|
|
12420
|
+
const value = trimmed.slice(colon + 1).trim();
|
|
12421
|
+
if (!key) {
|
|
12422
|
+
throw new Error(`Invalid header "${raw}" (missing name)`);
|
|
12423
|
+
}
|
|
12424
|
+
return { key, value };
|
|
12425
|
+
}
|
|
12426
|
+
return parseKeyValue(trimmed);
|
|
12427
|
+
}
|
|
12428
|
+
function parseJsonBody(options) {
|
|
12429
|
+
if (options.jsonFile) {
|
|
12430
|
+
const raw = fs14.readFileSync(options.jsonFile, "utf8");
|
|
12431
|
+
const parsed = JSON.parse(raw);
|
|
12432
|
+
return JSON.stringify(parsed);
|
|
12433
|
+
}
|
|
12434
|
+
if (options.json) {
|
|
12435
|
+
const parsed = JSON.parse(options.json);
|
|
12436
|
+
return JSON.stringify(parsed);
|
|
12437
|
+
}
|
|
12438
|
+
return void 0;
|
|
12439
|
+
}
|
|
12440
|
+
function buildRequestUrl(options, runtimeControllerUrl) {
|
|
12441
|
+
const selectedControllerUrl = options.controllerUrl ?? runtimeControllerUrl;
|
|
12442
|
+
const base = new URL(
|
|
12443
|
+
`${normalizeUrl5(resolveControllerUrl({ controllerUrl: selectedControllerUrl ?? null }))}/`
|
|
12444
|
+
);
|
|
12445
|
+
if (base.protocol !== "http:" && base.protocol !== "https:") {
|
|
12446
|
+
throw new Error("The configured controller URL must use http or https.");
|
|
12447
|
+
}
|
|
12448
|
+
if (base.username || base.password) {
|
|
12449
|
+
throw new Error("The configured controller URL must not contain embedded credentials.");
|
|
12450
|
+
}
|
|
12451
|
+
if (runtimeControllerUrl) {
|
|
12452
|
+
resolveRuntimeBoundControllerUrl(
|
|
12453
|
+
{ controllerUrl: runtimeControllerUrl },
|
|
12454
|
+
base.toString()
|
|
12455
|
+
);
|
|
12456
|
+
}
|
|
12457
|
+
const rawPath = options.path.trim();
|
|
12458
|
+
if (!rawPath) {
|
|
12459
|
+
throw new Error("Path is required");
|
|
12460
|
+
}
|
|
12461
|
+
const isAbsoluteReference = /^[a-z][a-z\d+.-]*:/i.test(rawPath) || rawPath.startsWith("//");
|
|
12462
|
+
const url = new URL(
|
|
12463
|
+
isAbsoluteReference ? rawPath : rawPath.startsWith("/") ? rawPath : `/${rawPath}`,
|
|
12464
|
+
base
|
|
12465
|
+
);
|
|
12466
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
12467
|
+
throw new Error("Controller API URLs must use http or https.");
|
|
12468
|
+
}
|
|
12469
|
+
if (url.username || url.password) {
|
|
12470
|
+
throw new Error("Controller API URLs must not contain embedded credentials.");
|
|
12471
|
+
}
|
|
12472
|
+
const crossOrigin = url.origin !== base.origin;
|
|
12473
|
+
if (crossOrigin && !options.allowCrossOrigin) {
|
|
12474
|
+
throw new Error(
|
|
12475
|
+
"Refusing a cross-origin controller API request. Set --server-url to that origin and use a relative path, or pass --allow-cross-origin only when you explicitly intend to send credentials and request data there."
|
|
12476
|
+
);
|
|
12477
|
+
}
|
|
12478
|
+
for (const pair of options.query ?? []) {
|
|
12479
|
+
const { key, value } = parseKeyValue(pair);
|
|
12480
|
+
url.searchParams.set(key, value);
|
|
12481
|
+
}
|
|
12482
|
+
return { url, crossOrigin };
|
|
12483
|
+
}
|
|
12484
|
+
function maybePrettyPrintJson(text2, pretty) {
|
|
12485
|
+
if (!pretty) return text2;
|
|
12486
|
+
if (!text2.trim()) return text2;
|
|
12487
|
+
try {
|
|
12488
|
+
return JSON.stringify(JSON.parse(text2), null, 2);
|
|
12489
|
+
} catch {
|
|
12490
|
+
return text2;
|
|
12491
|
+
}
|
|
12492
|
+
}
|
|
12493
|
+
async function executeControllerApiRequest(options, bodyOverride) {
|
|
12494
|
+
const resolved = resolveBearerTokenWithSource(options);
|
|
12495
|
+
const { url, crossOrigin } = buildRequestUrl(options, resolved.runtimeControllerUrl);
|
|
12496
|
+
const bearer = resolved.token;
|
|
12497
|
+
if (crossOrigin && (!bearer || resolved.source !== "explicit")) {
|
|
12498
|
+
throw new Error(
|
|
12499
|
+
"Cross-origin controller API requests require --allow-cross-origin and an explicit --access-token."
|
|
12500
|
+
);
|
|
12501
|
+
}
|
|
12502
|
+
const headers = new Headers();
|
|
12503
|
+
headers.set("accept", "application/json");
|
|
12504
|
+
for (const header of options.headers ?? []) {
|
|
12505
|
+
const { key, value } = parseHeader(header);
|
|
12506
|
+
headers.set(key, value);
|
|
12507
|
+
}
|
|
12508
|
+
const body = bodyOverride ?? parseJsonBody(options);
|
|
12509
|
+
if (body !== void 0 && !headers.has("content-type")) {
|
|
12510
|
+
headers.set("content-type", "application/json");
|
|
12511
|
+
}
|
|
12512
|
+
const init = {
|
|
12513
|
+
method: options.method,
|
|
12514
|
+
headers,
|
|
12515
|
+
body,
|
|
12516
|
+
redirect: "error"
|
|
12517
|
+
};
|
|
12518
|
+
const response = bearer ? (await fetchWithControllerAuth({
|
|
12519
|
+
url: url.toString(),
|
|
12520
|
+
init,
|
|
12521
|
+
accessToken: bearer,
|
|
12522
|
+
tokenSource: resolved.source,
|
|
12523
|
+
profile: resolved.profile,
|
|
12524
|
+
cwd: process.cwd()
|
|
12525
|
+
})).response : await fetch(url, init);
|
|
12526
|
+
const responseText = await response.text().catch(() => "");
|
|
12527
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
12528
|
+
const isJson = contentType.includes("application/json") || contentType.includes("+json");
|
|
12529
|
+
if (!response.ok) {
|
|
12530
|
+
if (response.status === 401 || response.status === 403) {
|
|
12531
|
+
throw formatAuthRejectedError({
|
|
12532
|
+
status: response.status,
|
|
12533
|
+
responseBody: responseText,
|
|
12534
|
+
retryCommand: "instafy login",
|
|
12535
|
+
advancedHint: "pass --access-token, or set INSTAFY_ACCESS_TOKEN / SUPABASE_ACCESS_TOKEN"
|
|
12536
|
+
});
|
|
12537
|
+
}
|
|
12538
|
+
const formattedBody = isJson ? maybePrettyPrintJson(responseText, true) : responseText;
|
|
12539
|
+
const prefix = `Request failed (${response.status} ${response.statusText})`;
|
|
12540
|
+
const suffix = formattedBody.trim() ? `: ${formattedBody}` : "";
|
|
12541
|
+
throw new Error(`${prefix}${suffix}`);
|
|
12542
|
+
}
|
|
12543
|
+
return { response, responseText, isJson };
|
|
12544
|
+
}
|
|
12545
|
+
async function requestControllerApiJson(options) {
|
|
12546
|
+
const body = options.jsonBody === void 0 ? void 0 : JSON.stringify(options.jsonBody);
|
|
12547
|
+
const { responseText, isJson } = await executeControllerApiRequest(
|
|
12548
|
+
{
|
|
12549
|
+
method: options.method,
|
|
12550
|
+
path: options.path,
|
|
12551
|
+
controllerUrl: options.controllerUrl,
|
|
12552
|
+
accessToken: options.accessToken,
|
|
12553
|
+
serviceToken: options.serviceToken,
|
|
12554
|
+
query: options.query,
|
|
12555
|
+
headers: options.headers,
|
|
12556
|
+
allowCrossOrigin: options.allowCrossOrigin
|
|
12557
|
+
},
|
|
12558
|
+
body
|
|
12559
|
+
);
|
|
12560
|
+
if (!isJson) {
|
|
12561
|
+
throw new Error("Expected JSON response from controller API");
|
|
12562
|
+
}
|
|
12563
|
+
return JSON.parse(responseText);
|
|
12564
|
+
}
|
|
12565
|
+
async function requestControllerApi(options) {
|
|
12566
|
+
const { responseText, isJson } = await executeControllerApiRequest(options);
|
|
12567
|
+
const pretty = options.pretty !== false;
|
|
12568
|
+
const formattedBody = isJson ? maybePrettyPrintJson(responseText, pretty) : responseText;
|
|
12569
|
+
if (formattedBody) console.log(formattedBody);
|
|
12570
|
+
}
|
|
12571
|
+
|
|
12572
|
+
// src/research-settings.ts
|
|
12573
|
+
init_project_manifest();
|
|
12574
|
+
var fields = [
|
|
12575
|
+
{ key: "maxPageLoads", label: "Fresh pages per task" },
|
|
12576
|
+
{ key: "maxPageLoadsPerSite", label: "Fresh pages per site per task" }
|
|
12577
|
+
];
|
|
12578
|
+
function integer(raw, label) {
|
|
12579
|
+
if (!/^[1-9]\d*$/.test(raw.trim()) || !Number.isSafeInteger(Number(raw))) {
|
|
12580
|
+
throw new Error(`${label} must be a positive whole number.`);
|
|
12581
|
+
}
|
|
12582
|
+
return Number(raw);
|
|
12583
|
+
}
|
|
12584
|
+
async function researchSettings(options) {
|
|
12585
|
+
const patch = {};
|
|
12586
|
+
for (const field of fields) {
|
|
12587
|
+
if (options[field.key] !== void 0) patch[field.key] = integer(options[field.key], field.label);
|
|
12588
|
+
}
|
|
12589
|
+
if (options.interactive) {
|
|
12590
|
+
if (options.json || Object.keys(patch).length) throw new Error("Use --interactive without --json or setting flags.");
|
|
12591
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY || process.env.CI === "true") {
|
|
12592
|
+
throw new Error("--interactive requires a terminal. Use --max-page-loads and --max-page-loads-per-site instead.");
|
|
12593
|
+
}
|
|
12594
|
+
}
|
|
12595
|
+
const project = [options.project, process.env.SPACE_ID, process.env.INSTAFY_SPACE_ID, process.env.PROJECT_ID, process.env.INSTAFY_PROJECT_ID].map((value) => value?.trim()).find(Boolean) ?? findProjectManifest(process.cwd()).manifest?.spaceId;
|
|
12596
|
+
if (!project) throw new Error("No space configured. Pass --space, set SPACE_ID, or run `instafy space init`.");
|
|
12597
|
+
const request = {
|
|
12598
|
+
path: `/projects/${encodeURIComponent(project)}/research-settings`,
|
|
12599
|
+
controllerUrl: options.controllerUrl,
|
|
12600
|
+
accessToken: options.accessToken
|
|
12601
|
+
};
|
|
12602
|
+
let settings = await requestControllerApiJson({ ...request, method: "GET" });
|
|
12603
|
+
if ((options.interactive || Object.keys(patch).length) && !settings.canManage) {
|
|
12604
|
+
throw new Error("Only space managers can change research settings.");
|
|
12605
|
+
}
|
|
12606
|
+
for (const field of fields) {
|
|
12607
|
+
if (!settings.limits?.[field.key] || !Number.isInteger(settings[field.key])) {
|
|
12608
|
+
throw new Error("This controller does not support research settings. Update it before changing the budget.");
|
|
12609
|
+
}
|
|
12610
|
+
}
|
|
12611
|
+
if (options.interactive) {
|
|
12612
|
+
const { isCancel, text: text2 } = await import("@clack/prompts");
|
|
12613
|
+
console.log("The parent and its helpers share one budget. Changes apply to new tasks; running tasks retain their allowance.");
|
|
12614
|
+
for (const field of fields) {
|
|
12615
|
+
const bounds = settings.limits[field.key];
|
|
12616
|
+
const entered = await text2({
|
|
12617
|
+
message: `${field.label} (${bounds.min}\u2013${bounds.max}; default ${bounds.default})`,
|
|
12618
|
+
defaultValue: String(settings[field.key]),
|
|
12619
|
+
placeholder: String(settings[field.key]),
|
|
12620
|
+
validate(raw) {
|
|
12621
|
+
try {
|
|
12622
|
+
const value2 = integer(raw || String(settings[field.key]), field.label);
|
|
12623
|
+
if (value2 < bounds.min || value2 > bounds.max) return `Enter a whole number from ${bounds.min} to ${bounds.max}.`;
|
|
12624
|
+
if (field.key === "maxPageLoadsPerSite" && value2 > (patch.maxPageLoads ?? settings.maxPageLoads)) return "The per-site limit cannot exceed the task limit.";
|
|
12625
|
+
} catch (error) {
|
|
12626
|
+
return error.message;
|
|
12627
|
+
}
|
|
12628
|
+
}
|
|
12629
|
+
});
|
|
12630
|
+
if (isCancel(entered)) throw new Error("Cancelled. No settings were changed.");
|
|
12631
|
+
const value = integer(String(entered), field.label);
|
|
12632
|
+
if (value !== settings[field.key]) patch[field.key] = value;
|
|
12633
|
+
}
|
|
12634
|
+
}
|
|
12635
|
+
for (const field of fields) {
|
|
12636
|
+
const value = patch[field.key];
|
|
12637
|
+
const bounds = settings.limits[field.key];
|
|
12638
|
+
if (value !== void 0 && (value < bounds.min || value > bounds.max)) throw new Error(`${field.label} must be between ${bounds.min} and ${bounds.max}.`);
|
|
12639
|
+
}
|
|
12640
|
+
if ((patch.maxPageLoadsPerSite ?? settings.maxPageLoadsPerSite) > (patch.maxPageLoads ?? settings.maxPageLoads)) {
|
|
12641
|
+
throw new Error("The per-site limit cannot exceed the task limit.");
|
|
12642
|
+
}
|
|
12643
|
+
if (Object.keys(patch).length) settings = await requestControllerApiJson({ ...request, method: "PATCH", jsonBody: patch });
|
|
12644
|
+
if (options.json) {
|
|
12645
|
+
console.log(JSON.stringify(settings, null, 2));
|
|
12646
|
+
return;
|
|
12647
|
+
}
|
|
12648
|
+
console.log(`Research budget for space ${project}`);
|
|
12649
|
+
for (const field of fields) console.log(`${field.label}: ${settings[field.key]}`);
|
|
12650
|
+
console.log("Shared by the parent and all helpers, including continued worker jobs. Cached reads are free. Changes apply to new tasks.");
|
|
12651
|
+
if (!settings.canManage) console.log("Read-only: only space managers can change this budget.");
|
|
12652
|
+
}
|
|
12653
|
+
|
|
11873
12654
|
// src/web-link.ts
|
|
11874
12655
|
var linkOnly2 = __toESM(require_linkOnly(), 1);
|
|
11875
12656
|
var WEB_LINK_EXIT_OK = 0;
|
|
@@ -11938,9 +12719,9 @@ function registerWebLinkCommand(web) {
|
|
|
11938
12719
|
// src/web-search.ts
|
|
11939
12720
|
var redaction2 = __toESM(require_redaction(), 1);
|
|
11940
12721
|
import dns3 from "node:dns";
|
|
11941
|
-
import
|
|
12722
|
+
import fs15 from "node:fs";
|
|
11942
12723
|
import https2 from "node:https";
|
|
11943
|
-
import
|
|
12724
|
+
import net6 from "node:net";
|
|
11944
12725
|
import path15 from "node:path";
|
|
11945
12726
|
import zlib2 from "node:zlib";
|
|
11946
12727
|
|
|
@@ -12147,7 +12928,7 @@ function compileProvider(id, raw) {
|
|
|
12147
12928
|
if (url.protocol !== "https:") return `${id}: request.url must be https`;
|
|
12148
12929
|
if (url.username || url.password || url.port && url.port !== "443" || url.hash) return `${id}: request.url must be a plain https address`;
|
|
12149
12930
|
const host = langInHost ? url.hostname.toLowerCase().replace(/^xx\./, "") : url.hostname.toLowerCase();
|
|
12150
|
-
if (
|
|
12931
|
+
if (net6.isIP(host.replace(/^\[|\]$/g, "")) || isLocalHostName(host)) return `${id}: request.url must name a public host`;
|
|
12151
12932
|
const never = NEVER_HOSTS.find((entry) => host === entry.host || host.endsWith(`.${entry.host}`));
|
|
12152
12933
|
if (never) return `${id}: ${never.host} is never used (${never.why})`;
|
|
12153
12934
|
if (/[{}]/.test(decodeURIComponent(url.href))) return `${id}: request.url may not hold placeholders`;
|
|
@@ -12270,9 +13051,9 @@ function compileProvider(id, raw) {
|
|
|
12270
13051
|
}
|
|
12271
13052
|
function resultLink(value) {
|
|
12272
13053
|
if (typeof value === "string" && value.startsWith("https://")) {
|
|
12273
|
-
const
|
|
13054
|
+
const fields2 = [...value.matchAll(/\{([\w-]+(?:\.[\w-]+)*)\}/g)];
|
|
12274
13055
|
const rest = value.replace(/\{([\w-]+(?:\.[\w-]+)*)\}/g, "");
|
|
12275
|
-
if (!
|
|
13056
|
+
if (!fields2.length || /[{}]/.test(rest)) return void 0;
|
|
12276
13057
|
try {
|
|
12277
13058
|
new URL(value.replace(/\{[^}]*\}/g, "x"));
|
|
12278
13059
|
} catch {
|
|
@@ -12330,8 +13111,8 @@ function loadSearchProviders(bundled = BUNDLED_SEARCH_PROVIDERS, copies = []) {
|
|
|
12330
13111
|
return { providers, chains, off: [...off].filter((id) => id in bundled.providers).sort(), keyEnvs: [...keyEnvs], warnings };
|
|
12331
13112
|
}
|
|
12332
13113
|
function fill(template, vars) {
|
|
12333
|
-
const
|
|
12334
|
-
if (
|
|
13114
|
+
const whole2 = /^\{([a-z]+)\}$/.exec(template);
|
|
13115
|
+
if (whole2 && whole2[1] === "count") return vars.count;
|
|
12335
13116
|
let missing = false;
|
|
12336
13117
|
const out = template.replace(/\{\{|\}\}|\{([a-z]+)(?::([a-z]+))?\}/g, (match, name, encoder) => {
|
|
12337
13118
|
if (!name) return match[0];
|
|
@@ -12420,7 +13201,7 @@ function sendOnce(request, target, timeoutMs) {
|
|
|
12420
13201
|
port: target.port,
|
|
12421
13202
|
method: request.method,
|
|
12422
13203
|
path: `${request.url.pathname}${request.url.search}`,
|
|
12423
|
-
servername:
|
|
13204
|
+
servername: net6.isIP(hostname) ? void 0 : hostname,
|
|
12424
13205
|
agent: false,
|
|
12425
13206
|
maxHeaderSize: 64 * 1024,
|
|
12426
13207
|
headers: { Host: request.url.host, ...request.headers, ...payload ? { "Content-Length": String(payload.length) } : {} }
|
|
@@ -12470,8 +13251,8 @@ function at(value, field) {
|
|
|
12470
13251
|
}
|
|
12471
13252
|
return current;
|
|
12472
13253
|
}
|
|
12473
|
-
function firstText(item,
|
|
12474
|
-
for (const field of
|
|
13254
|
+
function firstText(item, fields2) {
|
|
13255
|
+
for (const field of fields2) {
|
|
12475
13256
|
const value = at(item, field);
|
|
12476
13257
|
if (typeof value === "string" && value.trim()) return value;
|
|
12477
13258
|
}
|
|
@@ -12487,10 +13268,10 @@ function withoutTags(text2) {
|
|
|
12487
13268
|
return out;
|
|
12488
13269
|
}
|
|
12489
13270
|
function cleanText(raw, max) {
|
|
12490
|
-
const text2 = withoutTags(raw).replace(/&(#x[0-9a-f]{1,6}|#\d{1,7}|[a-z]{2,6}|#39);/gi, (
|
|
13271
|
+
const text2 = withoutTags(raw).replace(/&(#x[0-9a-f]{1,6}|#\d{1,7}|[a-z]{2,6}|#39);/gi, (whole2, name) => {
|
|
12491
13272
|
if (ENTITIES2[name.toLowerCase()]) return ENTITIES2[name.toLowerCase()];
|
|
12492
13273
|
const code = name[0] === "#" ? name[1]?.toLowerCase() === "x" ? parseInt(name.slice(2), 16) : parseInt(name.slice(1), 10) : NaN;
|
|
12493
|
-
return Number.isFinite(code) && code > 31 && code < 1114112 ? String.fromCodePoint(code) :
|
|
13274
|
+
return Number.isFinite(code) && code > 31 && code < 1114112 ? String.fromCodePoint(code) : whole2;
|
|
12494
13275
|
}).replace(/</g, "\u2039").replace(/[\u0000-\u001f\u007f-\u009f\u200b-\u200f\u202a-\u202e\u2066-\u2069\ufeff]/g, " ").replace(/\s+/g, " ").trim();
|
|
12495
13276
|
const redacted = redaction2.redactText(text2).text;
|
|
12496
13277
|
return redacted.length > max ? `${redacted.slice(0, max - 1)}\u2026` : redacted;
|
|
@@ -12507,7 +13288,7 @@ function resultAddress(raw) {
|
|
|
12507
13288
|
if (url.username || url.password) return null;
|
|
12508
13289
|
if (url.port && url.port !== "80" && url.port !== "443") return null;
|
|
12509
13290
|
const host = url.hostname.replace(/^\[|\]$/g, "");
|
|
12510
|
-
if (
|
|
13291
|
+
if (net6.isIP(host) ? isPrivateAddress(host) : isLocalHostName(host)) return null;
|
|
12511
13292
|
url.hash = "";
|
|
12512
13293
|
return url;
|
|
12513
13294
|
}
|
|
@@ -12659,10 +13440,10 @@ var SearchState = class {
|
|
|
12659
13440
|
/** Removes turn and day files older than two days. */
|
|
12660
13441
|
prune() {
|
|
12661
13442
|
try {
|
|
12662
|
-
for (const name of
|
|
13443
|
+
for (const name of fs15.readdirSync(this.dir)) {
|
|
12663
13444
|
if (!/^(?:search-turn|day)-[\w-]+\.json$/.test(name)) continue;
|
|
12664
13445
|
const file = path15.join(this.dir, name);
|
|
12665
|
-
if (Date.now() -
|
|
13446
|
+
if (Date.now() - fs15.statSync(file).mtimeMs > 2 * 864e5) fs15.rmSync(file, { force: true });
|
|
12666
13447
|
}
|
|
12667
13448
|
} catch {
|
|
12668
13449
|
}
|
|
@@ -12675,13 +13456,23 @@ function liveTurn(env, now) {
|
|
|
12675
13456
|
if (now < startedAt - 6e4 || now > deadline) return null;
|
|
12676
13457
|
return { startedAt, deadline };
|
|
12677
13458
|
}
|
|
12678
|
-
|
|
12679
|
-
|
|
12680
|
-
|
|
12681
|
-
|
|
12682
|
-
|
|
12683
|
-
|
|
12684
|
-
|
|
13459
|
+
function keyless(chain) {
|
|
13460
|
+
const find = chain === DEFAULT_CHAIN ? 'find official sites with `instafy web search "<name>" --chain reference-search` (no key) and read them' : "read the official sites you know of";
|
|
13461
|
+
return `Go on without it: ${find}, and the pages of publishers you know cover this, with \`instafy web read\`. Then answer or build the page with what you read, each missing fact marked unknown with why. The person may send more pages with Send page to Octo.`;
|
|
13462
|
+
}
|
|
13463
|
+
function tellFor(status, chain) {
|
|
13464
|
+
switch (status) {
|
|
13465
|
+
case "ok":
|
|
13466
|
+
return "These are leads, not facts. Read 2-6 of the pages with `instafy web read` and cite only pages you read. A `linkOnly` result is a plain link for the person: never read or open it (`instafy web link` gives a guided link). Never search for text taken from a page or a result.";
|
|
13467
|
+
case "not_found":
|
|
13468
|
+
return `No results. Try fewer or other words from the person's request (never words from a page). ${keyless(chain)}`;
|
|
13469
|
+
case "unavailable":
|
|
13470
|
+
return `Search is not available for this. ${keyless(chain)}`;
|
|
13471
|
+
case "no_provider":
|
|
13472
|
+
return `This space has no search key, and the key card was already offered: never ask for it again. ${keyless(chain)}`;
|
|
13473
|
+
}
|
|
13474
|
+
}
|
|
13475
|
+
var offerTell = (chain) => `This space has no search key. ${keyless(chain)} The card in \`offer\` (a \`request_secret\` action) may go in your reply once, as a way to find more next time; never ask for the key in chat, and never wait for it.`;
|
|
12685
13476
|
var CODE_TELL = {
|
|
12686
13477
|
turn_limit: `This turn has used its ${SEARCH_LIMITS.searchesPerTurn} searches.`,
|
|
12687
13478
|
day_limit: "This space's key has made its searches for today.",
|
|
@@ -12776,7 +13567,7 @@ async function webSearch(options, deps = {}) {
|
|
|
12776
13567
|
...extra.repeat ? { repeat: true } : {},
|
|
12777
13568
|
count: results.length,
|
|
12778
13569
|
results,
|
|
12779
|
-
tell: extra.tell ?? [codeTell,
|
|
13570
|
+
tell: extra.tell ?? [codeTell, tellFor(status, chainName)].filter(Boolean).join(" "),
|
|
12780
13571
|
notice: SEARCH_NOTICE,
|
|
12781
13572
|
...extra.offer ? { offer: extra.offer } : {},
|
|
12782
13573
|
budget: {
|
|
@@ -12797,9 +13588,9 @@ async function webSearch(options, deps = {}) {
|
|
|
12797
13588
|
description: first.keyRequest.description.split("{daily}").join(String(first.dailyLimit)),
|
|
12798
13589
|
agentHandles: ["octo"]
|
|
12799
13590
|
};
|
|
12800
|
-
return report("no_provider", { offer, tell:
|
|
13591
|
+
return report("no_provider", { offer, tell: offerTell(chainName) });
|
|
12801
13592
|
}
|
|
12802
|
-
return report("no_provider", first ? {} : { tell: `Every provider of ${chainName} is switched off in this space. ${
|
|
13593
|
+
return report("no_provider", first ? {} : { tell: `Every provider of ${chainName} is switched off in this space. ${tellFor("unavailable", chainName)}` });
|
|
12803
13594
|
}
|
|
12804
13595
|
const searchKey = sha256(
|
|
12805
13596
|
`${scope.key}|${chainName}|${options.provider ?? ""}|${query.toLowerCase()}|${site ?? ""}|${lang ?? ""}|${country ?? ""}|${count}`
|
|
@@ -13000,229 +13791,6 @@ function registerWebSearchCommand(web) {
|
|
|
13000
13791
|
return web;
|
|
13001
13792
|
}
|
|
13002
13793
|
|
|
13003
|
-
// src/api.ts
|
|
13004
|
-
init_config();
|
|
13005
|
-
init_errors();
|
|
13006
|
-
init_controller_fetch();
|
|
13007
|
-
import fs15 from "node:fs";
|
|
13008
|
-
function normalizeUrl5(raw) {
|
|
13009
|
-
const value = (raw ?? "").trim();
|
|
13010
|
-
if (!value) return "http://127.0.0.1:8788";
|
|
13011
|
-
return value.replace(/\/$/, "");
|
|
13012
|
-
}
|
|
13013
|
-
function normalizeToken6(raw) {
|
|
13014
|
-
if (!raw) return null;
|
|
13015
|
-
const trimmed = raw.trim();
|
|
13016
|
-
return trimmed.length ? trimmed : null;
|
|
13017
|
-
}
|
|
13018
|
-
function resolveBearerTokenWithSource(options) {
|
|
13019
|
-
const cwd = process.cwd();
|
|
13020
|
-
const profile = resolveActiveProfileName({ cwd });
|
|
13021
|
-
const stored = resolveConfiguredAccessToken({ profile, cwd });
|
|
13022
|
-
const explicit = normalizeToken6(options.accessToken);
|
|
13023
|
-
if (explicit) {
|
|
13024
|
-
return { token: explicit, source: "explicit", profile, runtimeControllerUrl: null };
|
|
13025
|
-
}
|
|
13026
|
-
const runtimeCredential = resolveRuntimeControllerCredential();
|
|
13027
|
-
if (runtimeCredential) {
|
|
13028
|
-
return {
|
|
13029
|
-
token: runtimeCredential.token,
|
|
13030
|
-
source: "env",
|
|
13031
|
-
profile: null,
|
|
13032
|
-
runtimeControllerUrl: runtimeCredential.controllerUrl
|
|
13033
|
-
};
|
|
13034
|
-
}
|
|
13035
|
-
const envKeys = [
|
|
13036
|
-
"INSTAFY_ACCESS_TOKEN",
|
|
13037
|
-
"SUPABASE_ACCESS_TOKEN"
|
|
13038
|
-
];
|
|
13039
|
-
for (const key of envKeys) {
|
|
13040
|
-
const value = normalizeToken6(process.env[key]);
|
|
13041
|
-
if (value) {
|
|
13042
|
-
return { token: value, source: "env", profile, runtimeControllerUrl: null };
|
|
13043
|
-
}
|
|
13044
|
-
}
|
|
13045
|
-
if (stored) {
|
|
13046
|
-
return { token: stored, source: "config", profile, runtimeControllerUrl: null };
|
|
13047
|
-
}
|
|
13048
|
-
return { token: null, source: "none", profile, runtimeControllerUrl: null };
|
|
13049
|
-
}
|
|
13050
|
-
function parseKeyValue(raw) {
|
|
13051
|
-
const trimmed = raw.trim();
|
|
13052
|
-
if (!trimmed) {
|
|
13053
|
-
throw new Error("Key/value pair cannot be empty");
|
|
13054
|
-
}
|
|
13055
|
-
const equals = trimmed.indexOf("=");
|
|
13056
|
-
if (equals === -1) {
|
|
13057
|
-
return { key: trimmed, value: "" };
|
|
13058
|
-
}
|
|
13059
|
-
const key = trimmed.slice(0, equals).trim();
|
|
13060
|
-
const value = trimmed.slice(equals + 1).trim();
|
|
13061
|
-
if (!key) {
|
|
13062
|
-
throw new Error(`Invalid pair "${raw}" (missing key)`);
|
|
13063
|
-
}
|
|
13064
|
-
return { key, value };
|
|
13065
|
-
}
|
|
13066
|
-
function parseHeader(raw) {
|
|
13067
|
-
const trimmed = raw.trim();
|
|
13068
|
-
if (!trimmed) {
|
|
13069
|
-
throw new Error("Header cannot be empty");
|
|
13070
|
-
}
|
|
13071
|
-
const colon = trimmed.indexOf(":");
|
|
13072
|
-
if (colon !== -1) {
|
|
13073
|
-
const key = trimmed.slice(0, colon).trim();
|
|
13074
|
-
const value = trimmed.slice(colon + 1).trim();
|
|
13075
|
-
if (!key) {
|
|
13076
|
-
throw new Error(`Invalid header "${raw}" (missing name)`);
|
|
13077
|
-
}
|
|
13078
|
-
return { key, value };
|
|
13079
|
-
}
|
|
13080
|
-
return parseKeyValue(trimmed);
|
|
13081
|
-
}
|
|
13082
|
-
function parseJsonBody(options) {
|
|
13083
|
-
if (options.jsonFile) {
|
|
13084
|
-
const raw = fs15.readFileSync(options.jsonFile, "utf8");
|
|
13085
|
-
const parsed = JSON.parse(raw);
|
|
13086
|
-
return JSON.stringify(parsed);
|
|
13087
|
-
}
|
|
13088
|
-
if (options.json) {
|
|
13089
|
-
const parsed = JSON.parse(options.json);
|
|
13090
|
-
return JSON.stringify(parsed);
|
|
13091
|
-
}
|
|
13092
|
-
return void 0;
|
|
13093
|
-
}
|
|
13094
|
-
function buildRequestUrl(options, runtimeControllerUrl) {
|
|
13095
|
-
const selectedControllerUrl = options.controllerUrl ?? runtimeControllerUrl;
|
|
13096
|
-
const base = new URL(
|
|
13097
|
-
`${normalizeUrl5(resolveControllerUrl({ controllerUrl: selectedControllerUrl ?? null }))}/`
|
|
13098
|
-
);
|
|
13099
|
-
if (base.protocol !== "http:" && base.protocol !== "https:") {
|
|
13100
|
-
throw new Error("The configured controller URL must use http or https.");
|
|
13101
|
-
}
|
|
13102
|
-
if (base.username || base.password) {
|
|
13103
|
-
throw new Error("The configured controller URL must not contain embedded credentials.");
|
|
13104
|
-
}
|
|
13105
|
-
if (runtimeControllerUrl) {
|
|
13106
|
-
resolveRuntimeBoundControllerUrl(
|
|
13107
|
-
{ controllerUrl: runtimeControllerUrl },
|
|
13108
|
-
base.toString()
|
|
13109
|
-
);
|
|
13110
|
-
}
|
|
13111
|
-
const rawPath = options.path.trim();
|
|
13112
|
-
if (!rawPath) {
|
|
13113
|
-
throw new Error("Path is required");
|
|
13114
|
-
}
|
|
13115
|
-
const isAbsoluteReference = /^[a-z][a-z\d+.-]*:/i.test(rawPath) || rawPath.startsWith("//");
|
|
13116
|
-
const url = new URL(
|
|
13117
|
-
isAbsoluteReference ? rawPath : rawPath.startsWith("/") ? rawPath : `/${rawPath}`,
|
|
13118
|
-
base
|
|
13119
|
-
);
|
|
13120
|
-
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
13121
|
-
throw new Error("Controller API URLs must use http or https.");
|
|
13122
|
-
}
|
|
13123
|
-
if (url.username || url.password) {
|
|
13124
|
-
throw new Error("Controller API URLs must not contain embedded credentials.");
|
|
13125
|
-
}
|
|
13126
|
-
const crossOrigin = url.origin !== base.origin;
|
|
13127
|
-
if (crossOrigin && !options.allowCrossOrigin) {
|
|
13128
|
-
throw new Error(
|
|
13129
|
-
"Refusing a cross-origin controller API request. Set --server-url to that origin and use a relative path, or pass --allow-cross-origin only when you explicitly intend to send credentials and request data there."
|
|
13130
|
-
);
|
|
13131
|
-
}
|
|
13132
|
-
for (const pair of options.query ?? []) {
|
|
13133
|
-
const { key, value } = parseKeyValue(pair);
|
|
13134
|
-
url.searchParams.set(key, value);
|
|
13135
|
-
}
|
|
13136
|
-
return { url, crossOrigin };
|
|
13137
|
-
}
|
|
13138
|
-
function maybePrettyPrintJson(text2, pretty) {
|
|
13139
|
-
if (!pretty) return text2;
|
|
13140
|
-
if (!text2.trim()) return text2;
|
|
13141
|
-
try {
|
|
13142
|
-
return JSON.stringify(JSON.parse(text2), null, 2);
|
|
13143
|
-
} catch {
|
|
13144
|
-
return text2;
|
|
13145
|
-
}
|
|
13146
|
-
}
|
|
13147
|
-
async function executeControllerApiRequest(options, bodyOverride) {
|
|
13148
|
-
const resolved = resolveBearerTokenWithSource(options);
|
|
13149
|
-
const { url, crossOrigin } = buildRequestUrl(options, resolved.runtimeControllerUrl);
|
|
13150
|
-
const bearer = resolved.token;
|
|
13151
|
-
if (crossOrigin && (!bearer || resolved.source !== "explicit")) {
|
|
13152
|
-
throw new Error(
|
|
13153
|
-
"Cross-origin controller API requests require --allow-cross-origin and an explicit --access-token."
|
|
13154
|
-
);
|
|
13155
|
-
}
|
|
13156
|
-
const headers = new Headers();
|
|
13157
|
-
headers.set("accept", "application/json");
|
|
13158
|
-
for (const header of options.headers ?? []) {
|
|
13159
|
-
const { key, value } = parseHeader(header);
|
|
13160
|
-
headers.set(key, value);
|
|
13161
|
-
}
|
|
13162
|
-
const body = bodyOverride ?? parseJsonBody(options);
|
|
13163
|
-
if (body !== void 0 && !headers.has("content-type")) {
|
|
13164
|
-
headers.set("content-type", "application/json");
|
|
13165
|
-
}
|
|
13166
|
-
const init = {
|
|
13167
|
-
method: options.method,
|
|
13168
|
-
headers,
|
|
13169
|
-
body,
|
|
13170
|
-
redirect: "error"
|
|
13171
|
-
};
|
|
13172
|
-
const response = bearer ? (await fetchWithControllerAuth({
|
|
13173
|
-
url: url.toString(),
|
|
13174
|
-
init,
|
|
13175
|
-
accessToken: bearer,
|
|
13176
|
-
tokenSource: resolved.source,
|
|
13177
|
-
profile: resolved.profile,
|
|
13178
|
-
cwd: process.cwd()
|
|
13179
|
-
})).response : await fetch(url, init);
|
|
13180
|
-
const responseText = await response.text().catch(() => "");
|
|
13181
|
-
const contentType = response.headers.get("content-type") ?? "";
|
|
13182
|
-
const isJson = contentType.includes("application/json") || contentType.includes("+json");
|
|
13183
|
-
if (!response.ok) {
|
|
13184
|
-
if (response.status === 401 || response.status === 403) {
|
|
13185
|
-
throw formatAuthRejectedError({
|
|
13186
|
-
status: response.status,
|
|
13187
|
-
responseBody: responseText,
|
|
13188
|
-
retryCommand: "instafy login",
|
|
13189
|
-
advancedHint: "pass --access-token, or set INSTAFY_ACCESS_TOKEN / SUPABASE_ACCESS_TOKEN"
|
|
13190
|
-
});
|
|
13191
|
-
}
|
|
13192
|
-
const formattedBody = isJson ? maybePrettyPrintJson(responseText, true) : responseText;
|
|
13193
|
-
const prefix = `Request failed (${response.status} ${response.statusText})`;
|
|
13194
|
-
const suffix = formattedBody.trim() ? `: ${formattedBody}` : "";
|
|
13195
|
-
throw new Error(`${prefix}${suffix}`);
|
|
13196
|
-
}
|
|
13197
|
-
return { response, responseText, isJson };
|
|
13198
|
-
}
|
|
13199
|
-
async function requestControllerApiJson(options) {
|
|
13200
|
-
const body = options.jsonBody === void 0 ? void 0 : JSON.stringify(options.jsonBody);
|
|
13201
|
-
const { responseText, isJson } = await executeControllerApiRequest(
|
|
13202
|
-
{
|
|
13203
|
-
method: options.method,
|
|
13204
|
-
path: options.path,
|
|
13205
|
-
controllerUrl: options.controllerUrl,
|
|
13206
|
-
accessToken: options.accessToken,
|
|
13207
|
-
serviceToken: options.serviceToken,
|
|
13208
|
-
query: options.query,
|
|
13209
|
-
headers: options.headers,
|
|
13210
|
-
allowCrossOrigin: options.allowCrossOrigin
|
|
13211
|
-
},
|
|
13212
|
-
body
|
|
13213
|
-
);
|
|
13214
|
-
if (!isJson) {
|
|
13215
|
-
throw new Error("Expected JSON response from controller API");
|
|
13216
|
-
}
|
|
13217
|
-
return JSON.parse(responseText);
|
|
13218
|
-
}
|
|
13219
|
-
async function requestControllerApi(options) {
|
|
13220
|
-
const { responseText, isJson } = await executeControllerApiRequest(options);
|
|
13221
|
-
const pretty = options.pretty !== false;
|
|
13222
|
-
const formattedBody = isJson ? maybePrettyPrintJson(responseText, pretty) : responseText;
|
|
13223
|
-
if (formattedBody) console.log(formattedBody);
|
|
13224
|
-
}
|
|
13225
|
-
|
|
13226
13794
|
// src/conversations.ts
|
|
13227
13795
|
init_project_manifest();
|
|
13228
13796
|
import kleur11 from "kleur";
|
|
@@ -14773,10 +15341,10 @@ function projectId(explicit) {
|
|
|
14773
15341
|
if (!value?.trim()) throw new Error("No space configured. Pass --space, set SPACE_ID, or run `instafy space init`.");
|
|
14774
15342
|
return uuid(value, "Space ID");
|
|
14775
15343
|
}
|
|
14776
|
-
function
|
|
15344
|
+
function object2(value, fields2, label) {
|
|
14777
15345
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object.`);
|
|
14778
15346
|
const record2 = value;
|
|
14779
|
-
if (Object.keys(record2).some((key) => !
|
|
15347
|
+
if (Object.keys(record2).some((key) => !fields2.includes(key))) throw new Error(`${label} contains an unsupported field.`);
|
|
14780
15348
|
return record2;
|
|
14781
15349
|
}
|
|
14782
15350
|
function text(value, label, max) {
|
|
@@ -14831,7 +15399,7 @@ async function readProposal(file) {
|
|
|
14831
15399
|
} catch {
|
|
14832
15400
|
throw new Error("Recommendation file must contain valid JSON.");
|
|
14833
15401
|
}
|
|
14834
|
-
const input3 =
|
|
15402
|
+
const input3 = object2(parsed, ["key", "title", "reason", "prompt", "message", "evidence"], "Recommendation");
|
|
14835
15403
|
const key = text(input3.key, "Recommendation key", 120);
|
|
14836
15404
|
if (!/^[a-z0-9][a-z0-9_-]{0,119}$/.test(key)) throw new Error("Recommendation key must be a lowercase slug using letters, digits, hyphens or underscores.");
|
|
14837
15405
|
if (!Array.isArray(input3.evidence) || input3.evidence.length < 1 || input3.evidence.length > 8) {
|
|
@@ -14844,7 +15412,7 @@ async function readProposal(file) {
|
|
|
14844
15412
|
prompt: text(input3.prompt, "Recommendation prompt", 4e3),
|
|
14845
15413
|
...input3.message === void 0 ? {} : { message: text(input3.message, "Recommendation message", 4e3) },
|
|
14846
15414
|
evidence: input3.evidence.map((value) => {
|
|
14847
|
-
const entry =
|
|
15415
|
+
const entry = object2(value, ["conversationId", "messageId"], "Evidence");
|
|
14848
15416
|
return {
|
|
14849
15417
|
conversationId: uuid(entry.conversationId, "Evidence conversationId"),
|
|
14850
15418
|
...entry.messageId === void 0 ? {} : { messageId: uuid(entry.messageId, "Evidence messageId") }
|
|
@@ -16397,7 +16965,7 @@ async function diagnosticsRunResult(options) {
|
|
|
16397
16965
|
|
|
16398
16966
|
// src/support.ts
|
|
16399
16967
|
import fs19 from "node:fs";
|
|
16400
|
-
import { randomUUID as
|
|
16968
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
16401
16969
|
import path20 from "node:path";
|
|
16402
16970
|
init_project_manifest();
|
|
16403
16971
|
var MAX_SUMMARY_CHARS = 500;
|
|
@@ -16690,7 +17258,7 @@ async function supportReport(options) {
|
|
|
16690
17258
|
printJson3(preview);
|
|
16691
17259
|
return;
|
|
16692
17260
|
}
|
|
16693
|
-
const clientRequestId = cleanText4(options.clientRequestId) ??
|
|
17261
|
+
const clientRequestId = cleanText4(options.clientRequestId) ?? randomUUID4();
|
|
16694
17262
|
if (!UUID2.test(clientRequestId)) {
|
|
16695
17263
|
throw new Error("--client-request-id must be a UUID.");
|
|
16696
17264
|
}
|
|
@@ -16905,7 +17473,7 @@ async function supportReply(options) {
|
|
|
16905
17473
|
`A follow-up must be at most ${MAX_MESSAGE_CHARS} characters and ${MAX_MESSAGE_BYTES} bytes.`
|
|
16906
17474
|
);
|
|
16907
17475
|
}
|
|
16908
|
-
const clientRequestId = cleanText4(options.clientRequestId) ??
|
|
17476
|
+
const clientRequestId = cleanText4(options.clientRequestId) ?? randomUUID4();
|
|
16909
17477
|
if (!UUID2.test(clientRequestId)) {
|
|
16910
17478
|
throw new Error("--client-request-id must be a UUID.");
|
|
16911
17479
|
}
|
|
@@ -18349,19 +18917,43 @@ function exitWithPreviewError(error) {
|
|
|
18349
18917
|
var previewCommand = program.command("preview").description("Look at sites running in this space, and show them in Studio as a private preview card");
|
|
18350
18918
|
failWithGroupHelp(previewCommand);
|
|
18351
18919
|
registerPreviewCheckCommand(previewCommand, { hasOpenPreview: hasOpenPreviewFor, uploadThumbnail: uploadCheckThumbnail });
|
|
18352
|
-
|
|
18920
|
+
var webCommand = registerWebSearchCommand(registerWebLinkCommand(registerWebCommand(program)));
|
|
18921
|
+
failWithGroupHelp(webCommand);
|
|
18922
|
+
var webSettingsCommand = webCommand.command("settings").description("Inspect or change the space's shared research budget for new tasks");
|
|
18923
|
+
addSpaceOption(webSettingsCommand, "Space UUID (defaults to .instafy/space.json or SPACE_ID)");
|
|
18924
|
+
addServerUrlOptions(webSettingsCommand);
|
|
18925
|
+
addAccessTokenOptions(webSettingsCommand, "Instafy access token");
|
|
18926
|
+
webSettingsCommand.option("--max-page-loads <n>", "Fresh pages allowed across the parent and all helpers").option("--max-page-loads-per-site <n>", "Fresh pages allowed from one site across the task").option("--interactive", "Interactively edit the shared research budget").option("--json", "Output JSON (noninteractive)").action(async (opts) => {
|
|
18927
|
+
try {
|
|
18928
|
+
await researchSettings({
|
|
18929
|
+
project: resolveSpaceIdOption(opts),
|
|
18930
|
+
controllerUrl: opts.serverUrl,
|
|
18931
|
+
accessToken: opts.accessToken,
|
|
18932
|
+
maxPageLoads: opts.maxPageLoads,
|
|
18933
|
+
maxPageLoadsPerSite: opts.maxPageLoadsPerSite,
|
|
18934
|
+
interactive: opts.interactive,
|
|
18935
|
+
json: opts.json
|
|
18936
|
+
});
|
|
18937
|
+
} catch (error) {
|
|
18938
|
+
console.error(kleur20.red(String(error)));
|
|
18939
|
+
process.exit(1);
|
|
18940
|
+
}
|
|
18941
|
+
});
|
|
18353
18942
|
var previewOpenCommand = previewCommand.command("open").description(
|
|
18354
|
-
"Open a private preview of a local port (exit 2: nothing listening, 3: port not allowed, 5: previews not available)"
|
|
18355
|
-
).
|
|
18943
|
+
"Open a private preview of a local port (exit 2: nothing listening, 3: port not allowed or another site's, 5: previews not available)"
|
|
18944
|
+
).option("--port <port>", "Local port the site listens on (1024-65535); with --start, leave it out and a free one is picked").requiredOption("--title <title>", "Card title (at most 120 characters)").option("--path <path>", "Path the preview opens at (default /)").option("--summary <text>", `One line of facts under the card's title, e.g. "Trip \xB7 3 days \xB7 14 places" (at most 80 characters); default: the page's own meta description`).option("--thumbnail <file>", "Use this picture on the card (JPEG, PNG or WebP, at most 512 KiB and 2000\xD72000) instead of taking one").option("--no-thumbnail", "Put no picture of the site on the card");
|
|
18356
18945
|
addSpaceOption(previewOpenCommand, "Space UUID (defaults to .instafy/space.json or SPACE_ID)");
|
|
18357
18946
|
addServerUrlOptions(previewOpenCommand);
|
|
18358
18947
|
addAccessTokenOptions(previewOpenCommand, "Instafy access token (defaults to the job token, then the saved `instafy login` token)");
|
|
18359
18948
|
previewOpenCommand.option("--conversation <id>", "Conversation for the card (only needed without a job token)").option("--rathole-bin <path>", "Path to rathole binary (or set RATHOLE_BIN)").option(
|
|
18360
18949
|
"--serve-cmd <command>",
|
|
18361
|
-
"How to start the site again when the process that started it exits
|
|
18950
|
+
"How to start the site, and to start it again when the process that started it exits or the space sleeps, run from the current directory (one command, no shell); {port} in it is the site's port; default: read from the process listening on the port"
|
|
18362
18951
|
).option(
|
|
18363
18952
|
"--start",
|
|
18364
|
-
"Start --serve-cmd in the background, in its own session, without Instafy credentials, logging to .instafy/agent/logs, and wait for the port (
|
|
18953
|
+
"Start --serve-cmd in the background, in its own session, without Instafy credentials, logging to .instafy/agent/logs, and wait for the port. Without --port it picks a free port (this same site's earlier one, when it has one) and puts it in {port}. A port where this same site already runs is used as it is; one where another site runs is refused"
|
|
18954
|
+
).option(
|
|
18955
|
+
"--replace",
|
|
18956
|
+
"Show this site on the port even when the port's open preview shows another site (another title or folder); without it that open is refused"
|
|
18365
18957
|
).option("--json", "Output JSON").action(async (opts) => {
|
|
18366
18958
|
try {
|
|
18367
18959
|
const result = await previewOpen({
|
|
@@ -18375,6 +18967,7 @@ previewOpenCommand.option("--conversation <id>", "Conversation for the card (onl
|
|
|
18375
18967
|
ratholeBin: opts.ratholeBin,
|
|
18376
18968
|
serveCmd: opts.serveCmd,
|
|
18377
18969
|
start: Boolean(opts.start),
|
|
18970
|
+
replace: Boolean(opts.replace),
|
|
18378
18971
|
summary: opts.summary,
|
|
18379
18972
|
thumbnail: opts.thumbnail !== false,
|
|
18380
18973
|
thumbnailFile: typeof opts.thumbnail === "string" ? opts.thumbnail : void 0
|
|
@@ -18400,7 +18993,7 @@ previewListCommand.option("--json", "Output JSON").action(async (opts) => {
|
|
|
18400
18993
|
exitWithPreviewError(error);
|
|
18401
18994
|
}
|
|
18402
18995
|
});
|
|
18403
|
-
var previewCloseCommand = previewCommand.command("close").description("Close a preview: stop its local client and revoke it").argument("<previewId>", "Preview ID (from `instafy preview list`)");
|
|
18996
|
+
var previewCloseCommand = previewCommand.command("close").description("Close a preview: stop its local client and the site `open --start` started for it, and revoke it").argument("<previewId>", "Preview ID (from `instafy preview list`)");
|
|
18404
18997
|
addSpaceOption(previewCloseCommand, "Space UUID (defaults to .instafy/space.json or SPACE_ID)");
|
|
18405
18998
|
addServerUrlOptions(previewCloseCommand);
|
|
18406
18999
|
addAccessTokenOptions(previewCloseCommand, "Instafy access token (defaults to the job token, then the saved `instafy login` token)");
|
|
@@ -18416,7 +19009,7 @@ previewCloseCommand.option("--json", "Output JSON").action(async (previewId, opt
|
|
|
18416
19009
|
console.log(JSON.stringify(result, null, 2));
|
|
18417
19010
|
return;
|
|
18418
19011
|
}
|
|
18419
|
-
console.log(kleur20.green(`Preview closed: ${result.previewId}`));
|
|
19012
|
+
console.log(kleur20.green(`Preview closed: ${result.previewId}${result.siteStopped ? "; its site was stopped" : ""}`));
|
|
18420
19013
|
} catch (error) {
|
|
18421
19014
|
exitWithPreviewError(error);
|
|
18422
19015
|
}
|