@alisio/alisio-code 0.1.0-alpha.8 → 0.1.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.
Files changed (56) hide show
  1. package/README.md +95 -16
  2. package/dist/analysis.d.ts +27 -0
  3. package/dist/analysis.js +145 -0
  4. package/dist/banner.js +2 -1
  5. package/dist/builtin.d.ts +3 -1
  6. package/dist/builtin.js +0 -24
  7. package/dist/main.js +140 -7
  8. package/dist/serve.d.ts +15 -0
  9. package/dist/serve.js +99 -0
  10. package/dist/tui/agent-manager.d.ts +105 -0
  11. package/dist/tui/agent-manager.js +269 -0
  12. package/dist/tui/agents.d.ts +5 -0
  13. package/dist/tui/agents.js +5 -0
  14. package/dist/tui/app.d.ts +42 -1
  15. package/dist/tui/app.js +1895 -97
  16. package/dist/tui/artifact-preview.d.ts +52 -0
  17. package/dist/tui/artifact-preview.js +212 -0
  18. package/dist/tui/artifacts.d.ts +54 -0
  19. package/dist/tui/artifacts.js +207 -0
  20. package/dist/tui/btw.d.ts +57 -0
  21. package/dist/tui/btw.js +76 -0
  22. package/dist/tui/components.d.ts +174 -12
  23. package/dist/tui/components.js +962 -66
  24. package/dist/tui/connect-input.d.ts +2 -0
  25. package/dist/tui/connect-input.js +1 -1
  26. package/dist/tui/exit.d.ts +10 -0
  27. package/dist/tui/exit.js +15 -0
  28. package/dist/tui/git-branch.d.ts +48 -0
  29. package/dist/tui/git-branch.js +99 -0
  30. package/dist/tui/goal.d.ts +75 -0
  31. package/dist/tui/goal.js +171 -0
  32. package/dist/tui/modes.d.ts +71 -0
  33. package/dist/tui/modes.js +90 -0
  34. package/dist/tui/open-path.d.ts +38 -0
  35. package/dist/tui/open-path.js +80 -0
  36. package/dist/tui/plan-review.d.ts +48 -0
  37. package/dist/tui/plan-review.js +76 -0
  38. package/dist/tui/queue.d.ts +2 -0
  39. package/dist/tui/queue.js +5 -1
  40. package/dist/tui/scroll.d.ts +8 -0
  41. package/dist/tui/scroll.js +12 -0
  42. package/dist/tui/settings-menu.d.ts +40 -0
  43. package/dist/tui/settings-menu.js +132 -0
  44. package/dist/tui/settings.d.ts +194 -0
  45. package/dist/tui/settings.js +482 -0
  46. package/dist/tui/state.d.ts +343 -7
  47. package/dist/tui/state.js +823 -54
  48. package/dist/tui/tasks.d.ts +123 -0
  49. package/dist/tui/tasks.js +369 -0
  50. package/dist/tui/theme.d.ts +42 -1
  51. package/dist/tui/theme.js +516 -1
  52. package/dist/tui/viewer-state.d.ts +7 -0
  53. package/dist/tui/viewer-state.js +36 -0
  54. package/dist/version.d.ts +7 -5
  55. package/dist/version.js +44 -8
  56. package/package.json +7 -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
- 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`, `/mcps`, `/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>.
@@ -0,0 +1,27 @@
1
+ import type { AnalysisStatus } from "@alisio/sdk";
2
+ type Common = {
3
+ db?: string;
4
+ python?: string;
5
+ cwd?: string;
6
+ config?: string;
7
+ trustProject?: boolean;
8
+ };
9
+ /** The unified runtime state and, when Python is missing, the install guidance for this system. */
10
+ export declare function analysisState(o: Common): Promise<{
11
+ status: AnalysisStatus;
12
+ guidance?: string;
13
+ ignored: string[];
14
+ }>;
15
+ /** Prints `alisio analysis status`. */
16
+ export declare function printAnalysisStatus(o: Common): Promise<void>;
17
+ /** `alisio analysis setup --extras analysis|science`: needs network; offline it fails cleanly. */
18
+ export declare function setupExtras(o: Common, extras: string | undefined): Promise<void>;
19
+ /**
20
+ * `alisio analysis setup --oci [--image <name>]`: pulls the image once and verifies it. With a
21
+ * digest-pinned reference the pull verifies the digest; with a tag it prints the digest to put in
22
+ * `analysis.oci.image` (Alisio never writes it: only you choose what runs your code).
23
+ */
24
+ export declare function setupOci(o: Common, image: string | undefined): Promise<void>;
25
+ /** `alisio analysis sweep [--force]`: the retention sweep now (it also runs by itself, daily). */
26
+ export declare function sweep(o: Common, force: boolean): Promise<void>;
27
+ export {};
@@ -0,0 +1,145 @@
1
+ /**
2
+ * `alisio analysis …` and the analysis line of `alisio doctor`: the state of the Python runtime,
3
+ * the optional extras (`setup --extras`, hash-locked, needs network), the optional container
4
+ * runtime (`setup --oci`) and the retention sweep (`sweep`). None of these is needed to run
5
+ * `python_run`: the base runtime is the discovered Python with only the standard library.
6
+ */
7
+ import { dirname, join } from "node:path";
8
+ async function context(o) {
9
+ const core = await import("@alisio/core");
10
+ const workspace = await core.findWorkspace(o.cwd ?? process.cwd());
11
+ const { config, provenance } = await core.loadConfigWithProvenance(workspace, {
12
+ file: o.config,
13
+ trustProject: o.trustProject,
14
+ });
15
+ const stateDir = o.db && o.db !== ":memory:" ? dirname(o.db) : core.stateHome();
16
+ const manager = new core.AnalysisRuntimeManager({
17
+ stateDir,
18
+ ...(o.python ? { python: o.python } : {}),
19
+ });
20
+ const oci = () => new core.OciRuntime({ ...config.analysis.oci });
21
+ return { core, config, provenance, stateDir, manager, oci };
22
+ }
23
+ /** The unified runtime state and, when Python is missing, the install guidance for this system. */
24
+ export async function analysisState(o) {
25
+ const { core, config, provenance, stateDir, manager, oci } = await context(o);
26
+ const janitor = {
27
+ lastSweep: () => import("node:fs/promises")
28
+ .then((fs) => fs.readFile(join(stateDir, "analysis", ".last-sweep"), "utf8"))
29
+ .then((text) => {
30
+ const value = Number(text.trim());
31
+ return Number.isFinite(value) ? value : undefined;
32
+ })
33
+ .catch(() => undefined),
34
+ };
35
+ const status = await core.analysisStatus({
36
+ config: config.analysis,
37
+ runtime: manager,
38
+ oci,
39
+ janitor,
40
+ enabled: config.analysis.enabled,
41
+ readOnly: false,
42
+ });
43
+ const guidance = status.python.found
44
+ ? undefined
45
+ : (async () => {
46
+ const resolved = await manager.interpreter();
47
+ return resolved.ok
48
+ ? undefined
49
+ : core.installHintsText(core.pythonInstallHints(await core.detectHintHost(resolved.found)));
50
+ })();
51
+ return {
52
+ status,
53
+ ...(guidance ? { guidance: await guidance } : {}),
54
+ ignored: provenance.ignored ?? [],
55
+ };
56
+ }
57
+ /** Prints `alisio analysis status`. */
58
+ export async function printAnalysisStatus(o) {
59
+ const { status, guidance, ignored } = await analysisState(o);
60
+ console.log(JSON.stringify(status, null, 2));
61
+ if (guidance)
62
+ process.stderr.write(`\n${guidance}\n`);
63
+ if (status.mode === "oci" && status.oci.available === false)
64
+ process.stderr.write(`\nThe container runtime is configured but not usable: ${status.oci.reason ?? "unknown reason"}\n`);
65
+ for (const key of ignored)
66
+ process.stderr.write(`\nIgnored ${key} from the project configuration: only your user configuration decides it.\n`);
67
+ }
68
+ /** `alisio analysis setup --extras analysis|science`: needs network; offline it fails cleanly. */
69
+ export async function setupExtras(o, extras) {
70
+ if (extras !== "analysis" && extras !== "science")
71
+ throw new Error("Choose the extras to install: --extras analysis or --extras science");
72
+ const { core, manager } = await context(o);
73
+ const preview = core.installPreview(extras);
74
+ const controller = new AbortController();
75
+ process.on("SIGINT", () => controller.abort(new Error("Interrupted")));
76
+ process.stderr.write(`Installing the ${extras} extras: ${preview.packages.join(", ")} (+ dependencies, ${preview.packageCount} packages, about ${Math.round(preview.estimatedBytes / (1024 * 1024))} MB). ` +
77
+ "Wheels only, hashes required, needs network.\n");
78
+ try {
79
+ const state = await manager.installExtras(extras, {
80
+ signal: controller.signal,
81
+ onOutput: (text) => process.stderr.write(text),
82
+ });
83
+ console.log(JSON.stringify(state, null, 2));
84
+ }
85
+ catch (error) {
86
+ // A clean message and a failing exit code; the base runtime is untouched.
87
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
88
+ process.exitCode = 1;
89
+ }
90
+ }
91
+ /**
92
+ * `alisio analysis setup --oci [--image <name>]`: pulls the image once and verifies it. With a
93
+ * digest-pinned reference the pull verifies the digest; with a tag it prints the digest to put in
94
+ * `analysis.oci.image` (Alisio never writes it: only you choose what runs your code).
95
+ */
96
+ export async function setupOci(o, image) {
97
+ const { core, config } = await context(o);
98
+ const reference = image ?? config.analysis.oci.image;
99
+ if (!reference)
100
+ throw new Error("Pass --image <name[:tag]> (or set analysis.oci.image in your user configuration) to pull.");
101
+ const runtime = new core.OciRuntime({ ...config.analysis.oci, image: undefined });
102
+ const status = await runtime.status();
103
+ if (!status.ok)
104
+ throw new Error(`${status.reason}${status.alternative ? `; ${status.alternative} is installed (set analysis.oci.engine to "${status.alternative}")` : ""}. The container runtime is optional; python_run works without it.`);
105
+ process.stderr.write(`Pulling ${reference} with ${status.engine} ${status.version}…\n`);
106
+ const controller = new AbortController();
107
+ process.on("SIGINT", () => controller.abort(new Error("Interrupted")));
108
+ const { digest } = await runtime.pull(reference, controller.signal);
109
+ const pinned = core.OCI_IMAGE_PATTERN.test(reference);
110
+ if (pinned) {
111
+ const wanted = reference.slice(reference.indexOf("@") + 1);
112
+ if (digest && !digest.endsWith(wanted))
113
+ throw new Error(`The pulled image reports ${digest}, not the pinned ${wanted}`);
114
+ console.log(`Verified ${reference}. Set analysis.runtime to "oci" to use it.`);
115
+ }
116
+ else if (digest)
117
+ console.log(`Pulled ${reference}. To use it, put this in your user configuration (${core.configHome()}):\n` +
118
+ ` "analysis": { "runtime": "oci", "oci": { "engine": "${status.engine}", "image": "${digest}" } }`);
119
+ else
120
+ throw new Error(`${reference} has no repository digest (a locally built image?). Push it to a registry or pull a published image.`);
121
+ }
122
+ /** `alisio analysis sweep [--force]`: the retention sweep now (it also runs by itself, daily). */
123
+ export async function sweep(o, force) {
124
+ const { core, config, stateDir } = await context(o);
125
+ const store = new core.SQLiteStore(o.db ?? join(stateDir, "sessions.sqlite"));
126
+ try {
127
+ const artifacts = new core.ArtifactStore({ root: stateDir, db: store.db });
128
+ const datasets = new core.DatasetService({ root: stateDir, db: store.db });
129
+ const blobs = new core.BlobStore({ root: join(stateDir, "blobs"), db: store.db });
130
+ const janitor = new core.AnalysisJanitor({
131
+ root: stateDir,
132
+ db: store.db,
133
+ artifacts,
134
+ datasets,
135
+ blobs,
136
+ retention: () => config.analysis.retention,
137
+ });
138
+ const report = await janitor.sweep({ force });
139
+ console.log(JSON.stringify(report, null, 2));
140
+ datasets.close();
141
+ }
142
+ finally {
143
+ store.close();
144
+ }
145
+ }
package/dist/banner.js CHANGED
@@ -21,7 +21,8 @@ export function terminalCapabilities(input) {
21
21
  const locale = env.LC_ALL || env.LC_CTYPE || env.LANG || "";
22
22
  return {
23
23
  color: input.tty && !dumb && env.NO_COLOR === undefined,
24
- unicode: !dumb && !/^(C|POSIX)(\.|$)/i.test(locale),
24
+ // `C.UTF-8` is a UTF-8 locale (the default on CI runners); only bare C/POSIX is ASCII.
25
+ unicode: !dumb && (/utf-?8/i.test(locale) || !/^(C|POSIX)(\.|$)/i.test(locale)),
25
26
  columns: input.columns > 0 ? input.columns : 80,
26
27
  interactive: input.tty,
27
28
  };
package/dist/builtin.d.ts CHANGED
@@ -2,7 +2,9 @@
2
2
  * Built-in plugin registry wired by the CLI. Built-ins load through the trusted path of the
3
3
  * plugin host (unprefixed names, `internal` effect) and can be disabled by configuration
4
4
  * (`builtinPlugins.<id>.enabled: false`) or `--disable-plugin <id>`. Add a built-in by
5
- * appending an entry here.
5
+ * appending an entry here. Dedicated model providers (DeepSeek, OpenCode Console, OpenCode Go)
6
+ * are no longer built-ins: users install them independently via
7
+ * `alisio install npm:@alisio/plugin-{deepseek,opencode,opencode-go}`.
6
8
  */
7
9
  import type { BuiltinPlugin } from "@alisio/core";
8
10
  export declare const BUILTIN_PLUGINS: BuiltinPlugin[];
package/dist/builtin.js CHANGED
@@ -1,31 +1,7 @@
1
- import { createDeepSeekPlugin } from "@alisio/plugin-deepseek";
2
1
  import { createMemoryPlugin } from "@alisio/plugin-memory";
3
2
  import { createOpenAICompatiblePlugin } from "@alisio/plugin-openai-compatible";
4
- import { createOpenCodePlugin } from "@alisio/plugin-opencode";
5
- import { createOpenCodeGoPlugin } from "@alisio/plugin-opencode-go";
6
3
  import { createSubagentsPlugin } from "@alisio/plugin-subagents";
7
4
  export const BUILTIN_PLUGINS = [
8
- {
9
- id: "deepseek",
10
- name: "DeepSeek",
11
- description: "Dedicated DeepSeek Chat Completions and Responses provider",
12
- categories: ["model-provider"],
13
- create: createDeepSeekPlugin,
14
- },
15
- {
16
- id: "opencode",
17
- name: "OpenCode Console (Zen)",
18
- description: "Dedicated OpenCode Console (Zen) multi-protocol provider",
19
- categories: ["model-provider"],
20
- create: createOpenCodePlugin,
21
- },
22
- {
23
- id: "opencode-go",
24
- name: "OpenCode Go",
25
- description: "Dedicated OpenCode Go multi-protocol provider",
26
- categories: ["model-provider"],
27
- create: createOpenCodeGoPlugin,
28
- },
29
5
  {
30
6
  id: "openai-compatible",
31
7
  name: "OpenAI compatible",
package/dist/main.js CHANGED
@@ -35,9 +35,12 @@ program
35
35
  .option("--api-mode <mode>", "chat or responses")
36
36
  .option("--allow-write", "Allow file writes")
37
37
  .option("--allow-process", "Allow arbitrary subprocesses; not sandboxed")
38
+ .option("--allow-analysis", "Allow Python analysis (python_run) without asking; not sandboxed, never allows shell")
39
+ .option("--python <path>", "Python 3.10+ interpreter for python_run (default: discovered)")
38
40
  .option("--allow-external", "Allow network tools: webfetch, websearch and provider-native search")
39
41
  .option("--allow-mcp", "Allow configured MCP servers and remote tool calls")
40
42
  .option("--allow-agents", "Allow messaging neighboring agents through Herdr")
43
+ .option("--add-dir <paths...>", "Additional directories tools may read/write outside the workspace (repeatable)")
41
44
  .option("--no-herdr", "Disable automatic Herdr lifecycle reports")
42
45
  .option("--read-only", "Disable writes, arbitrary processes, network tools, executable plugins and MCP")
43
46
  .option("--db <path>", "Session database")
@@ -54,6 +57,7 @@ const options = (cmd) => {
54
57
  baseURL: o.baseUrl,
55
58
  noHerdr: o.herdr === false,
56
59
  disablePlugins: o.disablePlugin,
60
+ addDirs: o.addDir,
57
61
  ...(o.agents
58
62
  ? { pluginOptions: { subagents: { agents: parseAgents(String(o.agents)) } } }
59
63
  : {}),
@@ -114,7 +118,7 @@ async function run(cmd, prompt, sessionId) {
114
118
  const trusted = await withProjectTrust(opts);
115
119
  return runTui({ ...trusted, ...(sessionId ? { session: sessionId } : {}) });
116
120
  }
117
- const { createApplication } = await import("@alisio/core");
121
+ const { createApplication, formatBytes, KIND_LABELS } = await import("@alisio/core");
118
122
  const app = await createApplication({
119
123
  ...(await cliDefaults()),
120
124
  ...opts,
@@ -123,12 +127,40 @@ async function run(cmd, prompt, sessionId) {
123
127
  process.stdout.write(`${JSON.stringify(event)}\n`);
124
128
  else if (event.type === "text_delta")
125
129
  process.stdout.write(String(event.data.delta));
130
+ else if (event.type === "request_retry") {
131
+ const retry = event.data;
132
+ process.stderr.write(`\nThe model did not respond; retrying (${retry.attempt}/${retry.of})…\n`);
133
+ }
134
+ else if (event.type === "truncation_recovery") {
135
+ const recovery = event.data;
136
+ process.stderr.write(`\nThe response was cut off by the output limit; retrying in smaller steps (${recovery.attempt}/${recovery.of})…\n`);
137
+ }
126
138
  else if (event.type === "tool_started")
127
139
  process.stderr.write(`\n→ ${event.data.name}\n`);
140
+ else if (event.type === "artifact_published")
141
+ process.stderr.write(artifactLine(event));
128
142
  },
129
143
  });
144
+ /** `artifact: Dashboard site.zip (48 KB) → /abs/path` (nothing is opened automatically). */
145
+ function artifactLine(event) {
146
+ const data = event.data;
147
+ return `artifact: ${KIND_LABELS[data.artifact.kind] ?? "File"} ${data.artifact.fileName} (${formatBytes(data.artifact.bytes)}) → ${data.path}\n`;
148
+ }
130
149
  for (const failure of app.mcpStartupFailures())
131
150
  process.stderr.write(`[startup] ${failure}\n`);
151
+ // The ACTIVE agent (built-in `build`/`plan` or a main-capable definition) drives the main
152
+ // session here too: its system prompt is appended and a read-only agent narrows the run. The
153
+ // TUI adds the picker and the model switch; effort is a TUI feature (validated against the
154
+ // active model's catalog).
155
+ const { agentCatalogFromState, agentRunOptions, resolveActiveAgent } = await import("./tui/agents.js");
156
+ let activeRunOptions = {};
157
+ try {
158
+ const reported = app.plugins.pluginState("subagents", "mainAgents");
159
+ activeRunOptions = agentRunOptions(resolveActiveAgent(agentCatalogFromState(reported), app.config.agents.active));
160
+ }
161
+ catch {
162
+ /* agent resolution is best-effort */
163
+ }
132
164
  const controller = new AbortController();
133
165
  const interrupt = () => controller.abort(new Error("Interrupted"));
134
166
  process.on("SIGINT", interrupt);
@@ -151,7 +183,10 @@ async function run(cmd, prompt, sessionId) {
151
183
  const match = /^\/skill:([a-z0-9-]+)\s*([\s\S]*)$/.exec(prompt);
152
184
  if (match?.[1])
153
185
  prompt = `${await app.skills.load(match[1])}\n\nUser request: ${match[2] ?? ""}`;
154
- await app.runner.run(session, template?.text ?? prompt, controller.signal, template ? { display: template.display } : {});
186
+ await app.runner.run(session, template?.text ?? prompt, controller.signal, {
187
+ ...activeRunOptions,
188
+ ...(template ? { display: template.display } : {}),
189
+ });
155
190
  if (!opts.json)
156
191
  process.stdout.write(`\nSession: ${session}\n`);
157
192
  return;
@@ -182,7 +217,7 @@ async function run(cmd, prompt, sessionId) {
182
217
  process.stderr.write(`[startup] ${JSON.stringify(d)}\n`);
183
218
  }
184
219
  if (!opts.quiet)
185
- console.log("Alisio · /exit /new /skill:name /command plugin.id:name args");
220
+ console.log("Alisio · /exit /new /btw question /skill:name /command plugin.id:name args");
186
221
  process.stdout.write("\nalisio › ");
187
222
  try {
188
223
  for await (const rawLine of rl) {
@@ -199,6 +234,22 @@ async function run(cmd, prompt, sessionId) {
199
234
  process.stdout.write("\nalisio › ");
200
235
  continue;
201
236
  }
237
+ if (line === "/btw" || line.startsWith("/btw ")) {
238
+ // Side question: printed here only, never added to the conversation.
239
+ try {
240
+ const { CommandCatalog } = await import("@alisio/core");
241
+ const result = await new CommandCatalog(app).execute("btw", line.slice(4), {
242
+ sessionId: session,
243
+ signal: controller.signal,
244
+ });
245
+ console.log(result.text ?? "");
246
+ }
247
+ catch (e) {
248
+ console.error(e instanceof Error ? e.message : String(e));
249
+ }
250
+ process.stdout.write("\nalisio › ");
251
+ continue;
252
+ }
202
253
  if (line.startsWith("/command ")) {
203
254
  const [name, ...args] = line.slice(9).split(" ");
204
255
  const handler = app.plugins.commands.get(name ?? "");
@@ -211,7 +262,10 @@ async function run(cmd, prompt, sessionId) {
211
262
  const input = match?.[1] ? `${await app.skills.load(match[1])}\n\n${match[2] ?? ""}` : line;
212
263
  try {
213
264
  const template = app.expandPrompt(input);
214
- await app.runner.run(session, template?.text ?? input, controller.signal, template ? { display: template.display } : {});
265
+ await app.runner.run(session, template?.text ?? input, controller.signal, {
266
+ ...activeRunOptions,
267
+ ...(template ? { display: template.display } : {}),
268
+ });
215
269
  }
216
270
  catch (e) {
217
271
  console.error(String(e));
@@ -225,7 +279,11 @@ async function run(cmd, prompt, sessionId) {
225
279
  }
226
280
  finally {
227
281
  process.off("SIGINT", interrupt);
282
+ // Background tasks end with Alisio: `run` waits for none of them, it stops them and says so.
283
+ const live = app.tasks.liveCount();
228
284
  await app.close();
285
+ if (live)
286
+ process.stderr.write(`${live} background task${live === 1 ? " was" : "s were"} still running and ${live === 1 ? "was" : "were"} stopped: background tasks end when Alisio exits.\n`);
229
287
  }
230
288
  }
231
289
  program
@@ -239,6 +297,19 @@ program
239
297
  .argument("[prompt]")
240
298
  .action((id, prompt, _opts, cmd) => run(cmd, prompt, id));
241
299
  program.action((_opts, cmd) => run(cmd));
300
+ program
301
+ .command("serve")
302
+ .description("Start the local web UI server (HTTP + SSE)")
303
+ .option("--port <port>", "Port to listen on (0 picks a free one)", "4317")
304
+ .option("--host <address>", "Address to bind; non-loopback requires --allow-remote", "127.0.0.1")
305
+ .option("--allow-remote", "Allow a non-loopback --host (no TLS; prefer an SSH tunnel)")
306
+ .option("--no-open", "Do not open the browser")
307
+ .option("--max-workspaces <n>", "Maximum workspaces open at once", "4")
308
+ .option("--max-runs <n>", "Maximum concurrent runs across all sessions", "4")
309
+ .action(async (flags, cmd) => {
310
+ const { serve } = await import("./serve.js");
311
+ await serve(flags, { ...(await cliDefaults()), ...options(cmd) }, VERSION);
312
+ });
242
313
  program
243
314
  .command("setup")
244
315
  .description("Write an example configuration without secrets (for AGENTS.md use /init)")
@@ -255,7 +326,7 @@ program
255
326
  console.log('To generate AGENTS.md for this project, run /init inside alisio (or: alisio run "/init" --allow-write).');
256
327
  });
257
328
  program.command("doctor").action(async (_opts, cmd) => {
258
- const { findWorkspace, loadConfigWithProvenance, overridesSavedProviderProfile, ProviderSettingsStore, RIPGREP_INSTALL_HINT, which, } = await import("@alisio/core");
329
+ const { DEFAULT_MAX_OUTPUT_TOKENS, findWorkspace, loadConfigWithProvenance, MAX_AUTO_OUTPUT_TOKENS, overridesSavedProviderProfile, ProviderSettingsStore, RIPGREP_INSTALL_HINT, which, } = await import("@alisio/core");
259
330
  const o = options(cmd);
260
331
  const workspace = await findWorkspace(o.cwd ?? process.cwd());
261
332
  const { config, provenance } = await loadConfigWithProvenance(workspace, {
@@ -278,6 +349,14 @@ program.command("doctor").action(async (_opts, cmd) => {
278
349
  workspace,
279
350
  git: (await which("git")) ?? null,
280
351
  ripgrep: (await which("rg")) ?? null,
352
+ // The per-request output budget is resolved when a run starts, from the model's catalog;
353
+ // doctor does not contact the provider, so it reports what the configuration decides.
354
+ outputTokens: config.limits.maxOutputTokens !== undefined
355
+ ? { limit: config.limits.maxOutputTokens, source: "set by you (limits.maxOutputTokens)" }
356
+ : {
357
+ limit: null,
358
+ source: `the model catalog's declared limit (capped at ${MAX_AUTO_OUTPUT_TOKENS}), else ${DEFAULT_MAX_OUTPUT_TOKENS}`,
359
+ },
281
360
  provider: {
282
361
  id: useSaved ? saved.profile.provider : "openai-compatible",
283
362
  baseURL: useSaved ? saved.profile.values.baseURL : config.provider.baseURL,
@@ -289,17 +368,71 @@ program.command("doctor").action(async (_opts, cmd) => {
289
368
  : model,
290
369
  auth: useSaved ? saved.profile.values.auth : config.provider.auth,
291
370
  keyConfigured: useSaved
292
- ? !!saved.credentials.apiKey || saved.profile.values.auth === "none"
371
+ ? !!saved.credentials.apiKey ||
372
+ saved.profile.values.auth === "none" ||
373
+ !!process.env[typeof saved.profile.values.apiKeyEnv === "string" && saved.profile.values.apiKeyEnv
374
+ ? saved.profile.values.apiKeyEnv
375
+ : saved.profile.provider === "deepseek"
376
+ ? "DEEPSEEK_API_KEY"
377
+ : "OPENAI_API_KEY"]
293
378
  : config.provider.auth === "none" || !!process.env[config.provider.apiKeyEnv],
294
379
  },
295
380
  };
296
- console.log(JSON.stringify(status, null, 2));
381
+ const { analysisState } = await import("./analysis.js");
382
+ const analysisInfo = await analysisState(o);
383
+ const { hints: _hints, ...pythonInfo } = analysisInfo.status.python;
384
+ console.log(JSON.stringify({
385
+ ...status,
386
+ python: { mode: analysisInfo.status.mode, ...pythonInfo },
387
+ analysis: {
388
+ mode: analysisInfo.status.mode,
389
+ oci: analysisInfo.status.oci,
390
+ limits: analysisInfo.status.limits,
391
+ retention: analysisInfo.status.retention,
392
+ ...(analysisInfo.ignored.length ? { ignored: analysisInfo.ignored } : {}),
393
+ },
394
+ }, null, 2));
395
+ if (analysisInfo.guidance)
396
+ process.stderr.write(`\n${analysisInfo.guidance}\n`);
397
+ for (const key of provenance.ignored ?? [])
398
+ process.stderr.write(`\nIgnored ${key} from the project configuration: only your user configuration decides it.\n`);
297
399
  if (!status.ripgrep)
298
400
  process.stderr.write(`\nWarning: ripgrep (rg) is not installed; search_text and list_files will not work. ${RIPGREP_INSTALL_HINT}\n`);
299
401
  if (!model || model === "YOUR_MODEL_ID")
300
402
  console.error("\nNo model configured yet: set provider.model in your config, --model, or ALISIO_MODEL " +
301
403
  "before starting a real conversation (it will otherwise fail on the first turn).");
302
404
  });
405
+ const analysis = program
406
+ .command("analysis")
407
+ .description("Python analysis runtime (python_run): status, optional extras and containers");
408
+ analysis
409
+ .command("status")
410
+ .description("Show the runtime: Python, extras, container engine, limits and retention")
411
+ .action(async (_opts, cmd) => {
412
+ const { printAnalysisStatus } = await import("./analysis.js");
413
+ await printAnalysisStatus(options(cmd));
414
+ });
415
+ analysis
416
+ .command("setup")
417
+ .description("Optional: install hash-locked Python extras (--extras) or pull the container image (--oci). " +
418
+ "python_run never needs this for the standard library.")
419
+ .option("--extras <set>", "analysis (pandas, numpy, matplotlib…) or science (adds scipy…)")
420
+ .option("--oci", "Pull the container image once and verify its digest (Docker or Podman)")
421
+ .option("--image <name>", "With --oci: the image to pull (default: analysis.oci.image)")
422
+ .action(async (flags, cmd) => {
423
+ const { setupExtras, setupOci } = await import("./analysis.js");
424
+ if (flags.oci)
425
+ return setupOci(options(cmd), flags.image);
426
+ return setupExtras(options(cmd), flags.extras);
427
+ });
428
+ analysis
429
+ .command("sweep")
430
+ .description("Run the retention sweep now (it also runs by itself at most once a day)")
431
+ .option("--force", "Ignore the once-a-day mark")
432
+ .action(async (flags, cmd) => {
433
+ const { sweep } = await import("./analysis.js");
434
+ await sweep(options(cmd), !!flags.force);
435
+ });
303
436
  const trust = program.command("trust").description("Inspect or revoke per-directory project trust");
304
437
  trust.command("list").action(async () => {
305
438
  const { listTrust } = await import("@alisio/core");
@@ -0,0 +1,15 @@
1
+ import type { AppOptions } from "@alisio/core";
2
+ export interface ServeFlags {
3
+ port: string;
4
+ host: string;
5
+ allowRemote?: boolean;
6
+ open: boolean;
7
+ maxWorkspaces: string;
8
+ maxRuns: string;
9
+ }
10
+ /** The operating system's URL opener for `platform`, as `[command, ...args]`. */
11
+ export declare function browserCommand(platform: NodeJS.Platform, url: string): string[];
12
+ /** Opens `url` in the default browser; failures are reported, never fatal. */
13
+ export declare function openBrowser(url: string): void;
14
+ /** Starts the server, prints the launch URL and stops cleanly on SIGINT/SIGTERM. */
15
+ export declare function serve(flags: ServeFlags, app: AppOptions & Record<string, unknown>, version: string): Promise<void>;