@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 +95 -16
- package/dist/tui/app.js +33 -4
- package/dist/tui/components.d.ts +2 -0
- package/dist/tui/components.js +16 -1
- package/dist/tui/exit.d.ts +10 -0
- package/dist/tui/exit.js +15 -0
- package/dist/tui/git-branch.d.ts +48 -0
- package/dist/tui/git-branch.js +99 -0
- package/dist/tui/settings.js +3 -3
- package/dist/tui/state.js +8 -2
- package/dist/version.d.ts +7 -5
- package/dist/version.js +44 -8
- package/package.json +9 -9
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
10
|
-
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
99
|
+
## License
|
|
22
100
|
|
|
23
|
-
|
|
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 ([
|
|
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
|
|
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
|
-
|
|
503
|
-
|
|
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);
|
package/dist/tui/components.d.ts
CHANGED
|
@@ -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;
|
package/dist/tui/components.js
CHANGED
|
@@ -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: "◆
|
|
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>;
|
package/dist/tui/exit.js
ADDED
|
@@ -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
|
+
}
|
package/dist/tui/settings.js
CHANGED
|
@@ -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:
|
|
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
|
|
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
|
|
3
|
-
* metadata stay in sync with the published package without a build-time constant to
|
|
4
|
-
* The manifest is resolved
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
*
|
|
12
|
-
*
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
49
|
-
"@alisio/plugin-deepseek": "0.1.0-alpha.
|
|
50
|
-
"@alisio/plugin-memory": "0.1.0-alpha.
|
|
51
|
-
"@alisio/plugin-openai-compatible": "0.1.0-alpha.
|
|
52
|
-
"@alisio/plugin-opencode": "0.1.0-alpha.
|
|
53
|
-
"@alisio/plugin-opencode-go": "0.1.0-alpha.
|
|
54
|
-
"@alisio/plugin-subagents": "0.1.0-alpha.
|
|
55
|
-
"@alisio/sdk": "0.1.0-alpha.
|
|
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
|
},
|