@alisio/alisio-code 0.1.0-alpha.5 → 0.1.0-alpha.7

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
@@ -127,6 +127,8 @@ async function run(cmd, prompt, sessionId) {
127
127
  process.stderr.write(`\n→ ${event.data.name}\n`);
128
128
  },
129
129
  });
130
+ for (const failure of app.mcpStartupFailures())
131
+ process.stderr.write(`[startup] ${failure}\n`);
130
132
  const controller = new AbortController();
131
133
  const interrupt = () => controller.abort(new Error("Interrupted"));
132
134
  process.on("SIGINT", interrupt);
@@ -253,7 +255,7 @@ program
253
255
  console.log('To generate AGENTS.md for this project, run /init inside alisio (or: alisio run "/init" --allow-write).');
254
256
  });
255
257
  program.command("doctor").action(async (_opts, cmd) => {
256
- const { findWorkspace, loadConfigWithProvenance, overridesSavedProviderProfile, ProviderSettingsStore, which, } = await import("@alisio/core");
258
+ const { findWorkspace, loadConfigWithProvenance, overridesSavedProviderProfile, ProviderSettingsStore, RIPGREP_INSTALL_HINT, which, } = await import("@alisio/core");
257
259
  const o = options(cmd);
258
260
  const workspace = await findWorkspace(o.cwd ?? process.cwd());
259
261
  const { config, provenance } = await loadConfigWithProvenance(workspace, {
@@ -292,6 +294,8 @@ program.command("doctor").action(async (_opts, cmd) => {
292
294
  },
293
295
  };
294
296
  console.log(JSON.stringify(status, null, 2));
297
+ if (!status.ripgrep)
298
+ process.stderr.write(`\nWarning: ripgrep (rg) is not installed; search_text and list_files will not work. ${RIPGREP_INSTALL_HINT}\n`);
295
299
  if (!model || model === "YOUR_MODEL_ID")
296
300
  console.error("\nNo model configured yet: set provider.model in your config, --model, or ALISIO_MODEL " +
297
301
  "before starting a real conversation (it will otherwise fail on the first turn).");
@@ -389,11 +393,12 @@ for (const name of ["list", "validate"]) {
389
393
  }
390
394
  const plugins = program.command("plugins");
391
395
  plugins.command("list").action(async (_opts, cmd) => {
392
- const { discoverPlugins } = await import("@alisio/core");
396
+ const { discoverPlugins, installedNpmPlugins } = await import("@alisio/core");
393
397
  const { configHome } = await import("@alisio/core");
394
398
  const { join, resolve } = await import("node:path");
399
+ const global = join(configHome(), "plugins");
395
400
  console.log(JSON.stringify({
396
- global: await discoverPlugins(join(configHome(), "plugins")),
401
+ global: [...(await discoverPlugins(global)), ...(await installedNpmPlugins(global))],
397
402
  project: await discoverPlugins(join(resolve(options(cmd).cwd ?? process.cwd()), ".alisio", "plugins")),
398
403
  explicit: options(cmd).plugin ?? [],
399
404
  }, null, 2));
@@ -468,6 +473,26 @@ mcp
468
473
  await app.close();
469
474
  }
470
475
  });
476
+ program
477
+ .command("install")
478
+ .description("Install an npm plugin package into the global plugins directory (~/.config/alisio/plugins)")
479
+ .argument("<spec>", 'npm package spec, e.g. "npm:plugin-openrouter" or "plugin-openrouter@1.2.3"')
480
+ .option("-y, --yes", "Skip the pre-install confirmation (npm may run lifecycle scripts)")
481
+ .option("--trust-plugin", "Explicit trust for this global install (same as --yes)")
482
+ .option("--update", "Refresh an already-installed plugin to the latest version, keeping its name")
483
+ .action(async (spec, _options, cmd) => {
484
+ const { cliInstall, configHome } = await import("@alisio/core");
485
+ const o = options(cmd);
486
+ await cliInstall({
487
+ spec,
488
+ configHome: configHome(),
489
+ yes: !!o.yes || !!o.trustPlugin,
490
+ update: !!o.update,
491
+ readOnly: !!o.readOnly,
492
+ json: !!o.json,
493
+ interactive: !!process.stdin.isTTY && !!process.stdout.isTTY && !o.json,
494
+ });
495
+ });
471
496
  try {
472
497
  await program.parseAsync();
473
498
  }
package/dist/tui/app.js CHANGED
@@ -102,6 +102,17 @@ export async function runTui(options) {
102
102
  modelList = undefined;
103
103
  throw e;
104
104
  }));
105
+ /**
106
+ * Reloads the active-provider catalog and repaints when it lands, so the context bar learns
107
+ * the ACTIVE model's real window (previously only the first /model picker or autocomplete
108
+ * call loaded it, leaving a fabricated 40k total on screen). Called at startup and after
109
+ * every provider/model switch; the bar honestly shows `?` until the catalog arrives.
110
+ */
111
+ const primeModels = () => {
112
+ void models()
113
+ .then(() => tui.requestRender())
114
+ .catch(() => { });
115
+ };
105
116
  const terminal = new ProcessTerminal();
106
117
  const copy = async (text) => copyText(text, {
107
118
  platform: process.platform,
@@ -184,7 +195,7 @@ export async function runTui(options) {
184
195
  const siblings = nodes.filter((n) => n.parentId === node.parentId);
185
196
  const child = viewedState();
186
197
  const budget = app.contextBudget(child?.model ?? view.model);
187
- const pct = child?.context && budget
198
+ const pct = child?.context && budget?.total
188
199
  ? ` · ctx ${Math.round((child.context.used / budget.total) * 100)}%`
189
200
  : "";
190
201
  const tokens = child
@@ -239,6 +250,9 @@ export async function runTui(options) {
239
250
  },
240
251
  { component: bottom, basis: "auto", shrink: 1, minSize: 3 },
241
252
  ]));
253
+ // Prime the active provider's catalog so the context bar shows the model's real window
254
+ // (or an honest `?`) instead of a fabricated total; repaint when the catalog lands.
255
+ primeModels();
242
256
  const sync = () => {
243
257
  main.sync(view.items);
244
258
  tui.requestRender();
@@ -600,10 +614,34 @@ export async function runTui(options) {
600
614
  placeholder: "provider-model-id",
601
615
  hint: "The provider returned no model catalog",
602
616
  step: inputFields.length + 1,
603
- steps: inputFields.length + 1,
617
+ steps: inputFields.length + 2,
604
618
  });
605
619
  if (!model)
606
620
  return;
621
+ // Local servers like llama.cpp omit `context_window` from GET /models, so the bar would show
622
+ // an honest `?`. When the discovered catalog cannot name the window (or is empty), ask ONE
623
+ // optional numeric override and persist it in the profile values when provided.
624
+ const catalogEntry = discovered.find((candidate) => candidate.id === model);
625
+ if (!catalogEntry?.contextWindow) {
626
+ const windowStep = discovered.length ? inputFields.length + 1 : inputFields.length + 2;
627
+ const entered = await askInput({
628
+ provider: registration.name,
629
+ label: "Context window in tokens (optional)",
630
+ initial: profile.contextWindow === undefined ? "" : String(profile.contextWindow),
631
+ placeholder: "131072",
632
+ hint: "For local servers that omit context_window; leave empty to keep the saved value or stay unknown",
633
+ step: windowStep,
634
+ steps: windowStep,
635
+ });
636
+ if (entered === undefined)
637
+ return;
638
+ if (entered.trim()) {
639
+ const parsed = Number(entered.trim());
640
+ if (!Number.isInteger(parsed) || parsed <= 0)
641
+ return error("Context window must be a positive number of tokens");
642
+ profile.contextWindow = parsed;
643
+ }
644
+ }
607
645
  await app.activateProvider(providerId, profile, credentials, model);
608
646
  activeProvider = app.providerInfo;
609
647
  modelList = undefined;
@@ -611,6 +649,7 @@ export async function runTui(options) {
611
649
  reset(initialViewState(model));
612
650
  notice(`Connected to ${registration.name} with model ${model}. Started a fresh session.`);
613
651
  refreshEstimate();
652
+ primeModels();
614
653
  };
615
654
  const chooseModel = async () => {
616
655
  const catalogs = await app.configuredProviderCatalogs(AbortSignal.timeout(20_000));
@@ -635,6 +674,7 @@ export async function runTui(options) {
635
674
  reset(initialViewState(selected.model));
636
675
  notice(`Provider changed to ${app.providers.get(selected.provider)?.name ?? selected.provider} with model ${selected.model}. Started a fresh session.`);
637
676
  refreshEstimate();
677
+ primeModels();
638
678
  })
639
679
  .catch(error);
640
680
  }, closePicker, true));
@@ -719,9 +759,45 @@ export async function runTui(options) {
719
759
  const manageMcp = () => {
720
760
  const openCatalog = () => {
721
761
  const entries = app.mcp.list();
722
- if (!entries.length)
762
+ const serverItems = mcpServerItems(entries);
763
+ const items = [
764
+ ...serverItems,
765
+ ...(app.mcpAllowPersisted()
766
+ ? [
767
+ {
768
+ value: "!revoke-global",
769
+ label: "Revoke global MCP consent",
770
+ description: "Clear mcp.allow from your user configuration and disconnect servers",
771
+ },
772
+ ]
773
+ : []),
774
+ ];
775
+ if (!items.length)
723
776
  return notice("No MCP servers are configured");
724
- showPicker(new Picker(`MCP servers (${entries.length})`, mcpServerItems(entries), (item) => {
777
+ showPicker(new Picker(`MCP servers (${entries.length})`, items, (item) => {
778
+ if (item.value === "!revoke-global") {
779
+ showPicker(new Picker("Revoke global MCP consent?", [
780
+ {
781
+ value: "revoke",
782
+ label: "Revoke and disconnect",
783
+ description: "Clears mcp.allow; future Alisio starts will not auto-grant or auto-connect MCP servers",
784
+ },
785
+ {
786
+ value: "cancel",
787
+ label: "Cancel",
788
+ description: "Keep global MCP consent enabled",
789
+ },
790
+ ], (choice) => {
791
+ if (choice.value !== "revoke")
792
+ return openCatalog();
793
+ void app
794
+ .revokeGlobalMcpConsent()
795
+ .then(() => notice("Global MCP consent revoked. Runtime permission dropped and configured servers disconnected."))
796
+ .catch(error)
797
+ .finally(openCatalog);
798
+ }, openCatalog, false, "Removes mcp.allow from your user configuration (~/.config/alisio/config.json) atomically. This run's --allow-mcp flag, if given, keeps permission for this session."));
799
+ return;
800
+ }
725
801
  const selected = app.mcp.info(item.value);
726
802
  const source = selected.source.kind === "global"
727
803
  ? "User"
@@ -801,11 +877,16 @@ export async function runTui(options) {
801
877
  const run = () => void action().catch(error).finally(openCatalog);
802
878
  if (app.mcpRuntimePermission() === "granted")
803
879
  return run();
804
- showPicker(new Picker("Grant MCP access for this session?", [
880
+ showPicker(new Picker("Grant MCP access?", [
881
+ {
882
+ value: "grant-session",
883
+ label: "Grant for this session only",
884
+ description: "May start the configured process or network connection; not saved",
885
+ },
805
886
  {
806
- value: "grant",
807
- label: "Grant and continue",
808
- description: "May start the configured process or network connection",
887
+ value: "grant-remember",
888
+ label: "Grant and remember for this user (global)",
889
+ description: "Persists mcp.allow in your user configuration for every session",
809
890
  },
810
891
  {
811
892
  value: "cancel",
@@ -813,18 +894,31 @@ export async function runTui(options) {
813
894
  description: "Make no permission or server changes",
814
895
  },
815
896
  ], (choice) => {
816
- if (choice.value !== "grant")
897
+ if (choice.value === "cancel")
817
898
  return openCatalog();
818
- try {
819
- app.grantMcpRuntimePermission({ source: "interactive-tui", confirmed: true });
820
- notice("MCP process/network access granted for this Alisio session only.");
821
- run();
899
+ if (choice.value === "grant-session") {
900
+ try {
901
+ app.grantMcpRuntimePermission({ source: "interactive-tui", confirmed: true });
902
+ notice("MCP process/network access granted for this Alisio session only.");
903
+ run();
904
+ }
905
+ catch (cause) {
906
+ error(cause);
907
+ openCatalog();
908
+ }
909
+ return;
822
910
  }
823
- catch (cause) {
911
+ void app
912
+ .rememberGlobalMcpConsent()
913
+ .then(() => {
914
+ notice("MCP process/network access granted globally (mcp.allow) for this user. Enabled servers will auto-connect on every start.");
915
+ run();
916
+ })
917
+ .catch((cause) => {
824
918
  error(cause);
825
919
  openCatalog();
826
- }
827
- }, openCatalog, false, "This grant is not saved. Configured server enablement is persisted separately. MCP servers and their tools run with your user privileges."));
920
+ });
921
+ }, openCatalog, false, "Session-only lasts until Alisio exits. Remembering writes mcp.allow=true to your user configuration so every start grants MCP access and auto-connects enabled servers, including headless runs. MCP servers and their tools run with your user privileges."));
828
922
  };
829
923
  openCatalog();
830
924
  };
@@ -1296,6 +1390,8 @@ export async function runTui(options) {
1296
1390
  transcript.addChild(banner);
1297
1391
  }
1298
1392
  tui.start();
1393
+ for (const failure of app.mcpStartupFailures())
1394
+ notice(failure);
1299
1395
  sync();
1300
1396
  refreshEstimate();
1301
1397
  // Discover context windows in the background; failures only mean "unknown".
@@ -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;
@@ -88,14 +89,14 @@ export declare function contextLevel(pct: number, compactionAt?: number): Level;
88
89
  export declare function contextPercent(used: number, total: number | undefined): number | undefined;
89
90
  /** The effective total the context bar measures against, and what it is derived from. */
90
91
  export interface ContextBudget {
91
- /** Effective total in tokens (model window, or the char budget converted to tokens). */
92
- total: number;
93
- /** Basis of the total: the model's context window, or the char-budget fallback. */
94
- basis: "window" | "chars";
92
+ /** Effective total in tokens: the model's context window, or absent when it is unknown. */
93
+ total?: number;
94
+ /** Basis of the total: the model's context window, or unknown (no fabricated total). */
95
+ basis: "window" | "unknown";
95
96
  /** Percentage of `total` at which the engine auto-compacts; the bar turns red there. */
96
97
  compactionAt: number;
97
98
  }
98
- export declare function formatContext(used: number, total: number | undefined, estimated: boolean, basis?: "window" | "chars"): string;
99
+ export declare function formatContext(used: number, total: number | undefined, estimated: boolean, basis?: "window" | "unknown"): string;
99
100
  export declare function formatDuration(ms: number): string;
100
101
  export declare const textWidth: (text: string) => number;
101
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) => ({
@@ -127,13 +133,14 @@ export function contextPercent(used, total) {
127
133
  }
128
134
  export function formatContext(used, total, estimated, basis) {
129
135
  const prefix = `${estimated ? "~" : ""}${formatTokens(used)} / `;
136
+ // Honest unknown: the model window could not be known, so the bar shows `?` instead of a
137
+ // fabricated total or percentage.
138
+ if (basis === "unknown")
139
+ return `${prefix}?`;
130
140
  const pct = contextPercent(used, total);
131
141
  if (pct === undefined || !total)
132
142
  return `${prefix}unknown`;
133
- // The `~` marks an estimate; the "char budget" suffix tells the user the bar is measured
134
- // against the fallback (est. tokens from limits.maxContextChars), not a model window.
135
- const basisSuffix = basis === "chars" ? " char budget" : "";
136
- return `${prefix}${formatTokens(total)} (${Math.round(pct)}%)${basisSuffix}`;
143
+ return `${prefix}${formatTokens(total)} (${Math.round(pct)}%)`;
137
144
  }
138
145
  export function formatDuration(ms) {
139
146
  if (ms < 1000)
package/dist/version.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  /**
2
- * Reads the package version at runtime so --version, doctor and MCP client metadata
3
- * stay in sync with the published package without a build-time constant to update.
4
- * Standalone binaries inject the version at build time via ALISIO_PACKAGE_VERSION.
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).
5
7
  */
6
8
  export declare function loadVersion(fromHere: string): string;
package/dist/version.js CHANGED
@@ -1,12 +1,18 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
- /** Fallback used when package.json is not readable (e.g. standalone binaries). */
5
- const FALLBACK = "0.1.0-alpha.1";
6
4
  /**
7
- * Reads the package version at runtime so --version, doctor and MCP client metadata
8
- * stay in sync with the published package without a build-time constant to update.
9
- * Standalone binaries inject the version at build time via ALISIO_PACKAGE_VERSION.
5
+ * Fallback used when neither the build-time injection nor the package manifest is readable
6
+ * (e.g. an unpackaged embed). Keep it a development marker, never a publish literal, so it
7
+ * cannot silently desync from the published package.
8
+ */
9
+ const FALLBACK = "dev";
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).
10
16
  */
11
17
  export function loadVersion(fromHere) {
12
18
  if (process.env.ALISIO_PACKAGE_VERSION)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alisio/alisio-code",
3
- "version": "0.1.0-alpha.5",
3
+ "version": "0.1.0-alpha.7",
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,13 +45,13 @@
45
45
  "alisio": "./dist/main.js"
46
46
  },
47
47
  "dependencies": {
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",
48
+ "@alisio/core": "0.1.0-alpha.6",
49
+ "@alisio/plugin-deepseek": "0.1.0-alpha.6",
50
+ "@alisio/plugin-memory": "0.1.0-alpha.6",
51
+ "@alisio/plugin-openai-compatible": "0.1.0-alpha.6",
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
55
  "@alisio/sdk": "0.1.0-alpha.5",
56
56
  "@earendil-works/pi-tui": "0.87.1",
57
57
  "commander": "15.0.0"