@alisio/alisio-code 0.5.2 → 0.6.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.
- package/dist/analysis.d.ts +24 -1
- package/dist/analysis.js +100 -1
- package/dist/forge.d.ts +13 -0
- package/dist/forge.js +46 -0
- package/dist/main.js +32 -4
- package/dist/tui/artifacts.d.ts +1 -1
- package/dist/tui/artifacts.js +5 -1
- package/dist/tui/components.js +5 -3
- package/dist/tui/state.d.ts +17 -1
- package/dist/tui/state.js +63 -3
- package/package.json +7 -7
package/dist/analysis.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AnalysisStatus } from "@alisio/sdk";
|
|
1
|
+
import type { AnalysisStatus, EngineDescriptor } from "@alisio/sdk";
|
|
2
2
|
type Common = {
|
|
3
3
|
db?: string;
|
|
4
4
|
python?: string;
|
|
@@ -6,6 +6,10 @@ type Common = {
|
|
|
6
6
|
config?: string;
|
|
7
7
|
trustProject?: boolean;
|
|
8
8
|
};
|
|
9
|
+
/** `alisio forge status` reuses this for its Python lines. */
|
|
10
|
+
export declare function pythonBackendDescriptors(o: Common): Promise<EngineDescriptor[]>;
|
|
11
|
+
/** A fixed-width table of the backends: readiness, isolation, profiles and why not. */
|
|
12
|
+
export declare function backendsTable(backends: readonly EngineDescriptor[]): string;
|
|
9
13
|
/** The unified runtime state and, when Python is missing, the install guidance for this system. */
|
|
10
14
|
export declare function analysisState(o: Common): Promise<{
|
|
11
15
|
status: AnalysisStatus;
|
|
@@ -24,4 +28,23 @@ export declare function setupExtras(o: Common, extras: string | undefined): Prom
|
|
|
24
28
|
export declare function setupOci(o: Common, image: string | undefined): Promise<void>;
|
|
25
29
|
/** `alisio analysis sweep [--force]`: the retention sweep now (it also runs by itself, daily). */
|
|
26
30
|
export declare function sweep(o: Common, force: boolean): Promise<void>;
|
|
31
|
+
export interface WasmSetupDeps {
|
|
32
|
+
/** Replaceable in tests; the global `fetch` otherwise. */
|
|
33
|
+
fetch?: typeof fetch;
|
|
34
|
+
manifest?: import("@alisio/core").PyodideManifest;
|
|
35
|
+
/** Whether someone can answer a question (a terminal). */
|
|
36
|
+
interactive?: boolean;
|
|
37
|
+
ask?: (question: string) => Promise<boolean>;
|
|
38
|
+
write?: (text: string) => void;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* `alisio analysis setup --wasm [--profile analysis|science] [--yes]`: the only place the wasm
|
|
42
|
+
* backend uses the network. Downloads the pinned Pyodide distribution into
|
|
43
|
+
* `<analysis dir>/pyodide/<version>/`, verifies every hash and size, and on any mismatch deletes
|
|
44
|
+
* what it fetched and leaves the backend not ready. It never touches the venv or the OCI image.
|
|
45
|
+
*/
|
|
46
|
+
export declare function setupWasm(o: Common, flags: {
|
|
47
|
+
profile?: string;
|
|
48
|
+
yes?: boolean;
|
|
49
|
+
}, deps?: WasmSetupDeps): Promise<void>;
|
|
27
50
|
export {};
|
package/dist/analysis.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* `python_run`: the base runtime is the discovered Python with only the standard library.
|
|
6
6
|
*/
|
|
7
7
|
import { dirname, join } from "node:path";
|
|
8
|
+
import { createInterface } from "node:readline/promises";
|
|
8
9
|
async function context(o) {
|
|
9
10
|
const core = await import("@alisio/core");
|
|
10
11
|
const workspace = await core.findWorkspace(o.cwd ?? process.cwd());
|
|
@@ -20,9 +21,41 @@ async function context(o) {
|
|
|
20
21
|
const oci = () => new core.OciRuntime({ ...config.analysis.oci });
|
|
21
22
|
return { core, config, provenance, stateDir, manager, oci };
|
|
22
23
|
}
|
|
24
|
+
/** The Python backends of this configuration, described with their live readiness. */
|
|
25
|
+
function describeBackends(c) {
|
|
26
|
+
const backends = c.core.createPythonBackends({
|
|
27
|
+
runtime: c.manager,
|
|
28
|
+
oci: { runtime: c.oci, mode: () => c.config.analysis.runtime },
|
|
29
|
+
wasm: { stateRoot: c.stateDir, settings: () => c.config.analysis.wasm },
|
|
30
|
+
});
|
|
31
|
+
return (signal) => c.core.describePythonBackends(backends, signal);
|
|
32
|
+
}
|
|
33
|
+
/** `alisio forge status` reuses this for its Python lines. */
|
|
34
|
+
export async function pythonBackendDescriptors(o) {
|
|
35
|
+
return describeBackends(await context(o))();
|
|
36
|
+
}
|
|
37
|
+
/** A fixed-width table of the backends: readiness, isolation, profiles and why not. */
|
|
38
|
+
export function backendsTable(backends) {
|
|
39
|
+
const rows = backends.map((b) => [
|
|
40
|
+
b.id.replace(/^python-/, ""),
|
|
41
|
+
b.isolation,
|
|
42
|
+
b.ready ? "ready" : "not ready",
|
|
43
|
+
b.profiles?.length ? b.profiles.join(",") : "-",
|
|
44
|
+
b.capabilities.networkDeniedByOS ? "denied" : "open",
|
|
45
|
+
b.reason ?? (b.isolation === "none" ? "host (not sandboxed)" : ""),
|
|
46
|
+
]);
|
|
47
|
+
const head = ["BACKEND", "ISOLATION", "STATE", "PROFILES", "NETWORK", "NOTE"];
|
|
48
|
+
const widths = head.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length)));
|
|
49
|
+
const line = (cells) => cells
|
|
50
|
+
.map((c, i) => c.padEnd(widths[i] ?? 0))
|
|
51
|
+
.join(" ")
|
|
52
|
+
.trimEnd();
|
|
53
|
+
return [line(head), ...rows.map(line)].join("\n");
|
|
54
|
+
}
|
|
23
55
|
/** The unified runtime state and, when Python is missing, the install guidance for this system. */
|
|
24
56
|
export async function analysisState(o) {
|
|
25
|
-
const
|
|
57
|
+
const ctx = await context(o);
|
|
58
|
+
const { core, config, provenance, stateDir, manager, oci } = ctx;
|
|
26
59
|
const janitor = {
|
|
27
60
|
lastSweep: () => import("node:fs/promises")
|
|
28
61
|
.then((fs) => fs.readFile(join(stateDir, "analysis", ".last-sweep"), "utf8"))
|
|
@@ -37,6 +70,7 @@ export async function analysisState(o) {
|
|
|
37
70
|
runtime: manager,
|
|
38
71
|
oci,
|
|
39
72
|
janitor,
|
|
73
|
+
backends: describeBackends(ctx),
|
|
40
74
|
enabled: config.analysis.enabled,
|
|
41
75
|
readOnly: false,
|
|
42
76
|
});
|
|
@@ -58,6 +92,8 @@ export async function analysisState(o) {
|
|
|
58
92
|
export async function printAnalysisStatus(o) {
|
|
59
93
|
const { status, guidance, ignored } = await analysisState(o);
|
|
60
94
|
console.log(JSON.stringify(status, null, 2));
|
|
95
|
+
if (status.backends?.length)
|
|
96
|
+
process.stderr.write(`\nPython backends (analysis.runtime=${status.mode}, analysis.isolation=${status.isolation ?? "preferred"}):\n${backendsTable(status.backends)}\n`);
|
|
61
97
|
if (guidance)
|
|
62
98
|
process.stderr.write(`\n${guidance}\n`);
|
|
63
99
|
if (status.mode === "oci" && status.oci.available === false)
|
|
@@ -143,3 +179,66 @@ export async function sweep(o, force) {
|
|
|
143
179
|
store.close();
|
|
144
180
|
}
|
|
145
181
|
}
|
|
182
|
+
const megabytes = (bytes) => `${Math.max(1, Math.round(bytes / (1024 * 1024)))} MB`;
|
|
183
|
+
async function confirm(question) {
|
|
184
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
185
|
+
try {
|
|
186
|
+
return /^y(es)?$/i.test((await rl.question(question)).trim());
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
rl.close();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* `alisio analysis setup --wasm [--profile analysis|science] [--yes]`: the only place the wasm
|
|
194
|
+
* backend uses the network. Downloads the pinned Pyodide distribution into
|
|
195
|
+
* `<analysis dir>/pyodide/<version>/`, verifies every hash and size, and on any mismatch deletes
|
|
196
|
+
* what it fetched and leaves the backend not ready. It never touches the venv or the OCI image.
|
|
197
|
+
*/
|
|
198
|
+
export async function setupWasm(o, flags, deps = {}) {
|
|
199
|
+
const profile = flags.profile ?? "analysis";
|
|
200
|
+
if (profile !== "analysis" && profile !== "science")
|
|
201
|
+
throw new Error("Choose the profile: --profile analysis (default) or --profile science");
|
|
202
|
+
const { core, stateDir } = await context(o);
|
|
203
|
+
const manifest = deps.manifest ?? core.PYODIDE_MANIFEST;
|
|
204
|
+
const write = deps.write ?? ((text) => void process.stderr.write(text));
|
|
205
|
+
const dir = core.pyodideDir(stateDir, manifest);
|
|
206
|
+
const bytes = core.pyodideDownloadSize(manifest, profile);
|
|
207
|
+
const hosts = [
|
|
208
|
+
...new Set([...manifest.core, ...Object.values(manifest.packages)].map((f) => new URL(f.url).host)),
|
|
209
|
+
];
|
|
210
|
+
write(`Pyodide ${manifest.version} (Python ${manifest.python}), profile ${profile}: about ${megabytes(bytes)} from ${hosts.join(", ")}.\n` +
|
|
211
|
+
`It goes to ${dir}. Every file is checked against a pinned SHA-256; this command is the only time the wasm backend uses the network.\n`);
|
|
212
|
+
if (!flags.yes) {
|
|
213
|
+
const interactive = deps.interactive ?? (!!process.stdin.isTTY && !!process.stderr.isTTY);
|
|
214
|
+
if (!interactive)
|
|
215
|
+
throw new Error("This download needs your confirmation: pass --yes in a non-interactive run.");
|
|
216
|
+
const ok = await (deps.ask ?? confirm)("Download now? [y/N] ");
|
|
217
|
+
if (!ok) {
|
|
218
|
+
write("Cancelled; nothing was downloaded.\n");
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const controller = new AbortController();
|
|
223
|
+
process.on("SIGINT", () => controller.abort(new Error("Interrupted")));
|
|
224
|
+
try {
|
|
225
|
+
const result = await core.installPyodide({
|
|
226
|
+
dir,
|
|
227
|
+
profile,
|
|
228
|
+
manifest,
|
|
229
|
+
signal: controller.signal,
|
|
230
|
+
...(deps.fetch ? { fetch: deps.fetch } : {}),
|
|
231
|
+
onProgress: (e) => write(` [${e.index}/${e.total}] ${e.file}\n`),
|
|
232
|
+
});
|
|
233
|
+
write(`Pyodide ${manifest.version} is ready (profile ${result.ok ? result.profile : profile}). ` +
|
|
234
|
+
'With analysis.runtime "auto" it is used when no container is ready; set analysis.runtime to "wasm" to require it.\n');
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
const integrity = error instanceof core.PyodideIntegrityError;
|
|
238
|
+
write(`${error instanceof Error ? error.message : String(error)}\n` +
|
|
239
|
+
(integrity
|
|
240
|
+
? "Nothing from this download was kept. The wasm backend is not ready.\n"
|
|
241
|
+
: "Nothing from this download was kept.\n"));
|
|
242
|
+
process.exitCode = 1;
|
|
243
|
+
}
|
|
244
|
+
}
|
package/dist/forge.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `alisio forge status`: what `GET /api/forge` will report, for the terminal. It builds the engine
|
|
3
|
+
* from the configuration alone (no application, no database): the WASM is checked against its
|
|
4
|
+
* pinned hash, the host files are looked for, and `--probe` runs a real isolate end to end.
|
|
5
|
+
*/
|
|
6
|
+
type Common = {
|
|
7
|
+
cwd?: string;
|
|
8
|
+
config?: string;
|
|
9
|
+
trustProject?: boolean;
|
|
10
|
+
json?: boolean;
|
|
11
|
+
};
|
|
12
|
+
export declare function printForgeStatus(o: Common, probe: boolean): Promise<void>;
|
|
13
|
+
export {};
|
package/dist/forge.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const mib = (bytes) => `${Math.round(bytes / (1024 * 1024))} MiB`;
|
|
2
|
+
export async function printForgeStatus(o, probe) {
|
|
3
|
+
const core = await import("@alisio/core");
|
|
4
|
+
const workspace = await core.findWorkspace(o.cwd ?? process.cwd());
|
|
5
|
+
const { config } = await core.loadConfigWithProvenance(workspace, {
|
|
6
|
+
file: o.config,
|
|
7
|
+
trustProject: o.trustProject,
|
|
8
|
+
});
|
|
9
|
+
const engine = core.forgeEngineFor(config.forge);
|
|
10
|
+
const { pythonBackendDescriptors } = await import("./analysis.js");
|
|
11
|
+
const python = await pythonBackendDescriptors(o);
|
|
12
|
+
const base = await core.describeForge(config.forge, engine);
|
|
13
|
+
const status = {
|
|
14
|
+
...base,
|
|
15
|
+
engines: [...base.engines, ...python],
|
|
16
|
+
python: { runtime: config.analysis.runtime, isolation: config.analysis.isolation },
|
|
17
|
+
};
|
|
18
|
+
const probed = probe ? await engine.probe() : undefined;
|
|
19
|
+
if (o.json) {
|
|
20
|
+
process.stdout.write(`${JSON.stringify({ ...status, ...(probed ? { probe: probed } : {}) })}\n`);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const js = status.javascript;
|
|
24
|
+
const descriptor = status.engines[0];
|
|
25
|
+
const lines = [
|
|
26
|
+
`JavaScript ${js.enabled ? "enabled" : "disabled"} ${descriptor?.ready ? "ready" : "not ready"}`,
|
|
27
|
+
` engine ${descriptor?.id ?? "javascript-quickjs"} ${js.version}`,
|
|
28
|
+
` isolation ${descriptor?.isolation ?? "none"} (${js.host} host${js.hostSetting === "auto" ? "" : `, forced by forge.javascript.host=${js.hostSetting}`})`,
|
|
29
|
+
` wasm sha256 ${js.wasmSha256}${js.verified ? " verified" : ""}`,
|
|
30
|
+
];
|
|
31
|
+
if (!descriptor?.ready && descriptor?.reason)
|
|
32
|
+
lines.push(` reason ${descriptor.reason}`);
|
|
33
|
+
if (js.verification)
|
|
34
|
+
lines.push(` problem ${js.verification}`);
|
|
35
|
+
lines.push(` limits timeout ${js.limits.timeoutMs} ms, heap ${mib(js.limits.memoryBytes)}, stack ${Math.round(js.limits.stackBytes / 1024)} KiB, ${js.limits.maxToolCalls} calls (${js.limits.maxConcurrency} at once), ${js.limits.maxOutputChars} output chars`, ` state ${status.state.enabled ? "on" : "off"} scratchpad ${status.scratch.enabled ? "on" : "off"}`);
|
|
36
|
+
lines.push("", `Python analysis.runtime=${config.analysis.runtime} isolation=${config.analysis.isolation}`);
|
|
37
|
+
for (const backend of python)
|
|
38
|
+
lines.push(` ${backend.id.replace(/^python-/, "").padEnd(10)} ${backend.isolation.padEnd(13)} ${backend.ready ? "ready " : "not ready"} network ${backend.capabilities.networkDeniedByOS ? "denied by the OS" : "not blocked"}${backend.profiles?.length ? ` profiles ${backend.profiles.join(",")}` : ""}${backend.reason ? ` (${backend.reason})` : ""}`);
|
|
39
|
+
if (probed)
|
|
40
|
+
lines.push(probed.ok
|
|
41
|
+
? ` probe ok, a ${probed.host} isolate ran a script in ${probed.bootMs} ms`
|
|
42
|
+
: ` probe FAILED (${probed.host} host): ${probed.reason}`);
|
|
43
|
+
process.stdout.write(`${lines.join("\n")}\n`);
|
|
44
|
+
if (probed && !probed.ok)
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
}
|
package/dist/main.js
CHANGED
|
@@ -419,6 +419,8 @@ program.command("doctor").action(async (_opts, cmd) => {
|
|
|
419
419
|
python: { mode: analysisInfo.status.mode, ...pythonInfo },
|
|
420
420
|
analysis: {
|
|
421
421
|
mode: analysisInfo.status.mode,
|
|
422
|
+
...(analysisInfo.status.isolation ? { isolation: analysisInfo.status.isolation } : {}),
|
|
423
|
+
...(analysisInfo.status.backends ? { backends: analysisInfo.status.backends } : {}),
|
|
422
424
|
oci: analysisInfo.status.oci,
|
|
423
425
|
limits: analysisInfo.status.limits,
|
|
424
426
|
retention: analysisInfo.status.retention,
|
|
@@ -447,13 +449,21 @@ analysis
|
|
|
447
449
|
});
|
|
448
450
|
analysis
|
|
449
451
|
.command("setup")
|
|
450
|
-
.description("Optional: install hash-locked Python extras (--extras)
|
|
451
|
-
"python_run never needs this for the standard library.")
|
|
452
|
+
.description("Optional: install hash-locked Python extras (--extras), pull the container image (--oci) or " +
|
|
453
|
+
"download the WebAssembly runtime (--wasm). python_run never needs this for the standard library.")
|
|
452
454
|
.option("--extras <set>", "analysis (pandas, numpy, matplotlib…) or science (adds scipy…)")
|
|
453
455
|
.option("--oci", "Pull the container image once and verify its digest (Docker or Podman)")
|
|
454
456
|
.option("--image <name>", "With --oci: the image to pull (default: analysis.oci.image)")
|
|
457
|
+
.option("--wasm", "Download the pinned Pyodide runtime for the wasm backend (needs network)")
|
|
458
|
+
.option("--profile <name>", "With --wasm: analysis (default) or science (adds scipy, scikit-learn…)")
|
|
459
|
+
.option("--yes", "With --wasm: do not ask for confirmation (required when not interactive)")
|
|
455
460
|
.action(async (flags, cmd) => {
|
|
456
|
-
const { setupExtras, setupOci } = await import("./analysis.js");
|
|
461
|
+
const { setupExtras, setupOci, setupWasm } = await import("./analysis.js");
|
|
462
|
+
if (flags.wasm)
|
|
463
|
+
return setupWasm(options(cmd), {
|
|
464
|
+
...(flags.profile ? { profile: flags.profile } : {}),
|
|
465
|
+
yes: !!flags.yes,
|
|
466
|
+
});
|
|
457
467
|
if (flags.oci)
|
|
458
468
|
return setupOci(options(cmd), flags.image);
|
|
459
469
|
return setupExtras(options(cmd), flags.extras);
|
|
@@ -466,6 +476,17 @@ analysis
|
|
|
466
476
|
const { sweep } = await import("./analysis.js");
|
|
467
477
|
await sweep(options(cmd), !!flags.force);
|
|
468
478
|
});
|
|
479
|
+
const forge = program
|
|
480
|
+
.command("forge")
|
|
481
|
+
.description("Forja: isolated code execution (js_run). Shows the engine and why it is or is not ready");
|
|
482
|
+
forge
|
|
483
|
+
.command("status")
|
|
484
|
+
.description("Show the engines: JavaScript (version, WASM hash, isolation, host, limits) and the Python backends")
|
|
485
|
+
.option("--probe", "Also start a real isolate and run a trivial script through the protocol")
|
|
486
|
+
.action(async (flags, cmd) => {
|
|
487
|
+
const { printForgeStatus } = await import("./forge.js");
|
|
488
|
+
await printForgeStatus(options(cmd), !!flags.probe);
|
|
489
|
+
});
|
|
469
490
|
const trust = program.command("trust").description("Inspect or revoke per-directory project trust");
|
|
470
491
|
trust.command("list").action(async () => {
|
|
471
492
|
const { listTrust } = await import("@alisio/core");
|
|
@@ -665,7 +686,14 @@ program
|
|
|
665
686
|
await cliUpdateAll(common);
|
|
666
687
|
});
|
|
667
688
|
try {
|
|
668
|
-
|
|
689
|
+
// The compiled binary starts its own Forja process isolates with this hidden command (spec D-21).
|
|
690
|
+
// It is dispatched before any configuration or database is loaded and is not part of the CLI.
|
|
691
|
+
if (process.argv.slice(2)[0] === "__forge-isolate") {
|
|
692
|
+
const { runProcessIsolate } = await import("@alisio/core/forge-isolate");
|
|
693
|
+
await runProcessIsolate();
|
|
694
|
+
}
|
|
695
|
+
else
|
|
696
|
+
await program.parseAsync();
|
|
669
697
|
}
|
|
670
698
|
catch (error) {
|
|
671
699
|
console.error(error instanceof Error ? error.message : String(error));
|
package/dist/tui/artifacts.d.ts
CHANGED
|
@@ -51,4 +51,4 @@ export declare const installApprovalTitle: (install: InstallPreview) => string;
|
|
|
51
51
|
/** Body of the `analysis.install` approval: the packages, the download estimate and the network. */
|
|
52
52
|
export declare function installApprovalDetail(install: InstallPreview): string;
|
|
53
53
|
/** Title of a capability approval in the TUI picker. */
|
|
54
|
-
export declare const capabilityApprovalTitle: (runtime: "managed" | "oci" | undefined) => string;
|
|
54
|
+
export declare const capabilityApprovalTitle: (runtime: "auto" | "managed" | "oci" | "wasm" | undefined) => string;
|
package/dist/tui/artifacts.js
CHANGED
|
@@ -204,4 +204,8 @@ export function installApprovalDetail(install) {
|
|
|
204
204
|
/** Title of a capability approval in the TUI picker. */
|
|
205
205
|
export const capabilityApprovalTitle = (runtime) => runtime === "oci"
|
|
206
206
|
? "Run Python analysis (container · no network)?"
|
|
207
|
-
:
|
|
207
|
+
: runtime === "wasm"
|
|
208
|
+
? "Run Python analysis (wasm · restricted process, network not blocked)?"
|
|
209
|
+
: runtime === "auto"
|
|
210
|
+
? "Run Python analysis (most isolated backend ready; the host only if none · see the engine line)?"
|
|
211
|
+
: "Run Python analysis (managed · not sandboxed)?";
|
package/dist/tui/components.js
CHANGED
|
@@ -8,7 +8,7 @@ import { branchDisplay } from "./git-branch.js";
|
|
|
8
8
|
import { initialPlanReviewState, reducePlanReview, } from "./plan-review.js";
|
|
9
9
|
import { initialQuestionState, reduceQuestions, } from "./questions.js";
|
|
10
10
|
import { scrollWindow } from "./scroll.js";
|
|
11
|
-
import { contextLevel, contextPercent, editSummary, entryKeyOf, fitIdentityParts, fitSegments, foldCandidateOf, formatContext, formatDuration, formatTokens, groupIdentity, groupToolEntries, humanizeToolName, isUiBlock, previewLinesFor, previewRows, quietFor, REASONING_EXPAND_MAX_LINES, reasoningDurationMs, runPhase, STALLED_MS, } from "./state.js";
|
|
11
|
+
import { contextLevel, contextPercent, editSummary, entryKeyOf, fitIdentityParts, fitSegments, foldCandidateOf, forgeLabel, formatContext, formatDuration, formatTokens, groupIdentity, groupToolEntries, humanizeToolName, isUiBlock, previewLinesFor, previewRows, quietFor, REASONING_EXPAND_MAX_LINES, reasoningDurationMs, runPhase, STALLED_MS, } from "./state.js";
|
|
12
12
|
import { highlightCode, imageTheme, levelColor, markdownDefaultTextStyle, markdownTheme, markdownTransform, sanitizeCode, style, } from "./theme.js";
|
|
13
13
|
export const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
14
14
|
/** Shared animation clock advanced by the app while work is running. */
|
|
@@ -643,7 +643,9 @@ export class ToolBlock {
|
|
|
643
643
|
? style.gray(formatDuration(i.durationMs))
|
|
644
644
|
: "";
|
|
645
645
|
const progress = i.status === "running" && i.progress ? ` ${style.dim(`· ${i.progress}`)}` : "";
|
|
646
|
-
const
|
|
646
|
+
const forge = forgeLabel(i.forge);
|
|
647
|
+
const forgePart = forge ? ` ${style.dim(`· ${forge}`)}` : "";
|
|
648
|
+
const head = `${marker}${icon} ${style.bold(humanizeToolName(i.name))} ${style.gray(i.summary)}${forgePart}${progress}`;
|
|
647
649
|
const gap = width - visibleWidth(head) - visibleWidth(right) - 1;
|
|
648
650
|
const lines = [
|
|
649
651
|
gap > 0 ? `${head}${" ".repeat(gap + 1)}${right}` : truncateToWidth(head, width),
|
|
@@ -728,7 +730,7 @@ export class ToolGroupBlock {
|
|
|
728
730
|
: `${style.bold(member.humanName)} ${style.dim(`(${member.name})`)}`;
|
|
729
731
|
lines.push(` ${micon} ${name}${member.durationMs !== undefined
|
|
730
732
|
? ` ${style.gray(`· ${formatDuration(member.durationMs)}`)}`
|
|
731
|
-
: ""}${member.summary ? ` ${style.gray(member.summary)}` : ""}`);
|
|
733
|
+
: ""}${member.summary ? ` ${style.gray(member.summary)}` : ""}${member.forge ? ` ${style.dim(`· ${member.forge}`)}` : ""}`);
|
|
732
734
|
const rich = richBodyLines(member, width);
|
|
733
735
|
if (rich.lines.length) {
|
|
734
736
|
lines.push(...rich.lines.map((line) => ` ${line}`));
|
package/dist/tui/state.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Message, ModelInfo, RunEvent, ToolResult, TreeNode, UiBlock } from "@alisio/sdk";
|
|
1
|
+
import type { EngineId, Message, ModelInfo, RunEvent, ToolResult, TreeNode, UiBlock } from "@alisio/sdk";
|
|
2
2
|
export type Level = "ok" | "warn" | "danger";
|
|
3
3
|
export declare function formatTokens(n: number): string;
|
|
4
4
|
/** Prefixes every model with its owning provider so identical model ids are never ambiguous. */
|
|
@@ -317,6 +317,8 @@ export type TranscriptItem = {
|
|
|
317
317
|
toolKind?: ToolKind;
|
|
318
318
|
/** Last non-empty `tool_progress` line while the tool runs (cleared on completion). */
|
|
319
319
|
progress?: string;
|
|
320
|
+
/** What a Forja execution (`js_run`, `python_run`) reported; kept after completion. */
|
|
321
|
+
forge?: ForgeLine;
|
|
320
322
|
/** Exit code derived safely from the JSON preview when the tool reports one (run_process/shell/search_text). */
|
|
321
323
|
exitCode?: number;
|
|
322
324
|
} | {
|
|
@@ -377,6 +379,18 @@ export declare function reasoningDurationMs(item: {
|
|
|
377
379
|
reasoningStartedAt?: number;
|
|
378
380
|
reasoningDoneAt?: number;
|
|
379
381
|
}): number | undefined;
|
|
382
|
+
/** What the row of a Forja execution shows: nested calls, the script's own note and the engine. */
|
|
383
|
+
export interface ForgeLine {
|
|
384
|
+
engine: EngineId;
|
|
385
|
+
/** Highest nested call number seen. */
|
|
386
|
+
started: number;
|
|
387
|
+
/** Nested calls that ended (succeeded, failed or denied). */
|
|
388
|
+
finished: number;
|
|
389
|
+
/** The script's own `progress({ message, completed, total })`, bounded. */
|
|
390
|
+
note?: string;
|
|
391
|
+
}
|
|
392
|
+
/** `page 2 (2/9) · 2/3 calls · wasm`; empty when there is nothing to say yet. */
|
|
393
|
+
export declare function forgeLabel(forge: ForgeLine | undefined): string;
|
|
380
394
|
export declare function reduceEvent(state: ViewState, event: RunEvent): ViewState;
|
|
381
395
|
/** No event for this long: the footer says how long the run has been quiet. */
|
|
382
396
|
export declare const QUIET_MS = 15000;
|
|
@@ -460,6 +474,8 @@ export interface ToolItemView {
|
|
|
460
474
|
summary: string;
|
|
461
475
|
/** Last progress line while running. */
|
|
462
476
|
progress?: string;
|
|
477
|
+
/** `2/3 calls · wasm` of a Forja execution (running or finished). */
|
|
478
|
+
forge?: string;
|
|
463
479
|
status: ToolStatus;
|
|
464
480
|
durationMs?: number;
|
|
465
481
|
preview?: string;
|
package/dist/tui/state.js
CHANGED
|
@@ -580,6 +580,54 @@ function updateTool(state, id, update) {
|
|
|
580
580
|
items[index] = { ...item, ...update };
|
|
581
581
|
return { ...state, items };
|
|
582
582
|
}
|
|
583
|
+
const FORGE_NOTE_MAX = 40;
|
|
584
|
+
const isForgeProgress = (value) => !!value &&
|
|
585
|
+
typeof value === "object" &&
|
|
586
|
+
value.kind === "forge" &&
|
|
587
|
+
typeof value.engine === "string";
|
|
588
|
+
/** Folds one `ForgeProgress` into the line (terminal call events are never coalesced away). */
|
|
589
|
+
function foldForgeProgress(current, progress) {
|
|
590
|
+
const next = current
|
|
591
|
+
? { ...current, engine: progress.engine }
|
|
592
|
+
: { engine: progress.engine, started: 0, finished: 0 };
|
|
593
|
+
if (progress.call) {
|
|
594
|
+
next.started = Math.max(next.started, progress.call.callId);
|
|
595
|
+
if (progress.call.status !== "started")
|
|
596
|
+
next.finished += 1;
|
|
597
|
+
}
|
|
598
|
+
else if (progress.message || progress.total !== undefined) {
|
|
599
|
+
const fraction = progress.completed !== undefined && progress.total !== undefined
|
|
600
|
+
? ` (${progress.completed}/${progress.total})`
|
|
601
|
+
: "";
|
|
602
|
+
const message = (progress.message ?? "").slice(0, FORGE_NOTE_MAX);
|
|
603
|
+
next.note = `${message}${fraction}`.trim();
|
|
604
|
+
}
|
|
605
|
+
return next;
|
|
606
|
+
}
|
|
607
|
+
/** The isolation tag of a Python engine; nothing for JavaScript (it is always isolated). */
|
|
608
|
+
const FORGE_ENGINE_TAG = {
|
|
609
|
+
"python-oci": "oci",
|
|
610
|
+
"python-wasm": "wasm",
|
|
611
|
+
"python-managed": "host!",
|
|
612
|
+
};
|
|
613
|
+
/** `page 2 (2/9) · 2/3 calls · wasm`; empty when there is nothing to say yet. */
|
|
614
|
+
export function forgeLabel(forge) {
|
|
615
|
+
if (!forge)
|
|
616
|
+
return "";
|
|
617
|
+
return [
|
|
618
|
+
forge.note,
|
|
619
|
+
forge.started > 0 ? `${forge.finished}/${forge.started} calls` : undefined,
|
|
620
|
+
FORGE_ENGINE_TAG[forge.engine],
|
|
621
|
+
]
|
|
622
|
+
.filter(Boolean)
|
|
623
|
+
.join(" · ");
|
|
624
|
+
}
|
|
625
|
+
const ENGINE_LINE = /^engine: (python-oci|python-wasm|python-managed) \(/m;
|
|
626
|
+
/** The Python engine a result text names on its last line (PD11), for rows without progress. */
|
|
627
|
+
const engineOfResult = (text) => {
|
|
628
|
+
const engine = typeof text === "string" ? ENGINE_LINE.exec(text)?.[1] : undefined;
|
|
629
|
+
return engine ? { engine: engine, started: 0, finished: 0 } : undefined;
|
|
630
|
+
};
|
|
583
631
|
/** Longest progress line kept on a running tool row. */
|
|
584
632
|
const PROGRESS_LINE_MAX = 80;
|
|
585
633
|
/** The last non-empty line of a `tool_progress` payload, bounded; empty when it has none. */
|
|
@@ -733,6 +781,11 @@ function reduceEventItems(state, event) {
|
|
|
733
781
|
});
|
|
734
782
|
}
|
|
735
783
|
case "tool_progress": {
|
|
784
|
+
if (isForgeProgress(d.data)) {
|
|
785
|
+
const row = state.items.findLast((i) => i.kind === "tool" && i.id === d.id);
|
|
786
|
+
const before = row?.kind === "tool" ? row.forge : undefined;
|
|
787
|
+
return updateTool(state, d.id, { forge: foldForgeProgress(before, d.data) });
|
|
788
|
+
}
|
|
736
789
|
const line = lastProgressLine(d.data);
|
|
737
790
|
return line ? updateTool(state, d.id, { progress: line }) : state;
|
|
738
791
|
}
|
|
@@ -757,6 +810,9 @@ function reduceEventItems(state, event) {
|
|
|
757
810
|
status: d.isError ? "error" : "ok",
|
|
758
811
|
...(typeof d.durationMs === "number" ? { durationMs: d.durationMs } : {}),
|
|
759
812
|
...(typeof d.preview === "string" ? { preview: d.preview } : {}),
|
|
813
|
+
...(running?.kind === "tool" && !running.forge && engineOfResult(d.preview)
|
|
814
|
+
? { forge: engineOfResult(d.preview) }
|
|
815
|
+
: {}),
|
|
760
816
|
...exitCodePart(d.preview),
|
|
761
817
|
...(isUiBlock(d.ui) ? { ui: d.ui } : {}),
|
|
762
818
|
...(isImagePart(d.image) ? { image: d.image } : {}),
|
|
@@ -1186,6 +1242,7 @@ export function toolItemView(item) {
|
|
|
1186
1242
|
kind: item.toolKind ?? toolKindOf(item.name),
|
|
1187
1243
|
summary: item.summary,
|
|
1188
1244
|
...(item.progress ? { progress: item.progress } : {}),
|
|
1245
|
+
...(forgeLabel(item.forge) ? { forge: forgeLabel(item.forge) } : {}),
|
|
1189
1246
|
status: item.status,
|
|
1190
1247
|
...(item.durationMs !== undefined ? { durationMs: item.durationMs } : {}),
|
|
1191
1248
|
...(item.preview !== undefined ? { preview: item.preview } : {}),
|
|
@@ -1351,11 +1408,13 @@ export function itemsFromHistory(messages) {
|
|
|
1351
1408
|
items.push({ kind: "assistant", text: m.text, reasoning: "", done: true });
|
|
1352
1409
|
for (const c of m.calls) {
|
|
1353
1410
|
const r = results.get(c.id);
|
|
1354
|
-
const
|
|
1411
|
+
const text = r?.result.content
|
|
1355
1412
|
.filter((p) => p.type === "text")
|
|
1356
1413
|
.map((x) => x.text)
|
|
1357
|
-
.join("\n")
|
|
1358
|
-
|
|
1414
|
+
.join("\n");
|
|
1415
|
+
const preview = text?.slice(0, 2_000);
|
|
1416
|
+
// The engine line closes a python_run result (PD11); read it before the preview clips it.
|
|
1417
|
+
const forge = c.name === "python_run" ? engineOfResult(text) : undefined;
|
|
1359
1418
|
items.push({
|
|
1360
1419
|
kind: "tool",
|
|
1361
1420
|
id: c.id,
|
|
@@ -1365,6 +1424,7 @@ export function itemsFromHistory(messages) {
|
|
|
1365
1424
|
status: r?.result.isError ? "error" : "ok",
|
|
1366
1425
|
preview,
|
|
1367
1426
|
toolKind: toolKindOf(c.name),
|
|
1427
|
+
...(forge ? { forge } : {}),
|
|
1368
1428
|
...exitCodePart(preview),
|
|
1369
1429
|
...richPartsOf(r?.result, c.name),
|
|
1370
1430
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alisio/alisio-code",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
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,12 +45,12 @@
|
|
|
45
45
|
"alisio": "./dist/main.js"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@alisio/core": "0.
|
|
49
|
-
"@alisio/plugin-memory": "0.
|
|
50
|
-
"@alisio/plugin-openai-compatible": "0.
|
|
51
|
-
"@alisio/plugin-subagents": "0.
|
|
52
|
-
"@alisio/sdk": "0.
|
|
53
|
-
"@alisio/server": "0.
|
|
48
|
+
"@alisio/core": "0.6.0",
|
|
49
|
+
"@alisio/plugin-memory": "0.6.0",
|
|
50
|
+
"@alisio/plugin-openai-compatible": "0.6.0",
|
|
51
|
+
"@alisio/plugin-subagents": "0.6.0",
|
|
52
|
+
"@alisio/sdk": "0.6.0",
|
|
53
|
+
"@alisio/server": "0.6.0",
|
|
54
54
|
"@earendil-works/pi-tui": "0.87.1",
|
|
55
55
|
"commander": "15.0.0"
|
|
56
56
|
},
|