farai 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -61,10 +61,11 @@ function isDefaultSessionTitle(value) {
61
61
  return !value?.trim() || DEFAULT_TITLES.has(value.trim().toLowerCase());
62
62
  }
63
63
  function normalizeSessionTitle(value, fallback = DEFAULT_SESSION_TITLE) {
64
- const clean = value.replace(/<[^>]+>/g, " ").replace(/^\s*(?:[-*#>]+|\d+[.)])\s*/, "").replace(/\s+/g, " ").trim().replace(/[.!?,;:]+$/, "").trim();
64
+ const clean = value.replace(/<[^>]+>/g, " ").replace(/[`*_#>~|]+/g, " ").replace(/["'\u201C\u201D\u2018\u2019()\[\]{}]/g, " ").replace(/^\s*(?:[-*#>]+|\d+[.)])\s*/, "").replace(/[^\p{L}\p{N}\s.\/&+-]+/gu, " ").replace(/\s+/g, " ").trim().replace(/[.!?,;:\/&+-]+$/, "").trim().toLowerCase();
65
65
  if (!clean)
66
66
  return fallback;
67
- return clean.length > 72 ? `${clean.slice(0, 69).trimEnd()}...` : clean;
67
+ const byWords = clean.split(" ").slice(0, TITLE_MAX_WORDS).join(" ");
68
+ return byWords.length > TITLE_MAX_CHARS ? byWords.slice(0, TITLE_MAX_CHARS).trimEnd() : byWords;
68
69
  }
69
70
  function titleFromPrompt(prompt, fallback = DEFAULT_SESSION_TITLE) {
70
71
  const first = prompt.split(`
@@ -73,12 +74,21 @@ function titleFromPrompt(prompt, fallback = DEFAULT_SESSION_TITLE) {
73
74
  return fallback;
74
75
  return normalizeSessionTitle(first.replace(LEADING_FILLER, ""), fallback);
75
76
  }
77
+ function titleFromModelText(text, fallback = DEFAULT_SESSION_TITLE) {
78
+ const stripped = text.replace(/<think>[\s\S]*?<\/think>/gi, "").replace(/<[^>]+>/g, " ").replace(/^\s*title\s*[:\-]\s*/i, "");
79
+ const first = stripped.split(`
80
+ `).map((line) => line.trim()).find(Boolean) ?? "";
81
+ return normalizeSessionTitle(first, fallback);
82
+ }
76
83
  function sessionDisplayName(session) {
77
84
  if (isDefaultSessionTitle(session?.title))
78
85
  return DEFAULT_SESSION_TITLE;
79
86
  return normalizeSessionTitle(session.title, DEFAULT_SESSION_TITLE);
80
87
  }
81
- var DEFAULT_SESSION_TITLE = "new session", DEFAULT_TITLES, LOW_INFORMATION, LEADING_FILLER;
88
+ var DEFAULT_SESSION_TITLE = "new session", DEFAULT_TITLES, LOW_INFORMATION, LEADING_FILLER, TITLE_MAX_WORDS = 6, TITLE_MAX_CHARS = 48, SESSION_TITLE_PROMPT = `Write a short title for this session, describing the user's overall task.
89
+ Rules: 3 to 6 words, at most 48 characters, lowercase, plain text only.
90
+ No quotes, no markdown, no emoji, no trailing punctuation, no prefixes like "title:".
91
+ Be general about the whole task, not a single step. Respond with the title only.`;
82
92
  var init_session_title = __esm(() => {
83
93
  DEFAULT_TITLES = new Set(["new session", "untitled", "untitled session"]);
84
94
  LOW_INFORMATION = /^(?:hi|hey|hello|halo|hai|yo|bro|test|testing|ping|p|ok|oke|okay|sip|thanks|thank you|makasih|terima kasih)[.!?\s]*$/i;
@@ -5398,7 +5408,7 @@ function parseManifest(value) {
5398
5408
  if (!value || typeof value !== "object")
5399
5409
  throw new Error("invalid farai tool manifest");
5400
5410
  const candidate = value;
5401
- if (typeof candidate.contract !== "string" || !candidate.contract || !Array.isArray(candidate.aptPackages) || !candidate.pinnedTools || typeof candidate.pinnedTools !== "object" || !candidate.pinnedAssets || typeof candidate.pinnedAssets !== "object" || !candidate.workflows || typeof candidate.workflows !== "object") {
5411
+ if (!Array.isArray(candidate.aptPackages) || !candidate.pinnedTools || typeof candidate.pinnedTools !== "object" || !candidate.pinnedAssets || typeof candidate.pinnedAssets !== "object" || !candidate.workflows || typeof candidate.workflows !== "object") {
5402
5412
  throw new Error("invalid farai tool manifest");
5403
5413
  }
5404
5414
  const aptPackages = candidate.aptPackages.filter((item) => typeof item === "string" && Boolean(item));
@@ -5445,7 +5455,6 @@ function parseManifest(value) {
5445
5455
  if (!Object.keys(pinnedAssets).length)
5446
5456
  throw new Error("empty pinned asset manifest");
5447
5457
  return {
5448
- contract: candidate.contract,
5449
5458
  aptPackages,
5450
5459
  pinnedTools,
5451
5460
  pinnedAssets,
@@ -5455,11 +5464,15 @@ function parseManifest(value) {
5455
5464
  function isSha256(value) {
5456
5465
  return typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
5457
5466
  }
5458
- var KALI_TOOL_MANIFEST_PATH, KALI_TOOL_MANIFEST;
5467
+ var KALI_TOOL_MANIFEST_PATH, KALI_IMAGE_CONTRACT_PATH, KALI_TOOL_MANIFEST, KALI_IMAGE_CONTRACT;
5459
5468
  var init_kali_tool_manifest = __esm(() => {
5460
5469
  init_file_read();
5461
5470
  KALI_TOOL_MANIFEST_PATH = join4(import.meta.dir, "..", "..", "docker", "kali", "farai-tool-manifest.json");
5471
+ KALI_IMAGE_CONTRACT_PATH = join4(import.meta.dir, "..", "..", "docker", "kali", "farai-image-contract");
5462
5472
  KALI_TOOL_MANIFEST = parseManifest(JSON.parse(readBoundedFileTextSync(KALI_TOOL_MANIFEST_PATH, 1024 * 1024, "kali tool manifest")));
5473
+ KALI_IMAGE_CONTRACT = readBoundedFileTextSync(KALI_IMAGE_CONTRACT_PATH, 1024, "kali image contract").trim();
5474
+ if (!KALI_IMAGE_CONTRACT)
5475
+ throw new Error("empty kali image contract");
5463
5476
  });
5464
5477
 
5465
5478
  // src/agent-tools/mcp-builtins.ts
@@ -7284,20 +7297,59 @@ class KaliContainerBackend {
7284
7297
  pullImageCommand() {
7285
7298
  return ["docker", "pull", this.image];
7286
7299
  }
7287
- async ensureImage() {
7288
- const current = await this.resolveImage();
7289
- if (current.exists && current.contract === KALI_IMAGE_CONTRACT) {
7300
+ async remoteImageDigest() {
7301
+ const inspect = await this.processRunner("docker", ["buildx", "imagetools", "inspect", this.image, "--format", "{{.Manifest.Digest}}"]);
7302
+ if (inspect.exitCode !== 0)
7303
+ return;
7304
+ return digestOf(inspect.stdout.trim());
7305
+ }
7306
+ async checkForImageUpdate() {
7307
+ const local = await this.resolveImage();
7308
+ if (!local.exists)
7290
7309
  return {
7291
- exitCode: 0,
7292
- stdout: "image ready",
7293
- stderr: "",
7294
- durationMs: 0,
7295
- timedOut: false
7310
+ exists: false,
7311
+ upToDate: false,
7312
+ ...local.error ? {
7313
+ error: local.error
7314
+ } : {}
7315
+ };
7316
+ const remote = await this.remoteImageDigest();
7317
+ if (!remote || !local.repoDigest)
7318
+ return {
7319
+ exists: true,
7320
+ upToDate: true
7296
7321
  };
7322
+ return {
7323
+ exists: true,
7324
+ upToDate: local.repoDigest === remote
7325
+ };
7326
+ }
7327
+ async ensureImage() {
7328
+ const local = await this.resolveImage();
7329
+ if (local.exists) {
7330
+ const remote = await this.remoteImageDigest();
7331
+ if (!remote || local.repoDigest !== undefined && local.repoDigest === remote) {
7332
+ return {
7333
+ exitCode: 0,
7334
+ stdout: "image ready",
7335
+ stderr: "",
7336
+ durationMs: 0,
7337
+ timedOut: false
7338
+ };
7339
+ }
7297
7340
  }
7298
7341
  const started = Date.now();
7299
7342
  const pulled = await this.pullRunner("docker", ["pull", this.image]);
7300
7343
  if (pulled.exitCode !== 0) {
7344
+ if (local.exists) {
7345
+ return {
7346
+ exitCode: 0,
7347
+ stdout: pulled.stdout || "using local kali image",
7348
+ stderr: "",
7349
+ durationMs: Date.now() - started,
7350
+ timedOut: false
7351
+ };
7352
+ }
7301
7353
  return {
7302
7354
  ...pulled,
7303
7355
  stderr: dockerFailure(pulled, `could not pull kali image ${this.image}; check network access to ${KALI_IMAGE_REPO}`),
@@ -7315,15 +7367,6 @@ class KaliContainerBackend {
7315
7367
  timedOut: false
7316
7368
  };
7317
7369
  }
7318
- if (resolved.contract !== KALI_IMAGE_CONTRACT) {
7319
- return {
7320
- exitCode: 1,
7321
- stdout: "",
7322
- stderr: `pulled kali image ${this.image} does not satisfy the farai capability contract (${resolved.contract ?? "missing"})`,
7323
- durationMs: Date.now() - started,
7324
- timedOut: false
7325
- };
7326
- }
7327
7370
  return {
7328
7371
  exitCode: 0,
7329
7372
  stdout: pulled.stdout || "image pulled",
@@ -7922,6 +7965,7 @@ function parseImageInspect(raw) {
7922
7965
  exists: true
7923
7966
  };
7924
7967
  const contract = image.Config?.Labels?.[KALI_IMAGE_CONTRACT_LABEL];
7968
+ const repoDigest = digestOf((image.RepoDigests ?? []).find((entry) => entry.includes("@sha256:")));
7925
7969
  return {
7926
7970
  exists: true,
7927
7971
  ...image.Id ? {
@@ -7929,6 +7973,9 @@ function parseImageInspect(raw) {
7929
7973
  } : {},
7930
7974
  ...contract ? {
7931
7975
  contract
7976
+ } : {},
7977
+ ...repoDigest ? {
7978
+ repoDigest
7932
7979
  } : {}
7933
7980
  };
7934
7981
  } catch {
@@ -7937,6 +7984,12 @@ function parseImageInspect(raw) {
7937
7984
  };
7938
7985
  }
7939
7986
  }
7987
+ function digestOf(reference) {
7988
+ if (!reference)
7989
+ return;
7990
+ const match = reference.match(/sha256:[a-f0-9]{64}/i);
7991
+ return match ? match[0].toLowerCase() : undefined;
7992
+ }
7940
7993
  function dockerFailure(result, fallback) {
7941
7994
  const detail = `${result.stderr}
7942
7995
  ${result.stdout}`.trim().replace(/\s+/g, " ");
@@ -7960,7 +8013,7 @@ function containerDoesNotExist2(result) {
7960
8013
  return /no such (container|object)/i.test(`${result.stdout}
7961
8014
  ${result.stderr}`);
7962
8015
  }
7963
- var CONTAINER_WORKSPACE_MOUNT = "/workspace", CONTAINER_WORKTREES_MOUNT = "/worktrees", kaliSessions, kaliPtySessions, containerStartLocks, globalContainerStartChain, CONTAINER_EXEC_MARKER_DIR = "/tmp/farai-exec", CONTAINER_EXEC_WRAPPER, CONTAINER_EXEC_KILLER, CONTAINER_PREFIX, KALI_IMAGE_CONTRACT, KALI_IMAGE_REPO = "ghcr.io/pajarori/farai-kali", DEFAULT_KALI_IMAGE, KALI_IMAGE_CONTRACT_LABEL = "org.farai.kali.contract", KALI_IMAGE_PULL_TIMEOUT_MS;
8016
+ var CONTAINER_WORKSPACE_MOUNT = "/workspace", CONTAINER_WORKTREES_MOUNT = "/worktrees", kaliSessions, kaliPtySessions, containerStartLocks, globalContainerStartChain, CONTAINER_EXEC_MARKER_DIR = "/tmp/farai-exec", CONTAINER_EXEC_WRAPPER, CONTAINER_EXEC_KILLER, CONTAINER_PREFIX, KALI_IMAGE_REPO = "ghcr.io/pajarori/farai-kali", KALI_IMAGE_TAG = "latest", DEFAULT_KALI_IMAGE, KALI_IMAGE_CONTRACT_LABEL = "org.farai.kali.contract", KALI_IMAGE_PULL_TIMEOUT_MS;
7964
8017
  var init_kali = __esm(() => {
7965
8018
  init_spawn_session();
7966
8019
  init_pty_session();
@@ -7978,8 +8031,7 @@ var init_kali = __esm(() => {
7978
8031
  CONTAINER_EXEC_KILLER = ["import os, signal, sys, time", "path = sys.argv[1]", "root = None", "marker_deadline = time.monotonic() + 0.25", "while root is None and time.monotonic() < marker_deadline:", " try:", " with open(path, encoding='ascii') as handle:", " root = int(handle.read().strip())", " except (FileNotFoundError, OSError, ValueError):", " time.sleep(0.01)", "if root is None:", " raise SystemExit(0)", "def direct_children(pid):", " try:", " with open(f'/proc/{pid}/task/{pid}/children', encoding='ascii') as handle:", " return [int(value) for value in handle.read().split()]", " except (FileNotFoundError, OSError, ValueError):", " return []", "def descendants(pid):", " found = []", " pending = direct_children(pid)", " seen = set()", " while pending:", " child = pending.pop()", " if child in seen:", " continue", " seen.add(child)", " found.append(child)", " pending.extend(direct_children(child))", " return found", "def alive(pid):", " try:", " os.kill(pid, 0)", " return True", " except ProcessLookupError:", " return False", " except PermissionError:", " return True", "targets = descendants(root)", "for pid in [*reversed(targets), root]:", " try:", " os.kill(pid, signal.SIGTERM)", " except (ProcessLookupError, PermissionError):", " pass", "deadline = time.monotonic() + 1.0", "while time.monotonic() < deadline and any(alive(pid) for pid in [root, *targets]):", " time.sleep(0.025)", "targets = list(dict.fromkeys([*targets, *descendants(root)]))", "for pid in [*reversed(targets), root]:", " try:", " os.kill(pid, signal.SIGKILL)", " except (ProcessLookupError, PermissionError):", " pass", "try:", " os.unlink(path)", "except FileNotFoundError:", " pass"].join(`
7979
8032
  `);
7980
8033
  CONTAINER_PREFIX = FARAI_CONTAINER_NAME_PREFIX;
7981
- KALI_IMAGE_CONTRACT = KALI_TOOL_MANIFEST.contract;
7982
- DEFAULT_KALI_IMAGE = `${KALI_IMAGE_REPO}:${KALI_IMAGE_CONTRACT}`;
8034
+ DEFAULT_KALI_IMAGE = `${KALI_IMAGE_REPO}:${KALI_IMAGE_TAG}`;
7983
8035
  KALI_IMAGE_PULL_TIMEOUT_MS = 30 * 60 * 1000;
7984
8036
  });
7985
8037
 
@@ -8732,8 +8784,8 @@ var init_process_output = __esm(() => {
8732
8784
 
8733
8785
  // src/version.ts
8734
8786
  function resolveFaraiVersion() {
8735
- if ("0.3.2")
8736
- return "0.3.2";
8787
+ if ("0.3.4")
8788
+ return "0.3.4";
8737
8789
  try {
8738
8790
  const parsed = JSON.parse(readBoundedFileTextSync(new URL("../package.json", import.meta.url), 1024 * 1024, "package metadata"));
8739
8791
  if (typeof parsed.version === "string" && parsed.version)
@@ -29373,40 +29425,15 @@ function renderCtfNotes(input) {
29373
29425
  // src/agent-tools/tool-guidance.ts
29374
29426
  function modelToolDescription(tool, _detailed = false) {
29375
29427
  const exact = EXACT_GUIDANCE[tool.name];
29376
- const highValue = new Set(["report_add_finding", "cvss_calculate", "internet_search", "agent_spawn"]);
29377
- if (!exact || !_detailed && !highValue.has(tool.name))
29428
+ if (!exact)
29378
29429
  return tool.description;
29379
29430
  return `${tool.description}
29380
29431
 
29381
29432
  model contract: ${exact}`;
29382
29433
  }
29383
- function toolGuidanceMatchesQuery(toolName, query) {
29384
- const normalized = query.toLowerCase();
29385
- const terms = toolName.split("_").filter((term) => term.length >= 3);
29386
- const words = new Set(normalized.match(/[a-z0-9]+/g) ?? []);
29387
- const fileIntent = /\b(file|path|write|edit|patch|markdown|\.md|report)\b/.test(normalized);
29388
- const fileTools = ["fs_read", "fs_list", "fs_grep", "fs_write", "fs_edit", "patch_apply", "code_write_script", "report_add_finding", "report_update_finding"];
29389
- return terms.some((term) => words.has(term)) || fileIntent && fileTools.includes(toolName) || normalized.includes("finding") && ["report_add_finding", "report_update_finding", "campaign_verify", "campaign_test", "cvss_calculate"].includes(toolName) || normalized.includes("email") && toolName.startsWith("email_") || normalized.includes("browser") && toolName.startsWith("browser_") || normalized.includes("proxy") && toolName.startsWith("proxy_") || normalized.includes("campaign") && toolName.startsWith("campaign_");
29390
- }
29391
- function modelToolSchema(schema, detailed = false, toolName) {
29392
- if (!detailed && !new Set(["report_add_finding", "cvss_calculate", "internet_search", "agent_spawn"]).has(toolName ?? "")) {
29393
- return compactSchemaNode(schema);
29394
- }
29434
+ function modelToolSchema(schema, _detailed = false, toolName) {
29395
29435
  return enrichSchemaNode(schema, [], toolName);
29396
29436
  }
29397
- function compactSchemaNode(value) {
29398
- if (Array.isArray(value))
29399
- return value.map(compactSchemaNode);
29400
- if (!isRecord9(value))
29401
- return value;
29402
- const compact = {};
29403
- for (const [key, child] of Object.entries(value)) {
29404
- if (key === "description")
29405
- continue;
29406
- compact[key] = compactSchemaNode(child);
29407
- }
29408
- return compact;
29409
- }
29410
29437
  function enrichSchemaNode(value, path, toolName) {
29411
29438
  if (Array.isArray(value))
29412
29439
  return value.map((item) => enrichSchemaNode(item, path, toolName));
@@ -33006,22 +33033,17 @@ class HeuristicPlanner {
33006
33033
  });
33007
33034
  }
33008
33035
  }
33009
- function buildToolsPayload(toolNames, availableTools, options = {}) {
33036
+ function buildToolsPayload(toolNames, availableTools, _options = {}) {
33010
33037
  const payload = [];
33011
33038
  const available = availableTools ? new Map(availableTools.map((tool) => [tool.name, tool])) : undefined;
33012
- let detailedCount = 0;
33013
33039
  for (const name of [...new Set(toolNames.map(canonicalToolName))].sort()) {
33014
33040
  const tool = available?.get(name) ?? getTool(name);
33015
33041
  if (!tool)
33016
33042
  continue;
33017
- const matched = Boolean(options.userText && toolGuidanceMatchesQuery(tool.name, options.userText));
33018
- const detailed = matched && (options.maxDetailedTools === undefined || detailedCount < options.maxDetailedTools);
33019
- if (detailed)
33020
- detailedCount += 1;
33021
33043
  payload.push({
33022
33044
  name: tool.name,
33023
- description: modelToolDescription(tool, detailed),
33024
- parameters: modelToolSchema(tool.inputSchema, detailed, tool.name)
33045
+ description: modelToolDescription(tool),
33046
+ parameters: modelToolSchema(tool.inputSchema, true, tool.name)
33025
33047
  });
33026
33048
  }
33027
33049
  return payload;
@@ -35335,10 +35357,7 @@ function mergeProviderToolCatalog(advertised, selected, availableTools) {
35335
35357
  const current = buildToolsPayload([definition.name], availableTools)[0];
35336
35358
  if (!current)
35337
35359
  continue;
35338
- const detailed = buildToolsPayload([definition.name], availableTools, {
35339
- userText: definition.name.replaceAll("_", " ")
35340
- })[0];
35341
- const isCurrent = sameProviderTool(prior, current) || (detailed ? sameProviderTool(prior, detailed) : false);
35360
+ const isCurrent = sameProviderTool(prior, current);
35342
35361
  merged.push(isCurrent ? prior : selectedByName.get(prior.name) ?? current);
35343
35362
  seen.add(prior.name);
35344
35363
  }
@@ -37048,7 +37067,9 @@ function validateToolArgs(schema, args) {
37048
37067
  if (validate(args))
37049
37068
  return;
37050
37069
  const error = validate.errors?.[0];
37051
- return error ? formatValidationError(error, schema) : "arguments do not match the tool input schema";
37070
+ if (!error)
37071
+ return "arguments do not match the tool input schema";
37072
+ return `${formatValidationError(error, schema)}${compositionShapes(error, schema)}`;
37052
37073
  }
37053
37074
  function compiledValidator(schema) {
37054
37075
  const cached = validatorCache.get(schema);
@@ -37109,6 +37130,58 @@ function formatValidationError(error, schema) {
37109
37130
  return `${fieldName(path)} ${error.message ?? `failed ${error.keyword} validation`}`;
37110
37131
  }
37111
37132
  }
37133
+ function compositionShapes(error, schema) {
37134
+ const compositionPath = compositionPointer(error.schemaPath);
37135
+ const branches = compositionPath ? resolveSchemaPointer(schema, compositionPath) : undefined;
37136
+ if (!Array.isArray(branches) || branches.length < 2)
37137
+ return "";
37138
+ const shapes = branches.map(describeBranch).filter((text2) => Boolean(text2));
37139
+ if (shapes.length < 2)
37140
+ return "";
37141
+ return `; provide exactly one shape: ${shapes.map((text2, index) => `${index + 1}) ${text2}`).join(" or ")}`;
37142
+ }
37143
+ function compositionPointer(schemaPath) {
37144
+ if (typeof schemaPath !== "string")
37145
+ return;
37146
+ const segments = schemaPath.split("/");
37147
+ for (let index = segments.length - 1;index >= 0; index -= 1) {
37148
+ if (segments[index] === "oneOf" || segments[index] === "anyOf") {
37149
+ return segments.slice(0, index + 1).join("/");
37150
+ }
37151
+ }
37152
+ return;
37153
+ }
37154
+ function describeBranch(branch) {
37155
+ if (!branch || typeof branch !== "object" || Array.isArray(branch))
37156
+ return;
37157
+ const record3 = branch;
37158
+ const required = Array.isArray(record3.required) ? record3.required.map(String) : [];
37159
+ if (required.length > 0)
37160
+ return `{ ${required.join(", ")} }`;
37161
+ if (typeof record3.type === "string")
37162
+ return `a ${record3.type}`;
37163
+ if (typeof record3.const !== "undefined")
37164
+ return JSON.stringify(record3.const);
37165
+ return;
37166
+ }
37167
+ function resolveSchemaPointer(schema, schemaPath) {
37168
+ if (typeof schemaPath !== "string")
37169
+ return;
37170
+ const pointer = schemaPath.startsWith("#") ? schemaPath.slice(1) : schemaPath;
37171
+ let node = schema;
37172
+ for (const raw of pointer.split("/")) {
37173
+ if (!raw)
37174
+ continue;
37175
+ const key = raw.replace(/~1/g, "/").replace(/~0/g, "~");
37176
+ if (Array.isArray(node))
37177
+ node = node[Number(key)];
37178
+ else if (node && typeof node === "object")
37179
+ node = node[key];
37180
+ else
37181
+ return;
37182
+ }
37183
+ return node;
37184
+ }
37112
37185
  function unexpectedFieldError(path, property, schema) {
37113
37186
  const field = joinFieldPath(path, property);
37114
37187
  const enumOwner = enumOwnerForValue(schema, property);
@@ -38026,6 +38099,7 @@ class AgentRuntime {
38026
38099
  this.maxTurnMs = resolveMaxTurnMs(options.maxTurnSeconds ?? config.maxTurnSeconds);
38027
38100
  this.maxCostUsd = positiveFinite(options.maxCostUsd ?? config.maxCostUsd);
38028
38101
  this.maxInputTokens = positiveFinite(options.maxInputTokens);
38102
+ this.sessionTitlesEnabled = options.enableSessionTitles === true;
38029
38103
  this.mailbox = new SessionMailbox(this.store, this.runtimeId);
38030
38104
  this.inputQueue = new SessionInputQueue(this.mailbox, (sessionId, type, payload) => this.event(sessionId, type, payload));
38031
38105
  this.userInputs = new SessionUserInputCoordinator({
@@ -39327,6 +39401,7 @@ class AgentRuntime {
39327
39401
  const activeCampaignRun = source === "user" && !trimmed.startsWith("/") && !trimmed.startsWith("!") ? this.campaignSupervisor.prepare(session.id, input) : undefined;
39328
39402
  if (activeCampaignRun)
39329
39403
  session = this.store.loadSession(session.id);
39404
+ let autoTitleBaseline;
39330
39405
  if (source === "user" && isDefaultSessionTitle(session.title)) {
39331
39406
  const title = titleFromPrompt(input);
39332
39407
  if (!isDefaultSessionTitle(title)) {
@@ -39335,6 +39410,7 @@ class AgentRuntime {
39335
39410
  });
39336
39411
  this.recordSession(session);
39337
39412
  }
39413
+ autoTitleBaseline = session.title ?? DEFAULT_SESSION_TITLE;
39338
39414
  }
39339
39415
  if (source === "user" && (trimmed === "/compact" || trimmed.startsWith("/compact "))) {
39340
39416
  const cursor2 = this.store.latestEventSequence(session.id);
@@ -39438,6 +39514,11 @@ class AgentRuntime {
39438
39514
  if (source === "user" && !this.shuttingDown && this.store.loadTurn(turn.id).status !== "cancelled") {
39439
39515
  this.mailboxDispatcher.wakeQueuedInputs(session.id);
39440
39516
  }
39517
+ if (autoTitleBaseline !== undefined && this.sessionTitlesEnabled && !trimmed.startsWith("/") && !trimmed.startsWith("!") && this.store.loadTurn(turn.id).status === "completed") {
39518
+ this.generateSessionTitle(session.id, input, response, autoTitleBaseline).catch(() => {
39519
+ return;
39520
+ });
39521
+ }
39441
39522
  const cursor = startedEvents.at(-1)?.sequence ?? 0;
39442
39523
  return {
39443
39524
  session,
@@ -39445,6 +39526,48 @@ class AgentRuntime {
39445
39526
  events: this.store.listEventsAfter(session.id, cursor, 1e4)
39446
39527
  };
39447
39528
  }
39529
+ async generateSessionTitle(sessionId, userText, assistantText, baseline) {
39530
+ let session = this.store.loadSession(sessionId);
39531
+ if (session.title !== baseline)
39532
+ return;
39533
+ let planner;
39534
+ if (this.planner)
39535
+ planner = this.planner;
39536
+ else
39537
+ planner = new ChatProviderPlanner(this.chatProviderOverride ?? await createChatProviderForSession(session, this.workspace));
39538
+ if (planner.compactionMode !== "model")
39539
+ return;
39540
+ const history = [{
39541
+ role: "user",
39542
+ text: userText.slice(0, 4000)
39543
+ }];
39544
+ const reply = sanitizeVisibleResponse(assistantText).trim();
39545
+ if (reply)
39546
+ history.push({
39547
+ role: "assistant",
39548
+ text: reply.slice(0, 4000)
39549
+ });
39550
+ const actions = await planner.plan({
39551
+ session,
39552
+ userText: "title",
39553
+ systemInstruction: SESSION_TITLE_PROMPT,
39554
+ history,
39555
+ tools: [],
39556
+ toolCatalog: [],
39557
+ toolChoice: "none"
39558
+ });
39559
+ const text2 = actions.filter((action) => action.kind === "respond").map((action) => action.text).join(" ");
39560
+ const title = titleFromModelText(text2, "");
39561
+ if (!title)
39562
+ return;
39563
+ session = this.store.loadSession(sessionId);
39564
+ if (session.title !== baseline)
39565
+ return;
39566
+ session = this.store.updateSession(sessionId, {
39567
+ title
39568
+ });
39569
+ this.recordSession(session);
39570
+ }
39448
39571
  async runAgentLoop(session, turn, contextMessage, assistantMessage, input, userAuthored = true, mailboxItems = []) {
39449
39572
  const responses = [];
39450
39573
  let planner;
@@ -43757,17 +43880,17 @@ async function runStartupContainerPreflight(workspace) {
43757
43880
  const backend2 = new KaliContainerBackend({
43758
43881
  workspace
43759
43882
  });
43760
- const image = await backend2.resolveImage().catch(() => {
43883
+ const update = await backend2.checkForImageUpdate().catch(() => {
43761
43884
  return;
43762
43885
  });
43763
- if (!image || image.error)
43886
+ if (!update || update.error)
43764
43887
  return "continue";
43765
- if (image.exists && image.contract === KALI_IMAGE_CONTRACT)
43888
+ if (update.exists && update.upToDate)
43766
43889
  return "continue";
43767
43890
  const config = loadConfig(workspace);
43768
43891
  if (config.updates?.prompt === false || !process.stdin.isTTY || !process.stdout.isTTY)
43769
43892
  return "continue";
43770
- const answer = await promptForImagePull(KALI_IMAGE_CONTRACT, image.exists);
43893
+ const answer = await promptForImagePull(update.exists);
43771
43894
  if (answer === "cancelled")
43772
43895
  return "cancelled";
43773
43896
  if (answer === "later")
@@ -43779,11 +43902,11 @@ async function runStartupContainerPreflight(workspace) {
43779
43902
  }
43780
43903
  return "continue";
43781
43904
  }
43782
- async function promptForImagePull(contract, exists) {
43905
+ async function promptForImagePull(exists) {
43783
43906
  console.log("");
43784
43907
  console.log(FARAI_BANNER);
43785
43908
  console.log("");
43786
- console.log(exists ? `kali container image is outdated (needs ${contract})` : `kali container image ${contract} is not installed`);
43909
+ console.log(exists ? "a newer kali container image is available" : "kali container image is not installed");
43787
43910
  const interfaceHandle = createInterface2({
43788
43911
  input: process.stdin,
43789
43912
  output: process.stdout
@@ -66247,9 +66370,10 @@ function StatusIndicator(props) {
66247
66370
  const value = tui.store.ui.statusDetail;
66248
66371
  return value && value !== "working" && value !== props.activity && !isFooterStatusDetail(value) ? ` \u2022 ${value}` : "";
66249
66372
  };
66373
+ const glyph = () => SPINNER_FRAMES[(props.spinnerFrame ?? 0) % SPINNER_FRAMES.length];
66250
66374
  const text2 = () => {
66251
66375
  if (props.activity) {
66252
- const value2 = dims().width >= 56 ? `\u2022 ${props.activity} (${fmtElapsed(props.elapsed)}${detail()} \u2022 esc to interrupt)` : `\u2022 ${props.activity} ${fmtElapsed(props.elapsed)} \xB7 esc interrupt`;
66376
+ const value2 = dims().width >= 56 ? `${glyph()} ${props.activity} (${fmtElapsed(props.elapsed)}${detail()} \u2022 esc to interrupt)` : `${glyph()} ${props.activity} ${fmtElapsed(props.elapsed)} \xB7 esc interrupt`;
66253
66377
  return truncateLine2(value2.toLowerCase(), Math.max(1, dims().width));
66254
66378
  }
66255
66379
  const value = tui.store.ui.statusDetail;
@@ -66268,6 +66392,7 @@ function StatusIndicator(props) {
66268
66392
  return _el$;
66269
66393
  })();
66270
66394
  }
66395
+ var SPINNER_FRAMES;
66271
66396
  var init_status_indicator = __esm(() => {
66272
66397
  init_solid2();
66273
66398
  init_solid2();
@@ -66279,6 +66404,7 @@ var init_status_indicator = __esm(() => {
66279
66404
  init_terminal();
66280
66405
  init_theme();
66281
66406
  init_footer_state();
66407
+ SPINNER_FRAMES = ["\xB7", "\u2022", "\xB7"];
66282
66408
  });
66283
66409
 
66284
66410
  // src/agent-tui/dialog/list-selection.ts
@@ -69434,7 +69560,9 @@ function BottomPane() {
69434
69560
  const dims = useTuiDimensions();
69435
69561
  const commandRegistryRevision = useCommandRegistryRevision();
69436
69562
  const [elapsed, setElapsed] = createSignal(0);
69563
+ const [spinner, setSpinner] = createSignal(0);
69437
69564
  let tick;
69565
+ let spinTick;
69438
69566
  const frame = () => tui.store.ui.overlayStack.at(-1);
69439
69567
  const listFrame = () => {
69440
69568
  const top = frame();
@@ -69510,16 +69638,24 @@ function BottomPane() {
69510
69638
  clearInterval(tick);
69511
69639
  tick = undefined;
69512
69640
  }
69641
+ if (spinTick) {
69642
+ clearInterval(spinTick);
69643
+ spinTick = undefined;
69644
+ }
69513
69645
  if (!started) {
69514
69646
  setElapsed(0);
69647
+ setSpinner(0);
69515
69648
  return;
69516
69649
  }
69517
69650
  setElapsed(Math.max(0, Math.floor((Date.now() - started) / 1000)));
69518
69651
  tick = setInterval(() => setElapsed(Math.max(0, Math.floor((Date.now() - started) / 1000))), 1000);
69652
+ spinTick = setInterval(() => setSpinner((value) => value + 1), 120);
69519
69653
  });
69520
69654
  onCleanup(() => {
69521
69655
  if (tick)
69522
69656
  clearInterval(tick);
69657
+ if (spinTick)
69658
+ clearInterval(spinTick);
69523
69659
  });
69524
69660
  return (() => {
69525
69661
  var _el$ = createElement("box");
@@ -69537,6 +69673,9 @@ function BottomPane() {
69537
69673
  get elapsed() {
69538
69674
  return elapsed();
69539
69675
  },
69676
+ get spinnerFrame() {
69677
+ return spinner();
69678
+ },
69540
69679
  get activity() {
69541
69680
  return statusActivity();
69542
69681
  }
@@ -70708,7 +70847,9 @@ async function launchOpenTui(workspace, sessionId) {
70708
70847
  const located = sessionId ? resolveSessionLocation(sessionId) : undefined;
70709
70848
  const effectiveWorkspace = located?.workspace ?? workspace;
70710
70849
  const effectiveSessionId = located?.id ?? sessionId;
70711
- const runtime = new AgentRuntime(effectiveWorkspace);
70850
+ const runtime = new AgentRuntime(effectiveWorkspace, undefined, {
70851
+ enableSessionTitles: true
70852
+ });
70712
70853
  let port;
70713
70854
  try {
70714
70855
  await runtime.recover();
@@ -73872,11 +74013,12 @@ async function doctor() {
73872
74013
  const backend2 = new KaliContainerBackend({
73873
74014
  workspace: process.cwd()
73874
74015
  });
73875
- const image = await backend2.resolveImage();
73876
- const capabilities = !image.exists ? "not installed (pulled on first run)" : image.contract === KALI_IMAGE_CONTRACT ? "ready" : "update available (pulled on first run)";
73877
- console.log(`kali image: ${backend2.image} (${image.exists ? "installed" : "missing"})`);
73878
- console.log(`kali contract: ${image.contract ?? "missing"} (expected ${KALI_IMAGE_CONTRACT})`);
73879
- console.log(`kali capabilities: ${capabilities}`);
74016
+ const update = await backend2.checkForImageUpdate().catch(() => {
74017
+ return;
74018
+ });
74019
+ const status = !update ? "unknown (docker unavailable)" : !update.exists ? "not installed (pulled on first run)" : update.upToDate ? "up to date" : "update available (pulled on first run)";
74020
+ console.log(`kali image: ${backend2.image}`);
74021
+ console.log(`kali image status: ${status}`);
73880
74022
  const {
73881
74023
  contentStatus: contentStatus2
73882
74024
  } = await Promise.resolve().then(() => (init_updater(), exports_updater));
@@ -74260,5 +74402,5 @@ Examples:
74260
74402
  `);
74261
74403
  }
74262
74404
 
74263
- //# debugId=4868BF6C23AAC2A664756E2164756E21
74405
+ //# debugId=32EC65247FDE45AE64756E2164756E21
74264
74406
  //# sourceMappingURL=index.js.map