@great-arrow/cli 0.1.34 → 0.1.36

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