@falkenslab/agent-kit 0.27.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -62,6 +62,13 @@ export interface ChatSettings {
62
62
  * state's `panel`, answered with `answer()`.
63
63
  */
64
64
  panels?: "view" | "state";
65
+ /**
66
+ * Signing in to an extension (#72): `true` (the default) opens the link in this computer's
67
+ * browser (`openBrowser()`: not over SSH or without a display), `false` never does, and a
68
+ * function opens it the host's way (Electron's `shell.openExternal`), saying whether it did.
69
+ * The link is shown either way, on a line of its own, for another computer.
70
+ */
71
+ openBrowser?: boolean | ((url: string) => Promise<boolean>);
65
72
  }
66
73
  /** One tool call in the transcript. */
67
74
  export interface TranscriptCall {
@@ -270,9 +277,10 @@ export declare function createChatController(options: Options | SessionOpener, s
270
277
  * person. The running session is untouched: it's there in a new conversation, or with `reopen()`.
271
278
  */
272
279
  /**
273
- * Signs in to an installed extension's remote servers (#61): gives the person the links (never
274
- * opens a browser), then says when the sign-in finishes. With `callbackUrl`, the address the
275
- * browser ended on (from another computer), finishes it instead.
280
+ * Signs in to an installed extension's remote servers (#61): opens each link in this computer's
281
+ * browser as `openBrowser` says (#72) and shows it on a line of its own, for another computer,
282
+ * then says when the sign-in finishes. With `callbackUrl`, the address the browser ended on
283
+ * (from another computer), finishes it instead.
276
284
  */
277
285
  signInExtension(name: string, callbackUrl?: string): Promise<SignIn>;
278
286
  /** Signs out of an installed extension's remote servers (#61). */
@@ -16,6 +16,7 @@ import { extensionOptionStates, listInstalled, removeExtension as removeInstalle
16
16
  import { extensionCatalog as readCatalog, installExtension as installFromCatalog, needsConfirmation, updateExtension as updateFromCatalog } from "../core/extensionCatalog.js";
17
17
  import { capHistory, loadHistory, saveHistory, slashCommandToken } from "./history.js";
18
18
  import { firstRun, openSession, runFolderOf, runLabel } from "./runs.js";
19
+ import { openBrowser } from "../core/openBrowser.js";
19
20
  const DEFAULT_EXIT_COMMANDS = ["/exit", "/quit"];
20
21
  const DEFAULT_HISTORY_LIMIT = 100;
21
22
  const DEFAULT_PROMPT_LABEL = "\n> ";
@@ -600,9 +601,10 @@ export async function createChatController(options, settings = {}) {
600
601
  * person. The running session is untouched: it's there in a new conversation, or with `reopen()`.
601
602
  */
602
603
  /**
603
- * Signs in to an installed extension's remote servers (#61): gives the person the links (never
604
- * opens a browser), then says when the sign-in finishes. With `callbackUrl`, the address the
605
- * browser ended on (from another computer), finishes it instead.
604
+ * Signs in to an installed extension's remote servers (#61): opens each link in this computer's
605
+ * browser as `openBrowser` says (#72) and shows it on a line of its own, for another computer,
606
+ * then says when the sign-in finishes. With `callbackUrl`, the address the browser ended on
607
+ * (from another computer), finishes it instead.
606
608
  */
607
609
  async signInExtension(name, callbackUrl) {
608
610
  const auth = current.extensionAuth;
@@ -612,8 +614,13 @@ export async function createChatController(options, settings = {}) {
612
614
  else if (result.status === "signed-in")
613
615
  notice(callbackUrl ? t().extensionSignedIn(name) : t().extensionAlreadySignedIn(name));
614
616
  else {
615
- for (const link of result.links)
616
- notice(t().extensionSignInOpen(name, link.url));
617
+ const open = settings.openBrowser === false ? undefined : typeof settings.openBrowser === "function" ? settings.openBrowser : openBrowser;
618
+ for (const link of result.links) {
619
+ const opened = open ? await open(link.url).catch(() => false) : false;
620
+ notice(opened ? t().extensionSignInOpened(name) : t().extensionSignInOpen(name));
621
+ // Alone on its line: a view shows it whole (a terminal link, #72), and it's copied whole.
622
+ notice(link.url);
623
+ }
617
624
  notice(t().extensionSignInPaste(name));
618
625
  // Waits in the background: the person signs in in their browser, whenever they do.
619
626
  void auth.connected(name, 5 * 60_000).then((done) => {
@@ -53,7 +53,8 @@ export const de = {
53
53
  extensionLacks: (name, what) => `(${name}: ${what})`,
54
54
  extensionTalksTo: (name, hosts) => `(${name} spricht mit ${hosts}: was dein Agent dorthin sendet, verlässt diesen Computer)`,
55
55
  extensionNeedsSignIn: (name) => `${name} braucht deine Anmeldung: /extensions sign-in ${name}`,
56
- extensionSignInOpen: (name, url) => `(zum Anmelden bei ${name} öffne ${url} : sobald du dort fertig bist, ist sie hier verbunden)`,
56
+ extensionSignInOpen: (name) => `(zum Anmelden bei ${name} öffne diesen Link; sobald du dort fertig bist, ist sie hier verbunden:)`,
57
+ extensionSignInOpened: (name) => `(Anmeldung bei ${name} in deinem Browser; sobald du dort fertig bist, ist sie hier verbunden. Der Link, für einen anderen Browser:)`,
57
58
  extensionSignInPaste: (name) => `(auf einem anderen Computer? Füge die Adresse ein, bei der der Browser landet: /extensions sign-in ${name} <diese Adresse>)`,
58
59
  extensionSignedIn: (name) => `(bei ${name} angemeldet: ihre Werkzeuge sind bereit)`,
59
60
  extensionSignInWaiting: (name) => `(warte noch auf die Anmeldung bei ${name}; /extensions sign-in ${name} gibt den Link erneut)`,
@@ -73,7 +73,9 @@ export interface Messages {
73
73
  /** In /extensions: a running extension whose remote server needs the person to sign in. */
74
74
  extensionNeedsSignIn(name: string): string;
75
75
  /** The link to sign in to an extension's remote server. */
76
- extensionSignInOpen(name: string, url: string): string;
76
+ /** Before the sign-in link, on its own line next (#72): when the browser couldn't be opened, and when it was. */
77
+ extensionSignInOpen(name: string): string;
78
+ extensionSignInOpened(name: string): string;
77
79
  /** How to finish signing in from another computer. */
78
80
  extensionSignInPaste(name: string): string;
79
81
  extensionSignedIn(name: string): string;
@@ -53,7 +53,8 @@ export const en = {
53
53
  extensionLacks: (name, what) => `(${name} ${what})`,
54
54
  extensionTalksTo: (name, hosts) => `(${name} talks to ${hosts}: what your agent sends there leaves this computer)`,
55
55
  extensionNeedsSignIn: (name) => `${name} needs you to sign in: /extensions sign-in ${name}`,
56
- extensionSignInOpen: (name, url) => `(to sign in to ${name}, open ${url} : when you're done there, it's connected here)`,
56
+ extensionSignInOpen: (name) => `(to sign in to ${name}, open this link; when you're done there, it's connected here:)`,
57
+ extensionSignInOpened: (name) => `(signing in to ${name} in your browser; when you're done there, it's connected here. The link, for another browser:)`,
57
58
  extensionSignInPaste: (name) => `(on another computer? Paste the address the browser ends on: /extensions sign-in ${name} <that address>)`,
58
59
  extensionSignedIn: (name) => `(signed in to ${name}: its tools are ready)`,
59
60
  extensionSignInWaiting: (name) => `(still waiting for the sign-in to ${name}; /extensions sign-in ${name} gives the link again)`,
@@ -53,7 +53,8 @@ export const es = {
53
53
  extensionLacks: (name, what) => `(${name}: ${what})`,
54
54
  extensionTalksTo: (name, hosts) => `(${name} habla con ${hosts}: lo que tu agente le envíe sale de este ordenador)`,
55
55
  extensionNeedsSignIn: (name) => `${name} necesita que inicies sesión: /extensions sign-in ${name}`,
56
- extensionSignInOpen: (name, url) => `(para iniciar sesión en ${name}, abre ${url} : cuando termines allí, queda conectada aquí)`,
56
+ extensionSignInOpen: (name) => `(para iniciar sesión en ${name}, abre este enlace; cuando termines allí, queda conectada aquí:)`,
57
+ extensionSignInOpened: (name) => `(iniciando sesión en ${name} en tu navegador; cuando termines allí, queda conectada aquí. El enlace, para otro navegador:)`,
57
58
  extensionSignInPaste: (name) => `(¿en otro ordenador? Pega la dirección en la que acaba el navegador: /extensions sign-in ${name} <esa dirección>)`,
58
59
  extensionSignedIn: (name) => `(sesión iniciada en ${name}: sus herramientas están listas)`,
59
60
  extensionSignInWaiting: (name) => `(sigo esperando el inicio de sesión en ${name}; /extensions sign-in ${name} vuelve a dar el enlace)`,
@@ -53,7 +53,8 @@ export const fr = {
53
53
  extensionLacks: (name, what) => `(${name} : ${what})`,
54
54
  extensionTalksTo: (name, hosts) => `(${name} échange avec ${hosts} : ce que votre agent y envoie quitte cet ordinateur)`,
55
55
  extensionNeedsSignIn: (name) => `${name} a besoin que vous vous connectiez : /extensions sign-in ${name}`,
56
- extensionSignInOpen: (name, url) => `(pour vous connecter à ${name}, ouvrez ${url} : une fois terminé là-bas, elle est connectée ici)`,
56
+ extensionSignInOpen: (name) => `(pour vous connecter à ${name}, ouvrez ce lien ; une fois terminé là-bas, elle est connectée ici :)`,
57
+ extensionSignInOpened: (name) => `(connexion à ${name} dans votre navigateur ; une fois terminé là-bas, elle est connectée ici. Le lien, pour un autre navigateur :)`,
57
58
  extensionSignInPaste: (name) => `(sur un autre ordinateur ? Collez l'adresse où le navigateur arrive : /extensions sign-in ${name} <cette adresse>)`,
58
59
  extensionSignedIn: (name) => `(connecté à ${name} : ses outils sont prêts)`,
59
60
  extensionSignInWaiting: (name) => `(toujours en attente de la connexion à ${name} ; /extensions sign-in ${name} redonne le lien)`,
@@ -47,8 +47,12 @@ async function packageBin(dir, name) {
47
47
  const bundled = json.bundleDependencies ?? json.bundledDependencies;
48
48
  const declared = Object.keys({ ...json.dependencies, ...json.optionalDependencies });
49
49
  const missing = bundled === true ? [] : declared.filter((dependency) => !(Array.isArray(bundled) && bundled.includes(dependency)));
50
+ // Only package.json is read (#71): one bundled into the program but still in `dependencies`
51
+ // looks missing too, so the message says both ways out.
50
52
  if (missing.length)
51
- return { problem: `has dependencies it doesn't bring (${missing.join(", ")}): the kit installs none, so a package must bundle them, or list them in bundleDependencies` };
53
+ return {
54
+ problem: `declares dependencies it doesn't bring (${missing.join(", ")}): the kit installs none. If they're bundled into its program, move them to devDependencies; otherwise list them in bundleDependencies`,
55
+ };
52
56
  const scripts = ["preinstall", "install", "postinstall"].filter((script) => json.scripts?.[script]);
53
57
  if (scripts.length)
54
58
  return { problem: `has install scripts (${scripts.join(", ")}), which the kit never runs` };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Whether a browser can be opened on this computer (#72): not over SSH, nor on Linux without a
3
+ * display (a server, a container). Elsewhere the desktop is there.
4
+ */
5
+ export declare function canOpenBrowser(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): boolean;
6
+ /**
7
+ * Opens an `http(s)` address in this computer's browser, with the system's own opener and no
8
+ * shell (an OAuth link is full of `&`, which `cmd /c start` would cut): `rundll32` on Windows,
9
+ * `open` on macOS, `xdg-open` elsewhere. False when it can't (`canOpenBrowser()`, another kind
10
+ * of address, no opener): then the person is given the link only.
11
+ */
12
+ export declare function openBrowser(url: string): Promise<boolean>;
@@ -0,0 +1,36 @@
1
+ import { spawn } from "node:child_process";
2
+ /**
3
+ * Whether a browser can be opened on this computer (#72): not over SSH, nor on Linux without a
4
+ * display (a server, a container). Elsewhere the desktop is there.
5
+ */
6
+ export function canOpenBrowser(env = process.env, platform = process.platform) {
7
+ if (env.SSH_CONNECTION || env.SSH_TTY || env.SSH_CLIENT)
8
+ return false;
9
+ if (platform === "win32" || platform === "darwin")
10
+ return true;
11
+ return Boolean(env.DISPLAY || env.WAYLAND_DISPLAY);
12
+ }
13
+ /**
14
+ * Opens an `http(s)` address in this computer's browser, with the system's own opener and no
15
+ * shell (an OAuth link is full of `&`, which `cmd /c start` would cut): `rundll32` on Windows,
16
+ * `open` on macOS, `xdg-open` elsewhere. False when it can't (`canOpenBrowser()`, another kind
17
+ * of address, no opener): then the person is given the link only.
18
+ */
19
+ export async function openBrowser(url) {
20
+ if (!/^https?:\/\//i.test(url) || !canOpenBrowser())
21
+ return false;
22
+ const [command, args] = process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", url]] : process.platform === "darwin" ? ["open", [url]] : ["xdg-open", [url]];
23
+ return await new Promise((resolve) => {
24
+ try {
25
+ const child = spawn(command, args, { detached: true, stdio: "ignore", windowsHide: true });
26
+ child.once("error", () => resolve(false));
27
+ child.once("spawn", () => {
28
+ child.unref();
29
+ resolve(true);
30
+ });
31
+ }
32
+ catch {
33
+ resolve(false);
34
+ }
35
+ });
36
+ }
package/dist/index.d.ts CHANGED
@@ -52,6 +52,7 @@ export { createSaveToSourcesServer, sourcesPromptSection, type SourceToolsOption
52
52
  export { runChatTui, type ChatTuiOptions } from "./tui/chatTui.js";
53
53
  export type { SessionOpener } from "./chat/runs.js";
54
54
  export { createChatController, type ChatController, type ChatSettings, type ChatState, type ChatEvent, type ChatPanel, type ChatQuestion, type TranscriptEntry, type TranscriptCall, type NoticeTone, } from "./chat/chatController.js";
55
+ export { openBrowser, canOpenBrowser } from "./core/openBrowser.js";
55
56
  export { createConsoleRenderer, type ConsoleRenderer, type ConsoleRendererOptions } from "./tui/consoleRenderer.js";
56
57
  export { runChatInk, type InkChatOptions, type HeaderInfo } from "./tui/ink/runChatInk.js";
57
58
  export type { ToolPhrase, ToolDetail, ResultFormatter } from "./tui/ink/toolGroup.js";
package/dist/index.js CHANGED
@@ -53,6 +53,7 @@ export { createManualLoginServer } from "./core/tools/manualLogin.js";
53
53
  export { createSaveToSourcesServer, sourcesPromptSection } from "./extensions/sources/tools.js";
54
54
  export { runChatTui } from "./tui/chatTui.js";
55
55
  export { createChatController, } from "./chat/chatController.js";
56
+ export { openBrowser, canOpenBrowser } from "./core/openBrowser.js";
56
57
  export { createConsoleRenderer } from "./tui/consoleRenderer.js";
57
58
  export { runChatInk } from "./tui/ink/runChatInk.js";
58
59
  export { createProgressView } from "./tui/ink/progressView.js";
@@ -4,6 +4,7 @@ import type { AgentEvent } from "../core/runner.js";
4
4
  import { type Theme } from "./theme.js";
5
5
  import type { ToolLabels } from "../core/toolLabels.js";
6
6
  import { type SessionOpener } from "../chat/runs.js";
7
+ import { type ChatSettings } from "../chat/chatController.js";
7
8
  export { capHistory, loadHistory, saveHistory, slashCommandToken } from "../chat/history.js";
8
9
  /** Options of the chats (`runChatTui()`, and `runChatInk()` through `InkChatOptions`). */
9
10
  export interface ChatTuiOptions {
@@ -14,6 +15,8 @@ export interface ChatTuiOptions {
14
15
  * own domain-specific tools (Playwright's `browser_*`, etc.).
15
16
  */
16
17
  formatAction?: (toolName: string, input: unknown) => string;
18
+ /** Signing in to an extension: whether its link opens in this computer's browser (the default), or how (see `ChatSettings.openBrowser`, #72). */
19
+ openBrowser?: ChatSettings["openBrowser"];
17
20
  /** Printed once before the first prompt, if given. */
18
21
  welcomeMessage?: string;
19
22
  /** Shown before the cursor at each prompt. */
@@ -8,6 +8,7 @@ import { applyTheme } from "./theme.js";
8
8
  import { createConsoleRenderer } from "./consoleRenderer.js";
9
9
  import { runDate } from "../chat/runs.js";
10
10
  import { createChatController } from "../chat/chatController.js";
11
+ import { linkedNotice } from "./hyperlink.js";
11
12
  export { capHistory, loadHistory, saveHistory, slashCommandToken } from "../chat/history.js";
12
13
  const DEFAULT_PROMPT_LABEL = "\n> ";
13
14
  const DEFAULT_EXIT_COMMANDS = ["/exit", "/quit"];
@@ -75,6 +76,7 @@ export async function runChatTui(options, tuiOptions = {}) {
75
76
  modeControl: tuiOptions.modeControl,
76
77
  toolLabels: tuiOptions.toolLabels,
77
78
  formatAction: tuiOptions.formatAction,
79
+ ...(tuiOptions.openBrowser !== undefined ? { openBrowser: tuiOptions.openBrowser } : {}),
78
80
  // Nothing here shows them.
79
81
  promptSuggestions: false,
80
82
  // /resume: a numbered list, and a number typed.
@@ -131,7 +133,8 @@ export async function runChatTui(options, tuiOptions = {}) {
131
133
  renderer.render(event.event);
132
134
  return;
133
135
  case "notice":
134
- writeLine(toned(event.text, event.tone));
136
+ // An address alone (a sign-in link) as a terminal link (#72).
137
+ writeLine(toned(linkedNotice(event.text), event.tone));
135
138
  return;
136
139
  case "turn-start":
137
140
  renderer.startTurn();
@@ -59,8 +59,10 @@ export async function runExtensionCommand(argv, options) {
59
59
  const ask = options.ask ?? askOnTerminal;
60
60
  const askSecret = options.askSecret ?? (options.ask ? options.ask : askSecretOnTerminal);
61
61
  /**
62
- * After installing `name` into `dir`: its options from `--set`, then asked for each still
63
- * without a value (on a terminal; none without one), saying which required ones are left.
62
+ * After installing `name` into `dir`: its options from `--set`, saved first so a prompt cut
63
+ * short loses none (#73), then asked for each still without a value, saying which required ones
64
+ * are left. Never asked with `--yes`, nor without a terminal unless the host asks itself (`ask`):
65
+ * a default stays, an optional one stays unset.
64
66
  */
65
67
  const configure = async (name, dir) => {
66
68
  const installed = (await listInstalled(options.dirs)).find((extension) => extension.name === name && extension.scopeDir === dir);
@@ -74,17 +76,24 @@ export async function runExtensionCommand(argv, options) {
74
76
  throw new Error(`--set takes key=value: "${pair}"`);
75
77
  typed[pair.slice(0, at)] = pair.slice(at + 1);
76
78
  }
79
+ if (Object.keys(typed).length)
80
+ await setExtensionOptions(installed, name, typed);
77
81
  const states = await describeOptionStates(installed);
82
+ const canAsk = !flag("yes") && (options.ask !== undefined || process.stdin.isTTY === true);
83
+ const asked = {};
78
84
  for (const [key, option] of Object.entries(declared)) {
85
+ if (!canAsk)
86
+ break;
79
87
  if (key in typed || states.get(key))
80
88
  continue;
81
89
  const question = `${option.title}${option.required ? "" : " (optional)"}${option.default !== undefined ? ` [${String(option.default)}]` : ""}: ${option.description}\n ${key} = `;
82
90
  const answer = (await (option.sensitive ? askSecret : ask)(question)).trim();
83
91
  if (answer)
84
- typed[key] = answer;
92
+ asked[key] = answer;
85
93
  }
86
- if (Object.keys(typed).length)
87
- await setExtensionOptions(installed, name, typed);
94
+ if (Object.keys(asked).length)
95
+ await setExtensionOptions(installed, name, asked);
96
+ Object.assign(typed, asked);
88
97
  const missing = Object.entries(declared).filter(([key, option]) => option.required && !(key in typed) && !states.get(key) && option.default === undefined).map(([key]) => key);
89
98
  if (missing.length)
90
99
  write(`It stays off until ${missing.join(", ")} ${missing.length === 1 ? "is" : "are"} set: \`${command} extension config ${name} ${missing[0]}\`.`);
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Terminal hyperlinks (OSC 8, #72): a long address the full-screen chat wraps stays one link,
3
+ * since every row it's cut into opens it whole (`wrap-ansi` reopens the link on each row).
4
+ */
5
+ /**
6
+ * Whether the terminal shows OSC 8 links, from what it says about itself: Windows Terminal,
7
+ * VS Code's, iTerm2, WezTerm, Konsole, a recent VTE (GNOME's), DomTerm. `FORCE_HYPERLINK=1` or `0`
8
+ * decides. Elsewhere none: a terminal that doesn't know the sequence may print it.
9
+ */
10
+ export declare function supportsHyperlinks(env?: NodeJS.ProcessEnv, isTTY?: boolean): boolean;
11
+ /** `text` as a link to `url` (OSC 8). */
12
+ export declare function hyperlink(url: string, text?: string): string;
13
+ /** A notice that is an address alone (a sign-in link, #72) as a link when the terminal shows them; any other text as it is. */
14
+ export declare function linkedNotice(text: string, linked?: boolean): string;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Terminal hyperlinks (OSC 8, #72): a long address the full-screen chat wraps stays one link,
3
+ * since every row it's cut into opens it whole (`wrap-ansi` reopens the link on each row).
4
+ */
5
+ /**
6
+ * Whether the terminal shows OSC 8 links, from what it says about itself: Windows Terminal,
7
+ * VS Code's, iTerm2, WezTerm, Konsole, a recent VTE (GNOME's), DomTerm. `FORCE_HYPERLINK=1` or `0`
8
+ * decides. Elsewhere none: a terminal that doesn't know the sequence may print it.
9
+ */
10
+ export function supportsHyperlinks(env = process.env, isTTY = process.stdout.isTTY === true) {
11
+ if (env.FORCE_HYPERLINK !== undefined)
12
+ return env.FORCE_HYPERLINK !== "0";
13
+ if (!isTTY)
14
+ return false;
15
+ if (env.WT_SESSION || env.KONSOLE_VERSION || env.DOMTERM)
16
+ return true;
17
+ if (["vscode", "iTerm.app", "WezTerm"].includes(env.TERM_PROGRAM ?? ""))
18
+ return true;
19
+ return Number(env.VTE_VERSION ?? 0) >= 5000;
20
+ }
21
+ /** `text` as a link to `url` (OSC 8). */
22
+ export function hyperlink(url, text = url) {
23
+ return `\x1b]8;;${url}\x07${text}\x1b]8;;\x07`;
24
+ }
25
+ /** A notice that is an address alone (a sign-in link, #72) as a link when the terminal shows them; any other text as it is. */
26
+ export function linkedNotice(text, linked = supportsHyperlinks()) {
27
+ return linked && /^https?:\/\/\S+$/.test(text) ? hyperlink(text) : text;
28
+ }
@@ -6,6 +6,7 @@ import { Select } from "@inkjs/ui";
6
6
  import { getInteractionPort, setInteractionPort } from "../../core/interaction.js";
7
7
  import { runChatTui } from "../chatTui.js";
8
8
  import { createChatController } from "../../chat/chatController.js";
9
+ import { linkedNotice } from "../hyperlink.js";
9
10
  import * as ui from "../ui.js";
10
11
  import { t } from "../../core/messages/index.js";
11
12
  import { applyLanguage } from "../language.js";
@@ -197,6 +198,7 @@ export async function runChatInk(options, tuiOptions = {}) {
197
198
  toolLabels: tuiOptions.toolLabels,
198
199
  formatAction: tuiOptions.formatAction,
199
200
  toolPhrase: tuiOptions.toolPhrase,
201
+ ...(tuiOptions.openBrowser !== undefined ? { openBrowser: tuiOptions.openBrowser } : {}),
200
202
  promptSuggestions: tuiOptions.promptSuggestions ?? true,
201
203
  // The /resume list in place of the prompt.
202
204
  pick: (title, choices) => input.pick(title, choices),
@@ -241,7 +243,8 @@ export async function runChatInk(options, tuiOptions = {}) {
241
243
  model.note(userLine(event.text), "user");
242
244
  return;
243
245
  case "notice":
244
- model.writeLine(toned(event.text, event.tone));
246
+ // An address alone (a sign-in link) as a terminal link, whole however it wraps (#72).
247
+ model.writeLine(toned(linkedNotice(event.text), event.tone));
245
248
  return;
246
249
  case "turn-start":
247
250
  input.setSuggestion(null);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@falkenslab/agent-kit",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "description": "Library for building an agent on top of the Claude Agent SDK",
5
5
  "author": "Francisco Vargas Ruiz",
6
6
  "license": "MIT",