@alisio/alisio-code 0.1.0-alpha.11 → 0.1.0-alpha.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,23 +1,102 @@
1
- # alisio
1
+ # @alisio/alisio-code
2
2
 
3
- **Alisio is an extensible, provider-agnostic coding-agent harness for your terminal.** It pairs a
4
- fast TUI with any OpenAI-compatible model, runs local tools behind explicit permissions, compacts
5
- long conversations into structured checkpoints, remembers decisions across sessions with an
6
- Engram-style memory plugin, and exposes a typed plugin SDK.
3
+ **Alisio is an extensible, provider-agnostic coding-agent harness for your terminal.** It pairs a fast
4
+ TUI with any OpenAI-compatible model, runs local tools behind explicit permissions, compacts long
5
+ conversations into structured checkpoints, remembers decisions across sessions with an Engram-style
6
+ memory plugin, and exposes a typed plugin SDK.
7
+
8
+ ## What it is
9
+
10
+ `alisio` is the CLI of the Alisio project. It connects to any OpenAI-compatible endpoint (Chat
11
+ Completions or Responses), drives its own agent tool loop, and runs interactively as a TUI or
12
+ headless for scripts and CI. Provider SDKs stay out of the harness: the CLI ships built-in DeepSeek,
13
+ OpenAI-compatible, OpenCode Console (Zen) and OpenCode Go providers, plus memory and subagents
14
+ plugins.
15
+
16
+ ## Installation
17
+
18
+ ```sh
19
+ npm i -g @alisio/alisio-code # or: pnpm add -g @alisio/alisio-code · bun add -g @alisio/alisio-code
20
+ alisio # the binary is installed as `alisio`
21
+ ```
22
+
23
+ Standalone binaries (Bun embedded, no Node required) are attached to GitHub releases.
24
+
25
+ ## Quick start
26
+
27
+ The fastest path is the TUI's `/connect` form: pick a provider, enter its settings and key, choose a
28
+ model, and Alisio persists the selection globally (`/model` re-opens the same selector). A plain
29
+ config start looks like:
30
+
31
+ ```sh
32
+ alisio setup # writes a secret-free .alisio/config.json in .alisio/
33
+ export DEEPSEEK_API_KEY=... # keys come from environment variables or /connect, never config files
34
+ alisio --config .alisio/config.json
35
+ ```
36
+
37
+ Run one prompt headless, emitting versioned JSONL events:
7
38
 
8
39
  ```sh
9
- npm install -g @alisio/alisio-code # or: pnpm add -g @alisio/alisio-code · bun add -g @alisio/alisio-code
10
- alisio # the CLI binary is installed as `alisio`
11
- export DEEPSEEK_API_KEY=... # keys are read from environment variables only
12
- alisio --config ./alisio.json
40
+ alisio run "Summarize this repository" --json
41
+ alisio run "/init" --allow-write # generate AGENTS.md from the built-in /init template
13
42
  ```
14
43
 
15
- - Terminal UI: streaming Markdown, tool blocks with diffs, context/token bar, slash commands, copy-on-select.
16
- - Headless: `alisio run "prompt" --json` emits versioned JSONL events.
17
- - Permissions: reads by default; `--allow-write`, `--allow-process` or interactive approval; `--read-only`.
18
- - Built-in memory plugin (disable with `--disable-plugin memory`); npm plugins via `--plugin <package>`.
19
- - Runs on Node.js >= 22.16 or Bun; standalone binaries are attached to GitHub releases.
44
+ ## Commands
45
+
46
+ | Command | Description |
47
+ | --- | --- |
48
+ | `alisio` | Interactive TUI (readline with `--no-tui`) |
49
+ | `alisio run <prompt>` | Headless run; `/name args` runs a prompt template |
50
+ | `alisio resume <session> [prompt]` | Resume a session |
51
+ | `alisio setup` | Write a secret-free example `.alisio/config.json` |
52
+ | `alisio doctor` | Environment and provider diagnostics |
53
+ | `alisio trust list` / `trust revoke <path>` | Inspect or revoke per-directory project trust |
54
+ | `alisio sessions list` / `sessions recover <id> --acknowledge` | List sessions; recover after a crash |
55
+ | `alisio skills list` / `skills validate` | Discover and validate Agent Skills |
56
+ | `alisio plugins list` / `plugins doctor` | List/load plugins and show their tools and commands |
57
+ | `alisio mcp list` / `mcp doctor <server>` | List configured MCP servers; connect to one (`--allow-mcp`) |
58
+ | `alisio context explain <path>` | Show which `AGENTS.md` files apply to a path |
59
+ | `alisio install npm:<package>` | Install an npm plugin into the global plugins directory |
60
+
61
+ ## Permissions and key flags
62
+
63
+ Reads run by default; anything else needs a flag or interactive approval:
64
+
65
+ | Flag | Effect |
66
+ | --- | --- |
67
+ | `--allow-write` | Allow file writes |
68
+ | `--allow-process` | Allow arbitrary subprocesses (not sandboxed) |
69
+ | `--allow-external` | Allow network tools: `webfetch`, `websearch`, provider-native search |
70
+ | `--allow-mcp` | Allow configured MCP servers and remote tool calls |
71
+ | `--allow-agents` | Allow messaging neighboring agents through Herdr |
72
+ | `--read-only` | Disable writes, subprocesses, network tools, executable plugins and MCP |
73
+ | `--plugin <path…>` / `--trust-project` | Load explicitly trusted plugins / trust project config |
74
+ | `--disable-plugin <ids…>` | Disable built-in plugins (e.g. `memory`) |
75
+ | `--json` | Emit versioned JSONL events (headless) |
76
+
77
+ Other global flags: `--cwd`, `--config`, `--model`, `--base-url`, `--api-mode`, `--db`,
78
+ `--no-tui`, `--no-banner`, `--no-herdr`, `--agents <json>`, `--quiet`, `-V`, `-h`.
79
+
80
+ ## In the TUI
81
+
82
+ `/connect` adds a provider; `/model` picks a saved provider/model; `/compact`, `/stats`, `/clear`,
83
+ `/sessions`, `/resume` manage conversations; `/plugins`, `/skills`, `/mcp`, `/settings` manage
84
+ extensions and preferences; `/memory` and `/agents` drive the built-in plugins; `/init` writes
85
+ `AGENTS.md`.
86
+
87
+ ## Requirements
88
+
89
+ Node.js **>= 22.16** (`node:sqlite` with FTS5) or Bun **>= 1.4.2**. Git and ripgrep on `PATH` for the
90
+ Git and search tools; `alisio doctor` reports what it detects.
91
+
92
+ ## Docs
93
+
94
+ Documentation (EN/ES): <https://gustavogutierrez.github.io/alisio/> — [quick start](https://gustavogutierrez.github.io/alisio/quick-start),
95
+ [configuration](https://gustavogutierrez.github.io/alisio/configuration), [plugins](https://gustavogutierrez.github.io/alisio/plugins),
96
+ [subagents](https://gustavogutierrez.github.io/alisio/subagents), [memory](https://gustavogutierrez.github.io/alisio/memory),
97
+ [TUI](https://gustavogutierrez.github.io/alisio/tui).
20
98
 
21
- Documentation: [English](https://gustavogutierrez.github.io/alisio/) · [Español](https://gustavogutierrez.github.io/alisio/es/)
99
+ ## License
22
100
 
23
- Maintainer: Gustavo Gutiérrez · License: MIT
101
+ MIT. Maintained by Gustavo Gutiérrez Mercado. Source: <https://github.com/GustavoGutierrez/alisio> ·
102
+ npm: <https://www.npmjs.com/settings/alisio/packages>.
package/dist/tui/app.js CHANGED
@@ -5,6 +5,8 @@ import { MAX_ATTACHMENTS_PER_MESSAGE, MAX_IMAGE_BYTES, pasteImageFromClipboard,
5
5
  import { copyText, nodeSpawn } from "./clipboard.js";
6
6
  import { AttachmentsBar, BannerBlock, clock, Footer, Header, QuestionPanel, Switch, TranscriptSync, TreePanel, } from "./components.js";
7
7
  import { ConnectInputPrompt } from "./connect-input.js";
8
+ import { bounded, EXIT_PENDING_CAP_MS, EXIT_SESSION_END_CAP_MS } from "./exit.js";
9
+ import { BRANCH_REFRESH_MS, createBranchCache } from "./git-branch.js";
8
10
  import { initialPanelState, reducePanel, visibleRows } from "./panel.js";
9
11
  import { summarizeAnswers } from "./questions.js";
10
12
  import { InteractiveQueue } from "./queue.js";
@@ -153,6 +155,21 @@ export async function runTui(options) {
153
155
  }, ms);
154
156
  tui.requestRender();
155
157
  };
158
+ // Git branch of the workspace for the header. Reading refs is a read operation, so it stays on
159
+ // under --read-only; the TTL cache means git is spawned at most once per ~10s, never per frame,
160
+ // and a missing repo/git or a slow spawn simply yields no branch segment.
161
+ const branchCache = createBranchCache();
162
+ let branchName;
163
+ const refreshBranch = () => {
164
+ void branchCache.read(app.workspace).then((branch) => {
165
+ if (branch !== branchName) {
166
+ branchName = branch;
167
+ tui.requestRender();
168
+ }
169
+ });
170
+ };
171
+ refreshBranch();
172
+ const branchTimer = setInterval(refreshBranch, BRANCH_REFRESH_MS);
156
173
  const headerInfo = () => {
157
174
  const policy = app.runner.policy, ask = app.runner.approvals;
158
175
  return {
@@ -162,6 +179,7 @@ export async function runTui(options) {
162
179
  provider: app.providers.get(activeProvider?.id ?? "")?.name,
163
180
  cwd: shortenPath(app.workspace, homedir()),
164
181
  session: shortId(session),
182
+ branch: branchName,
165
183
  write: policy.write ? "on" : ask ? "ask" : "off",
166
184
  process: policy.process ? "on" : ask ? "ask" : "off",
167
185
  mcp: app.mcpRuntimePermission() === "granted",
@@ -308,7 +326,13 @@ export async function runTui(options) {
308
326
  tui.requestRender();
309
327
  return;
310
328
  }
311
- if (["run_completed", "run_failed", "run_cancelled", "compaction_failed"].includes(event.type))
329
+ if ([
330
+ "run_completed",
331
+ "run_failed",
332
+ "run_cancelled",
333
+ "run_turns_exceeded",
334
+ "compaction_failed",
335
+ ].includes(event.type))
312
336
  terminalEvent = true;
313
337
  view = reduceEvent(view, event);
314
338
  sync();
@@ -479,7 +503,7 @@ export async function runTui(options) {
479
503
  const endSession = async (reason) => {
480
504
  if (!view.stats.runs || !app.plugins.hasSessionEndHooks)
481
505
  return;
482
- flashHint("Running session-end plugin hooks (bounded by pluginHooks.sessionEndTimeoutMs)…", 60_000);
506
+ flashHint("Running session-end plugin hooks…", 60_000);
483
507
  tui.renderNow?.();
484
508
  try {
485
509
  const { failures } = await app.endSession(session, reason);
@@ -499,8 +523,12 @@ export async function runTui(options) {
499
523
  return;
500
524
  stopping = true;
501
525
  controller?.abort(new Error("Exiting"));
502
- await Promise.race([pending?.catch(() => { }), new Promise((r) => setTimeout(r, 3000))]);
503
- await endSession("exit");
526
+ // Exit must feel instant: an in-flight turn gets up to 3s (the abort above settles it
527
+ // immediately in practice), then session-end hooks get a short cap — /clear still honors
528
+ // the full pluginHooks.sessionEndTimeoutMs. app.close() later in the main path is itself
529
+ // parallel and capped, so the whole exit path is bounded end to end.
530
+ await bounded(pending?.catch(() => { }), EXIT_PENDING_CAP_MS);
531
+ await bounded(endSession("exit"), EXIT_SESSION_END_CAP_MS);
504
532
  resolveExit();
505
533
  };
506
534
  const askInput = (input) => new Promise((resolve) => {
@@ -1540,6 +1568,7 @@ export async function runTui(options) {
1540
1568
  }
1541
1569
  finally {
1542
1570
  clearInterval(ticker);
1571
+ clearInterval(branchTimer);
1543
1572
  clearTimeout(hintTimer);
1544
1573
  process.off("SIGTERM", onSignal);
1545
1574
  process.off("SIGHUP", onSignal);
@@ -17,6 +17,8 @@ export interface HeaderInfo {
17
17
  provider?: string;
18
18
  cwd: string;
19
19
  session: string;
20
+ /** Git branch (or short SHA on a detached HEAD) of the session workspace; undefined when unavailable. */
21
+ branch?: string;
20
22
  write: PermissionState;
21
23
  process: PermissionState;
22
24
  mcp: boolean;
@@ -1,5 +1,7 @@
1
1
  import { Container, getCapabilities, Image, Key, Markdown, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi, } from "@earendil-works/pi-tui";
2
+ import { terminalCapabilities } from "../banner.js";
2
3
  import { attachmentCaption, MAX_ATTACHMENTS_PER_MESSAGE, } from "./attachments.js";
4
+ import { branchDisplay } from "./git-branch.js";
3
5
  import { initialQuestionState, reduceQuestions, } from "./questions.js";
4
6
  import { contextLevel, contextPercent, editSummary, fitSegments, formatContext, formatDuration, formatTokens, } from "./state.js";
5
7
  import { imageTheme, levelColor, markdownTheme, style } from "./theme.js";
@@ -28,7 +30,7 @@ export class Header {
28
30
  render(width) {
29
31
  const i = this.info(), v = this.view();
30
32
  const line1 = line([
31
- { text: "◆ alisio", priority: 8, paint: (t) => style.bold(style.brightCyan(t)) },
33
+ { text: "◆ Alisio Code", priority: 8, paint: (t) => style.bold(style.brightCyan(t)) },
32
34
  { text: `v${i.version}`, priority: 1, paint: style.gray },
33
35
  {
34
36
  text: v.model || "not connected",
@@ -53,6 +55,19 @@ export class Header {
53
55
  const line2 = line([
54
56
  { text: i.cwd, priority: 6, paint: style.cyan },
55
57
  { text: `session ${i.session}`, priority: 4, paint: style.gray },
58
+ // Priority 3 (below session's 4): the branch is ambient context, so on narrow terminals it
59
+ // drops first — before the session ID and permissions. At equal priority fitSegments drops
60
+ // the FIRST lowest-priority segment, which would mis-drop the session when branch sits
61
+ // after it, so the branch keeps a strictly lower priority instead.
62
+ ...(i.branch
63
+ ? [
64
+ {
65
+ text: branchDisplay(i.branch, terminalCapabilities({ env: process.env, columns: width, tty: true }).unicode),
66
+ priority: 3,
67
+ paint: style.cyan,
68
+ },
69
+ ]
70
+ : []),
56
71
  ...perms,
57
72
  ], width);
58
73
  return fit([line1, line2, style.gray("─".repeat(Math.max(0, width)))], width);
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Interactive-exit bounds: `/exit`, double Ctrl+C on an empty input, Ctrl+D and SIGINT/SIGTERM
3
+ * must feel instant, so each shutdown step is raced against a cap instead of awaiting teardown
4
+ * without limits. The plugin host's own hook timeout (`pluginHooks.sessionEndTimeoutMs`) still
5
+ * applies to `/clear`; exit deliberately uses shorter user-facing caps on the same hooks.
6
+ */
7
+ export declare const EXIT_PENDING_CAP_MS = 3000;
8
+ export declare const EXIT_SESSION_END_CAP_MS = 1500;
9
+ /** Resolves when `work` settles or after `capMs`, whichever comes first; never rejects. */
10
+ export declare function bounded(work: Promise<unknown> | undefined, capMs: number): Promise<void>;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Interactive-exit bounds: `/exit`, double Ctrl+C on an empty input, Ctrl+D and SIGINT/SIGTERM
3
+ * must feel instant, so each shutdown step is raced against a cap instead of awaiting teardown
4
+ * without limits. The plugin host's own hook timeout (`pluginHooks.sessionEndTimeoutMs`) still
5
+ * applies to `/clear`; exit deliberately uses shorter user-facing caps on the same hooks.
6
+ */
7
+ export const EXIT_PENDING_CAP_MS = 3_000;
8
+ export const EXIT_SESSION_END_CAP_MS = 1_500;
9
+ /** Resolves when `work` settles or after `capMs`, whichever comes first; never rejects. */
10
+ export function bounded(work, capMs) {
11
+ return Promise.race([
12
+ Promise.resolve(work).catch(() => { }),
13
+ new Promise((resolve) => setTimeout(resolve, capMs)),
14
+ ]).then(() => undefined);
15
+ }
@@ -0,0 +1,48 @@
1
+ import { execFile } from "node:child_process";
2
+ /** Hard cap for branch names shown in the header, so a hostile/long ref never breaks layout. */
3
+ export declare const BRANCH_MAX_LENGTH = 24;
4
+ /** How long a resolved branch is reused before git is asked again (avoids spawning per frame). */
5
+ export declare const BRANCH_TTL_MS = 10000;
6
+ /** Re-check cadence; matches the cache TTL so the header follows branch switches within ~10s. */
7
+ export declare const BRANCH_REFRESH_MS = 10000;
8
+ /** Bounded spawn: a slow or hung git must never stall the TUI. */
9
+ export declare const BRANCH_TIMEOUT_MS = 800;
10
+ type Spawn = typeof execFile;
11
+ /**
12
+ * Strips ANSI escapes and control characters, then truncates to `BRANCH_MAX_LENGTH` chars with an
13
+ * ellipsis. Branch names come from the workspace repository, not from user input, but a hostile or
14
+ * corrupted ref must never inject escape sequences or overflow the header line.
15
+ */
16
+ export declare function sanitizeBranchName(name: string): string;
17
+ /** Header segment text: `⎇ main` on unicode terminals, `branch main` otherwise. */
18
+ export declare function branchDisplay(branch: string, unicode: boolean): string;
19
+ export interface ReadBranchOptions {
20
+ timeoutMs?: number;
21
+ /** Injectable for tests; defaults to node:child_process execFile. */
22
+ spawn?: Spawn;
23
+ }
24
+ /**
25
+ * Reads the branch of the git repository at `workspace` (read-only ref query, safe under
26
+ * `--read-only`). Returns the branch name, the short commit SHA on a detached HEAD, or `undefined`
27
+ * when the directory is missing, is not a git repository, git itself is unavailable, or the call
28
+ * times out. All failures are silent: stderr is captured and never printed.
29
+ */
30
+ export declare function readGitBranch(workspace: string, options?: ReadBranchOptions): Promise<string | undefined>;
31
+ export interface BranchCache {
32
+ /** Cached branch for `workspace`, or a fresh read when the entry expired; dedupes in-flight reads. */
33
+ read(workspace: string): Promise<string | undefined>;
34
+ clear(workspace: string): void;
35
+ }
36
+ export interface BranchCacheOptions {
37
+ ttlMs?: number;
38
+ timeoutMs?: number;
39
+ spawn?: Spawn;
40
+ /** Injectable clock for expiry tests. */
41
+ now?: () => number;
42
+ }
43
+ /**
44
+ * TTL cache keyed by workspace so the TUI never spawns git on every frame: one read per workspace
45
+ * per `ttlMs` (hits and misses are both cached), with a single in-flight promise per key.
46
+ */
47
+ export declare function createBranchCache(options?: BranchCacheOptions): BranchCache;
48
+ export {};
@@ -0,0 +1,99 @@
1
+ import { execFile } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ /** Hard cap for branch names shown in the header, so a hostile/long ref never breaks layout. */
4
+ export const BRANCH_MAX_LENGTH = 24;
5
+ /** How long a resolved branch is reused before git is asked again (avoids spawning per frame). */
6
+ export const BRANCH_TTL_MS = 10_000;
7
+ /** Re-check cadence; matches the cache TTL so the header follows branch switches within ~10s. */
8
+ export const BRANCH_REFRESH_MS = BRANCH_TTL_MS;
9
+ /** Bounded spawn: a slow or hung git must never stall the TUI. */
10
+ export const BRANCH_TIMEOUT_MS = 800;
11
+ /**
12
+ * Strips ANSI escapes and control characters, then truncates to `BRANCH_MAX_LENGTH` chars with an
13
+ * ellipsis. Branch names come from the workspace repository, not from user input, but a hostile or
14
+ * corrupted ref must never inject escape sequences or overflow the header line.
15
+ */
16
+ export function sanitizeBranchName(name) {
17
+ const clean = name
18
+ .replace(/\x1b\[[0-9;]*[A-Za-z]/g, "")
19
+ .replace(/[\u0000-\u001f\u007f]/g, "")
20
+ .trim();
21
+ const chars = [...clean];
22
+ return chars.length > BRANCH_MAX_LENGTH
23
+ ? `${chars.slice(0, BRANCH_MAX_LENGTH - 1).join("")}…`
24
+ : clean;
25
+ }
26
+ /** Header segment text: `⎇ main` on unicode terminals, `branch main` otherwise. */
27
+ export function branchDisplay(branch, unicode) {
28
+ return `${unicode ? "⎇" : "branch"} ${branch}`;
29
+ }
30
+ function runGit(spawn, args, cwd, timeoutMs) {
31
+ return new Promise((resolve, reject) => {
32
+ spawn("git", args, { cwd, timeout: timeoutMs, windowsHide: true }, (error, stdout) => {
33
+ if (error)
34
+ reject(error);
35
+ else
36
+ resolve(stdout);
37
+ });
38
+ });
39
+ }
40
+ /**
41
+ * Reads the branch of the git repository at `workspace` (read-only ref query, safe under
42
+ * `--read-only`). Returns the branch name, the short commit SHA on a detached HEAD, or `undefined`
43
+ * when the directory is missing, is not a git repository, git itself is unavailable, or the call
44
+ * times out. All failures are silent: stderr is captured and never printed.
45
+ */
46
+ export async function readGitBranch(workspace, options = {}) {
47
+ const timeoutMs = options.timeoutMs ?? BRANCH_TIMEOUT_MS;
48
+ if (!existsSync(workspace))
49
+ return undefined;
50
+ const spawn = options.spawn ?? execFile;
51
+ try {
52
+ const ref = (await runGit(spawn, ["rev-parse", "--abbrev-ref", "HEAD"], workspace, timeoutMs)).trim();
53
+ if (ref && ref !== "HEAD")
54
+ return sanitizeBranchName(ref);
55
+ if (ref === "HEAD") {
56
+ const sha = (await runGit(spawn, ["rev-parse", "--short", "HEAD"], workspace, timeoutMs)).trim();
57
+ return sha ? sanitizeBranchName(sha) : undefined;
58
+ }
59
+ return undefined;
60
+ }
61
+ catch {
62
+ // Not a repository, git missing, or timed out: the header simply shows no branch segment.
63
+ return undefined;
64
+ }
65
+ }
66
+ /**
67
+ * TTL cache keyed by workspace so the TUI never spawns git on every frame: one read per workspace
68
+ * per `ttlMs` (hits and misses are both cached), with a single in-flight promise per key.
69
+ */
70
+ export function createBranchCache(options = {}) {
71
+ const ttlMs = options.ttlMs ?? BRANCH_TTL_MS;
72
+ const now = options.now ?? Date.now;
73
+ const entries = new Map();
74
+ const inflight = new Map();
75
+ const read = (workspace) => {
76
+ const hit = entries.get(workspace);
77
+ if (hit && hit.expires > now())
78
+ return Promise.resolve(hit.branch);
79
+ const pending = inflight.get(workspace);
80
+ if (pending)
81
+ return pending;
82
+ const promise = readGitBranch(workspace, {
83
+ timeoutMs: options.timeoutMs,
84
+ spawn: options.spawn,
85
+ })
86
+ .then((branch) => {
87
+ entries.set(workspace, { branch, expires: now() + ttlMs });
88
+ inflight.delete(workspace);
89
+ return branch;
90
+ })
91
+ .catch(() => {
92
+ inflight.delete(workspace);
93
+ return undefined;
94
+ });
95
+ inflight.set(workspace, promise);
96
+ return promise;
97
+ };
98
+ return { read, clear: (workspace) => void entries.delete(workspace) };
99
+ }
@@ -11,7 +11,7 @@ export const WEBSEARCH_PROVIDERS = [
11
11
  export const defaultConfig = {
12
12
  compaction: { auto: true, threshold: 0.85, keepTurns: 2, maxOutputTokens: 16_000 },
13
13
  context: { claudeMdFallback: false, maxBytes: 32 * 1024 },
14
- limits: { maxTurns: 20, maxOutputTokens: 4_096, maxContextChars: 160_000, timeoutMs: 300_000 },
14
+ limits: { maxTurns: 100, maxOutputTokens: 16_384, maxContextChars: 800_000, timeoutMs: 300_000 },
15
15
  tui: { paddingX: 1, skillSlashCommands: true },
16
16
  mcp: { allow: false },
17
17
  websearch: { provider: undefined },
@@ -112,7 +112,7 @@ export const SETTINGS_DEFINITIONS = [
112
112
  valueType: "number",
113
113
  values: [5, 10, 15, 20, 30, 50, 100],
114
114
  read: (config) => config.limits.maxTurns,
115
- description: "Maximum agent-loop turns per run before the run ends. Applied from the next run; longer tasks may need a higher budget.",
115
+ description: "Maximum agent-loop turns per run. Reaching the limit ends the run softly: everything so far is kept and the session stays usable, so you can just say continue. Raise it for long read-heavy audits. Applied from the next run.",
116
116
  },
117
117
  {
118
118
  id: "limits.maxOutputTokens",
@@ -128,7 +128,7 @@ export const SETTINGS_DEFINITIONS = [
128
128
  label: "Context char budget",
129
129
  category: "Limits",
130
130
  valueType: "number",
131
- values: [80_000, 120_000, 160_000, 240_000, 320_000],
131
+ values: [80_000, 120_000, 160_000, 240_000, 320_000, 800_000],
132
132
  read: (config) => config.limits.maxContextChars,
133
133
  description: "Hard context limit in characters (instruction files + transcript + tool list) per run; the budget fallback that auto-compaction measures when the model window is unknown. Applied from the next run.",
134
134
  },
package/dist/tui/state.js CHANGED
@@ -529,7 +529,8 @@ export function reduceEvent(state, event) {
529
529
  }
530
530
  case "run_completed":
531
531
  case "run_failed":
532
- case "run_cancelled": {
532
+ case "run_cancelled":
533
+ case "run_turns_exceeded": {
533
534
  const ended = {
534
535
  ...state,
535
536
  streaming: false,
@@ -541,6 +542,11 @@ export function reduceEvent(state, event) {
541
542
  };
542
543
  if (event.type === "run_failed")
543
544
  return addItem(ended, { kind: "error", text: String(d.error ?? "Run failed") });
545
+ if (event.type === "run_turns_exceeded")
546
+ return addItem(ended, {
547
+ kind: "notice",
548
+ text: "Turn limit reached — the answer may be incomplete. Continue with another prompt or raise limits.maxTurns (/settings → Max turns).",
549
+ });
544
550
  if (event.type === "run_cancelled") {
545
551
  const reason = String(d.error ?? "cancelled");
546
552
  return addItem(ended, {
@@ -591,7 +597,7 @@ export function reduceEvent(state, event) {
591
597
  case "response_truncated":
592
598
  return addItem(state, {
593
599
  kind: "notice",
594
- text: "Response cut by max output tokens — the answer may be incomplete. Raise limits.maxOutputTokens to allow longer answers.",
600
+ text: "Response cut by max output tokens — the answer may be incomplete. Raise limits.maxOutputTokens (/settings → Agent max output tokens) to allow longer answers.",
595
601
  });
596
602
  case "model_changed":
597
603
  return {
package/dist/version.d.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  /**
2
- * Reads the package version at runtime so --version, doctor, plugin metadata and MCP client
3
- * metadata stay in sync with the published package without a build-time constant to update.
4
- * The manifest is resolved relative to the module (`src/version.ts` → `../package.json` in the
5
- * source tree; `dist/version.js` → `../package.json` of the installed package). Standalone
6
- * binaries inject the version at build time via ALISIO_PACKAGE_VERSION (scripts/binary-build.ts).
2
+ * Reads the package version at runtime so --version, the TUI header, doctor, plugin metadata and
3
+ * MCP client metadata stay in sync with the published package without a build-time constant to
4
+ * update. The manifest is resolved by walking UP from the module directory until a package.json
5
+ * with a string `version` is found (bounded, any depth: module, then up to 8 parents), so deeply
6
+ * nested call sites like `src/tui/app.ts` and compiled `dist/*` resolve the same manifest as
7
+ * `src/main.ts`. Standalone binaries inject the version at build time via
8
+ * ALISIO_PACKAGE_VERSION (scripts/binary-build.ts).
7
9
  */
8
10
  export declare function loadVersion(fromHere: string): string;
package/dist/version.js CHANGED
@@ -8,19 +8,55 @@ import { fileURLToPath } from "node:url";
8
8
  */
9
9
  const FALLBACK = "dev";
10
10
  /**
11
- * Reads the package version at runtime so --version, doctor, plugin metadata and MCP client
12
- * metadata stay in sync with the published package without a build-time constant to update.
13
- * The manifest is resolved relative to the module (`src/version.ts` → `../package.json` in the
14
- * source tree; `dist/version.js` → `../package.json` of the installed package). Standalone
15
- * binaries inject the version at build time via ALISIO_PACKAGE_VERSION (scripts/binary-build.ts).
11
+ * How many parent directories the loader walks up before giving up. Bounded so a missing
12
+ * manifest can never make the lookup climb an unbounded path.
13
+ */
14
+ const MAX_PARENT_HOPS = 8;
15
+ /** Manifests whose `name` identifies the CLI package; preferred when several are hit. */
16
+ const CLI_MANIFEST_NAMES = new Set(["@alisio/alisio-code", "alisio"]);
17
+ /**
18
+ * Walks up from `dir` looking for a package manifest. Returns the version of the nearest
19
+ * manifest that declares a string `version`, preferring a manifest whose `name` identifies the
20
+ * CLI package (`@alisio/alisio-code`/`alisio`) when several are hit — e.g. running from source
21
+ * inside the monorepo, where the repo-root manifest is also a candidate. `undefined` when no
22
+ * manifest with a version is reached within the bound.
23
+ */
24
+ function findManifestVersion(dir) {
25
+ let nearest;
26
+ let current = dir;
27
+ for (let hop = 0; hop <= MAX_PARENT_HOPS; hop++) {
28
+ try {
29
+ const parsed = JSON.parse(readFileSync(join(current, "package.json"), "utf8"));
30
+ if (typeof parsed.version === "string" && parsed.version) {
31
+ if (typeof parsed.name === "string" && CLI_MANIFEST_NAMES.has(parsed.name))
32
+ return parsed.version;
33
+ nearest ??= parsed.version;
34
+ }
35
+ }
36
+ catch {
37
+ // No manifest here; keep walking up.
38
+ }
39
+ const parent = dirname(current);
40
+ if (parent === current)
41
+ break; // Filesystem root.
42
+ current = parent;
43
+ }
44
+ return nearest;
45
+ }
46
+ /**
47
+ * Reads the package version at runtime so --version, the TUI header, doctor, plugin metadata and
48
+ * MCP client metadata stay in sync with the published package without a build-time constant to
49
+ * update. The manifest is resolved by walking UP from the module directory until a package.json
50
+ * with a string `version` is found (bounded, any depth: module, then up to 8 parents), so deeply
51
+ * nested call sites like `src/tui/app.ts` and compiled `dist/*` resolve the same manifest as
52
+ * `src/main.ts`. Standalone binaries inject the version at build time via
53
+ * ALISIO_PACKAGE_VERSION (scripts/binary-build.ts).
16
54
  */
17
55
  export function loadVersion(fromHere) {
18
56
  if (process.env.ALISIO_PACKAGE_VERSION)
19
57
  return process.env.ALISIO_PACKAGE_VERSION;
20
58
  try {
21
- const manifest = join(dirname(fileURLToPath(fromHere)), "..", "package.json");
22
- const parsed = JSON.parse(readFileSync(manifest, "utf8"));
23
- return typeof parsed.version === "string" && parsed.version ? parsed.version : FALLBACK;
59
+ return findManifestVersion(dirname(fileURLToPath(fromHere))) ?? FALLBACK;
24
60
  }
25
61
  catch {
26
62
  return FALLBACK;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alisio/alisio-code",
3
- "version": "0.1.0-alpha.11",
3
+ "version": "0.1.0-alpha.13",
4
4
  "description": "Alisio: an extensible, provider-agnostic coding-agent harness for your terminal. TUI, OpenAI-compatible providers, permissioned local tools, context compaction, persistent memory and a typed plugin SDK.",
5
5
  "author": "Gustavo Gutiérrez",
6
6
  "license": "MIT",
@@ -45,14 +45,14 @@
45
45
  "alisio": "./dist/main.js"
46
46
  },
47
47
  "dependencies": {
48
- "@alisio/core": "0.1.0-alpha.9",
49
- "@alisio/plugin-deepseek": "0.1.0-alpha.7",
50
- "@alisio/plugin-memory": "0.1.0-alpha.6",
51
- "@alisio/plugin-openai-compatible": "0.1.0-alpha.7",
52
- "@alisio/plugin-opencode": "0.1.0-alpha.6",
53
- "@alisio/plugin-opencode-go": "0.1.0-alpha.6",
54
- "@alisio/plugin-subagents": "0.1.0-alpha.6",
55
- "@alisio/sdk": "0.1.0-alpha.5",
48
+ "@alisio/core": "0.1.0-alpha.11",
49
+ "@alisio/plugin-deepseek": "0.1.0-alpha.9",
50
+ "@alisio/plugin-memory": "0.1.0-alpha.7",
51
+ "@alisio/plugin-openai-compatible": "0.1.0-alpha.9",
52
+ "@alisio/plugin-opencode": "0.1.0-alpha.8",
53
+ "@alisio/plugin-opencode-go": "0.1.0-alpha.8",
54
+ "@alisio/plugin-subagents": "0.1.0-alpha.8",
55
+ "@alisio/sdk": "0.1.0-alpha.7",
56
56
  "@earendil-works/pi-tui": "0.87.1",
57
57
  "commander": "15.0.0"
58
58
  },