@alisio/alisio-code 0.1.0-alpha.4 → 0.1.0-alpha.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js CHANGED
@@ -389,11 +389,12 @@ for (const name of ["list", "validate"]) {
389
389
  }
390
390
  const plugins = program.command("plugins");
391
391
  plugins.command("list").action(async (_opts, cmd) => {
392
- const { discoverPlugins } = await import("@alisio/core");
392
+ const { discoverPlugins, installedNpmPlugins } = await import("@alisio/core");
393
393
  const { configHome } = await import("@alisio/core");
394
394
  const { join, resolve } = await import("node:path");
395
+ const global = join(configHome(), "plugins");
395
396
  console.log(JSON.stringify({
396
- global: await discoverPlugins(join(configHome(), "plugins")),
397
+ global: [...(await discoverPlugins(global)), ...(await installedNpmPlugins(global))],
397
398
  project: await discoverPlugins(join(resolve(options(cmd).cwd ?? process.cwd()), ".alisio", "plugins")),
398
399
  explicit: options(cmd).plugin ?? [],
399
400
  }, null, 2));
@@ -468,6 +469,26 @@ mcp
468
469
  await app.close();
469
470
  }
470
471
  });
472
+ program
473
+ .command("install")
474
+ .description("Install an npm plugin package into the global plugins directory (~/.config/alisio/plugins)")
475
+ .argument("<spec>", 'npm package spec, e.g. "npm:plugin-openrouter" or "plugin-openrouter@1.2.3"')
476
+ .option("-y, --yes", "Skip the pre-install confirmation (npm may run lifecycle scripts)")
477
+ .option("--trust-plugin", "Explicit trust for this global install (same as --yes)")
478
+ .option("--update", "Refresh an already-installed plugin to the latest version, keeping its name")
479
+ .action(async (spec, _options, cmd) => {
480
+ const { cliInstall, configHome } = await import("@alisio/core");
481
+ const o = options(cmd);
482
+ await cliInstall({
483
+ spec,
484
+ configHome: configHome(),
485
+ yes: !!o.yes || !!o.trustPlugin,
486
+ update: !!o.update,
487
+ readOnly: !!o.readOnly,
488
+ json: !!o.json,
489
+ interactive: !!process.stdin.isTTY && !!process.stdout.isTTY && !o.json,
490
+ });
491
+ });
471
492
  try {
472
493
  await program.parseAsync();
473
494
  }
package/dist/tui/app.js CHANGED
@@ -183,8 +183,10 @@ export async function runTui(options) {
183
183
  path.unshift(cur.label);
184
184
  const siblings = nodes.filter((n) => n.parentId === node.parentId);
185
185
  const child = viewedState();
186
- const window = app.contextWindow(child?.model ?? view.model);
187
- const pct = child?.context && window ? ` · ctx ${Math.round((child.context.used / window) * 100)}%` : "";
186
+ const budget = app.contextBudget(child?.model ?? view.model);
187
+ const pct = child?.context && budget
188
+ ? ` · ctx ${Math.round((child.context.used / budget.total) * 100)}%`
189
+ : "";
188
190
  const tokens = child
189
191
  ? ` · ↑${formatTokens(child.stats.input)} ↓${formatTokens(child.stats.output)}`
190
192
  : "";
@@ -195,7 +197,7 @@ export async function runTui(options) {
195
197
  : panelState.focus === "view"
196
198
  ? viewHint()
197
199
  : undefined;
198
- const footer = new Footer(() => viewedState() ?? view, () => app.contextWindow((viewedState() ?? view).model), () => panelHint() ?? hint, () => [...app.plugins.status.values()].map((s) => s.text));
200
+ const footer = new Footer(() => viewedState() ?? view, () => app.contextBudget((viewedState() ?? view).model), () => panelHint() ?? hint, () => [...app.plugins.status.values()].map((s) => s.text));
199
201
  const treePanel = new TreePanel(() => {
200
202
  const entry = panelEntry();
201
203
  if (!entry)
@@ -866,7 +868,7 @@ export async function runTui(options) {
866
868
  };
867
869
  const statsReport = () => {
868
870
  const s = view.stats;
869
- const window = app.contextWindow(view.model);
871
+ const budget = app.contextBudget(view.model);
870
872
  const tools = Object.entries(s.tools)
871
873
  .map(([name, t]) => `| \`${name}\` | ${t.calls} | ${t.errors} |`)
872
874
  .join("\n");
@@ -878,7 +880,7 @@ export async function runTui(options) {
878
880
  `- Tokens: in ${formatTokens(s.input)} · out ${formatTokens(s.output)} · cached ${formatTokens(s.cached)}`,
879
881
  `- Runs: ${s.runs} · turns: ${s.turns}${s.lastRunMs !== undefined ? ` · last run ${formatDuration(s.lastRunMs)}` : ""}`,
880
882
  `- Duration: ${formatDuration(Date.now() - s.startedAt)}`,
881
- `- Context: ${formatContext(view.context?.used ?? 0, window, view.context?.estimated ?? true)}`,
883
+ `- Context: ${formatContext(view.context?.used ?? 0, budget?.total, view.context?.estimated ?? true, budget?.basis)}`,
882
884
  "",
883
885
  tools ? `| Tool | Calls | Errors |\n| --- | --- | --- |\n${tools}` : "No tool calls yet.",
884
886
  "",
@@ -2,7 +2,7 @@ import type { PanelNode } from "@alisio/sdk";
2
2
  import { type Component, Container } from "@earendil-works/pi-tui";
3
3
  import { type PendingAttachment } from "./attachments.ts";
4
4
  import { type QuestionPanelState, type QuestionSpec } from "./questions.ts";
5
- import { type TranscriptItem, type ViewState } from "./state.ts";
5
+ import { type ContextBudget, type TranscriptItem, type ViewState } from "./state.ts";
6
6
  export declare const SPINNER: string[];
7
7
  /** Shared animation clock advanced by the app while work is running. */
8
8
  export declare const clock: {
@@ -31,10 +31,10 @@ export declare class Header implements Component {
31
31
  }
32
32
  export declare class Footer implements Component {
33
33
  private view;
34
- private window;
34
+ private budget;
35
35
  private hint;
36
36
  private statuses;
37
- constructor(view: () => ViewState, window: () => number | undefined, hint: () => string | undefined, statuses?: () => string[]);
37
+ constructor(view: () => ViewState, budget: () => ContextBudget | undefined, hint: () => string | undefined, statuses?: () => string[]);
38
38
  invalidate(): void;
39
39
  render(width: number): string[];
40
40
  }
@@ -60,20 +60,20 @@ export class Header {
60
60
  }
61
61
  export class Footer {
62
62
  view;
63
- window;
63
+ budget;
64
64
  hint;
65
65
  statuses;
66
- constructor(view, window, hint, statuses = () => []) {
66
+ constructor(view, budget, hint, statuses = () => []) {
67
67
  this.view = view;
68
- this.window = window;
68
+ this.budget = budget;
69
69
  this.hint = hint;
70
70
  this.statuses = statuses;
71
71
  }
72
72
  invalidate() { }
73
73
  render(width) {
74
- const v = this.view(), total = this.window(), used = v.context?.used ?? 0, estimated = v.context?.estimated ?? true;
74
+ const v = this.view(), budget = this.budget(), total = budget?.total, used = v.context?.used ?? 0, estimated = v.context?.estimated ?? true;
75
75
  const pct = contextPercent(used, total);
76
- const level = contextLevel(pct ?? 0);
76
+ const level = contextLevel(pct ?? 0, budget?.compactionAt ?? 85);
77
77
  const cells = 10, filled = pct === undefined ? 0 : Math.min(cells, Math.round((pct / 100) * cells));
78
78
  const bar = pct === undefined
79
79
  ? style.gray("░".repeat(cells))
@@ -86,7 +86,7 @@ export class Footer {
86
86
  const s = v.stats;
87
87
  const segments = [
88
88
  {
89
- text: `ctx ${formatContext(used, total, estimated)}`,
89
+ text: `ctx ${formatContext(used, total, estimated, budget?.basis)}`,
90
90
  priority: 10,
91
91
  paint: pct === undefined ? style.gray : levelColor(level),
92
92
  },
@@ -38,6 +38,7 @@ export interface PluginCatalogView {
38
38
  diagnostic?: string;
39
39
  }
40
40
  /** Text markers remain meaningful without color: [x] active, [ ] inactive, [!] failed, [*] pending. */
41
+ /** Group headings are derived from the primary category (or "General") of each plugin. */
41
42
  export declare function pluginCatalogItems(entries: PluginCatalogView[]): {
42
43
  value: string;
43
44
  label: string;
@@ -84,9 +85,18 @@ export declare function configuredProviderModelItems(catalogs: ProviderCatalogVi
84
85
  provider: string;
85
86
  model: string;
86
87
  }): ConfiguredProviderModelItem[];
87
- export declare function contextLevel(pct: number): Level;
88
+ export declare function contextLevel(pct: number, compactionAt?: number): Level;
88
89
  export declare function contextPercent(used: number, total: number | undefined): number | undefined;
89
- export declare function formatContext(used: number, total: number | undefined, estimated: boolean): string;
90
+ /** The effective total the context bar measures against, and what it is derived from. */
91
+ export interface ContextBudget {
92
+ /** Effective total in tokens (model window, or the char budget converted to tokens). */
93
+ total: number;
94
+ /** Basis of the total: the model's context window, or the char-budget fallback. */
95
+ basis: "window" | "chars";
96
+ /** Percentage of `total` at which the engine auto-compacts; the bar turns red there. */
97
+ compactionAt: number;
98
+ }
99
+ export declare function formatContext(used: number, total: number | undefined, estimated: boolean, basis?: "window" | "chars"): string;
90
100
  export declare function formatDuration(ms: number): string;
91
101
  export declare const textWidth: (text: string) => number;
92
102
  export declare function truncatePlain(text: string, width: number): string;
package/dist/tui/state.js CHANGED
@@ -23,6 +23,7 @@ export function providerModelItems(provider, models, current) {
23
23
  }));
24
24
  }
25
25
  /** Text markers remain meaningful without color: [x] active, [ ] inactive, [!] failed, [*] pending. */
26
+ /** Group headings are derived from the primary category (or "General") of each plugin. */
26
27
  export function pluginCatalogItems(entries) {
27
28
  const marker = (entry) => entry.status === "active"
28
29
  ? "[x]"
@@ -31,11 +32,16 @@ export function pluginCatalogItems(entries) {
31
32
  : entry.status === "failed"
32
33
  ? "[!]"
33
34
  : "[*]";
34
- return entries.map((entry) => ({
35
+ const grouped = new Map();
36
+ for (const entry of entries) {
37
+ const primary = entry.categories[0] ?? "General";
38
+ grouped.set(primary, [...(grouped.get(primary) ?? []), entry]);
39
+ }
40
+ return [...grouped].flatMap(([title, group]) => group.map((entry, index) => ({
35
41
  value: entry.id,
36
- label: `${marker(entry)} ${entry.name} · ${entry.builtin ? "built-in" : entry.source}`,
42
+ label: `${index === 0 ? `${title} · ` : ""}${marker(entry)} ${entry.name} · ${entry.builtin ? "built-in" : entry.source}`,
37
43
  description: `${entry.status}${entry.categories.length ? ` · ${entry.categories.join(", ")}` : ""} · ${entry.description}`,
38
- }));
44
+ })));
39
45
  }
40
46
  export const pluginToggleNeedsConfirmation = (entry) => !entry.builtin;
41
47
  const mcpSourceTitle = (kind) => ({
@@ -117,18 +123,23 @@ export function configuredProviderModelItems(catalogs, current) {
117
123
  }
118
124
  return items;
119
125
  }
120
- export function contextLevel(pct) {
121
- return pct < 60 ? "ok" : pct < 85 ? "warn" : "danger";
126
+ export function contextLevel(pct, compactionAt = 85) {
127
+ // Warning band starts a quarter below the auto-compaction point; danger is exactly there.
128
+ const warn = Math.max(0, compactionAt - 25);
129
+ return pct < warn ? "ok" : pct < compactionAt ? "warn" : "danger";
122
130
  }
123
131
  export function contextPercent(used, total) {
124
132
  return total && total > 0 ? (used / total) * 100 : undefined;
125
133
  }
126
- export function formatContext(used, total, estimated) {
134
+ export function formatContext(used, total, estimated, basis) {
127
135
  const prefix = `${estimated ? "~" : ""}${formatTokens(used)} / `;
128
136
  const pct = contextPercent(used, total);
129
- return pct === undefined || !total
130
- ? `${prefix}unknown`
131
- : `${prefix}${formatTokens(total)} (${Math.round(pct)}%)`;
137
+ if (pct === undefined || !total)
138
+ return `${prefix}unknown`;
139
+ // The `~` marks an estimate; the "char budget" suffix tells the user the bar is measured
140
+ // against the fallback (est. tokens from limits.maxContextChars), not a model window.
141
+ const basisSuffix = basis === "chars" ? " char budget" : "";
142
+ return `${prefix}${formatTokens(total)} (${Math.round(pct)}%)${basisSuffix}`;
132
143
  }
133
144
  export function formatDuration(ms) {
134
145
  if (ms < 1000)
@@ -461,6 +472,9 @@ export function reduceEvent(state, event) {
461
472
  const checkpoint = typeof d.summarizedTokens === "number" && typeof d.checkpointTokens === "number"
462
473
  ? ` · checkpoint ~${formatTokens(d.summarizedTokens)} → ~${formatTokens(d.checkpointTokens)} tokens`
463
474
  : "";
475
+ const partial = d.partial
476
+ ? " · partial: the summary was cut by max output tokens; consider raising compaction.maxOutputTokens"
477
+ : "";
464
478
  return addItem({
465
479
  ...state,
466
480
  compacting: false,
@@ -468,7 +482,7 @@ export function reduceEvent(state, event) {
468
482
  }, {
469
483
  kind: "notice",
470
484
  text: [
471
- `Context compacted (${String(d.reason ?? "manual")}): ${String(d.replaced ?? 0)} messages summarized, ~${formatTokens(Number(d.before ?? 0))} → ~${formatTokens(Number(d.after ?? 0))} tokens${checkpoint}`,
485
+ `Context compacted (${String(d.reason ?? "manual")}): ${String(d.replaced ?? 0)} messages summarized, ~${formatTokens(Number(d.before ?? 0))} → ~${formatTokens(Number(d.after ?? 0))} tokens${checkpoint}${partial}`,
472
486
  ...reports,
473
487
  ].join("\n"),
474
488
  });
@@ -487,6 +501,11 @@ export function reduceEvent(state, event) {
487
501
  return addItem({ ...state, compacting: false }, { kind: "notice", text: `Compaction skipped: ${String(d.detail ?? "nothing to compact")}` });
488
502
  case "compaction_failed":
489
503
  return addItem({ ...state, compacting: false }, { kind: "error", text: `Compaction failed: ${String(d.error ?? "unknown error")}` });
504
+ case "response_truncated":
505
+ return addItem(state, {
506
+ kind: "notice",
507
+ text: "Response cut by max output tokens — the answer may be incomplete. Raise limits.maxOutputTokens to allow longer answers.",
508
+ });
490
509
  case "model_changed":
491
510
  return {
492
511
  ...state,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alisio/alisio-code",
3
- "version": "0.1.0-alpha.4",
3
+ "version": "0.1.0-alpha.6",
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.3",
49
- "@alisio/plugin-deepseek": "0.1.0-alpha.2",
50
- "@alisio/plugin-memory": "0.1.0-alpha.3",
51
- "@alisio/plugin-openai-compatible": "0.1.0-alpha.3",
52
- "@alisio/plugin-opencode": "0.1.0-alpha.2",
53
- "@alisio/plugin-opencode-go": "0.1.0-alpha.2",
54
- "@alisio/plugin-subagents": "0.1.0-alpha.3",
55
- "@alisio/sdk": "0.1.0-alpha.2",
48
+ "@alisio/core": "0.1.0-alpha.5",
49
+ "@alisio/plugin-deepseek": "0.1.0-alpha.5",
50
+ "@alisio/plugin-memory": "0.1.0-alpha.5",
51
+ "@alisio/plugin-openai-compatible": "0.1.0-alpha.5",
52
+ "@alisio/plugin-opencode": "0.1.0-alpha.5",
53
+ "@alisio/plugin-opencode-go": "0.1.0-alpha.5",
54
+ "@alisio/plugin-subagents": "0.1.0-alpha.5",
55
+ "@alisio/sdk": "0.1.0-alpha.5",
56
56
  "@earendil-works/pi-tui": "0.87.1",
57
57
  "commander": "15.0.0"
58
58
  },