@great-arrow/cli 0.1.35 → 0.1.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1451 -1237
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -9399,7 +9399,7 @@ function isNodeError(value) {
|
|
|
9399
9399
|
|
|
9400
9400
|
// src/lib/version.ts
|
|
9401
9401
|
var CLI_PACKAGE_NAME = "@great-arrow/cli";
|
|
9402
|
-
var CLI_VERSION = "0.1.
|
|
9402
|
+
var CLI_VERSION = "0.1.37";
|
|
9403
9403
|
var GAD_INSTALL_VERSION = "2026.08.08";
|
|
9404
9404
|
var GAD_INSTALL_MIN_VERSION = "2026.06.22";
|
|
9405
9405
|
var GAD_INSTALL_HEADER = "X-GAD-Install-Version";
|
|
@@ -16261,1384 +16261,1598 @@ function askCommand() {
|
|
|
16261
16261
|
return cmd;
|
|
16262
16262
|
}
|
|
16263
16263
|
|
|
16264
|
-
//
|
|
16265
|
-
|
|
16266
|
-
|
|
16267
|
-
|
|
16268
|
-
lmstudio: { baseUrl: "http://localhost:1234", label: "LM Studio" }
|
|
16269
|
-
};
|
|
16270
|
-
var LOCAL_DEFAULT_BASE_URL = LOCAL_PROVIDER_PRESETS.ollama.baseUrl;
|
|
16271
|
-
var MAX_LOCAL_MODELS = 50;
|
|
16272
|
-
var MAX_LOCAL_MODEL_ID_LEN = 128;
|
|
16273
|
-
var LOCAL_MODEL_ID_RE = /^[a-zA-Z0-9][a-zA-Z0-9._:/-]{0,127}$/;
|
|
16274
|
-
function isValidLocalModelId(id) {
|
|
16275
|
-
return typeof id === "string" && id.length > 0 && id.length <= MAX_LOCAL_MODEL_ID_LEN && LOCAL_MODEL_ID_RE.test(id);
|
|
16276
|
-
}
|
|
16277
|
-
function isValidLocalBaseUrl(value) {
|
|
16278
|
-
if (typeof value !== "string" || value.length > 200) return false;
|
|
16279
|
-
let url;
|
|
16280
|
-
try {
|
|
16281
|
-
url = new URL(value);
|
|
16282
|
-
} catch {
|
|
16283
|
-
return false;
|
|
16284
|
-
}
|
|
16285
|
-
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
|
|
16286
|
-
if (url.username || url.password) return false;
|
|
16287
|
-
const host = url.hostname.toLowerCase();
|
|
16288
|
-
return host === "localhost" || host === "127.0.0.1" || host === "[::1]" || host === "::1";
|
|
16289
|
-
}
|
|
16290
|
-
|
|
16291
|
-
// ../../src/lib/ai/local/delegate.ts
|
|
16292
|
-
var LOCAL_CAPABLE_HEADER = "x-gad-local-capable";
|
|
16293
|
-
var LOCAL_CAPABLE_VALUE = "1";
|
|
16294
|
-
var LOCAL_TURN_HEADER = "x-gad-local-turn";
|
|
16295
|
-
var UNFINISHED_TURN_AFTER_MS = 6 * 6e4;
|
|
16296
|
-
|
|
16297
|
-
// ../../src/lib/chat/agentic-steps.ts
|
|
16298
|
-
var MAX_AGENTIC_STEPS = 6;
|
|
16299
|
-
|
|
16300
|
-
// ../../src/lib/ai/local/endpoint.ts
|
|
16301
|
-
function localApiUrl(baseUrl, path10) {
|
|
16302
|
-
return `${baseUrl.replace(/\/+$/, "")}${path10}`;
|
|
16303
|
-
}
|
|
16264
|
+
// src/lib/glance-footer.ts
|
|
16265
|
+
import { readFile as readFile4, writeFile as writeFile4, mkdir as mkdir4 } from "node:fs/promises";
|
|
16266
|
+
import { homedir as homedir4 } from "node:os";
|
|
16267
|
+
import { join as join6, dirname as dirname5 } from "node:path";
|
|
16304
16268
|
|
|
16305
|
-
//
|
|
16306
|
-
var
|
|
16307
|
-
var
|
|
16308
|
-
|
|
16309
|
-
|
|
16310
|
-
|
|
16311
|
-
|
|
16312
|
-
|
|
16313
|
-
|
|
16314
|
-
|
|
16315
|
-
|
|
16316
|
-
|
|
16317
|
-
const slot = next[at];
|
|
16318
|
-
if (delta.id) slot.id = delta.id;
|
|
16319
|
-
if (delta.name) slot.name = delta.name;
|
|
16320
|
-
if (delta.argumentsFragment) {
|
|
16321
|
-
slot.arguments = (slot.arguments + delta.argumentsFragment).slice(
|
|
16322
|
-
0,
|
|
16323
|
-
MAX_TOOL_ARGUMENT_CHARS
|
|
16324
|
-
);
|
|
16325
|
-
}
|
|
16326
|
-
}
|
|
16327
|
-
return next;
|
|
16328
|
-
}
|
|
16329
|
-
function finalizeToolCalls(calls) {
|
|
16330
|
-
return calls.filter((call) => call.name.length > 0).slice(0, MAX_TOOL_CALLS_PER_STEP).map((call, index) => ({
|
|
16331
|
-
...call,
|
|
16332
|
-
id: call.id || `call_${index}`,
|
|
16333
|
-
arguments: call.arguments || "{}"
|
|
16334
|
-
}));
|
|
16335
|
-
}
|
|
16336
|
-
function parseToolArguments(raw) {
|
|
16337
|
-
if (raw.length > MAX_TOOL_ARGUMENT_CHARS) return null;
|
|
16338
|
-
let parsed;
|
|
16269
|
+
// src/lib/seen.ts
|
|
16270
|
+
var SEEN_TIMEOUT_MS = 1e3;
|
|
16271
|
+
var MAX_KEYS = 50;
|
|
16272
|
+
async function reportSeen(opts) {
|
|
16273
|
+
const keys = [
|
|
16274
|
+
...new Set(
|
|
16275
|
+
opts.keys.filter((k) => typeof k === "string" && k !== "")
|
|
16276
|
+
)
|
|
16277
|
+
].slice(0, MAX_KEYS);
|
|
16278
|
+
if (keys.length === 0) return 0;
|
|
16279
|
+
const controller = new AbortController();
|
|
16280
|
+
const timer = setTimeout(() => controller.abort(), SEEN_TIMEOUT_MS);
|
|
16339
16281
|
try {
|
|
16340
|
-
|
|
16282
|
+
const response = await fetch(
|
|
16283
|
+
`${opts.apiUrl.replace(/\/$/, "")}/api/attention/seen`,
|
|
16284
|
+
{
|
|
16285
|
+
method: "POST",
|
|
16286
|
+
headers: {
|
|
16287
|
+
"Content-Type": "application/json",
|
|
16288
|
+
Authorization: `Bearer ${opts.token}`
|
|
16289
|
+
},
|
|
16290
|
+
body: JSON.stringify({ keys }),
|
|
16291
|
+
signal: controller.signal
|
|
16292
|
+
}
|
|
16293
|
+
);
|
|
16294
|
+
if (!response.ok) return 0;
|
|
16295
|
+
const data = await response.json().catch(() => null);
|
|
16296
|
+
return typeof data?.seen === "number" ? data.seen : 0;
|
|
16341
16297
|
} catch {
|
|
16342
|
-
return
|
|
16343
|
-
}
|
|
16344
|
-
|
|
16345
|
-
return null;
|
|
16298
|
+
return 0;
|
|
16299
|
+
} finally {
|
|
16300
|
+
clearTimeout(timer);
|
|
16346
16301
|
}
|
|
16347
|
-
return parsed;
|
|
16348
16302
|
}
|
|
16349
|
-
|
|
16350
|
-
|
|
16303
|
+
|
|
16304
|
+
// src/lib/glance-footer.ts
|
|
16305
|
+
var FETCH_TIMEOUT_MS = 1500;
|
|
16306
|
+
var THROTTLE_INTERVAL_MS = 4 * 60 * 60 * 1e3;
|
|
16307
|
+
var TIP_THROTTLE_INTERVAL_MS = 20 * 60 * 60 * 1e3;
|
|
16308
|
+
var URGENT_THRESHOLD = 15;
|
|
16309
|
+
var SKIP_COMMANDS = /* @__PURE__ */ new Set(["status", "needs", "login", "logout", "help"]);
|
|
16310
|
+
async function maybeShowGlanceFooter(argv) {
|
|
16351
16311
|
try {
|
|
16352
|
-
|
|
16353
|
-
|
|
16354
|
-
|
|
16312
|
+
if (!shouldRunGlanceFooter(argv)) return;
|
|
16313
|
+
const auth = await resolveAuth();
|
|
16314
|
+
const workspaceKey = auth.activeWorkspace ?? "default";
|
|
16315
|
+
const state = await loadGlanceState();
|
|
16316
|
+
const entry = state[workspaceKey] ?? {};
|
|
16317
|
+
const now = Date.now();
|
|
16318
|
+
const askForTip = shouldRequestTip({ state: entry, now });
|
|
16319
|
+
const payload = await fetchGlance(auth.apiUrl, auth.token, askForTip);
|
|
16320
|
+
if (!payload) return;
|
|
16321
|
+
const showNeedsYou = payload.urgency !== null && shouldShowGlance({
|
|
16322
|
+
urgency: payload.urgency,
|
|
16323
|
+
topItemId: topSignature(payload),
|
|
16324
|
+
state: entry,
|
|
16325
|
+
now
|
|
16326
|
+
});
|
|
16327
|
+
const tip = payload.tip;
|
|
16328
|
+
if (!showNeedsYou && !tip) return;
|
|
16329
|
+
const printed = showNeedsYou ? renderFooter(payload, auth.apiUrl) : [];
|
|
16330
|
+
if (tip) renderTip(tip, auth.apiUrl);
|
|
16331
|
+
state[workspaceKey] = {
|
|
16332
|
+
...entry,
|
|
16333
|
+
...showNeedsYou ? { lastShownAt: now, lastTopItemId: topSignature(payload) } : {},
|
|
16334
|
+
...tip ? { lastTipAt: now } : {}
|
|
16335
|
+
};
|
|
16336
|
+
await saveGlanceState(state);
|
|
16337
|
+
await reportSeen({
|
|
16338
|
+
apiUrl: auth.apiUrl,
|
|
16339
|
+
token: auth.token,
|
|
16340
|
+
keys: printed
|
|
16341
|
+
});
|
|
16342
|
+
} catch (err) {
|
|
16343
|
+
debugSwallow("glance-footer", err);
|
|
16355
16344
|
}
|
|
16356
|
-
if (!text2) return "{}";
|
|
16357
|
-
return text2.length > MAX_TOOL_RESULT_CHARS ? `${text2.slice(0, MAX_TOOL_RESULT_CHARS)}\u2026 (shortened)` : text2;
|
|
16358
|
-
}
|
|
16359
|
-
function assistantToolCallMessage(calls) {
|
|
16360
|
-
return {
|
|
16361
|
-
role: "assistant",
|
|
16362
|
-
content: "",
|
|
16363
|
-
toolCalls: calls.map((call) => ({
|
|
16364
|
-
id: call.id,
|
|
16365
|
-
type: "function",
|
|
16366
|
-
function: { name: call.name, arguments: call.arguments }
|
|
16367
|
-
}))
|
|
16368
|
-
};
|
|
16369
16345
|
}
|
|
16370
|
-
function
|
|
16371
|
-
return
|
|
16372
|
-
|
|
16373
|
-
|
|
16374
|
-
|
|
16375
|
-
|
|
16376
|
-
|
|
16346
|
+
function shouldRunGlanceFooter(argv) {
|
|
16347
|
+
if (!process.stderr.isTTY) return false;
|
|
16348
|
+
if (process.env.CI) return false;
|
|
16349
|
+
if (process.env.GAD_NO_GLANCE) return false;
|
|
16350
|
+
if (argv.includes("--json")) return false;
|
|
16351
|
+
if (argv.includes("--quiet")) return false;
|
|
16352
|
+
if (argv.includes("--stdout")) return false;
|
|
16353
|
+
if (argv.includes("--no-glance")) return false;
|
|
16354
|
+
const command = firstCommand(argv);
|
|
16355
|
+
if (!command) return false;
|
|
16356
|
+
if (SKIP_COMMANDS.has(command)) return false;
|
|
16357
|
+
return true;
|
|
16377
16358
|
}
|
|
16378
|
-
|
|
16379
|
-
|
|
16380
|
-
|
|
16381
|
-
constructor(message) {
|
|
16382
|
-
super(message);
|
|
16383
|
-
this.name = "LocalRunError";
|
|
16384
|
-
}
|
|
16385
|
-
};
|
|
16386
|
-
var CHAT_PATH = {
|
|
16387
|
-
ollama: "/api/chat",
|
|
16388
|
-
lmstudio: "/v1/chat/completions"
|
|
16389
|
-
};
|
|
16390
|
-
async function* readLines(body) {
|
|
16391
|
-
const reader = body.getReader();
|
|
16392
|
-
const decoder = new TextDecoder();
|
|
16393
|
-
let buffer = "";
|
|
16394
|
-
while (true) {
|
|
16395
|
-
const { done, value } = await reader.read();
|
|
16396
|
-
if (done) break;
|
|
16397
|
-
buffer += decoder.decode(value, { stream: true });
|
|
16398
|
-
let index = buffer.indexOf("\n");
|
|
16399
|
-
while (index >= 0) {
|
|
16400
|
-
const line = buffer.slice(0, index).trim();
|
|
16401
|
-
buffer = buffer.slice(index + 1);
|
|
16402
|
-
if (line) yield line;
|
|
16403
|
-
index = buffer.indexOf("\n");
|
|
16404
|
-
}
|
|
16359
|
+
function firstCommand(argv) {
|
|
16360
|
+
for (const token of argv.slice(2)) {
|
|
16361
|
+
if (!token.startsWith("-")) return token;
|
|
16405
16362
|
}
|
|
16406
|
-
|
|
16407
|
-
if (tail) yield tail;
|
|
16408
|
-
}
|
|
16409
|
-
function numberOrUndefined(value) {
|
|
16410
|
-
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : void 0;
|
|
16363
|
+
return null;
|
|
16411
16364
|
}
|
|
16412
|
-
function
|
|
16413
|
-
|
|
16365
|
+
async function fetchGlance(apiUrl, token, includeTip) {
|
|
16366
|
+
const controller = new AbortController();
|
|
16367
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
16414
16368
|
try {
|
|
16415
|
-
|
|
16369
|
+
const response = await fetch(`${apiUrl}/api/mcp`, {
|
|
16370
|
+
method: "POST",
|
|
16371
|
+
headers: {
|
|
16372
|
+
"Content-Type": "application/json",
|
|
16373
|
+
Authorization: `Bearer ${token}`
|
|
16374
|
+
},
|
|
16375
|
+
body: JSON.stringify({
|
|
16376
|
+
jsonrpc: "2.0",
|
|
16377
|
+
id: `cli-glance-${Date.now()}`,
|
|
16378
|
+
method: "tools/call",
|
|
16379
|
+
params: {
|
|
16380
|
+
name: "glance",
|
|
16381
|
+
// Omitted entirely rather than sent as `false`, so an older server
|
|
16382
|
+
// that does not know this argument sees the request it always saw.
|
|
16383
|
+
arguments: includeTip ? { include_tip: true } : {}
|
|
16384
|
+
}
|
|
16385
|
+
}),
|
|
16386
|
+
signal: controller.signal
|
|
16387
|
+
});
|
|
16388
|
+
if (!response.ok) return null;
|
|
16389
|
+
const data = await response.json();
|
|
16390
|
+
if (data.error) return null;
|
|
16391
|
+
return normalizeGlance(
|
|
16392
|
+
parseMcpPayload(data.result?.content ?? data.result)
|
|
16393
|
+
);
|
|
16416
16394
|
} catch {
|
|
16417
|
-
return
|
|
16395
|
+
return null;
|
|
16396
|
+
} finally {
|
|
16397
|
+
clearTimeout(timer);
|
|
16418
16398
|
}
|
|
16419
16399
|
}
|
|
16420
|
-
function
|
|
16421
|
-
|
|
16422
|
-
|
|
16423
|
-
const
|
|
16424
|
-
|
|
16425
|
-
|
|
16426
|
-
|
|
16427
|
-
|
|
16428
|
-
|
|
16429
|
-
|
|
16430
|
-
|
|
16431
|
-
|
|
16432
|
-
|
|
16433
|
-
|
|
16434
|
-
|
|
16435
|
-
|
|
16436
|
-
|
|
16437
|
-
|
|
16438
|
-
|
|
16439
|
-
|
|
16440
|
-
|
|
16441
|
-
|
|
16400
|
+
function normalizeGlance(raw) {
|
|
16401
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
16402
|
+
const obj = raw;
|
|
16403
|
+
const needsYou = asArray2(obj.needsYou).flatMap((item) => {
|
|
16404
|
+
if (typeof item !== "object" || item === null) return [];
|
|
16405
|
+
const it = item;
|
|
16406
|
+
if (typeof it.title !== "string") return [];
|
|
16407
|
+
return [
|
|
16408
|
+
{
|
|
16409
|
+
kind: typeof it.kind === "string" ? it.kind : "nudge",
|
|
16410
|
+
title: it.title,
|
|
16411
|
+
action: typeof it.action === "string" ? it.action : null,
|
|
16412
|
+
href: typeof it.href === "string" && it.href !== "" ? it.href : null,
|
|
16413
|
+
...typeof it.rank === "number" ? { rank: it.rank } : {},
|
|
16414
|
+
signalKey: typeof it.signalKey === "string" && it.signalKey !== "" ? it.signalKey : null,
|
|
16415
|
+
workspace: typeof it.workspace === "string" && it.workspace !== "" ? it.workspace : null,
|
|
16416
|
+
// Parsed like every other field: a non-string is dropped rather than
|
|
16417
|
+
// coerced, so a malformed payload cannot render as a currency claim.
|
|
16418
|
+
freshness: typeof it.freshness === "string" && it.freshness !== "" ? it.freshness : null,
|
|
16419
|
+
meta: typeof it.meta === "string" && it.meta !== "" ? it.meta : null
|
|
16420
|
+
}
|
|
16421
|
+
];
|
|
16422
|
+
});
|
|
16423
|
+
const atRisk = asArray2(obj.atRisk).flatMap((item) => {
|
|
16424
|
+
if (typeof item !== "object" || item === null) return [];
|
|
16425
|
+
const it = item;
|
|
16426
|
+
if (typeof it.title !== "string") return [];
|
|
16427
|
+
return [
|
|
16428
|
+
{
|
|
16429
|
+
classification: typeof it.classification === "string" ? it.classification : null,
|
|
16430
|
+
riskLevel: typeof it.riskLevel === "string" ? it.riskLevel : "high",
|
|
16431
|
+
title: it.title,
|
|
16432
|
+
branch: typeof it.branch === "string" ? it.branch : null,
|
|
16433
|
+
aheadMain: typeof it.aheadMain === "number" ? it.aheadMain : null,
|
|
16434
|
+
recommendedAction: typeof it.recommendedAction === "string" ? it.recommendedAction : null
|
|
16435
|
+
}
|
|
16436
|
+
];
|
|
16437
|
+
});
|
|
16438
|
+
const integrations = asArray2(obj.integrations).flatMap((item) => {
|
|
16439
|
+
if (typeof item !== "object" || item === null) return [];
|
|
16440
|
+
const it = item;
|
|
16441
|
+
if (typeof it.kind !== "string") return [];
|
|
16442
|
+
return [
|
|
16443
|
+
{
|
|
16444
|
+
kind: it.kind,
|
|
16445
|
+
staleFor: typeof it.staleFor === "string" ? it.staleFor : null,
|
|
16446
|
+
refresh: typeof it.refresh === "string" ? it.refresh : null
|
|
16447
|
+
}
|
|
16448
|
+
];
|
|
16449
|
+
});
|
|
16450
|
+
const agents = asArray2(obj.agents).flatMap((item) => {
|
|
16451
|
+
if (typeof item !== "object" || item === null) return [];
|
|
16452
|
+
const it = item;
|
|
16453
|
+
if (typeof it.name !== "string") return [];
|
|
16454
|
+
return [
|
|
16455
|
+
{
|
|
16456
|
+
name: it.name,
|
|
16457
|
+
tool: typeof it.tool === "string" ? it.tool : null,
|
|
16458
|
+
card: typeof it.card === "string" ? it.card : null
|
|
16459
|
+
}
|
|
16460
|
+
];
|
|
16461
|
+
});
|
|
16462
|
+
let topGoal = null;
|
|
16463
|
+
if (typeof obj.topGoal === "object" && obj.topGoal !== null) {
|
|
16464
|
+
const g = obj.topGoal;
|
|
16465
|
+
if (typeof g.title === "string") {
|
|
16466
|
+
topGoal = {
|
|
16467
|
+
title: g.title,
|
|
16468
|
+
done: typeof g.done === "number" ? g.done : 0,
|
|
16469
|
+
total: typeof g.total === "number" ? g.total : 0,
|
|
16470
|
+
eta: typeof g.eta === "string" ? g.eta : null
|
|
16471
|
+
};
|
|
16472
|
+
}
|
|
16473
|
+
}
|
|
16474
|
+
let warRoom = null;
|
|
16475
|
+
if (typeof obj.warRoom === "object" && obj.warRoom !== null) {
|
|
16476
|
+
const w = obj.warRoom;
|
|
16477
|
+
warRoom = {
|
|
16478
|
+
priorityCount: typeof w.priorityCount === "number" ? w.priorityCount : 0,
|
|
16479
|
+
unreadTotal: typeof w.unreadTotal === "number" ? w.unreadTotal : 0,
|
|
16480
|
+
openRequestCount: typeof w.openRequestCount === "number" ? w.openRequestCount : 0,
|
|
16481
|
+
topSender: typeof w.topSender === "string" ? w.topSender : null,
|
|
16482
|
+
newestSender: typeof w.newestSender === "string" ? w.newestSender : null,
|
|
16483
|
+
newestPreview: typeof w.newestPreview === "string" ? w.newestPreview : null
|
|
16484
|
+
};
|
|
16485
|
+
}
|
|
16486
|
+
let tip = null;
|
|
16487
|
+
if (typeof obj.tip === "object" && obj.tip !== null) {
|
|
16488
|
+
const t = obj.tip;
|
|
16489
|
+
if (typeof t.text === "string" && t.text.trim() !== "") {
|
|
16490
|
+
tip = {
|
|
16491
|
+
id: typeof t.id === "string" ? t.id : "",
|
|
16492
|
+
text: t.text,
|
|
16493
|
+
deepLink: typeof t.deepLink === "string" ? t.deepLink : null,
|
|
16494
|
+
whySeen: typeof t.whySeen === "string" ? t.whySeen : null
|
|
16495
|
+
};
|
|
16496
|
+
}
|
|
16442
16497
|
}
|
|
16443
|
-
if (!parsed || typeof parsed !== "object") return null;
|
|
16444
|
-
const row = parsed;
|
|
16445
|
-
if (typeof row.error === "string") throw new LocalRunError(row.error);
|
|
16446
|
-
const message = row.message;
|
|
16447
|
-
const delta = typeof message?.content === "string" ? message.content : "";
|
|
16448
16498
|
return {
|
|
16449
|
-
|
|
16450
|
-
|
|
16451
|
-
|
|
16452
|
-
|
|
16499
|
+
tip,
|
|
16500
|
+
workspace: typeof obj.workspace === "string" && obj.workspace !== "" ? obj.workspace : null,
|
|
16501
|
+
// An older server sends no `reads` at all, so this normalizes to an empty
|
|
16502
|
+
// list rather than inventing the pinned workspace as the read set — the
|
|
16503
|
+
// CLI then renders no breadth cue, which is honest, instead of a claim it
|
|
16504
|
+
// was never told.
|
|
16505
|
+
reads: Array.isArray(obj.reads) ? obj.reads.flatMap((r) => {
|
|
16506
|
+
if (typeof r !== "object" || r === null) return [];
|
|
16507
|
+
const row = r;
|
|
16508
|
+
return typeof row.id === "string" && typeof row.name === "string" ? [{ id: row.id, name: row.name }] : [];
|
|
16509
|
+
}) : [],
|
|
16510
|
+
needsYou,
|
|
16511
|
+
atRisk,
|
|
16512
|
+
topGoal,
|
|
16513
|
+
integrations,
|
|
16514
|
+
agents,
|
|
16515
|
+
warRoom,
|
|
16516
|
+
// Parsed like every other field: both halves must be the right type or the
|
|
16517
|
+
// whole line is dropped, so a malformed payload cannot render as a dollar.
|
|
16518
|
+
spend: (() => {
|
|
16519
|
+
const raw2 = obj.spend;
|
|
16520
|
+
if (typeof raw2 !== "object" || raw2 === null) return null;
|
|
16521
|
+
const sp = raw2;
|
|
16522
|
+
return typeof sp.line === "string" && sp.line !== "" && typeof sp.totalCostUsd === "number" && Number.isFinite(sp.totalCostUsd) ? { line: sp.line, totalCostUsd: sp.totalCostUsd } : null;
|
|
16523
|
+
})(),
|
|
16524
|
+
urgency: typeof obj.urgency === "number" ? obj.urgency : null,
|
|
16525
|
+
generatedAt: typeof obj.generatedAt === "string" ? obj.generatedAt : (/* @__PURE__ */ new Date()).toISOString()
|
|
16453
16526
|
};
|
|
16454
16527
|
}
|
|
16455
|
-
function
|
|
16456
|
-
|
|
16457
|
-
if (!Array.isArray(raw)) return [];
|
|
16458
|
-
const out = [];
|
|
16459
|
-
raw.forEach((entry, fallbackPosition) => {
|
|
16460
|
-
if (!entry || typeof entry !== "object") return;
|
|
16461
|
-
const row = entry;
|
|
16462
|
-
const fn = row.function;
|
|
16463
|
-
out.push({
|
|
16464
|
-
position: typeof row.index === "number" ? row.index : fallbackPosition,
|
|
16465
|
-
id: typeof row.id === "string" ? row.id : void 0,
|
|
16466
|
-
name: typeof fn?.name === "string" ? fn.name : void 0,
|
|
16467
|
-
argumentsFragment: typeof fn?.arguments === "string" ? fn.arguments : void 0
|
|
16468
|
-
});
|
|
16469
|
-
});
|
|
16470
|
-
return out;
|
|
16528
|
+
function asArray2(value) {
|
|
16529
|
+
return Array.isArray(value) ? value : [];
|
|
16471
16530
|
}
|
|
16472
|
-
function
|
|
16473
|
-
if (
|
|
16474
|
-
|
|
16475
|
-
if (!payload || payload === "[DONE]") return null;
|
|
16476
|
-
let parsed;
|
|
16477
|
-
try {
|
|
16478
|
-
parsed = JSON.parse(payload);
|
|
16479
|
-
} catch {
|
|
16480
|
-
return null;
|
|
16531
|
+
function topSignature(payload) {
|
|
16532
|
+
if (payload.warRoom && payload.warRoom.priorityCount > 0) {
|
|
16533
|
+
return `warroom:${payload.warRoom.newestSender ?? "peer"}`;
|
|
16481
16534
|
}
|
|
16482
|
-
|
|
16483
|
-
|
|
16484
|
-
const
|
|
16485
|
-
|
|
16486
|
-
const
|
|
16487
|
-
|
|
16488
|
-
|
|
16489
|
-
return
|
|
16490
|
-
delta,
|
|
16491
|
-
toolCalls: readOpenAiToolCalls(deltaObj),
|
|
16492
|
-
input: numberOrUndefined(usage?.prompt_tokens),
|
|
16493
|
-
output: numberOrUndefined(usage?.completion_tokens)
|
|
16494
|
-
};
|
|
16535
|
+
const top = payload.needsYou[0];
|
|
16536
|
+
if (top) return `needs:${top.kind}:${top.title}`;
|
|
16537
|
+
const integration = payload.integrations[0];
|
|
16538
|
+
if (integration) return `stale:${integration.kind}`;
|
|
16539
|
+
const agent = payload.agents[0];
|
|
16540
|
+
if (agent) return `agent:${agent.name}`;
|
|
16541
|
+
if (payload.topGoal) return `goal:${payload.topGoal.title}`;
|
|
16542
|
+
return "empty";
|
|
16495
16543
|
}
|
|
16496
|
-
function
|
|
16497
|
-
|
|
16498
|
-
|
|
16499
|
-
|
|
16500
|
-
|
|
16501
|
-
|
|
16502
|
-
} : {
|
|
16503
|
-
role: "tool",
|
|
16504
|
-
content: message.content,
|
|
16505
|
-
tool_call_id: message.toolCallId ?? ""
|
|
16506
|
-
};
|
|
16507
|
-
}
|
|
16508
|
-
if (message.role === "assistant" && message.toolCalls?.length) {
|
|
16509
|
-
return {
|
|
16510
|
-
role: "assistant",
|
|
16511
|
-
content: message.content,
|
|
16512
|
-
tool_calls: message.toolCalls.map(
|
|
16513
|
-
(call) => provider === "ollama" ? {
|
|
16514
|
-
function: {
|
|
16515
|
-
name: call.function.name,
|
|
16516
|
-
arguments: safeParseArguments(call.function.arguments)
|
|
16517
|
-
}
|
|
16518
|
-
} : call
|
|
16519
|
-
)
|
|
16520
|
-
};
|
|
16521
|
-
}
|
|
16522
|
-
return { role: message.role, content: message.content };
|
|
16544
|
+
function shouldShowGlance(input) {
|
|
16545
|
+
const { urgency, topItemId, state, now } = input;
|
|
16546
|
+
if (urgency > URGENT_THRESHOLD) return false;
|
|
16547
|
+
if (state.lastShownAt === void 0) return true;
|
|
16548
|
+
if (state.lastTopItemId !== topItemId) return true;
|
|
16549
|
+
return now - state.lastShownAt >= THROTTLE_INTERVAL_MS;
|
|
16523
16550
|
}
|
|
16524
|
-
function
|
|
16551
|
+
function shouldRequestTip(input) {
|
|
16552
|
+
const { state, now } = input;
|
|
16553
|
+
if (state.lastTipAt === void 0) return true;
|
|
16554
|
+
return now - state.lastTipAt >= TIP_THROTTLE_INTERVAL_MS;
|
|
16555
|
+
}
|
|
16556
|
+
function getGlanceStatePath() {
|
|
16557
|
+
return join6(homedir4(), ".gad", "glance-state.json");
|
|
16558
|
+
}
|
|
16559
|
+
async function loadGlanceState() {
|
|
16525
16560
|
try {
|
|
16526
|
-
const
|
|
16527
|
-
|
|
16561
|
+
const content = await readFile4(getGlanceStatePath(), "utf-8");
|
|
16562
|
+
const parsed = JSON.parse(content);
|
|
16563
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
16564
|
+
return parsed;
|
|
16565
|
+
}
|
|
16566
|
+
return {};
|
|
16528
16567
|
} catch {
|
|
16529
16568
|
return {};
|
|
16530
16569
|
}
|
|
16531
16570
|
}
|
|
16532
|
-
async function
|
|
16533
|
-
|
|
16534
|
-
|
|
16571
|
+
async function saveGlanceState(state) {
|
|
16572
|
+
const path10 = getGlanceStatePath();
|
|
16573
|
+
await mkdir4(dirname5(path10), { recursive: true });
|
|
16574
|
+
await writeFile4(path10, JSON.stringify(state), "utf-8");
|
|
16575
|
+
}
|
|
16576
|
+
function glanceItemUrl(href, apiUrl) {
|
|
16577
|
+
if (!href) return null;
|
|
16578
|
+
if (/^https?:\/\//i.test(href)) return href;
|
|
16579
|
+
if (!href.startsWith("/") || href.startsWith("//")) return null;
|
|
16580
|
+
return `${apiUrl.replace(/\/$/, "")}${href}`;
|
|
16581
|
+
}
|
|
16582
|
+
function renderFooter(payload, apiUrl) {
|
|
16583
|
+
const c = createColor(false);
|
|
16584
|
+
const lines = [];
|
|
16585
|
+
const printed = [];
|
|
16586
|
+
const wr = payload.warRoom;
|
|
16587
|
+
if (wr && wr.priorityCount > 0) {
|
|
16588
|
+
lines.push(
|
|
16589
|
+
`${c.yellow("!")} ${wr.priorityCount} war-room message${wr.priorityCount === 1 ? "" : "s"} for you${wr.newestSender ? ` \u2014 ${wr.newestSender}` : ""}`
|
|
16590
|
+
);
|
|
16535
16591
|
}
|
|
16536
|
-
const
|
|
16537
|
-
|
|
16538
|
-
|
|
16539
|
-
|
|
16540
|
-
|
|
16541
|
-
|
|
16542
|
-
|
|
16543
|
-
|
|
16544
|
-
|
|
16545
|
-
|
|
16546
|
-
|
|
16547
|
-
|
|
16548
|
-
|
|
16549
|
-
|
|
16550
|
-
|
|
16551
|
-
|
|
16552
|
-
if (!response.ok || !response.body) {
|
|
16553
|
-
throw new LocalRunError(`refused with ${response.status}`);
|
|
16592
|
+
const unpushed = (payload.atRisk ?? []).filter(
|
|
16593
|
+
(item) => item.classification === "unpushed"
|
|
16594
|
+
);
|
|
16595
|
+
if (unpushed.length > 0) {
|
|
16596
|
+
const commits = unpushed.reduce(
|
|
16597
|
+
(sum, item) => sum + (item.aheadMain ?? 0),
|
|
16598
|
+
0
|
|
16599
|
+
);
|
|
16600
|
+
const scale = commits > 0 ? `${commits} commit${commits === 1 ? "" : "s"} on ${unpushed.length} branch${unpushed.length === 1 ? "" : "es"}` : `${unpushed.length} branch${unpushed.length === 1 ? "" : "es"}`;
|
|
16601
|
+
lines.push(`${c.yellow("!")} ${scale} exist on this machine only`);
|
|
16602
|
+
const first = unpushed[0];
|
|
16603
|
+
if (first?.branch) {
|
|
16604
|
+
lines.push(
|
|
16605
|
+
` ${c.dim("\u2192")} ${c.cyan(`git push -u origin ${first.branch}`)}`
|
|
16606
|
+
);
|
|
16607
|
+
}
|
|
16554
16608
|
}
|
|
16555
|
-
const
|
|
16556
|
-
|
|
16557
|
-
|
|
16558
|
-
|
|
16559
|
-
|
|
16560
|
-
|
|
16561
|
-
|
|
16562
|
-
|
|
16563
|
-
if (piece.input !== void 0) inputTokens = piece.input;
|
|
16564
|
-
if (piece.output !== void 0) outputTokens = piece.output;
|
|
16565
|
-
if (piece.toolCalls?.length) {
|
|
16566
|
-
toolCalls = accumulateToolCalls(toolCalls, piece.toolCalls);
|
|
16609
|
+
for (const item of payload.needsYou.slice(0, 2)) {
|
|
16610
|
+
lines.push(`${c.yellow("!")} ${item.title}`);
|
|
16611
|
+
if (item.signalKey) printed.push(item.signalKey);
|
|
16612
|
+
if (item.action) {
|
|
16613
|
+
lines.push(` ${c.dim("\u2192")} ${c.cyan(item.action)}`);
|
|
16614
|
+
} else {
|
|
16615
|
+
const url = glanceItemUrl(item.href, apiUrl);
|
|
16616
|
+
if (url) lines.push(` ${c.dim("\u2192")} ${c.cyan(url)}`);
|
|
16567
16617
|
}
|
|
16568
|
-
|
|
16569
|
-
|
|
16570
|
-
|
|
16618
|
+
}
|
|
16619
|
+
if (lines.length === 0) {
|
|
16620
|
+
const integration = payload.integrations[0];
|
|
16621
|
+
if (integration) {
|
|
16622
|
+
const stale = integration.staleFor ? ` (stale ${integration.staleFor})` : "";
|
|
16623
|
+
lines.push(`${c.yellow("!")} ${integration.kind}${stale}`);
|
|
16624
|
+
if (integration.refresh) {
|
|
16625
|
+
lines.push(` ${c.dim("\u2192")} ${c.cyan(integration.refresh)}`);
|
|
16626
|
+
}
|
|
16571
16627
|
}
|
|
16572
16628
|
}
|
|
16573
|
-
|
|
16574
|
-
|
|
16575
|
-
|
|
16576
|
-
return
|
|
16577
|
-
}
|
|
16578
|
-
|
|
16579
|
-
// ../../src/lib/ai/local/run-turn.ts
|
|
16580
|
-
var TOOL_EXEC_PATH = "/api/chat/tool-exec";
|
|
16581
|
-
var MAX_REPORTED_PROMPT_CHARS = 4e6;
|
|
16582
|
-
function charactersIn(messages) {
|
|
16583
|
-
return messages.reduce((total, message) => total + message.content.length, 0);
|
|
16629
|
+
if (lines.length === 0) return [];
|
|
16630
|
+
lines.push(`${c.dim("\u2192")} ${c.cyan("gad status")} for the full picture.`);
|
|
16631
|
+
process.stderr.write("\n" + brandedBlock("needs you", lines) + "\n");
|
|
16632
|
+
return printed;
|
|
16584
16633
|
}
|
|
16585
|
-
|
|
16586
|
-
const
|
|
16587
|
-
|
|
16588
|
-
|
|
16589
|
-
|
|
16590
|
-
|
|
16591
|
-
|
|
16592
|
-
}
|
|
16593
|
-
const response = await doFetch(TOOL_EXEC_PATH, {
|
|
16594
|
-
method: "POST",
|
|
16595
|
-
headers: { "content-type": "application/json" },
|
|
16596
|
-
body: JSON.stringify({
|
|
16597
|
-
grant: handoff.toolGrant,
|
|
16598
|
-
tool: call.name,
|
|
16599
|
-
args,
|
|
16600
|
-
turnId: handoff.turnId
|
|
16601
|
-
}),
|
|
16602
|
-
cache: "no-store",
|
|
16603
|
-
signal
|
|
16604
|
-
});
|
|
16605
|
-
const payload = await response.json().catch(() => null);
|
|
16606
|
-
if (!response.ok || !payload) {
|
|
16607
|
-
return {
|
|
16608
|
-
result: {
|
|
16609
|
-
error: payload?.error ?? "unavailable",
|
|
16610
|
-
message: payload?.message ?? "That did not work."
|
|
16611
|
-
},
|
|
16612
|
-
exhausted: payload?.error === "step_budget_exhausted"
|
|
16613
|
-
};
|
|
16614
|
-
}
|
|
16615
|
-
return { result: payload.result, exhausted: false };
|
|
16634
|
+
function renderTip(tip, apiUrl) {
|
|
16635
|
+
const c = createColor(false);
|
|
16636
|
+
const lines = [`${c.yellow("\u{1F4A1}")} ${tip.text}`];
|
|
16637
|
+
if (tip.whySeen) lines.push(` ${c.dim(tip.whySeen)}`);
|
|
16638
|
+
const url = glanceItemUrl(tip.deepLink, apiUrl);
|
|
16639
|
+
if (url) lines.push(` ${c.dim("\u2192")} ${c.cyan(url)}`);
|
|
16640
|
+
process.stderr.write("\n" + brandedBlock("tip", lines) + "\n");
|
|
16616
16641
|
}
|
|
16617
|
-
|
|
16618
|
-
|
|
16619
|
-
const
|
|
16620
|
-
const
|
|
16621
|
-
|
|
16622
|
-
let text2 = "";
|
|
16623
|
-
let inputTokens;
|
|
16624
|
-
let outputTokens;
|
|
16625
|
-
let promptCharacters = 0;
|
|
16626
|
-
let toolCallCount = 0;
|
|
16627
|
-
let toolsStillOffered = offered.length > 0 && !!handoff.toolGrant;
|
|
16628
|
-
for (let pass = 0; pass <= MAX_AGENTIC_STEPS; pass++) {
|
|
16629
|
-
promptCharacters += charactersIn(messages);
|
|
16630
|
-
const result = await runLocalChat({
|
|
16631
|
-
baseUrl: handoff.baseUrl,
|
|
16632
|
-
provider: handoff.provider,
|
|
16633
|
-
modelId: handoff.modelId,
|
|
16634
|
-
messages,
|
|
16635
|
-
...toolsStillOffered ? { tools: offered } : {},
|
|
16636
|
-
signal: opts.signal,
|
|
16637
|
-
onDelta: opts.onDelta,
|
|
16638
|
-
fetchImpl: doFetch
|
|
16639
|
-
});
|
|
16640
|
-
if (result.inputTokens !== void 0) inputTokens = result.inputTokens;
|
|
16641
|
-
if (result.outputTokens !== void 0) outputTokens = result.outputTokens;
|
|
16642
|
-
text2 += result.text;
|
|
16643
|
-
if (!toolsStillOffered || result.toolCalls.length === 0) break;
|
|
16644
|
-
messages.push(assistantToolCallMessage(result.toolCalls));
|
|
16645
|
-
for (const call of result.toolCalls) {
|
|
16646
|
-
toolCallCount += 1;
|
|
16647
|
-
opts.onToolStep?.(call.name);
|
|
16648
|
-
const answer = await callApp(call, handoff, opts.signal, doFetch);
|
|
16649
|
-
messages.push(toolResultMessage(call, answer.result));
|
|
16650
|
-
if (answer.exhausted) toolsStillOffered = false;
|
|
16651
|
-
}
|
|
16652
|
-
if (pass === MAX_AGENTIC_STEPS - 1) toolsStillOffered = false;
|
|
16653
|
-
}
|
|
16654
|
-
return {
|
|
16655
|
-
text: text2,
|
|
16656
|
-
inputTokens,
|
|
16657
|
-
outputTokens,
|
|
16658
|
-
// The settle route refuses a report with an implausible figure in it, and
|
|
16659
|
-
// refusing the report would lose the whole turn. Keep it inside what that
|
|
16660
|
-
// route accepts; a clipped ceiling only ever counts fewer words, never more.
|
|
16661
|
-
promptCharacters: Math.min(promptCharacters, MAX_REPORTED_PROMPT_CHARS),
|
|
16662
|
-
toolCallCount
|
|
16663
|
-
};
|
|
16642
|
+
var GAD_MARK = "\u{1F3F9}";
|
|
16643
|
+
function brandedBlock(title, lines) {
|
|
16644
|
+
const header = `${GAD_MARK} Great Arrow \xB7 ${title}`;
|
|
16645
|
+
const rule = "\u2500".repeat(Math.min(60, [...header].length));
|
|
16646
|
+
return [header, rule, ...lines].join("\n");
|
|
16664
16647
|
}
|
|
16665
16648
|
|
|
16666
|
-
// ../../src/lib/
|
|
16667
|
-
var
|
|
16668
|
-
var
|
|
16669
|
-
|
|
16670
|
-
|
|
16671
|
-
var HANDOFF_TIMEOUT_MS = 6e4;
|
|
16672
|
-
var SETTLE_TIMEOUT_MS = 3e4;
|
|
16673
|
-
var LocalTurnFailed = class extends Error {
|
|
16674
|
-
stage;
|
|
16675
|
-
/** Set once the turn was handed back, so a caller knows the conversation is
|
|
16676
|
-
* not left with an unfinished reply in it. */
|
|
16677
|
-
closed;
|
|
16678
|
-
constructor(stage, message, closed) {
|
|
16679
|
-
super(message);
|
|
16680
|
-
this.name = "LocalTurnFailed";
|
|
16681
|
-
this.stage = stage;
|
|
16682
|
-
this.closed = closed;
|
|
16683
|
-
}
|
|
16649
|
+
// ../../src/lib/account/local-model-prefs-shared.ts
|
|
16650
|
+
var LOCAL_PROVIDERS = ["ollama", "lmstudio"];
|
|
16651
|
+
var LOCAL_PROVIDER_PRESETS = {
|
|
16652
|
+
ollama: { baseUrl: "http://localhost:11434", label: "Ollama" },
|
|
16653
|
+
lmstudio: { baseUrl: "http://localhost:1234", label: "LM Studio" }
|
|
16684
16654
|
};
|
|
16685
|
-
|
|
16686
|
-
|
|
16687
|
-
|
|
16688
|
-
|
|
16689
|
-
|
|
16655
|
+
var LOCAL_DEFAULT_BASE_URL = LOCAL_PROVIDER_PRESETS.ollama.baseUrl;
|
|
16656
|
+
var BROWSER_REACHABLE_LOCAL_BASE_URLS = Object.values(LOCAL_PROVIDER_PRESETS).map((preset) => preset.baseUrl);
|
|
16657
|
+
var BROWSER_REACHABLE_ORIGINS = new Set(
|
|
16658
|
+
BROWSER_REACHABLE_LOCAL_BASE_URLS.map((url) => new URL(url).origin)
|
|
16659
|
+
);
|
|
16660
|
+
var MAX_LOCAL_MODELS = 50;
|
|
16661
|
+
var MAX_LOCAL_MODEL_ID_LEN = 128;
|
|
16662
|
+
var LOCAL_MODEL_ID_RE = /^[a-zA-Z0-9][a-zA-Z0-9._:/-]{0,127}$/;
|
|
16663
|
+
function isValidLocalModelId(id) {
|
|
16664
|
+
return typeof id === "string" && id.length > 0 && id.length <= MAX_LOCAL_MODEL_ID_LEN && LOCAL_MODEL_ID_RE.test(id);
|
|
16690
16665
|
}
|
|
16691
|
-
function
|
|
16692
|
-
|
|
16666
|
+
function isValidLocalBaseUrl(value) {
|
|
16667
|
+
if (typeof value !== "string" || value.length > 200) return false;
|
|
16668
|
+
let url;
|
|
16669
|
+
try {
|
|
16670
|
+
url = new URL(value);
|
|
16671
|
+
} catch {
|
|
16672
|
+
return false;
|
|
16673
|
+
}
|
|
16674
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
|
|
16675
|
+
if (url.username || url.password) return false;
|
|
16676
|
+
const host = url.hostname.toLowerCase();
|
|
16677
|
+
return host === "localhost" || host === "127.0.0.1" || host === "[::1]" || host === "::1";
|
|
16693
16678
|
}
|
|
16694
|
-
|
|
16695
|
-
|
|
16696
|
-
|
|
16697
|
-
|
|
16698
|
-
|
|
16699
|
-
|
|
16700
|
-
|
|
16701
|
-
|
|
16702
|
-
|
|
16703
|
-
|
|
16704
|
-
});
|
|
16705
|
-
};
|
|
16679
|
+
|
|
16680
|
+
// ../../src/lib/ai/local/delegate.ts
|
|
16681
|
+
var LOCAL_CAPABLE_HEADER = "x-gad-local-capable";
|
|
16682
|
+
var LOCAL_CAPABLE_VALUE = "1";
|
|
16683
|
+
var LOCAL_TURN_HEADER = "x-gad-local-turn";
|
|
16684
|
+
var UNFINISHED_TURN_AFTER_MS = 6 * 6e4;
|
|
16685
|
+
|
|
16686
|
+
// ../../src/lib/ai/local/endpoint.ts
|
|
16687
|
+
function localApiUrl(baseUrl, path10) {
|
|
16688
|
+
return `${baseUrl.replace(/\/+$/, "")}${path10}`;
|
|
16706
16689
|
}
|
|
16707
|
-
|
|
16690
|
+
|
|
16691
|
+
// ../../src/lib/ai/local/residency.ts
|
|
16692
|
+
var LOCAL_MODEL_LOAD_DEADLINE_MS = 9e4;
|
|
16693
|
+
var RESIDENCY_PROBE_TIMEOUT_MS = 1500;
|
|
16694
|
+
async function modelIsProvablyAbsent(baseUrl, provider, modelId, opts = {}) {
|
|
16695
|
+
if (provider !== "ollama") return false;
|
|
16696
|
+
if (!isValidLocalBaseUrl(baseUrl)) return false;
|
|
16697
|
+
if (!modelId) return false;
|
|
16698
|
+
const doFetch = opts.fetchImpl ?? (typeof fetch === "function" ? fetch : null);
|
|
16699
|
+
if (!doFetch || typeof AbortController !== "function") return false;
|
|
16708
16700
|
const controller = new AbortController();
|
|
16709
|
-
const timer = setTimeout(
|
|
16701
|
+
const timer = setTimeout(
|
|
16702
|
+
() => controller.abort(),
|
|
16703
|
+
opts.timeoutMs ?? RESIDENCY_PROBE_TIMEOUT_MS
|
|
16704
|
+
);
|
|
16710
16705
|
try {
|
|
16711
|
-
|
|
16706
|
+
const response = await doFetch(localApiUrl(baseUrl, "/api/ps"), {
|
|
16707
|
+
method: "GET",
|
|
16708
|
+
cache: "no-store",
|
|
16709
|
+
signal: controller.signal
|
|
16710
|
+
});
|
|
16711
|
+
if (!response.ok) return false;
|
|
16712
|
+
const json = await response.json();
|
|
16713
|
+
if (!Array.isArray(json.models)) return false;
|
|
16714
|
+
const running = /* @__PURE__ */ new Set();
|
|
16715
|
+
for (const entry of json.models) {
|
|
16716
|
+
if (typeof entry?.model === "string") running.add(entry.model);
|
|
16717
|
+
if (typeof entry?.name === "string") running.add(entry.name);
|
|
16718
|
+
}
|
|
16719
|
+
return !running.has(modelId);
|
|
16720
|
+
} catch {
|
|
16721
|
+
return false;
|
|
16712
16722
|
} finally {
|
|
16713
16723
|
clearTimeout(timer);
|
|
16714
16724
|
}
|
|
16715
16725
|
}
|
|
16716
|
-
function
|
|
16717
|
-
|
|
16718
|
-
|
|
16719
|
-
|
|
16720
|
-
|
|
16721
|
-
|
|
16722
|
-
|
|
16723
|
-
|
|
16724
|
-
return null;
|
|
16725
|
-
}
|
|
16726
|
-
if (!Array.isArray(row.messages) || row.messages.length === 0) return null;
|
|
16727
|
-
return {
|
|
16728
|
-
turnId: row.turnId,
|
|
16729
|
-
sessionId: row.sessionId,
|
|
16730
|
-
provider: row.provider,
|
|
16731
|
-
baseUrl: row.baseUrl,
|
|
16732
|
-
modelId: row.modelId,
|
|
16733
|
-
modelLabel: typeof row.modelLabel === "string" && row.modelLabel ? row.modelLabel : row.modelId,
|
|
16734
|
-
deadlineMs: typeof row.deadlineMs === "number" ? row.deadlineMs : 12e4,
|
|
16735
|
-
firstWordDeadlineMs: typeof row.firstWordDeadlineMs === "number" ? row.firstWordDeadlineMs : 25e3,
|
|
16736
|
-
tools: Array.isArray(row.tools) ? row.tools : [],
|
|
16737
|
-
toolGrant: typeof row.toolGrant === "string" ? row.toolGrant : null,
|
|
16738
|
-
messages: row.messages,
|
|
16739
|
-
citations: Array.isArray(row.citations) ? row.citations : []
|
|
16740
|
-
};
|
|
16726
|
+
async function firstWordDeadlineMsFor(handoff, opts = {}) {
|
|
16727
|
+
const absent = await modelIsProvablyAbsent(
|
|
16728
|
+
handoff.baseUrl,
|
|
16729
|
+
handoff.provider,
|
|
16730
|
+
handoff.modelId,
|
|
16731
|
+
opts
|
|
16732
|
+
);
|
|
16733
|
+
return absent ? Math.max(handoff.firstWordDeadlineMs, LOCAL_MODEL_LOAD_DEADLINE_MS) : handoff.firstWordDeadlineMs;
|
|
16741
16734
|
}
|
|
16742
|
-
|
|
16743
|
-
|
|
16744
|
-
|
|
16745
|
-
|
|
16746
|
-
|
|
16747
|
-
|
|
16748
|
-
|
|
16749
|
-
|
|
16750
|
-
|
|
16751
|
-
|
|
16752
|
-
|
|
16753
|
-
|
|
16754
|
-
|
|
16755
|
-
|
|
16756
|
-
|
|
16757
|
-
|
|
16758
|
-
|
|
16759
|
-
|
|
16760
|
-
|
|
16761
|
-
|
|
16762
|
-
|
|
16763
|
-
|
|
16764
|
-
|
|
16765
|
-
|
|
16766
|
-
|
|
16767
|
-
);
|
|
16768
|
-
} catch (err) {
|
|
16769
|
-
throw new LocalTurnFailed(
|
|
16770
|
-
"handoff",
|
|
16771
|
-
err instanceof Error && err.name === "AbortError" ? "Great Arrow did not answer in time." : "Could not reach Great Arrow.",
|
|
16772
|
-
true
|
|
16773
|
-
);
|
|
16735
|
+
|
|
16736
|
+
// ../../src/lib/chat/agentic-steps.ts
|
|
16737
|
+
var MAX_AGENTIC_STEPS = 6;
|
|
16738
|
+
|
|
16739
|
+
// ../../src/lib/ai/local/tool-loop.ts
|
|
16740
|
+
var MAX_TOOL_CALLS_PER_STEP = 4;
|
|
16741
|
+
var MAX_TOOL_ARGUMENT_CHARS = 8e3;
|
|
16742
|
+
var MAX_TOOL_RESULT_CHARS = 6e3;
|
|
16743
|
+
function accumulateToolCalls(current, deltas) {
|
|
16744
|
+
const next = current.map((call) => ({ ...call }));
|
|
16745
|
+
for (const delta of deltas) {
|
|
16746
|
+
const at = delta.position;
|
|
16747
|
+
if (!Number.isInteger(at) || at < 0 || at >= MAX_TOOL_CALLS_PER_STEP) {
|
|
16748
|
+
continue;
|
|
16749
|
+
}
|
|
16750
|
+
while (next.length <= at) next.push({ id: "", name: "", arguments: "" });
|
|
16751
|
+
const slot = next[at];
|
|
16752
|
+
if (delta.id) slot.id = delta.id;
|
|
16753
|
+
if (delta.name) slot.name = delta.name;
|
|
16754
|
+
if (delta.argumentsFragment) {
|
|
16755
|
+
slot.arguments = (slot.arguments + delta.argumentsFragment).slice(
|
|
16756
|
+
0,
|
|
16757
|
+
MAX_TOOL_ARGUMENT_CHARS
|
|
16758
|
+
);
|
|
16759
|
+
}
|
|
16774
16760
|
}
|
|
16775
|
-
|
|
16776
|
-
|
|
16777
|
-
|
|
16778
|
-
|
|
16779
|
-
|
|
16780
|
-
|
|
16761
|
+
return next;
|
|
16762
|
+
}
|
|
16763
|
+
function finalizeToolCalls(calls) {
|
|
16764
|
+
return calls.filter((call) => call.name.length > 0).slice(0, MAX_TOOL_CALLS_PER_STEP).map((call, index) => ({
|
|
16765
|
+
...call,
|
|
16766
|
+
id: call.id || `call_${index}`,
|
|
16767
|
+
arguments: call.arguments || "{}"
|
|
16768
|
+
}));
|
|
16769
|
+
}
|
|
16770
|
+
function parseToolArguments(raw) {
|
|
16771
|
+
if (raw.length > MAX_TOOL_ARGUMENT_CHARS) return null;
|
|
16772
|
+
let parsed;
|
|
16773
|
+
try {
|
|
16774
|
+
parsed = JSON.parse(raw.trim() || "{}");
|
|
16775
|
+
} catch {
|
|
16776
|
+
return null;
|
|
16781
16777
|
}
|
|
16782
|
-
if (
|
|
16783
|
-
try {
|
|
16784
|
-
await response.text();
|
|
16785
|
-
} catch {
|
|
16786
|
-
return null;
|
|
16787
|
-
}
|
|
16778
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
16788
16779
|
return null;
|
|
16789
16780
|
}
|
|
16790
|
-
|
|
16781
|
+
return parsed;
|
|
16782
|
+
}
|
|
16783
|
+
function clampToolResult(value) {
|
|
16784
|
+
let text2;
|
|
16791
16785
|
try {
|
|
16792
|
-
|
|
16786
|
+
text2 = typeof value === "string" ? value : JSON.stringify(value);
|
|
16793
16787
|
} catch {
|
|
16794
|
-
|
|
16788
|
+
text2 = "";
|
|
16795
16789
|
}
|
|
16796
|
-
|
|
16797
|
-
|
|
16798
|
-
|
|
16799
|
-
|
|
16800
|
-
|
|
16801
|
-
|
|
16802
|
-
|
|
16790
|
+
if (!text2) return "{}";
|
|
16791
|
+
return text2.length > MAX_TOOL_RESULT_CHARS ? `${text2.slice(0, MAX_TOOL_RESULT_CHARS)}\u2026 (shortened)` : text2;
|
|
16792
|
+
}
|
|
16793
|
+
function assistantToolCallMessage(calls) {
|
|
16794
|
+
return {
|
|
16795
|
+
role: "assistant",
|
|
16796
|
+
content: "",
|
|
16797
|
+
toolCalls: calls.map((call) => ({
|
|
16798
|
+
id: call.id,
|
|
16799
|
+
type: "function",
|
|
16800
|
+
function: { name: call.name, arguments: call.arguments }
|
|
16801
|
+
}))
|
|
16802
|
+
};
|
|
16803
|
+
}
|
|
16804
|
+
function toolResultMessage(call, result) {
|
|
16805
|
+
return {
|
|
16806
|
+
role: "tool",
|
|
16807
|
+
content: clampToolResult(result),
|
|
16808
|
+
toolCallId: call.id,
|
|
16809
|
+
name: call.name
|
|
16810
|
+
};
|
|
16811
|
+
}
|
|
16812
|
+
|
|
16813
|
+
// ../../src/lib/ai/local/generate.ts
|
|
16814
|
+
var LocalRunError = class extends Error {
|
|
16815
|
+
constructor(message) {
|
|
16816
|
+
super(message);
|
|
16817
|
+
this.name = "LocalRunError";
|
|
16803
16818
|
}
|
|
16804
|
-
|
|
16819
|
+
};
|
|
16820
|
+
var CHAT_PATH = {
|
|
16821
|
+
ollama: "/api/chat",
|
|
16822
|
+
lmstudio: "/v1/chat/completions"
|
|
16823
|
+
};
|
|
16824
|
+
async function* readLines(body) {
|
|
16825
|
+
const reader = body.getReader();
|
|
16826
|
+
const decoder = new TextDecoder();
|
|
16827
|
+
let buffer = "";
|
|
16828
|
+
while (true) {
|
|
16829
|
+
const { done, value } = await reader.read();
|
|
16830
|
+
if (done) break;
|
|
16831
|
+
buffer += decoder.decode(value, { stream: true });
|
|
16832
|
+
let index = buffer.indexOf("\n");
|
|
16833
|
+
while (index >= 0) {
|
|
16834
|
+
const line = buffer.slice(0, index).trim();
|
|
16835
|
+
buffer = buffer.slice(index + 1);
|
|
16836
|
+
if (line) yield line;
|
|
16837
|
+
index = buffer.indexOf("\n");
|
|
16838
|
+
}
|
|
16839
|
+
}
|
|
16840
|
+
const tail = buffer.trim();
|
|
16841
|
+
if (tail) yield tail;
|
|
16805
16842
|
}
|
|
16806
|
-
|
|
16843
|
+
function numberOrUndefined(value) {
|
|
16844
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : void 0;
|
|
16845
|
+
}
|
|
16846
|
+
function argumentsAsText(value) {
|
|
16847
|
+
if (typeof value === "string") return value;
|
|
16807
16848
|
try {
|
|
16808
|
-
|
|
16809
|
-
SETTLE_TIMEOUT_MS,
|
|
16810
|
-
(signal) => opts.doFetch(`${origin(opts.apiUrl)}${SETTLE_PATH}`, {
|
|
16811
|
-
method: "POST",
|
|
16812
|
-
headers: authHeaders(opts.token),
|
|
16813
|
-
body: JSON.stringify({
|
|
16814
|
-
turnId: opts.handoff.turnId,
|
|
16815
|
-
sessionId: opts.handoff.sessionId,
|
|
16816
|
-
modelId: opts.handoff.modelId,
|
|
16817
|
-
outcome: opts.outcome,
|
|
16818
|
-
text: opts.text.slice(0, MAX_REPORTED_TEXT_CHARS),
|
|
16819
|
-
...opts.inputTokens !== void 0 ? { inputTokens: opts.inputTokens } : {},
|
|
16820
|
-
...opts.outputTokens !== void 0 ? { outputTokens: opts.outputTokens } : {},
|
|
16821
|
-
latencyMs: Math.min(
|
|
16822
|
-
Math.max(0, Math.round(opts.latencyMs)),
|
|
16823
|
-
MAX_REPORTED_LATENCY_MS
|
|
16824
|
-
),
|
|
16825
|
-
promptCharacters: opts.promptCharacters
|
|
16826
|
-
}),
|
|
16827
|
-
cache: "no-store",
|
|
16828
|
-
signal
|
|
16829
|
-
})
|
|
16830
|
-
);
|
|
16831
|
-
if (!response.ok) return false;
|
|
16832
|
-
const body = await response.json();
|
|
16833
|
-
return body?.recorded === true;
|
|
16849
|
+
return JSON.stringify(value ?? {});
|
|
16834
16850
|
} catch {
|
|
16835
|
-
return
|
|
16851
|
+
return "{}";
|
|
16836
16852
|
}
|
|
16837
16853
|
}
|
|
16838
|
-
|
|
16839
|
-
const
|
|
16840
|
-
|
|
16841
|
-
const
|
|
16842
|
-
|
|
16843
|
-
|
|
16844
|
-
|
|
16845
|
-
|
|
16846
|
-
|
|
16847
|
-
|
|
16854
|
+
function readOllamaToolCalls(message) {
|
|
16855
|
+
const raw = message?.tool_calls;
|
|
16856
|
+
if (!Array.isArray(raw)) return [];
|
|
16857
|
+
const out = [];
|
|
16858
|
+
raw.forEach((entry, position) => {
|
|
16859
|
+
if (!entry || typeof entry !== "object") return;
|
|
16860
|
+
const fn = entry.function;
|
|
16861
|
+
if (typeof fn?.name !== "string") return;
|
|
16862
|
+
out.push({
|
|
16863
|
+
position,
|
|
16864
|
+
name: fn.name,
|
|
16865
|
+
argumentsFragment: argumentsAsText(fn.arguments)
|
|
16866
|
+
});
|
|
16848
16867
|
});
|
|
16849
|
-
|
|
16850
|
-
|
|
16851
|
-
|
|
16852
|
-
|
|
16853
|
-
handoff.deadlineMs,
|
|
16854
|
-
opts.deadlineMs ?? handoff.deadlineMs
|
|
16855
|
-
);
|
|
16856
|
-
const controller = new AbortController();
|
|
16857
|
-
const startedAt = Date.now();
|
|
16858
|
-
let words = "";
|
|
16859
|
-
let sawFirstWord = false;
|
|
16860
|
-
const firstWord = setTimeout(
|
|
16861
|
-
() => {
|
|
16862
|
-
if (!sawFirstWord) controller.abort();
|
|
16863
|
-
},
|
|
16864
|
-
Math.min(handoff.firstWordDeadlineMs, ceiling)
|
|
16865
|
-
);
|
|
16866
|
-
const whole = setTimeout(() => controller.abort(), ceiling);
|
|
16868
|
+
return out;
|
|
16869
|
+
}
|
|
16870
|
+
function readOllamaLine(line) {
|
|
16871
|
+
let parsed;
|
|
16867
16872
|
try {
|
|
16868
|
-
|
|
16869
|
-
handoff,
|
|
16870
|
-
signal: controller.signal,
|
|
16871
|
-
onDelta: (delta) => {
|
|
16872
|
-
sawFirstWord = true;
|
|
16873
|
-
words += delta;
|
|
16874
|
-
opts.onWords?.(delta);
|
|
16875
|
-
},
|
|
16876
|
-
...opts.onApp ? { onToolStep: opts.onApp } : {},
|
|
16877
|
-
fetchImpl: doFetch
|
|
16878
|
-
});
|
|
16879
|
-
const recorded = await handBack({
|
|
16880
|
-
apiUrl: opts.apiUrl,
|
|
16881
|
-
token: opts.token,
|
|
16882
|
-
handoff,
|
|
16883
|
-
outcome: "finished",
|
|
16884
|
-
text: result.text,
|
|
16885
|
-
inputTokens: result.inputTokens,
|
|
16886
|
-
outputTokens: result.outputTokens,
|
|
16887
|
-
promptCharacters: result.promptCharacters,
|
|
16888
|
-
latencyMs: Date.now() - startedAt,
|
|
16889
|
-
doFetch: base
|
|
16890
|
-
});
|
|
16891
|
-
return {
|
|
16892
|
-
answeredHere: true,
|
|
16893
|
-
text: result.text,
|
|
16894
|
-
modelLabel: handoff.modelLabel,
|
|
16895
|
-
appsUsed: result.toolCallCount,
|
|
16896
|
-
recorded
|
|
16897
|
-
};
|
|
16873
|
+
parsed = JSON.parse(line);
|
|
16898
16874
|
} catch {
|
|
16899
|
-
|
|
16900
|
-
apiUrl: opts.apiUrl,
|
|
16901
|
-
token: opts.token,
|
|
16902
|
-
handoff,
|
|
16903
|
-
outcome: "stopped",
|
|
16904
|
-
text: words,
|
|
16905
|
-
inputTokens: void 0,
|
|
16906
|
-
outputTokens: void 0,
|
|
16907
|
-
promptCharacters: 0,
|
|
16908
|
-
latencyMs: Date.now() - startedAt,
|
|
16909
|
-
doFetch: base
|
|
16910
|
-
});
|
|
16911
|
-
throw new LocalTurnFailed(
|
|
16912
|
-
"writing",
|
|
16913
|
-
controller.signal.aborted ? `${handoff.modelLabel} took too long, so we stopped waiting. Ask again and Great Arrow will answer it.` : `${handoff.modelLabel} could not finish that. Check ${handoff.baseUrl} is still running.`,
|
|
16914
|
-
closed
|
|
16915
|
-
);
|
|
16916
|
-
} finally {
|
|
16917
|
-
clearTimeout(firstWord);
|
|
16918
|
-
clearTimeout(whole);
|
|
16919
|
-
}
|
|
16920
|
-
}
|
|
16921
|
-
|
|
16922
|
-
// ../../src/lib/ai/local/catalog.ts
|
|
16923
|
-
var EMBEDDING_ONLY_PATTERNS = [
|
|
16924
|
-
/nomic-embed/i,
|
|
16925
|
-
/(?:^|[^a-z0-9])bge-/i,
|
|
16926
|
-
/all-minilm/i,
|
|
16927
|
-
/mxbai-embed/i,
|
|
16928
|
-
/text-embedding/i
|
|
16929
|
-
];
|
|
16930
|
-
function isEmbeddingOnlyModelId(id) {
|
|
16931
|
-
return EMBEDDING_ONLY_PATTERNS.some((pattern) => pattern.test(id));
|
|
16932
|
-
}
|
|
16933
|
-
function positiveInt(value) {
|
|
16934
|
-
return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.min(Math.floor(value), 1e7) : 0;
|
|
16935
|
-
}
|
|
16936
|
-
function firstString(...candidates) {
|
|
16937
|
-
for (const candidate of candidates) {
|
|
16938
|
-
if (typeof candidate === "string" && candidate.length > 0) return candidate;
|
|
16875
|
+
return null;
|
|
16939
16876
|
}
|
|
16940
|
-
return
|
|
16877
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
16878
|
+
const row = parsed;
|
|
16879
|
+
if (typeof row.error === "string") throw new LocalRunError(row.error);
|
|
16880
|
+
const message = row.message;
|
|
16881
|
+
const delta = typeof message?.content === "string" ? message.content : "";
|
|
16882
|
+
return {
|
|
16883
|
+
delta,
|
|
16884
|
+
toolCalls: readOllamaToolCalls(message),
|
|
16885
|
+
input: numberOrUndefined(row.prompt_eval_count),
|
|
16886
|
+
output: numberOrUndefined(row.eval_count)
|
|
16887
|
+
};
|
|
16941
16888
|
}
|
|
16942
|
-
function
|
|
16943
|
-
|
|
16889
|
+
function readOpenAiToolCalls(deltaObj) {
|
|
16890
|
+
const raw = deltaObj?.tool_calls;
|
|
16891
|
+
if (!Array.isArray(raw)) return [];
|
|
16944
16892
|
const out = [];
|
|
16945
|
-
|
|
16946
|
-
|
|
16947
|
-
|
|
16948
|
-
|
|
16949
|
-
const { id, label, contextLength } = read(entry);
|
|
16950
|
-
if (!isValidLocalModelId(id)) continue;
|
|
16951
|
-
if (seen.has(id)) continue;
|
|
16952
|
-
if (isEmbeddingOnlyModelId(id)) continue;
|
|
16953
|
-
seen.add(id);
|
|
16893
|
+
raw.forEach((entry, fallbackPosition) => {
|
|
16894
|
+
if (!entry || typeof entry !== "object") return;
|
|
16895
|
+
const row = entry;
|
|
16896
|
+
const fn = row.function;
|
|
16954
16897
|
out.push({
|
|
16955
|
-
|
|
16956
|
-
|
|
16957
|
-
|
|
16958
|
-
|
|
16959
|
-
// tool calls is worse than one that honestly offers chat only.
|
|
16960
|
-
toolCapable: false,
|
|
16961
|
-
contextLength: positiveInt(contextLength)
|
|
16898
|
+
position: typeof row.index === "number" ? row.index : fallbackPosition,
|
|
16899
|
+
id: typeof row.id === "string" ? row.id : void 0,
|
|
16900
|
+
name: typeof fn?.name === "string" ? fn.name : void 0,
|
|
16901
|
+
argumentsFragment: typeof fn?.arguments === "string" ? fn.arguments : void 0
|
|
16962
16902
|
});
|
|
16963
|
-
}
|
|
16903
|
+
});
|
|
16964
16904
|
return out;
|
|
16965
16905
|
}
|
|
16966
|
-
function
|
|
16967
|
-
if (!
|
|
16968
|
-
const
|
|
16969
|
-
|
|
16970
|
-
|
|
16971
|
-
|
|
16972
|
-
|
|
16973
|
-
}
|
|
16974
|
-
|
|
16975
|
-
if (!json || typeof json !== "object") return [];
|
|
16976
|
-
const data = json.data;
|
|
16977
|
-
return collect(data, (entry) => ({
|
|
16978
|
-
id: entry.id,
|
|
16979
|
-
label: firstString(entry.id),
|
|
16980
|
-
contextLength: positiveInt(entry.max_context_length) || positiveInt(entry.loaded_context_length) || positiveInt(entry.context_length)
|
|
16981
|
-
}));
|
|
16982
|
-
}
|
|
16983
|
-
function normalizeShowResponse(json) {
|
|
16984
|
-
if (!json || typeof json !== "object") {
|
|
16985
|
-
return { toolCapable: false, contextLength: 0 };
|
|
16986
|
-
}
|
|
16987
|
-
const o = json;
|
|
16988
|
-
let toolCapable = false;
|
|
16989
|
-
if (Array.isArray(o.capabilities)) {
|
|
16990
|
-
toolCapable = o.capabilities.some(
|
|
16991
|
-
(c) => typeof c === "string" && c.toLowerCase() === "tools"
|
|
16992
|
-
);
|
|
16993
|
-
}
|
|
16994
|
-
if (!toolCapable) {
|
|
16995
|
-
const template = typeof o.template === "string" ? o.template : "";
|
|
16996
|
-
toolCapable = /\.tools\b/i.test(template);
|
|
16997
|
-
}
|
|
16998
|
-
return { toolCapable, contextLength: readContextLength(o.model_info) };
|
|
16999
|
-
}
|
|
17000
|
-
function readContextLength(modelInfo) {
|
|
17001
|
-
if (!modelInfo || typeof modelInfo !== "object") return 0;
|
|
17002
|
-
for (const [key, value] of Object.entries(
|
|
17003
|
-
modelInfo
|
|
17004
|
-
)) {
|
|
17005
|
-
if (!key.endsWith(".context_length") && key !== "context_length") continue;
|
|
17006
|
-
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
17007
|
-
return Math.floor(value);
|
|
17008
|
-
}
|
|
16906
|
+
function readOpenAiLine(line) {
|
|
16907
|
+
if (!line.startsWith("data:")) return null;
|
|
16908
|
+
const payload = line.slice(5).trim();
|
|
16909
|
+
if (!payload || payload === "[DONE]") return null;
|
|
16910
|
+
let parsed;
|
|
16911
|
+
try {
|
|
16912
|
+
parsed = JSON.parse(payload);
|
|
16913
|
+
} catch {
|
|
16914
|
+
return null;
|
|
17009
16915
|
}
|
|
17010
|
-
return
|
|
16916
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
16917
|
+
const row = parsed;
|
|
16918
|
+
const choices = Array.isArray(row.choices) ? row.choices : [];
|
|
16919
|
+
const first = choices[0];
|
|
16920
|
+
const deltaObj = first?.delta;
|
|
16921
|
+
const delta = typeof deltaObj?.content === "string" ? deltaObj.content : "";
|
|
16922
|
+
const usage = row.usage;
|
|
16923
|
+
return {
|
|
16924
|
+
delta,
|
|
16925
|
+
toolCalls: readOpenAiToolCalls(deltaObj),
|
|
16926
|
+
input: numberOrUndefined(usage?.prompt_tokens),
|
|
16927
|
+
output: numberOrUndefined(usage?.completion_tokens)
|
|
16928
|
+
};
|
|
17011
16929
|
}
|
|
17012
|
-
|
|
17013
|
-
|
|
17014
|
-
|
|
17015
|
-
|
|
17016
|
-
|
|
17017
|
-
|
|
17018
|
-
|
|
17019
|
-
|
|
17020
|
-
|
|
17021
|
-
|
|
17022
|
-
|
|
17023
|
-
super(detail);
|
|
17024
|
-
this.name = "LocalAppUnreachable";
|
|
17025
|
-
this.baseUrl = baseUrl;
|
|
16930
|
+
function wireMessage(message, provider) {
|
|
16931
|
+
if (message.role === "tool") {
|
|
16932
|
+
return provider === "ollama" ? {
|
|
16933
|
+
role: "tool",
|
|
16934
|
+
content: message.content,
|
|
16935
|
+
tool_name: message.name ?? ""
|
|
16936
|
+
} : {
|
|
16937
|
+
role: "tool",
|
|
16938
|
+
content: message.content,
|
|
16939
|
+
tool_call_id: message.toolCallId ?? ""
|
|
16940
|
+
};
|
|
17026
16941
|
}
|
|
17027
|
-
|
|
17028
|
-
|
|
17029
|
-
|
|
17030
|
-
|
|
17031
|
-
|
|
17032
|
-
|
|
17033
|
-
|
|
17034
|
-
|
|
17035
|
-
|
|
17036
|
-
|
|
17037
|
-
|
|
17038
|
-
|
|
16942
|
+
if (message.role === "assistant" && message.toolCalls?.length) {
|
|
16943
|
+
return {
|
|
16944
|
+
role: "assistant",
|
|
16945
|
+
content: message.content,
|
|
16946
|
+
tool_calls: message.toolCalls.map(
|
|
16947
|
+
(call) => provider === "ollama" ? {
|
|
16948
|
+
function: {
|
|
16949
|
+
name: call.function.name,
|
|
16950
|
+
arguments: safeParseArguments(call.function.arguments)
|
|
16951
|
+
}
|
|
16952
|
+
} : call
|
|
16953
|
+
)
|
|
16954
|
+
};
|
|
17039
16955
|
}
|
|
16956
|
+
return { role: message.role, content: message.content };
|
|
17040
16957
|
}
|
|
17041
|
-
|
|
16958
|
+
function safeParseArguments(raw) {
|
|
17042
16959
|
try {
|
|
17043
|
-
const
|
|
17044
|
-
|
|
17045
|
-
localApiUrl(baseUrl, "/api/show"),
|
|
17046
|
-
{
|
|
17047
|
-
method: "POST",
|
|
17048
|
-
headers: { "content-type": "application/json" },
|
|
17049
|
-
body: JSON.stringify({ model: modelId })
|
|
17050
|
-
},
|
|
17051
|
-
timeoutMs
|
|
17052
|
-
);
|
|
17053
|
-
if (!response.ok) return { toolCapable: false, contextLength: 0 };
|
|
17054
|
-
return normalizeShowResponse(await response.json());
|
|
16960
|
+
const parsed = JSON.parse(raw || "{}");
|
|
16961
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
17055
16962
|
} catch {
|
|
17056
|
-
return {
|
|
16963
|
+
return {};
|
|
17057
16964
|
}
|
|
17058
16965
|
}
|
|
17059
|
-
async function
|
|
16966
|
+
async function runLocalChat(opts) {
|
|
17060
16967
|
if (!isValidLocalBaseUrl(opts.baseUrl)) {
|
|
17061
|
-
throw new
|
|
16968
|
+
throw new LocalRunError("unusable address");
|
|
17062
16969
|
}
|
|
17063
16970
|
const doFetch = opts.fetchImpl ?? fetch;
|
|
17064
|
-
const
|
|
17065
|
-
const
|
|
17066
|
-
|
|
17067
|
-
|
|
17068
|
-
|
|
17069
|
-
|
|
17070
|
-
|
|
17071
|
-
|
|
17072
|
-
|
|
17073
|
-
|
|
17074
|
-
|
|
17075
|
-
|
|
17076
|
-
|
|
17077
|
-
|
|
17078
|
-
`It answered with ${response.status}.`
|
|
17079
|
-
);
|
|
17080
|
-
}
|
|
17081
|
-
const json = await response.json().catch(() => null);
|
|
17082
|
-
const models = opts.provider === "ollama" ? normalizeOllamaTags(json) : normalizeLmStudioModels(json);
|
|
17083
|
-
if (opts.provider !== "ollama") return models;
|
|
17084
|
-
const looked = models.slice(0, MAX_CAPABILITY_LOOKUPS);
|
|
17085
|
-
const details = await Promise.all(
|
|
17086
|
-
looked.map(
|
|
17087
|
-
(model) => capabilitiesOf(doFetch, opts.baseUrl, model.id, timeoutMs)
|
|
17088
|
-
)
|
|
17089
|
-
);
|
|
17090
|
-
return models.map((model, index) => {
|
|
17091
|
-
const detail = details[index];
|
|
17092
|
-
if (!detail) return model;
|
|
17093
|
-
return {
|
|
17094
|
-
...model,
|
|
17095
|
-
toolCapable: detail.toolCapable,
|
|
17096
|
-
// The list call never reports a window, so only ever widen from the
|
|
17097
|
-
// lookup — a zero means "not reported", not "no room".
|
|
17098
|
-
contextLength: detail.contextLength || model.contextLength
|
|
17099
|
-
};
|
|
16971
|
+
const url = localApiUrl(opts.baseUrl, CHAT_PATH[opts.provider]);
|
|
16972
|
+
const response = await doFetch(url, {
|
|
16973
|
+
method: "POST",
|
|
16974
|
+
headers: { "content-type": "application/json" },
|
|
16975
|
+
body: JSON.stringify({
|
|
16976
|
+
model: opts.modelId,
|
|
16977
|
+
messages: opts.messages.map(
|
|
16978
|
+
(message) => wireMessage(message, opts.provider)
|
|
16979
|
+
),
|
|
16980
|
+
...opts.tools?.length ? { tools: opts.tools } : {},
|
|
16981
|
+
stream: true
|
|
16982
|
+
}),
|
|
16983
|
+
cache: "no-store",
|
|
16984
|
+
signal: opts.signal
|
|
17100
16985
|
});
|
|
17101
|
-
|
|
17102
|
-
|
|
17103
|
-
// src/commands/local.ts
|
|
17104
|
-
var CHECK_TIMEOUT_MS = 3e4;
|
|
17105
|
-
function readProvider(raw) {
|
|
17106
|
-
const value = typeof raw === "string" ? raw : "ollama";
|
|
17107
|
-
if (!LOCAL_PROVIDERS.includes(value)) {
|
|
17108
|
-
throw validationError(
|
|
17109
|
-
`Unknown app "${value}".`,
|
|
17110
|
-
`Use one of: ${LOCAL_PROVIDERS.join(", ")}.`
|
|
17111
|
-
);
|
|
16986
|
+
if (!response.ok || !response.body) {
|
|
16987
|
+
throw new LocalRunError(`refused with ${response.status}`);
|
|
17112
16988
|
}
|
|
17113
|
-
|
|
17114
|
-
|
|
17115
|
-
|
|
17116
|
-
|
|
17117
|
-
|
|
17118
|
-
|
|
17119
|
-
const
|
|
17120
|
-
|
|
17121
|
-
|
|
17122
|
-
|
|
17123
|
-
|
|
17124
|
-
|
|
17125
|
-
isTTY: isTTY(),
|
|
17126
|
-
noColor: g.noColor
|
|
17127
|
-
});
|
|
17128
|
-
let models;
|
|
17129
|
-
try {
|
|
17130
|
-
models = await findInstalledModels({ baseUrl, provider });
|
|
17131
|
-
} catch (err) {
|
|
17132
|
-
if (err instanceof LocalAppUnreachable) {
|
|
17133
|
-
throw new CliError(
|
|
17134
|
-
`Could not reach ${LOCAL_PROVIDER_PRESETS[provider].label} at ${err.baseUrl}. ${err.message}`,
|
|
17135
|
-
2 /* NETWORK_ERROR */,
|
|
17136
|
-
`Start it on this computer and try again, or point somewhere else with --url.`
|
|
17137
|
-
);
|
|
17138
|
-
}
|
|
17139
|
-
throw err;
|
|
17140
|
-
}
|
|
17141
|
-
const saved = await client.mcpCall(
|
|
17142
|
-
"model_local_set",
|
|
17143
|
-
{
|
|
17144
|
-
enabled: true,
|
|
17145
|
-
provider,
|
|
17146
|
-
base_url: baseUrl,
|
|
17147
|
-
models: models.map((model) => ({
|
|
17148
|
-
id: model.id,
|
|
17149
|
-
label: model.label,
|
|
17150
|
-
tool_capable: model.toolCapable,
|
|
17151
|
-
context_length: model.contextLength
|
|
17152
|
-
})),
|
|
17153
|
-
...typeof options.use === "string" && options.use ? { default_model_id: options.use } : {}
|
|
17154
|
-
},
|
|
17155
|
-
CHECK_TIMEOUT_MS
|
|
17156
|
-
);
|
|
17157
|
-
if (g.json) {
|
|
17158
|
-
process.stdout.write(JSON.stringify({ models, saved }) + "\n");
|
|
17159
|
-
return;
|
|
17160
|
-
}
|
|
17161
|
-
if (models.length === 0) {
|
|
17162
|
-
fmt.warn(
|
|
17163
|
-
`${LOCAL_PROVIDER_PRESETS[provider].label} is running but has no models yet. Install one, then run this again.`
|
|
17164
|
-
);
|
|
17165
|
-
return;
|
|
17166
|
-
}
|
|
17167
|
-
const chosen = saved.preferences?.defaultModelId ?? null;
|
|
17168
|
-
process.stdout.write(
|
|
17169
|
-
`Found ${models.length} ${models.length === 1 ? "model" : "models"} on this computer:
|
|
17170
|
-
`
|
|
17171
|
-
);
|
|
17172
|
-
for (const model of models) {
|
|
17173
|
-
const mark = model.id === chosen ? "*" : " ";
|
|
17174
|
-
const limits = model.toolCapable ? "can look things up" : "answers from your library only";
|
|
17175
|
-
process.stdout.write(`${mark} ${model.label} \u2014 ${limits}
|
|
17176
|
-
`);
|
|
16989
|
+
const readLine = opts.provider === "ollama" ? readOllamaLine : readOpenAiLine;
|
|
16990
|
+
let text2 = "";
|
|
16991
|
+
let toolCalls = [];
|
|
16992
|
+
let inputTokens;
|
|
16993
|
+
let outputTokens;
|
|
16994
|
+
for await (const line of readLines(response.body)) {
|
|
16995
|
+
const piece = readLine(line);
|
|
16996
|
+
if (!piece) continue;
|
|
16997
|
+
if (piece.input !== void 0) inputTokens = piece.input;
|
|
16998
|
+
if (piece.output !== void 0) outputTokens = piece.output;
|
|
16999
|
+
if (piece.toolCalls?.length) {
|
|
17000
|
+
toolCalls = accumulateToolCalls(toolCalls, piece.toolCalls);
|
|
17177
17001
|
}
|
|
17178
|
-
if (
|
|
17179
|
-
|
|
17180
|
-
|
|
17181
|
-
`);
|
|
17182
|
-
} else {
|
|
17183
|
-
process.stdout.write(
|
|
17184
|
-
`
|
|
17185
|
-
Pick one with: gad local check --use <model>
|
|
17186
|
-
`
|
|
17187
|
-
);
|
|
17002
|
+
if (piece.delta) {
|
|
17003
|
+
text2 += piece.delta;
|
|
17004
|
+
opts.onDelta(piece.delta);
|
|
17188
17005
|
}
|
|
17006
|
+
}
|
|
17007
|
+
const asked = finalizeToolCalls(toolCalls);
|
|
17008
|
+
if (!text2 && asked.length === 0)
|
|
17009
|
+
throw new LocalRunError("no answer produced");
|
|
17010
|
+
return { text: text2, toolCalls: asked, inputTokens, outputTokens };
|
|
17011
|
+
}
|
|
17012
|
+
|
|
17013
|
+
// ../../src/lib/ai/local/run-turn.ts
|
|
17014
|
+
var TOOL_EXEC_PATH = "/api/chat/tool-exec";
|
|
17015
|
+
var MAX_REPORTED_PROMPT_CHARS = 4e6;
|
|
17016
|
+
function charactersIn(messages) {
|
|
17017
|
+
return messages.reduce((total, message) => total + message.content.length, 0);
|
|
17018
|
+
}
|
|
17019
|
+
async function callApp(call, handoff, signal, doFetch) {
|
|
17020
|
+
const args = parseToolArguments(call.arguments);
|
|
17021
|
+
if (!args) {
|
|
17022
|
+
return {
|
|
17023
|
+
result: { error: "unreadable_request", message: "Write that as JSON." },
|
|
17024
|
+
exhausted: false
|
|
17025
|
+
};
|
|
17026
|
+
}
|
|
17027
|
+
const response = await doFetch(TOOL_EXEC_PATH, {
|
|
17028
|
+
method: "POST",
|
|
17029
|
+
headers: { "content-type": "application/json" },
|
|
17030
|
+
body: JSON.stringify({
|
|
17031
|
+
grant: handoff.toolGrant,
|
|
17032
|
+
tool: call.name,
|
|
17033
|
+
args,
|
|
17034
|
+
turnId: handoff.turnId
|
|
17035
|
+
}),
|
|
17036
|
+
cache: "no-store",
|
|
17037
|
+
signal
|
|
17189
17038
|
});
|
|
17039
|
+
const payload = await response.json().catch(() => null);
|
|
17040
|
+
if (!response.ok || !payload) {
|
|
17041
|
+
return {
|
|
17042
|
+
result: {
|
|
17043
|
+
error: payload?.error ?? "unavailable",
|
|
17044
|
+
message: payload?.message ?? "That did not work."
|
|
17045
|
+
},
|
|
17046
|
+
exhausted: payload?.error === "step_budget_exhausted"
|
|
17047
|
+
};
|
|
17048
|
+
}
|
|
17049
|
+
return { result: payload.result, exhausted: false };
|
|
17190
17050
|
}
|
|
17191
|
-
function
|
|
17192
|
-
|
|
17193
|
-
|
|
17194
|
-
|
|
17195
|
-
|
|
17196
|
-
|
|
17197
|
-
|
|
17198
|
-
|
|
17199
|
-
|
|
17051
|
+
async function runLocalTurn(opts) {
|
|
17052
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
17053
|
+
const { handoff } = opts;
|
|
17054
|
+
const messages = [...handoff.messages];
|
|
17055
|
+
const offered = handoff.tools ?? [];
|
|
17056
|
+
let text2 = "";
|
|
17057
|
+
let inputTokens;
|
|
17058
|
+
let outputTokens;
|
|
17059
|
+
let promptCharacters = 0;
|
|
17060
|
+
let toolCallCount = 0;
|
|
17061
|
+
let toolsStillOffered = offered.length > 0 && !!handoff.toolGrant;
|
|
17062
|
+
for (let pass = 0; pass <= MAX_AGENTIC_STEPS; pass++) {
|
|
17063
|
+
promptCharacters += charactersIn(messages);
|
|
17064
|
+
const result = await runLocalChat({
|
|
17065
|
+
baseUrl: handoff.baseUrl,
|
|
17066
|
+
provider: handoff.provider,
|
|
17067
|
+
modelId: handoff.modelId,
|
|
17068
|
+
messages,
|
|
17069
|
+
...toolsStillOffered ? { tools: offered } : {},
|
|
17070
|
+
signal: opts.signal,
|
|
17071
|
+
onDelta: opts.onDelta,
|
|
17072
|
+
fetchImpl: doFetch
|
|
17200
17073
|
});
|
|
17201
|
-
|
|
17202
|
-
|
|
17203
|
-
|
|
17204
|
-
|
|
17205
|
-
|
|
17206
|
-
|
|
17207
|
-
|
|
17208
|
-
|
|
17209
|
-
|
|
17210
|
-
|
|
17211
|
-
|
|
17212
|
-
if (err instanceof LocalTurnFailed) {
|
|
17213
|
-
throw new CliError(
|
|
17214
|
-
err.message,
|
|
17215
|
-
2 /* NETWORK_ERROR */,
|
|
17216
|
-
err.closed ? void 0 : "Your conversation may show this one as unfinished."
|
|
17217
|
-
);
|
|
17218
|
-
}
|
|
17219
|
-
throw err;
|
|
17220
|
-
}
|
|
17221
|
-
if (g.json) {
|
|
17222
|
-
process.stdout.write(JSON.stringify(report) + "\n");
|
|
17223
|
-
return;
|
|
17224
|
-
}
|
|
17225
|
-
if (!report.answeredHere) {
|
|
17226
|
-
fmt.warn(
|
|
17227
|
-
"Great Arrow answered this one itself \u2014 it does that for harder questions. The reply is in your conversation."
|
|
17228
|
-
);
|
|
17229
|
-
return;
|
|
17230
|
-
}
|
|
17231
|
-
process.stdout.write("\n");
|
|
17232
|
-
if (!report.recorded) {
|
|
17233
|
-
fmt.warn("Answered, but we could not save it to your conversation.");
|
|
17074
|
+
if (result.inputTokens !== void 0) inputTokens = result.inputTokens;
|
|
17075
|
+
if (result.outputTokens !== void 0) outputTokens = result.outputTokens;
|
|
17076
|
+
text2 += result.text;
|
|
17077
|
+
if (!toolsStillOffered || result.toolCalls.length === 0) break;
|
|
17078
|
+
messages.push(assistantToolCallMessage(result.toolCalls));
|
|
17079
|
+
for (const call of result.toolCalls) {
|
|
17080
|
+
toolCallCount += 1;
|
|
17081
|
+
opts.onToolStep?.(call.name);
|
|
17082
|
+
const answer = await callApp(call, handoff, opts.signal, doFetch);
|
|
17083
|
+
messages.push(toolResultMessage(call, answer.result));
|
|
17084
|
+
if (answer.exhausted) toolsStillOffered = false;
|
|
17234
17085
|
}
|
|
17235
|
-
|
|
17236
|
-
}
|
|
17237
|
-
|
|
17238
|
-
|
|
17086
|
+
if (pass === MAX_AGENTIC_STEPS - 1) toolsStillOffered = false;
|
|
17087
|
+
}
|
|
17088
|
+
return {
|
|
17089
|
+
text: text2,
|
|
17090
|
+
inputTokens,
|
|
17091
|
+
outputTokens,
|
|
17092
|
+
// The settle route refuses a report with an implausible figure in it, and
|
|
17093
|
+
// refusing the report would lose the whole turn. Keep it inside what that
|
|
17094
|
+
// route accepts; a clipped ceiling only ever counts fewer words, never more.
|
|
17095
|
+
promptCharacters: Math.min(promptCharacters, MAX_REPORTED_PROMPT_CHARS),
|
|
17096
|
+
toolCallCount
|
|
17097
|
+
};
|
|
17239
17098
|
}
|
|
17240
17099
|
|
|
17241
|
-
// src/lib/
|
|
17242
|
-
var
|
|
17243
|
-
|
|
17244
|
-
|
|
17245
|
-
|
|
17246
|
-
|
|
17247
|
-
|
|
17248
|
-
|
|
17249
|
-
|
|
17250
|
-
|
|
17251
|
-
|
|
17252
|
-
|
|
17253
|
-
|
|
17254
|
-
|
|
17255
|
-
|
|
17256
|
-
|
|
17257
|
-
|
|
17258
|
-
|
|
17259
|
-
|
|
17260
|
-
|
|
17261
|
-
|
|
17262
|
-
|
|
17263
|
-
|
|
17264
|
-
|
|
17100
|
+
// ../../src/lib/ai/local/client.ts
|
|
17101
|
+
var CHAT_PATH2 = "/api/chat";
|
|
17102
|
+
var SETTLE_PATH = "/api/chat/local-turn";
|
|
17103
|
+
var MAX_REPORTED_TEXT_CHARS = 2e5;
|
|
17104
|
+
var MAX_REPORTED_LATENCY_MS = 36e5;
|
|
17105
|
+
var HANDOFF_TIMEOUT_MS = 6e4;
|
|
17106
|
+
var SETTLE_TIMEOUT_MS = 3e4;
|
|
17107
|
+
var LocalTurnFailed = class extends Error {
|
|
17108
|
+
stage;
|
|
17109
|
+
/** Set once the turn was handed back, so a caller knows the conversation is
|
|
17110
|
+
* not left with an unfinished reply in it. */
|
|
17111
|
+
closed;
|
|
17112
|
+
constructor(stage, message, closed) {
|
|
17113
|
+
super(message);
|
|
17114
|
+
this.name = "LocalTurnFailed";
|
|
17115
|
+
this.stage = stage;
|
|
17116
|
+
this.closed = closed;
|
|
17117
|
+
}
|
|
17118
|
+
};
|
|
17119
|
+
function authHeaders(token) {
|
|
17120
|
+
return {
|
|
17121
|
+
"content-type": "application/json",
|
|
17122
|
+
authorization: `Bearer ${token}`
|
|
17123
|
+
};
|
|
17265
17124
|
}
|
|
17266
|
-
function
|
|
17267
|
-
|
|
17268
|
-
if (env4.NO_COLOR !== void 0 && env4.NO_COLOR !== "") return false;
|
|
17269
|
-
if (!isTTY2) return false;
|
|
17270
|
-
if (isTruthyFlag(env4.CI)) return false;
|
|
17271
|
-
const term = (env4.TERM ?? "").trim().toLowerCase();
|
|
17272
|
-
if (term === "" || term === "dumb") return false;
|
|
17273
|
-
if (term.startsWith("screen") || term.startsWith("tmux")) return false;
|
|
17274
|
-
const program3 = (env4.TERM_PROGRAM ?? "").trim().toLowerCase();
|
|
17275
|
-
if (UNSUPPORTED_TERM_PROGRAMS.has(program3)) return false;
|
|
17276
|
-
if (SUPPORTED_TERM_PROGRAMS.has(program3)) return true;
|
|
17277
|
-
if (SUPPORTED_TERMS.has(term)) return true;
|
|
17278
|
-
if (isTruthyFlag(env4.WT_SESSION)) return true;
|
|
17279
|
-
if (isTruthyFlag(env4.KONSOLE_VERSION)) return true;
|
|
17280
|
-
if (env4.TERMINAL_EMULATOR === "JetBrains-JediTerm") return true;
|
|
17281
|
-
const vte = Number.parseInt(env4.VTE_VERSION ?? "", 10);
|
|
17282
|
-
if (Number.isFinite(vte) && vte >= MIN_VTE_VERSION) return true;
|
|
17283
|
-
return false;
|
|
17125
|
+
function origin(apiUrl) {
|
|
17126
|
+
return apiUrl.replace(/\/+$/, "");
|
|
17284
17127
|
}
|
|
17285
|
-
|
|
17286
|
-
|
|
17287
|
-
|
|
17288
|
-
|
|
17289
|
-
|
|
17128
|
+
function appAwareFetch(apiUrl, token, base) {
|
|
17129
|
+
return (input, init) => {
|
|
17130
|
+
const target = typeof input === "string" ? input : String(input);
|
|
17131
|
+
if (!target.startsWith("/")) return base(input, init);
|
|
17132
|
+
return base(`${origin(apiUrl)}${target}`, {
|
|
17133
|
+
...init,
|
|
17134
|
+
headers: {
|
|
17135
|
+
...init?.headers,
|
|
17136
|
+
...authHeaders(token)
|
|
17137
|
+
}
|
|
17138
|
+
});
|
|
17139
|
+
};
|
|
17290
17140
|
}
|
|
17291
|
-
|
|
17292
|
-
// src/lib/glance-footer.ts
|
|
17293
|
-
import { readFile as readFile4, writeFile as writeFile4, mkdir as mkdir4 } from "node:fs/promises";
|
|
17294
|
-
import { homedir as homedir4 } from "node:os";
|
|
17295
|
-
import { join as join6, dirname as dirname5 } from "node:path";
|
|
17296
|
-
|
|
17297
|
-
// src/lib/seen.ts
|
|
17298
|
-
var SEEN_TIMEOUT_MS = 1e3;
|
|
17299
|
-
var MAX_KEYS = 50;
|
|
17300
|
-
async function reportSeen(opts) {
|
|
17301
|
-
const keys = [
|
|
17302
|
-
...new Set(
|
|
17303
|
-
opts.keys.filter((k) => typeof k === "string" && k !== "")
|
|
17304
|
-
)
|
|
17305
|
-
].slice(0, MAX_KEYS);
|
|
17306
|
-
if (keys.length === 0) return 0;
|
|
17141
|
+
async function withTimeout(ms, run2) {
|
|
17307
17142
|
const controller = new AbortController();
|
|
17308
|
-
const timer = setTimeout(() => controller.abort(),
|
|
17143
|
+
const timer = setTimeout(() => controller.abort(), ms);
|
|
17144
|
+
try {
|
|
17145
|
+
return await run2(controller.signal);
|
|
17146
|
+
} finally {
|
|
17147
|
+
clearTimeout(timer);
|
|
17148
|
+
}
|
|
17149
|
+
}
|
|
17150
|
+
function readHandoff(payload) {
|
|
17151
|
+
if (!payload || typeof payload !== "object") return null;
|
|
17152
|
+
const row = payload;
|
|
17153
|
+
const strings = ["turnId", "sessionId", "baseUrl", "modelId"];
|
|
17154
|
+
for (const key of strings) {
|
|
17155
|
+
if (typeof row[key] !== "string" || !row[key]) return null;
|
|
17156
|
+
}
|
|
17157
|
+
if (!LOCAL_PROVIDERS.includes(row.provider)) {
|
|
17158
|
+
return null;
|
|
17159
|
+
}
|
|
17160
|
+
if (!Array.isArray(row.messages) || row.messages.length === 0) return null;
|
|
17161
|
+
return {
|
|
17162
|
+
turnId: row.turnId,
|
|
17163
|
+
sessionId: row.sessionId,
|
|
17164
|
+
provider: row.provider,
|
|
17165
|
+
baseUrl: row.baseUrl,
|
|
17166
|
+
modelId: row.modelId,
|
|
17167
|
+
modelLabel: typeof row.modelLabel === "string" && row.modelLabel ? row.modelLabel : row.modelId,
|
|
17168
|
+
deadlineMs: typeof row.deadlineMs === "number" ? row.deadlineMs : 12e4,
|
|
17169
|
+
firstWordDeadlineMs: typeof row.firstWordDeadlineMs === "number" ? row.firstWordDeadlineMs : 25e3,
|
|
17170
|
+
tools: Array.isArray(row.tools) ? row.tools : [],
|
|
17171
|
+
toolGrant: typeof row.toolGrant === "string" ? row.toolGrant : null,
|
|
17172
|
+
messages: row.messages,
|
|
17173
|
+
citations: Array.isArray(row.citations) ? row.citations : []
|
|
17174
|
+
};
|
|
17175
|
+
}
|
|
17176
|
+
async function askForTheTurn(opts) {
|
|
17177
|
+
let response;
|
|
17178
|
+
try {
|
|
17179
|
+
response = await withTimeout(
|
|
17180
|
+
HANDOFF_TIMEOUT_MS,
|
|
17181
|
+
(signal) => opts.doFetch(`${origin(opts.apiUrl)}${CHAT_PATH2}`, {
|
|
17182
|
+
method: "POST",
|
|
17183
|
+
headers: {
|
|
17184
|
+
...authHeaders(opts.token),
|
|
17185
|
+
[LOCAL_CAPABLE_HEADER]: LOCAL_CAPABLE_VALUE
|
|
17186
|
+
},
|
|
17187
|
+
body: JSON.stringify({
|
|
17188
|
+
messages: [
|
|
17189
|
+
{
|
|
17190
|
+
id: `local-${Date.now()}`,
|
|
17191
|
+
role: "user",
|
|
17192
|
+
parts: [{ type: "text", text: opts.question }]
|
|
17193
|
+
}
|
|
17194
|
+
],
|
|
17195
|
+
...opts.sessionId ? { sessionId: opts.sessionId } : {},
|
|
17196
|
+
...opts.timezone ? { timezone: opts.timezone } : {}
|
|
17197
|
+
}),
|
|
17198
|
+
cache: "no-store",
|
|
17199
|
+
signal
|
|
17200
|
+
})
|
|
17201
|
+
);
|
|
17202
|
+
} catch (err) {
|
|
17203
|
+
throw new LocalTurnFailed(
|
|
17204
|
+
"handoff",
|
|
17205
|
+
err instanceof Error && err.name === "AbortError" ? "Great Arrow did not answer in time." : "Could not reach Great Arrow.",
|
|
17206
|
+
true
|
|
17207
|
+
);
|
|
17208
|
+
}
|
|
17209
|
+
if (!response.ok) {
|
|
17210
|
+
throw new LocalTurnFailed(
|
|
17211
|
+
"handoff",
|
|
17212
|
+
response.status === 401 || response.status === 403 ? "Sign in again \u2014 this computer is not allowed to ask for work." : `Great Arrow refused that (${response.status}).`,
|
|
17213
|
+
true
|
|
17214
|
+
);
|
|
17215
|
+
}
|
|
17216
|
+
if (response.headers.get(LOCAL_TURN_HEADER) !== "1") {
|
|
17217
|
+
try {
|
|
17218
|
+
await response.text();
|
|
17219
|
+
} catch {
|
|
17220
|
+
return null;
|
|
17221
|
+
}
|
|
17222
|
+
return null;
|
|
17223
|
+
}
|
|
17224
|
+
let payload;
|
|
17225
|
+
try {
|
|
17226
|
+
payload = await response.json();
|
|
17227
|
+
} catch {
|
|
17228
|
+
payload = null;
|
|
17229
|
+
}
|
|
17230
|
+
const handoff = readHandoff(payload);
|
|
17231
|
+
if (!handoff) {
|
|
17232
|
+
throw new LocalTurnFailed(
|
|
17233
|
+
"handoff",
|
|
17234
|
+
"That work order made no sense.",
|
|
17235
|
+
true
|
|
17236
|
+
);
|
|
17237
|
+
}
|
|
17238
|
+
return handoff;
|
|
17239
|
+
}
|
|
17240
|
+
async function handBack(opts) {
|
|
17309
17241
|
try {
|
|
17310
|
-
const response = await
|
|
17311
|
-
|
|
17312
|
-
{
|
|
17242
|
+
const response = await withTimeout(
|
|
17243
|
+
SETTLE_TIMEOUT_MS,
|
|
17244
|
+
(signal) => opts.doFetch(`${origin(opts.apiUrl)}${SETTLE_PATH}`, {
|
|
17313
17245
|
method: "POST",
|
|
17314
|
-
headers:
|
|
17315
|
-
|
|
17316
|
-
|
|
17317
|
-
|
|
17318
|
-
|
|
17319
|
-
|
|
17320
|
-
|
|
17246
|
+
headers: authHeaders(opts.token),
|
|
17247
|
+
body: JSON.stringify({
|
|
17248
|
+
turnId: opts.handoff.turnId,
|
|
17249
|
+
sessionId: opts.handoff.sessionId,
|
|
17250
|
+
modelId: opts.handoff.modelId,
|
|
17251
|
+
outcome: opts.outcome,
|
|
17252
|
+
text: opts.text.slice(0, MAX_REPORTED_TEXT_CHARS),
|
|
17253
|
+
...opts.inputTokens !== void 0 ? { inputTokens: opts.inputTokens } : {},
|
|
17254
|
+
...opts.outputTokens !== void 0 ? { outputTokens: opts.outputTokens } : {},
|
|
17255
|
+
latencyMs: Math.min(
|
|
17256
|
+
Math.max(0, Math.round(opts.latencyMs)),
|
|
17257
|
+
MAX_REPORTED_LATENCY_MS
|
|
17258
|
+
),
|
|
17259
|
+
promptCharacters: opts.promptCharacters
|
|
17260
|
+
}),
|
|
17261
|
+
cache: "no-store",
|
|
17262
|
+
signal
|
|
17263
|
+
})
|
|
17321
17264
|
);
|
|
17322
|
-
if (!response.ok) return
|
|
17323
|
-
const
|
|
17324
|
-
return
|
|
17265
|
+
if (!response.ok) return false;
|
|
17266
|
+
const body = await response.json();
|
|
17267
|
+
return body?.recorded === true;
|
|
17325
17268
|
} catch {
|
|
17326
|
-
return
|
|
17269
|
+
return false;
|
|
17270
|
+
}
|
|
17271
|
+
}
|
|
17272
|
+
async function answerOnThisComputer(opts) {
|
|
17273
|
+
const base = opts.fetchImpl ?? fetch;
|
|
17274
|
+
const doFetch = appAwareFetch(opts.apiUrl, opts.token, base);
|
|
17275
|
+
const handoff = await askForTheTurn({
|
|
17276
|
+
apiUrl: opts.apiUrl,
|
|
17277
|
+
token: opts.token,
|
|
17278
|
+
question: opts.question,
|
|
17279
|
+
sessionId: opts.sessionId ?? null,
|
|
17280
|
+
timezone: opts.timezone,
|
|
17281
|
+
doFetch: base
|
|
17282
|
+
});
|
|
17283
|
+
if (!handoff) {
|
|
17284
|
+
return { answeredHere: false, reason: "answered_by_great_arrow" };
|
|
17285
|
+
}
|
|
17286
|
+
const ceiling = Math.min(
|
|
17287
|
+
handoff.deadlineMs,
|
|
17288
|
+
opts.deadlineMs ?? handoff.deadlineMs
|
|
17289
|
+
);
|
|
17290
|
+
const controller = new AbortController();
|
|
17291
|
+
const startedAt = Date.now();
|
|
17292
|
+
let words = "";
|
|
17293
|
+
let sawFirstWord = false;
|
|
17294
|
+
const firstWord = setTimeout(
|
|
17295
|
+
() => {
|
|
17296
|
+
if (!sawFirstWord) controller.abort();
|
|
17297
|
+
},
|
|
17298
|
+
Math.min(await firstWordDeadlineMsFor(handoff), ceiling)
|
|
17299
|
+
);
|
|
17300
|
+
const whole = setTimeout(() => controller.abort(), ceiling);
|
|
17301
|
+
try {
|
|
17302
|
+
const result = await runLocalTurn({
|
|
17303
|
+
handoff,
|
|
17304
|
+
signal: controller.signal,
|
|
17305
|
+
onDelta: (delta) => {
|
|
17306
|
+
sawFirstWord = true;
|
|
17307
|
+
words += delta;
|
|
17308
|
+
opts.onWords?.(delta);
|
|
17309
|
+
},
|
|
17310
|
+
...opts.onApp ? { onToolStep: opts.onApp } : {},
|
|
17311
|
+
fetchImpl: doFetch
|
|
17312
|
+
});
|
|
17313
|
+
const recorded = await handBack({
|
|
17314
|
+
apiUrl: opts.apiUrl,
|
|
17315
|
+
token: opts.token,
|
|
17316
|
+
handoff,
|
|
17317
|
+
outcome: "finished",
|
|
17318
|
+
text: result.text,
|
|
17319
|
+
inputTokens: result.inputTokens,
|
|
17320
|
+
outputTokens: result.outputTokens,
|
|
17321
|
+
promptCharacters: result.promptCharacters,
|
|
17322
|
+
latencyMs: Date.now() - startedAt,
|
|
17323
|
+
doFetch: base
|
|
17324
|
+
});
|
|
17325
|
+
return {
|
|
17326
|
+
answeredHere: true,
|
|
17327
|
+
text: result.text,
|
|
17328
|
+
modelLabel: handoff.modelLabel,
|
|
17329
|
+
appsUsed: result.toolCallCount,
|
|
17330
|
+
recorded
|
|
17331
|
+
};
|
|
17332
|
+
} catch {
|
|
17333
|
+
const closed = await handBack({
|
|
17334
|
+
apiUrl: opts.apiUrl,
|
|
17335
|
+
token: opts.token,
|
|
17336
|
+
handoff,
|
|
17337
|
+
outcome: "stopped",
|
|
17338
|
+
text: words,
|
|
17339
|
+
inputTokens: void 0,
|
|
17340
|
+
outputTokens: void 0,
|
|
17341
|
+
promptCharacters: 0,
|
|
17342
|
+
latencyMs: Date.now() - startedAt,
|
|
17343
|
+
doFetch: base
|
|
17344
|
+
});
|
|
17345
|
+
throw new LocalTurnFailed(
|
|
17346
|
+
"writing",
|
|
17347
|
+
controller.signal.aborted ? `${handoff.modelLabel} took too long, so we stopped waiting. Ask again and Great Arrow will answer it. If this was the first question since you started ${handoff.provider === "ollama" ? "Ollama" : "LM Studio"}, the model was probably still loading \u2014 it is quicker once it has.` : `${handoff.modelLabel} could not finish that. Check ${handoff.baseUrl} is still running.`,
|
|
17348
|
+
closed
|
|
17349
|
+
);
|
|
17327
17350
|
} finally {
|
|
17328
|
-
clearTimeout(
|
|
17351
|
+
clearTimeout(firstWord);
|
|
17352
|
+
clearTimeout(whole);
|
|
17329
17353
|
}
|
|
17330
17354
|
}
|
|
17331
17355
|
|
|
17332
|
-
// src/lib/
|
|
17333
|
-
var
|
|
17334
|
-
|
|
17335
|
-
|
|
17336
|
-
|
|
17337
|
-
|
|
17338
|
-
|
|
17339
|
-
|
|
17340
|
-
|
|
17341
|
-
|
|
17342
|
-
|
|
17343
|
-
|
|
17344
|
-
|
|
17345
|
-
|
|
17346
|
-
|
|
17347
|
-
|
|
17348
|
-
if (
|
|
17349
|
-
|
|
17350
|
-
|
|
17351
|
-
|
|
17352
|
-
|
|
17353
|
-
|
|
17354
|
-
|
|
17355
|
-
|
|
17356
|
-
|
|
17357
|
-
|
|
17358
|
-
|
|
17359
|
-
|
|
17360
|
-
|
|
17361
|
-
|
|
17362
|
-
|
|
17356
|
+
// ../../src/lib/ai/local/catalog.ts
|
|
17357
|
+
var EMBEDDING_ONLY_PATTERNS = [
|
|
17358
|
+
/nomic-embed/i,
|
|
17359
|
+
/(?:^|[^a-z0-9])bge-/i,
|
|
17360
|
+
/all-minilm/i,
|
|
17361
|
+
/mxbai-embed/i,
|
|
17362
|
+
/text-embedding/i
|
|
17363
|
+
];
|
|
17364
|
+
function isEmbeddingOnlyModelId(id) {
|
|
17365
|
+
return EMBEDDING_ONLY_PATTERNS.some((pattern) => pattern.test(id));
|
|
17366
|
+
}
|
|
17367
|
+
function positiveInt(value) {
|
|
17368
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.min(Math.floor(value), 1e7) : 0;
|
|
17369
|
+
}
|
|
17370
|
+
function firstString(...candidates) {
|
|
17371
|
+
for (const candidate of candidates) {
|
|
17372
|
+
if (typeof candidate === "string" && candidate.length > 0) return candidate;
|
|
17373
|
+
}
|
|
17374
|
+
return void 0;
|
|
17375
|
+
}
|
|
17376
|
+
function collect(entries, read) {
|
|
17377
|
+
if (!Array.isArray(entries)) return [];
|
|
17378
|
+
const out = [];
|
|
17379
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17380
|
+
for (const entry of entries) {
|
|
17381
|
+
if (out.length >= MAX_LOCAL_MODELS) break;
|
|
17382
|
+
if (!entry || typeof entry !== "object") continue;
|
|
17383
|
+
const { id, label, contextLength } = read(entry);
|
|
17384
|
+
if (!isValidLocalModelId(id)) continue;
|
|
17385
|
+
if (seen.has(id)) continue;
|
|
17386
|
+
if (isEmbeddingOnlyModelId(id)) continue;
|
|
17387
|
+
seen.add(id);
|
|
17388
|
+
out.push({
|
|
17389
|
+
id,
|
|
17390
|
+
label: typeof label === "string" && label.length > 0 ? label : id,
|
|
17391
|
+
// Never assumed. A model is only marked tool-capable once the provider
|
|
17392
|
+
// says so (see normalizeShowResponse) — a model that silently ignores
|
|
17393
|
+
// tool calls is worse than one that honestly offers chat only.
|
|
17394
|
+
toolCapable: false,
|
|
17395
|
+
contextLength: positiveInt(contextLength)
|
|
17363
17396
|
});
|
|
17364
|
-
} catch (err) {
|
|
17365
|
-
debugSwallow("glance-footer", err);
|
|
17366
17397
|
}
|
|
17398
|
+
return out;
|
|
17367
17399
|
}
|
|
17368
|
-
function
|
|
17369
|
-
if (!
|
|
17370
|
-
|
|
17371
|
-
|
|
17372
|
-
|
|
17373
|
-
|
|
17374
|
-
|
|
17375
|
-
if (argv.includes("--no-glance")) return false;
|
|
17376
|
-
const command = firstCommand(argv);
|
|
17377
|
-
if (!command) return false;
|
|
17378
|
-
if (SKIP_COMMANDS.has(command)) return false;
|
|
17379
|
-
return true;
|
|
17400
|
+
function normalizeOllamaTags(json) {
|
|
17401
|
+
if (!json || typeof json !== "object") return [];
|
|
17402
|
+
const models = json.models;
|
|
17403
|
+
return collect(models, (entry) => ({
|
|
17404
|
+
id: firstString(entry.model, entry.name),
|
|
17405
|
+
label: firstString(entry.name, entry.model)
|
|
17406
|
+
}));
|
|
17380
17407
|
}
|
|
17381
|
-
function
|
|
17382
|
-
|
|
17383
|
-
|
|
17408
|
+
function normalizeLmStudioModels(json) {
|
|
17409
|
+
if (!json || typeof json !== "object") return [];
|
|
17410
|
+
const data = json.data;
|
|
17411
|
+
return collect(data, (entry) => ({
|
|
17412
|
+
id: entry.id,
|
|
17413
|
+
label: firstString(entry.id),
|
|
17414
|
+
contextLength: positiveInt(entry.max_context_length) || positiveInt(entry.loaded_context_length) || positiveInt(entry.context_length)
|
|
17415
|
+
}));
|
|
17416
|
+
}
|
|
17417
|
+
function normalizeShowResponse(json) {
|
|
17418
|
+
if (!json || typeof json !== "object") {
|
|
17419
|
+
return { toolCapable: false, contextLength: 0 };
|
|
17420
|
+
}
|
|
17421
|
+
const o = json;
|
|
17422
|
+
let toolCapable = false;
|
|
17423
|
+
if (Array.isArray(o.capabilities)) {
|
|
17424
|
+
toolCapable = o.capabilities.some(
|
|
17425
|
+
(c) => typeof c === "string" && c.toLowerCase() === "tools"
|
|
17426
|
+
);
|
|
17427
|
+
}
|
|
17428
|
+
if (!toolCapable) {
|
|
17429
|
+
const template = typeof o.template === "string" ? o.template : "";
|
|
17430
|
+
toolCapable = /\.tools\b/i.test(template);
|
|
17431
|
+
}
|
|
17432
|
+
return { toolCapable, contextLength: readContextLength(o.model_info) };
|
|
17433
|
+
}
|
|
17434
|
+
function readContextLength(modelInfo) {
|
|
17435
|
+
if (!modelInfo || typeof modelInfo !== "object") return 0;
|
|
17436
|
+
for (const [key, value] of Object.entries(
|
|
17437
|
+
modelInfo
|
|
17438
|
+
)) {
|
|
17439
|
+
if (!key.endsWith(".context_length") && key !== "context_length") continue;
|
|
17440
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
17441
|
+
return Math.floor(value);
|
|
17442
|
+
}
|
|
17443
|
+
}
|
|
17444
|
+
return 0;
|
|
17445
|
+
}
|
|
17446
|
+
|
|
17447
|
+
// ../../src/lib/ai/local/discover.ts
|
|
17448
|
+
var LIST_PATH = {
|
|
17449
|
+
ollama: "/api/tags",
|
|
17450
|
+
lmstudio: "/v1/models"
|
|
17451
|
+
};
|
|
17452
|
+
var DEFAULT_TIMEOUT_MS4 = 1e4;
|
|
17453
|
+
var MAX_CAPABILITY_LOOKUPS = 12;
|
|
17454
|
+
var LocalAppUnreachable = class extends Error {
|
|
17455
|
+
baseUrl;
|
|
17456
|
+
constructor(baseUrl, detail) {
|
|
17457
|
+
super(detail);
|
|
17458
|
+
this.name = "LocalAppUnreachable";
|
|
17459
|
+
this.baseUrl = baseUrl;
|
|
17384
17460
|
}
|
|
17385
|
-
|
|
17386
|
-
|
|
17387
|
-
async function fetchGlance(apiUrl, token) {
|
|
17461
|
+
};
|
|
17462
|
+
async function ask(doFetch, url, init, timeoutMs) {
|
|
17388
17463
|
const controller = new AbortController();
|
|
17389
|
-
const timer = setTimeout(() => controller.abort(),
|
|
17464
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
17390
17465
|
try {
|
|
17391
|
-
|
|
17392
|
-
|
|
17393
|
-
|
|
17394
|
-
"Content-Type": "application/json",
|
|
17395
|
-
Authorization: `Bearer ${token}`
|
|
17396
|
-
},
|
|
17397
|
-
body: JSON.stringify({
|
|
17398
|
-
jsonrpc: "2.0",
|
|
17399
|
-
id: `cli-glance-${Date.now()}`,
|
|
17400
|
-
method: "tools/call",
|
|
17401
|
-
params: { name: "glance", arguments: {} }
|
|
17402
|
-
}),
|
|
17466
|
+
return await doFetch(url, {
|
|
17467
|
+
...init,
|
|
17468
|
+
cache: "no-store",
|
|
17403
17469
|
signal: controller.signal
|
|
17404
17470
|
});
|
|
17405
|
-
if (!response.ok) return null;
|
|
17406
|
-
const data = await response.json();
|
|
17407
|
-
if (data.error) return null;
|
|
17408
|
-
return normalizeGlance(
|
|
17409
|
-
parseMcpPayload(data.result?.content ?? data.result)
|
|
17410
|
-
);
|
|
17411
|
-
} catch {
|
|
17412
|
-
return null;
|
|
17413
17471
|
} finally {
|
|
17414
17472
|
clearTimeout(timer);
|
|
17415
17473
|
}
|
|
17416
17474
|
}
|
|
17417
|
-
function
|
|
17418
|
-
|
|
17419
|
-
|
|
17420
|
-
|
|
17421
|
-
|
|
17422
|
-
const it = item;
|
|
17423
|
-
if (typeof it.title !== "string") return [];
|
|
17424
|
-
return [
|
|
17425
|
-
{
|
|
17426
|
-
kind: typeof it.kind === "string" ? it.kind : "nudge",
|
|
17427
|
-
title: it.title,
|
|
17428
|
-
action: typeof it.action === "string" ? it.action : null,
|
|
17429
|
-
href: typeof it.href === "string" && it.href !== "" ? it.href : null,
|
|
17430
|
-
...typeof it.rank === "number" ? { rank: it.rank } : {},
|
|
17431
|
-
signalKey: typeof it.signalKey === "string" && it.signalKey !== "" ? it.signalKey : null,
|
|
17432
|
-
workspace: typeof it.workspace === "string" && it.workspace !== "" ? it.workspace : null,
|
|
17433
|
-
// Parsed like every other field: a non-string is dropped rather than
|
|
17434
|
-
// coerced, so a malformed payload cannot render as a currency claim.
|
|
17435
|
-
freshness: typeof it.freshness === "string" && it.freshness !== "" ? it.freshness : null,
|
|
17436
|
-
meta: typeof it.meta === "string" && it.meta !== "" ? it.meta : null
|
|
17437
|
-
}
|
|
17438
|
-
];
|
|
17439
|
-
});
|
|
17440
|
-
const atRisk = asArray2(obj.atRisk).flatMap((item) => {
|
|
17441
|
-
if (typeof item !== "object" || item === null) return [];
|
|
17442
|
-
const it = item;
|
|
17443
|
-
if (typeof it.title !== "string") return [];
|
|
17444
|
-
return [
|
|
17445
|
-
{
|
|
17446
|
-
classification: typeof it.classification === "string" ? it.classification : null,
|
|
17447
|
-
riskLevel: typeof it.riskLevel === "string" ? it.riskLevel : "high",
|
|
17448
|
-
title: it.title,
|
|
17449
|
-
branch: typeof it.branch === "string" ? it.branch : null,
|
|
17450
|
-
aheadMain: typeof it.aheadMain === "number" ? it.aheadMain : null,
|
|
17451
|
-
recommendedAction: typeof it.recommendedAction === "string" ? it.recommendedAction : null
|
|
17452
|
-
}
|
|
17453
|
-
];
|
|
17454
|
-
});
|
|
17455
|
-
const integrations = asArray2(obj.integrations).flatMap((item) => {
|
|
17456
|
-
if (typeof item !== "object" || item === null) return [];
|
|
17457
|
-
const it = item;
|
|
17458
|
-
if (typeof it.kind !== "string") return [];
|
|
17459
|
-
return [
|
|
17460
|
-
{
|
|
17461
|
-
kind: it.kind,
|
|
17462
|
-
staleFor: typeof it.staleFor === "string" ? it.staleFor : null,
|
|
17463
|
-
refresh: typeof it.refresh === "string" ? it.refresh : null
|
|
17464
|
-
}
|
|
17465
|
-
];
|
|
17466
|
-
});
|
|
17467
|
-
const agents = asArray2(obj.agents).flatMap((item) => {
|
|
17468
|
-
if (typeof item !== "object" || item === null) return [];
|
|
17469
|
-
const it = item;
|
|
17470
|
-
if (typeof it.name !== "string") return [];
|
|
17471
|
-
return [
|
|
17475
|
+
async function capabilitiesOf(doFetch, baseUrl, modelId, timeoutMs) {
|
|
17476
|
+
try {
|
|
17477
|
+
const response = await ask(
|
|
17478
|
+
doFetch,
|
|
17479
|
+
localApiUrl(baseUrl, "/api/show"),
|
|
17472
17480
|
{
|
|
17473
|
-
|
|
17474
|
-
|
|
17475
|
-
|
|
17476
|
-
}
|
|
17477
|
-
|
|
17478
|
-
|
|
17479
|
-
|
|
17480
|
-
|
|
17481
|
-
|
|
17482
|
-
|
|
17483
|
-
topGoal = {
|
|
17484
|
-
title: g.title,
|
|
17485
|
-
done: typeof g.done === "number" ? g.done : 0,
|
|
17486
|
-
total: typeof g.total === "number" ? g.total : 0,
|
|
17487
|
-
eta: typeof g.eta === "string" ? g.eta : null
|
|
17488
|
-
};
|
|
17489
|
-
}
|
|
17490
|
-
}
|
|
17491
|
-
let warRoom = null;
|
|
17492
|
-
if (typeof obj.warRoom === "object" && obj.warRoom !== null) {
|
|
17493
|
-
const w = obj.warRoom;
|
|
17494
|
-
warRoom = {
|
|
17495
|
-
priorityCount: typeof w.priorityCount === "number" ? w.priorityCount : 0,
|
|
17496
|
-
unreadTotal: typeof w.unreadTotal === "number" ? w.unreadTotal : 0,
|
|
17497
|
-
openRequestCount: typeof w.openRequestCount === "number" ? w.openRequestCount : 0,
|
|
17498
|
-
topSender: typeof w.topSender === "string" ? w.topSender : null,
|
|
17499
|
-
newestSender: typeof w.newestSender === "string" ? w.newestSender : null,
|
|
17500
|
-
newestPreview: typeof w.newestPreview === "string" ? w.newestPreview : null
|
|
17501
|
-
};
|
|
17481
|
+
method: "POST",
|
|
17482
|
+
headers: { "content-type": "application/json" },
|
|
17483
|
+
body: JSON.stringify({ model: modelId })
|
|
17484
|
+
},
|
|
17485
|
+
timeoutMs
|
|
17486
|
+
);
|
|
17487
|
+
if (!response.ok) return { toolCapable: false, contextLength: 0 };
|
|
17488
|
+
return normalizeShowResponse(await response.json());
|
|
17489
|
+
} catch {
|
|
17490
|
+
return { toolCapable: false, contextLength: 0 };
|
|
17502
17491
|
}
|
|
17503
|
-
return {
|
|
17504
|
-
workspace: typeof obj.workspace === "string" && obj.workspace !== "" ? obj.workspace : null,
|
|
17505
|
-
// An older server sends no `reads` at all, so this normalizes to an empty
|
|
17506
|
-
// list rather than inventing the pinned workspace as the read set — the
|
|
17507
|
-
// CLI then renders no breadth cue, which is honest, instead of a claim it
|
|
17508
|
-
// was never told.
|
|
17509
|
-
reads: Array.isArray(obj.reads) ? obj.reads.flatMap((r) => {
|
|
17510
|
-
if (typeof r !== "object" || r === null) return [];
|
|
17511
|
-
const row = r;
|
|
17512
|
-
return typeof row.id === "string" && typeof row.name === "string" ? [{ id: row.id, name: row.name }] : [];
|
|
17513
|
-
}) : [],
|
|
17514
|
-
needsYou,
|
|
17515
|
-
atRisk,
|
|
17516
|
-
topGoal,
|
|
17517
|
-
integrations,
|
|
17518
|
-
agents,
|
|
17519
|
-
warRoom,
|
|
17520
|
-
// Parsed like every other field: both halves must be the right type or the
|
|
17521
|
-
// whole line is dropped, so a malformed payload cannot render as a dollar.
|
|
17522
|
-
spend: (() => {
|
|
17523
|
-
const raw2 = obj.spend;
|
|
17524
|
-
if (typeof raw2 !== "object" || raw2 === null) return null;
|
|
17525
|
-
const sp = raw2;
|
|
17526
|
-
return typeof sp.line === "string" && sp.line !== "" && typeof sp.totalCostUsd === "number" && Number.isFinite(sp.totalCostUsd) ? { line: sp.line, totalCostUsd: sp.totalCostUsd } : null;
|
|
17527
|
-
})(),
|
|
17528
|
-
urgency: typeof obj.urgency === "number" ? obj.urgency : null,
|
|
17529
|
-
generatedAt: typeof obj.generatedAt === "string" ? obj.generatedAt : (/* @__PURE__ */ new Date()).toISOString()
|
|
17530
|
-
};
|
|
17531
17492
|
}
|
|
17532
|
-
function
|
|
17533
|
-
|
|
17493
|
+
async function findInstalledModels(opts) {
|
|
17494
|
+
if (!isValidLocalBaseUrl(opts.baseUrl)) {
|
|
17495
|
+
throw new LocalAppUnreachable(opts.baseUrl, "That address is not usable.");
|
|
17496
|
+
}
|
|
17497
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
17498
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS4;
|
|
17499
|
+
const url = localApiUrl(opts.baseUrl, LIST_PATH[opts.provider]);
|
|
17500
|
+
let response;
|
|
17501
|
+
try {
|
|
17502
|
+
response = await ask(doFetch, url, { method: "GET" }, timeoutMs);
|
|
17503
|
+
} catch (err) {
|
|
17504
|
+
throw new LocalAppUnreachable(
|
|
17505
|
+
opts.baseUrl,
|
|
17506
|
+
err instanceof Error && err.name === "AbortError" ? "It did not answer in time." : "Nothing answered there."
|
|
17507
|
+
);
|
|
17508
|
+
}
|
|
17509
|
+
if (!response.ok) {
|
|
17510
|
+
throw new LocalAppUnreachable(
|
|
17511
|
+
opts.baseUrl,
|
|
17512
|
+
`It answered with ${response.status}.`
|
|
17513
|
+
);
|
|
17514
|
+
}
|
|
17515
|
+
const json = await response.json().catch(() => null);
|
|
17516
|
+
const models = opts.provider === "ollama" ? normalizeOllamaTags(json) : normalizeLmStudioModels(json);
|
|
17517
|
+
if (opts.provider !== "ollama") return models;
|
|
17518
|
+
const looked = models.slice(0, MAX_CAPABILITY_LOOKUPS);
|
|
17519
|
+
const details = await Promise.all(
|
|
17520
|
+
looked.map(
|
|
17521
|
+
(model) => capabilitiesOf(doFetch, opts.baseUrl, model.id, timeoutMs)
|
|
17522
|
+
)
|
|
17523
|
+
);
|
|
17524
|
+
return models.map((model, index) => {
|
|
17525
|
+
const detail = details[index];
|
|
17526
|
+
if (!detail) return model;
|
|
17527
|
+
return {
|
|
17528
|
+
...model,
|
|
17529
|
+
toolCapable: detail.toolCapable,
|
|
17530
|
+
// The list call never reports a window, so only ever widen from the
|
|
17531
|
+
// lookup — a zero means "not reported", not "no room".
|
|
17532
|
+
contextLength: detail.contextLength || model.contextLength
|
|
17533
|
+
};
|
|
17534
|
+
});
|
|
17534
17535
|
}
|
|
17535
|
-
|
|
17536
|
-
|
|
17537
|
-
|
|
17536
|
+
|
|
17537
|
+
// ../../src/lib/ai/local/usage-format.ts
|
|
17538
|
+
var LOCAL_USAGE_DEFAULT_DAYS = 30;
|
|
17539
|
+
function formatLocalUsageLine(summary) {
|
|
17540
|
+
if (summary.answers <= 0) {
|
|
17541
|
+
return "No answers have run on your computer yet.";
|
|
17538
17542
|
}
|
|
17539
|
-
const
|
|
17540
|
-
if (
|
|
17541
|
-
|
|
17542
|
-
|
|
17543
|
-
|
|
17544
|
-
if (agent) return `agent:${agent.name}`;
|
|
17545
|
-
if (payload.topGoal) return `goal:${payload.topGoal.title}`;
|
|
17546
|
-
return "empty";
|
|
17543
|
+
const window = summary.days === 1 ? "day" : `${summary.days} days`;
|
|
17544
|
+
if (summary.answers === 1) {
|
|
17545
|
+
return `1 answer ran on your computer in the last ${window}. It cost nothing.`;
|
|
17546
|
+
}
|
|
17547
|
+
return `${summary.answers} answers ran on your computer in the last ${window}. They cost nothing.`;
|
|
17547
17548
|
}
|
|
17548
|
-
|
|
17549
|
-
|
|
17550
|
-
|
|
17551
|
-
|
|
17552
|
-
if (
|
|
17553
|
-
|
|
17549
|
+
|
|
17550
|
+
// src/commands/local.ts
|
|
17551
|
+
var CHECK_TIMEOUT_MS = 3e4;
|
|
17552
|
+
function throwIfRefused(payload, what) {
|
|
17553
|
+
if (typeof payload.error !== "string") return;
|
|
17554
|
+
throw new CliError(
|
|
17555
|
+
`Great Arrow could not ${what} (${payload.error}).`,
|
|
17556
|
+
2 /* NETWORK_ERROR */,
|
|
17557
|
+
payload.hint
|
|
17558
|
+
);
|
|
17554
17559
|
}
|
|
17555
|
-
function
|
|
17556
|
-
|
|
17560
|
+
function readProvider(raw) {
|
|
17561
|
+
const value = typeof raw === "string" ? raw : "ollama";
|
|
17562
|
+
if (!LOCAL_PROVIDERS.includes(value)) {
|
|
17563
|
+
throw validationError(
|
|
17564
|
+
`Unknown app "${value}".`,
|
|
17565
|
+
`Use one of: ${LOCAL_PROVIDERS.join(", ")}.`
|
|
17566
|
+
);
|
|
17567
|
+
}
|
|
17568
|
+
return value;
|
|
17557
17569
|
}
|
|
17558
|
-
|
|
17559
|
-
|
|
17560
|
-
const
|
|
17561
|
-
const
|
|
17562
|
-
|
|
17563
|
-
|
|
17570
|
+
function checkCommand() {
|
|
17571
|
+
return new Command("check").description("Find the models installed on this computer").option("--app <name>", "ollama or lmstudio", "ollama").option("--url <address>", "Where that app is listening").option("--use <model>", "Make this the model that answers you").action(async (options, command) => {
|
|
17572
|
+
const g = globalOutputOpts(command);
|
|
17573
|
+
const provider = readProvider(options.app);
|
|
17574
|
+
const baseUrl = typeof options.url === "string" && options.url ? options.url : LOCAL_PROVIDER_PRESETS[provider].baseUrl;
|
|
17575
|
+
const auth = await resolveAuth();
|
|
17576
|
+
const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
|
|
17577
|
+
const fmt = createFormatter({
|
|
17578
|
+
json: g.json,
|
|
17579
|
+
quiet: g.quiet,
|
|
17580
|
+
isTTY: isTTY(),
|
|
17581
|
+
noColor: g.noColor
|
|
17582
|
+
});
|
|
17583
|
+
let models;
|
|
17584
|
+
try {
|
|
17585
|
+
models = await findInstalledModels({ baseUrl, provider });
|
|
17586
|
+
} catch (err) {
|
|
17587
|
+
if (err instanceof LocalAppUnreachable) {
|
|
17588
|
+
throw new CliError(
|
|
17589
|
+
`Could not reach ${LOCAL_PROVIDER_PRESETS[provider].label} at ${err.baseUrl}. ${err.message}`,
|
|
17590
|
+
2 /* NETWORK_ERROR */,
|
|
17591
|
+
`Start it on this computer and try again, or point somewhere else with --url.`
|
|
17592
|
+
);
|
|
17593
|
+
}
|
|
17594
|
+
throw err;
|
|
17564
17595
|
}
|
|
17565
|
-
|
|
17566
|
-
|
|
17567
|
-
|
|
17568
|
-
|
|
17569
|
-
|
|
17570
|
-
|
|
17571
|
-
|
|
17572
|
-
|
|
17573
|
-
|
|
17596
|
+
const saved = await client.mcpCall(
|
|
17597
|
+
"model_local_set",
|
|
17598
|
+
{
|
|
17599
|
+
enabled: true,
|
|
17600
|
+
provider,
|
|
17601
|
+
base_url: baseUrl,
|
|
17602
|
+
models: models.map((model) => ({
|
|
17603
|
+
id: model.id,
|
|
17604
|
+
label: model.label,
|
|
17605
|
+
tool_capable: model.toolCapable,
|
|
17606
|
+
context_length: model.contextLength
|
|
17607
|
+
})),
|
|
17608
|
+
...typeof options.use === "string" && options.use ? { default_model_id: options.use } : {}
|
|
17609
|
+
},
|
|
17610
|
+
CHECK_TIMEOUT_MS
|
|
17611
|
+
);
|
|
17612
|
+
if (g.json) {
|
|
17613
|
+
process.stdout.write(JSON.stringify({ models, saved }) + "\n");
|
|
17614
|
+
return;
|
|
17615
|
+
}
|
|
17616
|
+
throwIfRefused(saved, "record what is on this computer");
|
|
17617
|
+
if (models.length === 0) {
|
|
17618
|
+
fmt.warn(
|
|
17619
|
+
`${LOCAL_PROVIDER_PRESETS[provider].label} is running but has no models yet. Install one, then run this again.`
|
|
17620
|
+
);
|
|
17621
|
+
return;
|
|
17622
|
+
}
|
|
17623
|
+
const chosen = saved.preferences?.defaultModelId ?? null;
|
|
17624
|
+
process.stdout.write(
|
|
17625
|
+
`Found ${models.length} ${models.length === 1 ? "model" : "models"} on this computer:
|
|
17626
|
+
`
|
|
17627
|
+
);
|
|
17628
|
+
for (const model of models) {
|
|
17629
|
+
const mark = model.id === chosen ? "*" : " ";
|
|
17630
|
+
const limits = model.toolCapable ? "can look things up" : "answers from your library only";
|
|
17631
|
+
process.stdout.write(`${mark} ${model.label} \u2014 ${limits}
|
|
17632
|
+
`);
|
|
17633
|
+
}
|
|
17634
|
+
if (chosen) {
|
|
17635
|
+
process.stdout.write(`
|
|
17636
|
+
Answering with ${chosen}.
|
|
17637
|
+
`);
|
|
17638
|
+
} else {
|
|
17639
|
+
process.stdout.write(
|
|
17640
|
+
`
|
|
17641
|
+
Pick one with: gad local check --use <model>
|
|
17642
|
+
`
|
|
17643
|
+
);
|
|
17644
|
+
}
|
|
17645
|
+
});
|
|
17574
17646
|
}
|
|
17575
|
-
function
|
|
17576
|
-
|
|
17577
|
-
|
|
17578
|
-
|
|
17579
|
-
|
|
17647
|
+
function statusCommand2() {
|
|
17648
|
+
return new Command("status").description("Show what is set up for models on this computer").action(async (_options, command) => {
|
|
17649
|
+
const g = globalOutputOpts(command);
|
|
17650
|
+
const auth = await resolveAuth();
|
|
17651
|
+
const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
|
|
17652
|
+
const payload = await client.mcpCall(
|
|
17653
|
+
"model_local_get",
|
|
17654
|
+
{},
|
|
17655
|
+
CHECK_TIMEOUT_MS
|
|
17656
|
+
);
|
|
17657
|
+
if (g.json) {
|
|
17658
|
+
process.stdout.write(JSON.stringify(payload) + "\n");
|
|
17659
|
+
return;
|
|
17660
|
+
}
|
|
17661
|
+
throwIfRefused(payload, "read your on-device settings");
|
|
17662
|
+
process.stdout.write(renderStatus(payload, g.noColor || !isTTY()) + "\n");
|
|
17663
|
+
});
|
|
17580
17664
|
}
|
|
17581
|
-
function
|
|
17582
|
-
const c = createColor(
|
|
17665
|
+
function renderStatus(payload, plain) {
|
|
17666
|
+
const c = createColor(plain);
|
|
17667
|
+
const prefs = payload.preferences ?? {};
|
|
17668
|
+
const models = Array.isArray(prefs.models) ? prefs.models : [];
|
|
17669
|
+
const provider = LOCAL_PROVIDERS.includes(
|
|
17670
|
+
prefs.provider ?? ""
|
|
17671
|
+
) ? LOCAL_PROVIDER_PRESETS[prefs.provider] : null;
|
|
17672
|
+
const appLabel = provider?.label ?? prefs.provider ?? "Ollama";
|
|
17673
|
+
const baseUrl = prefs.baseUrl ?? provider?.baseUrl ?? "";
|
|
17674
|
+
const chosen = models.find((model) => model.id === prefs.defaultModelId);
|
|
17675
|
+
const chosenName = chosen?.label ?? chosen?.id ?? null;
|
|
17583
17676
|
const lines = [];
|
|
17584
|
-
|
|
17585
|
-
|
|
17586
|
-
if (wr && wr.priorityCount > 0) {
|
|
17677
|
+
lines.push(baseUrl ? `${appLabel} at ${baseUrl}` : appLabel);
|
|
17678
|
+
if (prefs.enabled !== true) {
|
|
17587
17679
|
lines.push(
|
|
17588
|
-
`${c.
|
|
17680
|
+
`${c.dim("\xB7")} Off \u2014 Great Arrow writes every answer on its own side.`
|
|
17589
17681
|
);
|
|
17682
|
+
} else if (chosenName) {
|
|
17683
|
+
lines.push(`${c.green("\u2713")} On \u2014 answering with ${chosenName}.`);
|
|
17684
|
+
} else {
|
|
17685
|
+
lines.push(`${c.yellow("!")} On, but no model has been picked yet.`);
|
|
17590
17686
|
}
|
|
17591
|
-
|
|
17592
|
-
|
|
17687
|
+
lines.push(
|
|
17688
|
+
models.length === 0 ? `No models recorded on this computer yet.` : `${models.length} model${models.length === 1 ? "" : "s"} recorded on this computer.`
|
|
17593
17689
|
);
|
|
17594
|
-
if (
|
|
17595
|
-
|
|
17596
|
-
(
|
|
17597
|
-
0
|
|
17690
|
+
if (payload.available === false) {
|
|
17691
|
+
lines.push(
|
|
17692
|
+
`${c.yellow("!")} Your account is not in the group this is switched on for yet.`
|
|
17598
17693
|
);
|
|
17599
|
-
|
|
17600
|
-
|
|
17601
|
-
const first = unpushed[0];
|
|
17602
|
-
if (first?.branch) {
|
|
17603
|
-
lines.push(
|
|
17604
|
-
` ${c.dim("\u2192")} ${c.cyan(`git push -u origin ${first.branch}`)}`
|
|
17605
|
-
);
|
|
17694
|
+
if (payload.availability_note) {
|
|
17695
|
+
lines.push(` ${c.dim(payload.availability_note)}`);
|
|
17606
17696
|
}
|
|
17607
17697
|
}
|
|
17608
|
-
|
|
17609
|
-
lines.push(
|
|
17610
|
-
|
|
17611
|
-
|
|
17612
|
-
|
|
17613
|
-
|
|
17614
|
-
|
|
17615
|
-
|
|
17616
|
-
|
|
17698
|
+
if (payload.usage) {
|
|
17699
|
+
lines.push("");
|
|
17700
|
+
lines.push(
|
|
17701
|
+
formatLocalUsageLine({
|
|
17702
|
+
answers: payload.usage.answers ?? 0,
|
|
17703
|
+
days: payload.usage.days ?? LOCAL_USAGE_DEFAULT_DAYS,
|
|
17704
|
+
lastAnswerAt: null,
|
|
17705
|
+
models: []
|
|
17706
|
+
})
|
|
17707
|
+
);
|
|
17617
17708
|
}
|
|
17618
|
-
|
|
17619
|
-
|
|
17620
|
-
|
|
17621
|
-
|
|
17622
|
-
|
|
17623
|
-
|
|
17624
|
-
|
|
17709
|
+
lines.push("");
|
|
17710
|
+
lines.push(
|
|
17711
|
+
`${c.dim("\u2192")} ${c.cyan("gad local check")} to look again \xB7 ${c.cyan("gad local off")} to stop.`
|
|
17712
|
+
);
|
|
17713
|
+
return brandedBlock("on this computer", lines);
|
|
17714
|
+
}
|
|
17715
|
+
function offCommand() {
|
|
17716
|
+
return new Command("off").description("Stop answering with a model on this computer").action(async (_options, command) => {
|
|
17717
|
+
const g = globalOutputOpts(command);
|
|
17718
|
+
const auth = await resolveAuth();
|
|
17719
|
+
const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
|
|
17720
|
+
const fmt = createFormatter({
|
|
17721
|
+
json: g.json,
|
|
17722
|
+
quiet: g.quiet,
|
|
17723
|
+
isTTY: isTTY(),
|
|
17724
|
+
noColor: g.noColor
|
|
17725
|
+
});
|
|
17726
|
+
const saved = await client.mcpCall(
|
|
17727
|
+
"model_local_set",
|
|
17728
|
+
{ enabled: false },
|
|
17729
|
+
CHECK_TIMEOUT_MS
|
|
17730
|
+
);
|
|
17731
|
+
if (g.json) {
|
|
17732
|
+
process.stdout.write(JSON.stringify(saved) + "\n");
|
|
17733
|
+
return;
|
|
17734
|
+
}
|
|
17735
|
+
throwIfRefused(saved, "turn this off");
|
|
17736
|
+
fmt.success(
|
|
17737
|
+
"Great Arrow will write every answer on its own side from now on. Your models stay on this computer \u2014 gad local check turns it back on."
|
|
17738
|
+
);
|
|
17739
|
+
});
|
|
17740
|
+
}
|
|
17741
|
+
function askHereCommand() {
|
|
17742
|
+
return new Command("ask").description("Answer a question with a model on this computer").argument("<question>", "What you want to know").option("--session <id>", "Continue an existing conversation").action(async (question, options, command) => {
|
|
17743
|
+
const g = globalOutputOpts(command);
|
|
17744
|
+
const auth = await resolveAuth();
|
|
17745
|
+
let report;
|
|
17746
|
+
try {
|
|
17747
|
+
report = await answerOnThisComputer({
|
|
17748
|
+
apiUrl: auth.apiUrl,
|
|
17749
|
+
token: auth.token,
|
|
17750
|
+
question,
|
|
17751
|
+
sessionId: typeof options.session === "string" ? options.session : null,
|
|
17752
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
17753
|
+
...g.json ? {} : { onWords: (delta) => process.stdout.write(delta) }
|
|
17754
|
+
});
|
|
17755
|
+
} catch (err) {
|
|
17756
|
+
if (err instanceof LocalTurnFailed) {
|
|
17757
|
+
throw new CliError(
|
|
17758
|
+
err.message,
|
|
17759
|
+
2 /* NETWORK_ERROR */,
|
|
17760
|
+
err.closed ? void 0 : "Your conversation may show this one as unfinished."
|
|
17761
|
+
);
|
|
17625
17762
|
}
|
|
17763
|
+
throw err;
|
|
17764
|
+
}
|
|
17765
|
+
if (g.json) {
|
|
17766
|
+
process.stdout.write(JSON.stringify(report) + "\n");
|
|
17767
|
+
return;
|
|
17768
|
+
}
|
|
17769
|
+
if (report.answeredHere) process.stdout.write("\n");
|
|
17770
|
+
if (!g.quiet) {
|
|
17771
|
+
process.stderr.write(
|
|
17772
|
+
"\n" + renderVenue(report, g.noColor || !isTTY()) + "\n"
|
|
17773
|
+
);
|
|
17774
|
+
}
|
|
17775
|
+
});
|
|
17776
|
+
}
|
|
17777
|
+
function renderVenue(report, plain) {
|
|
17778
|
+
const c = createColor(plain);
|
|
17779
|
+
const lines = [];
|
|
17780
|
+
if (report.answeredHere) {
|
|
17781
|
+
lines.push(
|
|
17782
|
+
`${c.green("\u2713")} Written on this computer by ${report.modelLabel}. It cost nothing.`
|
|
17783
|
+
);
|
|
17784
|
+
if (report.appsUsed > 0) {
|
|
17785
|
+
lines.push(
|
|
17786
|
+
`${c.dim("\xB7")} It looked up ${report.appsUsed} thing${report.appsUsed === 1 ? "" : "s"} in your workspace.`
|
|
17787
|
+
);
|
|
17626
17788
|
}
|
|
17789
|
+
lines.push(
|
|
17790
|
+
report.recorded ? `${c.dim("\xB7")} Saved to your conversation.` : `${c.yellow("!")} We could not save it to your conversation.`
|
|
17791
|
+
);
|
|
17792
|
+
} else {
|
|
17793
|
+
lines.push(
|
|
17794
|
+
`${c.dim("\xB7")} Written by Great Arrow, not on this computer \u2014 it keeps the harder ones. The reply is in your conversation.`
|
|
17795
|
+
);
|
|
17627
17796
|
}
|
|
17628
|
-
|
|
17629
|
-
lines.push(`${c.dim("\u2192")} ${c.cyan("gad status")} for the full picture.`);
|
|
17630
|
-
process.stderr.write("\n" + brandedBlock("needs you", lines) + "\n");
|
|
17631
|
-
return printed;
|
|
17797
|
+
return brandedBlock("where this was answered", lines);
|
|
17632
17798
|
}
|
|
17633
|
-
|
|
17634
|
-
|
|
17635
|
-
|
|
17636
|
-
|
|
17637
|
-
|
|
17799
|
+
function localCommand() {
|
|
17800
|
+
return new Command("local").description("Answer with a model running on this computer").addCommand(checkCommand()).addCommand(statusCommand2()).addCommand(offCommand()).addCommand(askHereCommand());
|
|
17801
|
+
}
|
|
17802
|
+
|
|
17803
|
+
// src/lib/hyperlink.ts
|
|
17804
|
+
var SUPPORTED_TERM_PROGRAMS = /* @__PURE__ */ new Set([
|
|
17805
|
+
"ghostty",
|
|
17806
|
+
"iterm.app",
|
|
17807
|
+
"wezterm",
|
|
17808
|
+
"vscode",
|
|
17809
|
+
"hyper",
|
|
17810
|
+
"warpterminal",
|
|
17811
|
+
"rio",
|
|
17812
|
+
"contour",
|
|
17813
|
+
"tabby"
|
|
17814
|
+
]);
|
|
17815
|
+
var SUPPORTED_TERMS = /* @__PURE__ */ new Set([
|
|
17816
|
+
"xterm-kitty",
|
|
17817
|
+
"alacritty",
|
|
17818
|
+
"foot",
|
|
17819
|
+
"contour"
|
|
17820
|
+
]);
|
|
17821
|
+
var UNSUPPORTED_TERM_PROGRAMS = /* @__PURE__ */ new Set(["apple_terminal"]);
|
|
17822
|
+
var MIN_VTE_VERSION = 5e3;
|
|
17823
|
+
function isTruthyFlag(value) {
|
|
17824
|
+
if (value === void 0) return false;
|
|
17825
|
+
const v = value.trim().toLowerCase();
|
|
17826
|
+
return v !== "" && v !== "0" && v !== "false";
|
|
17827
|
+
}
|
|
17828
|
+
function supportsHyperlinks(env4, isTTY2) {
|
|
17829
|
+
if (isTruthyFlag(env4.FORCE_HYPERLINK)) return true;
|
|
17830
|
+
if (env4.NO_COLOR !== void 0 && env4.NO_COLOR !== "") return false;
|
|
17831
|
+
if (!isTTY2) return false;
|
|
17832
|
+
if (isTruthyFlag(env4.CI)) return false;
|
|
17833
|
+
const term = (env4.TERM ?? "").trim().toLowerCase();
|
|
17834
|
+
if (term === "" || term === "dumb") return false;
|
|
17835
|
+
if (term.startsWith("screen") || term.startsWith("tmux")) return false;
|
|
17836
|
+
const program3 = (env4.TERM_PROGRAM ?? "").trim().toLowerCase();
|
|
17837
|
+
if (UNSUPPORTED_TERM_PROGRAMS.has(program3)) return false;
|
|
17838
|
+
if (SUPPORTED_TERM_PROGRAMS.has(program3)) return true;
|
|
17839
|
+
if (SUPPORTED_TERMS.has(term)) return true;
|
|
17840
|
+
if (isTruthyFlag(env4.WT_SESSION)) return true;
|
|
17841
|
+
if (isTruthyFlag(env4.KONSOLE_VERSION)) return true;
|
|
17842
|
+
if (env4.TERMINAL_EMULATOR === "JetBrains-JediTerm") return true;
|
|
17843
|
+
const vte = Number.parseInt(env4.VTE_VERSION ?? "", 10);
|
|
17844
|
+
if (Number.isFinite(vte) && vte >= MIN_VTE_VERSION) return true;
|
|
17845
|
+
return false;
|
|
17846
|
+
}
|
|
17847
|
+
var OSC = "\x1B]8;;";
|
|
17848
|
+
var ST = "\x1B\\";
|
|
17849
|
+
function hyperlink(text2, url, opts) {
|
|
17850
|
+
if (!opts.enabled) return text2 === url ? url : `${text2} (${url})`;
|
|
17851
|
+
return `${OSC}${url}${ST}${text2}${OSC}${ST}`;
|
|
17638
17852
|
}
|
|
17639
17853
|
|
|
17640
17854
|
// src/commands/status.ts
|
|
17641
|
-
function
|
|
17855
|
+
function statusCommand3() {
|
|
17642
17856
|
const cmd = new Command("status").alias("needs").description(
|
|
17643
17857
|
"Show what needs you: blockers, Goal progress, stale data, agents"
|
|
17644
17858
|
).option("--json", "Output the raw glance payload as JSON").option("--quiet", "Suppress decorative output").option("--no-color", "Disable color output").action(async (options, command) => {
|
|
@@ -17674,7 +17888,7 @@ function statusCommand2() {
|
|
|
17674
17888
|
fmt.warn("No status available.");
|
|
17675
17889
|
return;
|
|
17676
17890
|
}
|
|
17677
|
-
|
|
17891
|
+
renderStatus2(payload, g.noColor || g.quiet, auth.apiUrl);
|
|
17678
17892
|
await reportSeen({
|
|
17679
17893
|
apiUrl: auth.apiUrl,
|
|
17680
17894
|
token: auth.token,
|
|
@@ -17683,7 +17897,7 @@ function statusCommand2() {
|
|
|
17683
17897
|
});
|
|
17684
17898
|
return cmd;
|
|
17685
17899
|
}
|
|
17686
|
-
function
|
|
17900
|
+
function renderStatus2(payload, plain, apiUrl) {
|
|
17687
17901
|
const c = createColor(plain);
|
|
17688
17902
|
const links = supportsHyperlinks(process.env, isTTY());
|
|
17689
17903
|
const link = (text2, url) => c.cyan(hyperlink(text2, url, { enabled: links }));
|
|
@@ -21638,7 +21852,7 @@ program2.addCommand(work_default());
|
|
|
21638
21852
|
program2.addCommand(worktree_default);
|
|
21639
21853
|
program2.addCommand(install_hooks_default);
|
|
21640
21854
|
program2.addCommand(guardrailsCommand());
|
|
21641
|
-
program2.addCommand(
|
|
21855
|
+
program2.addCommand(statusCommand3());
|
|
21642
21856
|
program2.addCommand(doctorCommand());
|
|
21643
21857
|
program2.addCommand(integrations_default);
|
|
21644
21858
|
program2.addCommand(mcp_default);
|