@timurproko/a1 0.1.1-dev.11 → 0.1.1-dev.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -8
- package/bin/cli.js +14 -2
- package/bin/module-identity.js +75 -0
- package/bin/ui.js +8 -0
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/src/cli/capabilities.d.ts +15 -0
- package/dist/src/cli/capabilities.js +7 -0
- package/dist/src/cli/dispatch.d.ts +9 -3
- package/dist/src/cli/dispatch.js +104 -16
- package/dist/src/cli/index.d.ts +2 -0
- package/dist/src/cli/index.js +2 -0
- package/dist/src/cli/packages.d.ts +23 -0
- package/dist/src/cli/packages.js +84 -0
- package/dist/src/foundation/agent-engine-contracts/index.d.ts +1 -0
- package/dist/src/foundation/agent-engine-contracts/index.js +1 -0
- package/dist/src/foundation/agent-engine-contracts/package-ports.d.ts +54 -0
- package/dist/src/foundation/agent-engine-contracts/package-ports.js +31 -0
- package/dist/src/foundation/pi-component-adapter/shell-presenters-transcript.d.ts +15 -0
- package/dist/src/foundation/pi-component-adapter/shell-presenters-transcript.js +33 -0
- package/dist/src/foundation/pi-engine-adapter/adapter.d.ts +8 -1
- package/dist/src/foundation/pi-engine-adapter/adapter.js +28 -1
- package/dist/src/foundation/pi-engine-adapter/index.d.ts +1 -0
- package/dist/src/foundation/pi-engine-adapter/index.js +1 -0
- package/dist/src/foundation/pi-engine-adapter/package-integration.d.ts +13 -0
- package/dist/src/foundation/pi-engine-adapter/package-integration.js +112 -0
- package/dist/src/foundation/pi-engine-adapter/runtime-integration.d.ts +18 -1
- package/dist/src/foundation/pi-engine-adapter/runtime-integration.js +37 -2
- package/dist/src/foundation/pi-owned-ui-integration/session-shell.js +14 -2
- package/docs/features/launch-profiles.md +2 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -10,22 +10,48 @@ npm install --global @timurproko/a1@latest
|
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
12
|
a1 # A1-owned UI and profile: ~/.a1/agent
|
|
13
|
-
a1 pi # untouched vanilla Pi oracle: ~/.pi/agent
|
|
14
|
-
a1 sandbox # unchanged isolated vanilla Pi profile: ~/.a1/sandbox
|
|
15
13
|
a1 version # show Installed, Release (latest), and Next versions
|
|
16
14
|
a1 update # update to npm latest
|
|
17
15
|
a1 update:next # update to npm next
|
|
18
16
|
```
|
|
19
17
|
|
|
18
|
+
Prerelease builds — what `a1 update:next` installs — add two development profiles
|
|
19
|
+
for comparing against pinned Pi and for experimenting against an isolated profile.
|
|
20
|
+
A release build does not carry them.
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
a1 pi # untouched vanilla Pi oracle: ~/.pi/agent
|
|
24
|
+
a1 sandbox # unchanged isolated vanilla Pi profile: ~/.a1/sandbox
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Extensions
|
|
28
|
+
|
|
29
|
+
Pi extension packages install into A1's own profile, so bare `a1` loads them and
|
|
30
|
+
`a1 pi` and `a1 sandbox` do not. Sources are Pi's: `npm:`, git, or a local path.
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
a1 install npm:pi-mcp-adapter # install a package into ~/.a1/agent
|
|
34
|
+
a1 remove npm:pi-mcp-adapter # remove it again (alias: a1 uninstall)
|
|
35
|
+
a1 list # list packages installed for a1
|
|
36
|
+
a1 update --extensions # update every installed package
|
|
37
|
+
a1 update npm:pi-mcp-adapter # update one of them
|
|
38
|
+
a1 update --models # refresh model catalogs
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
A running session loads a newly installed package after a restart. Pi's own
|
|
42
|
+
profile at `~/.pi/agent` is managed by Pi itself.
|
|
43
|
+
|
|
20
44
|
## Develop
|
|
21
45
|
|
|
22
46
|
```sh
|
|
23
|
-
npm ci
|
|
24
|
-
npm run build
|
|
25
|
-
npm start
|
|
26
|
-
npm run
|
|
27
|
-
npm
|
|
28
|
-
npm run test:
|
|
47
|
+
npm ci # install exact locked dependencies
|
|
48
|
+
npm run build # compile TypeScript and the process guardian into dist
|
|
49
|
+
npm start # build and launch an isolated development `a1`
|
|
50
|
+
npm run start:pi # build and launch an isolated development `a1 pi`
|
|
51
|
+
npm run start:sandbox # build and launch an isolated development `a1 sandbox`
|
|
52
|
+
npm run test:fast # typecheck + fast suite, no build needed
|
|
53
|
+
npm test # same as test:fast
|
|
54
|
+
npm run test:full # complete non-physical suite
|
|
29
55
|
```
|
|
30
56
|
|
|
31
57
|
## Release
|
package/bin/cli.js
CHANGED
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
const packageRoot = new URL("..", import.meta.url);
|
|
4
4
|
const { fileURLToPath } = await import("node:url");
|
|
5
|
-
const {
|
|
5
|
+
const { readFile } = await import("node:fs/promises");
|
|
6
|
+
const { cliCapabilities, dispatchCli } = await import("../dist/src/cli/index.js");
|
|
7
|
+
|
|
8
|
+
// Which commands this build exposes follows from the build's own version, so a
|
|
9
|
+
// released a1 cannot be argued into offering the development profiles.
|
|
10
|
+
const capabilities = cliCapabilities(JSON.parse(await readFile(new URL("package.json", packageRoot), "utf8")).version);
|
|
6
11
|
|
|
7
12
|
process.exitCode = await dispatchCli(process.argv.slice(2), {
|
|
8
13
|
launch: async intent => {
|
|
@@ -21,6 +26,13 @@ process.exitCode = await dispatchCli(process.argv.slice(2), {
|
|
|
21
26
|
const { runSelfUpdate } = await import("../dist/src/foundation/release/index.js");
|
|
22
27
|
return await runSelfUpdate({ packageRoot: fileURLToPath(packageRoot), channel });
|
|
23
28
|
},
|
|
29
|
+
packages: async request => {
|
|
30
|
+
const [{ runPackageCommand }, { createPiPackagesPort }] = await Promise.all([
|
|
31
|
+
import("../dist/src/cli/index.js"),
|
|
32
|
+
import("../dist/src/foundation/pi-engine-adapter/index.js"),
|
|
33
|
+
]);
|
|
34
|
+
return await runPackageCommand(request, { createPort: createPiPackagesPort });
|
|
35
|
+
},
|
|
24
36
|
}, {
|
|
25
37
|
stderr: message => process.stderr.write(message),
|
|
26
|
-
});
|
|
38
|
+
}, capabilities);
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One pi-tui module identity per process.
|
|
3
|
+
*
|
|
4
|
+
* npm can materialize @earendil-works/pi-tui twice under A1's package root:
|
|
5
|
+
* once as A1's direct dependency at the node_modules root, and once nested
|
|
6
|
+
* inside @earendil-works/pi-coding-agent's isolated dependency tree. A1's
|
|
7
|
+
* owned UI imports the root copy while pinned Pi's extension loader hands
|
|
8
|
+
* extensions the nested copy, so every TUI class exists twice: `instanceof`
|
|
9
|
+
* checks and prototype patches made by extensions land on classes the
|
|
10
|
+
* renderer never uses — extension chrome silently disappears and routed
|
|
11
|
+
* input dead-ends.
|
|
12
|
+
*
|
|
13
|
+
* Repair runs at launch, not at install: npm's `prepare` hook is skipped for
|
|
14
|
+
* registry installs, so an installed A1 must self-heal the same way a source
|
|
15
|
+
* checkout does. The root copy is replaced with a junction (Windows) or
|
|
16
|
+
* directory symlink to the nested copy so every loader resolves the same
|
|
17
|
+
* files and therefore the same module instances.
|
|
18
|
+
*
|
|
19
|
+
* This lives in bin/ (shipped, plain JS) rather than src/ deliberately: its
|
|
20
|
+
* whole job is repairing the node_modules layout, which the Pi API boundary
|
|
21
|
+
* policy rightly forbids ordinary production code from touching.
|
|
22
|
+
*
|
|
23
|
+
* The repair is idempotent and fail-open: a hoisted tree (single copy), an
|
|
24
|
+
* already-linked root, a version mismatch, or a filesystem that refuses the
|
|
25
|
+
* link all leave the tree as it was — launch proceeds with a warning rather
|
|
26
|
+
* than failing, because a degraded UI beats no UI.
|
|
27
|
+
*/
|
|
28
|
+
import { existsSync, lstatSync, readFileSync, renameSync, rmSync, symlinkSync } from "node:fs";
|
|
29
|
+
import { join } from "node:path";
|
|
30
|
+
|
|
31
|
+
function packageVersion(directory) {
|
|
32
|
+
return JSON.parse(readFileSync(join(directory, "package.json"), "utf8")).version;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Collapse a duplicated @earendil-works/pi-tui under `packageRoot` onto the
|
|
37
|
+
* copy pinned Pi resolves, so extensions and the owned UI share one module
|
|
38
|
+
* instance. Must run before anything in the process imports pi-tui.
|
|
39
|
+
* Returns a discriminated outcome; never throws.
|
|
40
|
+
*/
|
|
41
|
+
export function ensureSinglePiTuiModule(packageRoot) {
|
|
42
|
+
const rootCopy = join(packageRoot, "node_modules", "@earendil-works", "pi-tui");
|
|
43
|
+
const nestedCopy = join(
|
|
44
|
+
packageRoot,
|
|
45
|
+
"node_modules", "@earendil-works", "pi-coding-agent",
|
|
46
|
+
"node_modules", "@earendil-works", "pi-tui",
|
|
47
|
+
);
|
|
48
|
+
try {
|
|
49
|
+
if (!existsSync(nestedCopy)) return { kind: "single-copy" };
|
|
50
|
+
if (existsSync(rootCopy) && lstatSync(rootCopy).isSymbolicLink()) return { kind: "already-linked" };
|
|
51
|
+
if (existsSync(rootCopy)) {
|
|
52
|
+
const rootVersion = packageVersion(rootCopy);
|
|
53
|
+
const nestedVersion = packageVersion(nestedCopy);
|
|
54
|
+
if (rootVersion !== nestedVersion) return { kind: "version-mismatch", rootVersion, nestedVersion };
|
|
55
|
+
const retired = `${rootCopy}.duplicate`;
|
|
56
|
+
rmSync(retired, { recursive: true, force: true });
|
|
57
|
+
renameSync(rootCopy, retired);
|
|
58
|
+
rmSync(retired, { recursive: true, force: true });
|
|
59
|
+
}
|
|
60
|
+
symlinkSync(nestedCopy, rootCopy, "junction");
|
|
61
|
+
return { kind: "linked" };
|
|
62
|
+
} catch (error) {
|
|
63
|
+
return { kind: "failed", message: error instanceof Error ? error.message : String(error) };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Launch-entry wrapper: repair, and warn on stderr when the tree stays split. */
|
|
68
|
+
export function ensureSinglePiTuiModuleAtLaunch(packageRoot, warn) {
|
|
69
|
+
const outcome = ensureSinglePiTuiModule(packageRoot);
|
|
70
|
+
if (outcome.kind === "version-mismatch") {
|
|
71
|
+
warn(`a1: pi-tui is duplicated at incompatible versions (${outcome.rootVersion} vs ${outcome.nestedVersion}); extension UI may not render.\n`);
|
|
72
|
+
} else if (outcome.kind === "failed") {
|
|
73
|
+
warn(`a1: could not unify the duplicated pi-tui module (${outcome.message}); extension UI may not render.\n`);
|
|
74
|
+
}
|
|
75
|
+
}
|
package/bin/ui.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
const { ensureSinglePiTuiModuleAtLaunch } = await import("./module-identity.js");
|
|
4
|
+
const { fileURLToPath } = await import("node:url");
|
|
5
|
+
|
|
6
|
+
// Before the composition loads pinned Pi's terminal stack: collapse npm's
|
|
7
|
+
// duplicated copies of it so extensions and the owned UI share one module
|
|
8
|
+
// identity (see bin/module-identity.js for the full story).
|
|
9
|
+
ensureSinglePiTuiModuleAtLaunch(fileURLToPath(new URL("..", import.meta.url)), message => process.stderr.write(message));
|
|
10
|
+
|
|
3
11
|
const { runSelectedInteractiveRuntime } = await import("../dist/src/features/launch/index.js");
|
|
4
12
|
|
|
5
13
|
runSelectedInteractiveRuntime(process.env.A1_LAUNCH_PROFILE ?? "a1", {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"platform": "darwin",
|
|
6
6
|
"architecture": "arm64",
|
|
7
7
|
"capability": "unsupported",
|
|
8
|
-
"builtAt": "2026-08-
|
|
8
|
+
"builtAt": "2026-08-23T14:26:10.351Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian",
|
|
11
11
|
"sha256": "7524bf568992517f50ed53196c861c5edeba0d7275633bb4735ab45bd5963a28",
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"platform": "linux",
|
|
6
6
|
"architecture": "x64",
|
|
7
7
|
"capability": "supported",
|
|
8
|
-
"builtAt": "2026-08-
|
|
8
|
+
"builtAt": "2026-08-23T14:26:49.615Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian",
|
|
11
11
|
"sha256": "29e22fe29de2828982bc67ef418c4adcaab1490281477b7bea592ec6fe621bcd",
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
"platform": "win32",
|
|
6
6
|
"architecture": "x64",
|
|
7
7
|
"capability": "supported",
|
|
8
|
-
"builtAt": "2026-08-
|
|
8
|
+
"builtAt": "2026-08-23T14:26:50.453Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian.exe",
|
|
11
|
-
"sha256": "
|
|
11
|
+
"sha256": "1073746062839c69cc4de5df47cc2eecc9211f26e80518395b2987f958c86a02",
|
|
12
12
|
"size": 172544
|
|
13
13
|
},
|
|
14
14
|
"provenance": {
|
|
Binary file
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pi` and `sandbox` exist to compare A1 against pinned Pi and to try resources
|
|
3
|
+
* against an isolated profile. Both are development instruments rather than
|
|
4
|
+
* product, so a stable release does not carry them: what a released `a1` exposes
|
|
5
|
+
* is the product plus its maintenance and package commands.
|
|
6
|
+
*
|
|
7
|
+
* Which build this is comes from its own version. A prerelease version is a
|
|
8
|
+
* `next`-channel build, which is where that work happens; a release version is
|
|
9
|
+
* not. Nothing to configure, and no way for a released build to be talked into it.
|
|
10
|
+
*/
|
|
11
|
+
export interface CliCapabilities {
|
|
12
|
+
readonly developmentProfiles: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function cliCapabilities(version: string): CliCapabilities;
|
|
15
|
+
export declare function isPrereleaseVersion(version: string): boolean;
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { type InteractiveLaunchIntent, type LaunchProfileId } from "../features/launch/index.js";
|
|
2
|
+
import type { CliCapabilities } from "./capabilities.js";
|
|
3
|
+
import type { PackageCommandRequest } from "./packages.js";
|
|
2
4
|
export type UpdateChannel = "stable" | "next";
|
|
3
5
|
export interface CliHandlers {
|
|
4
6
|
readonly launch: (intent: InteractiveLaunchIntent) => Promise<number>;
|
|
5
7
|
readonly version: () => Promise<number>;
|
|
6
8
|
readonly update: (channel: UpdateChannel) => Promise<number>;
|
|
9
|
+
readonly packages: (request: PackageCommandRequest) => Promise<number>;
|
|
7
10
|
}
|
|
8
11
|
export interface CliOutput {
|
|
9
12
|
readonly stderr: (message: string) => void;
|
|
10
13
|
}
|
|
11
|
-
export declare
|
|
12
|
-
export declare function dispatchCli(arguments_: readonly string[], handlers: CliHandlers, output: CliOutput): Promise<number>;
|
|
14
|
+
export declare function cliUsage(capabilities: CliCapabilities): string;
|
|
15
|
+
export declare function dispatchCli(arguments_: readonly string[], handlers: CliHandlers, output: CliOutput, capabilities: CliCapabilities): Promise<number>;
|
|
13
16
|
export type CliCommand = {
|
|
14
17
|
readonly kind: "launch";
|
|
15
18
|
readonly profileId: LaunchProfileId;
|
|
@@ -18,8 +21,11 @@ export type CliCommand = {
|
|
|
18
21
|
} | {
|
|
19
22
|
readonly kind: "update";
|
|
20
23
|
readonly channel: UpdateChannel;
|
|
24
|
+
} | {
|
|
25
|
+
readonly kind: "packages";
|
|
26
|
+
readonly request: PackageCommandRequest;
|
|
21
27
|
} | {
|
|
22
28
|
readonly kind: "error";
|
|
23
29
|
readonly message: string;
|
|
24
30
|
};
|
|
25
|
-
export declare function parseCliCommand(arguments_: readonly string[]): CliCommand;
|
|
31
|
+
export declare function parseCliCommand(arguments_: readonly string[], capabilities: CliCapabilities): CliCommand;
|
package/dist/src/cli/dispatch.js
CHANGED
|
@@ -1,35 +1,123 @@
|
|
|
1
1
|
import { interactiveLaunchIntent } from "../features/launch/index.js";
|
|
2
2
|
import { PRODUCT_TEXT } from "../product-identity.js";
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
export function cliUsage(capabilities) {
|
|
4
|
+
return PRODUCT_TEXT.usage([
|
|
5
|
+
"",
|
|
6
|
+
...(capabilities.developmentProfiles ? ["pi", "sandbox"] : []),
|
|
7
|
+
"version",
|
|
8
|
+
"update [self|<source>|--extensions|--models]",
|
|
9
|
+
"update:next",
|
|
10
|
+
"install <source>",
|
|
11
|
+
"remove <source>",
|
|
12
|
+
"list",
|
|
13
|
+
]);
|
|
14
|
+
}
|
|
15
|
+
const PROFILE_WORDS = new Set(["pi", "sandbox"]);
|
|
16
|
+
export async function dispatchCli(arguments_, handlers, output, capabilities) {
|
|
17
|
+
const command = parseCliCommand(arguments_, capabilities);
|
|
6
18
|
if (command.kind === "error") {
|
|
7
|
-
output.stderr(`${command.message}\n${
|
|
19
|
+
output.stderr(`${command.message}\n${cliUsage(capabilities)}\n`);
|
|
8
20
|
return 2;
|
|
9
21
|
}
|
|
10
22
|
if (command.kind === "launch")
|
|
11
23
|
return await handlers.launch(interactiveLaunchIntent(command.profileId));
|
|
12
24
|
if (command.kind === "version")
|
|
13
25
|
return await handlers.version();
|
|
26
|
+
if (command.kind === "packages")
|
|
27
|
+
return await handlers.packages(command.request);
|
|
14
28
|
return await handlers.update(command.channel);
|
|
15
29
|
}
|
|
16
|
-
export function parseCliCommand(arguments_) {
|
|
30
|
+
export function parseCliCommand(arguments_, capabilities) {
|
|
17
31
|
if (arguments_.length === 0)
|
|
18
32
|
return { kind: "launch", profileId: "a1" };
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return { kind: "launch", profileId: command };
|
|
24
|
-
if (command === "ui")
|
|
25
|
-
return { kind: "error", message: `The ui subcommand was removed; run bare ${PRODUCT_TEXT.commandName} for the owned UI.` };
|
|
33
|
+
const [command, ...rest] = arguments_;
|
|
34
|
+
if (capabilities.developmentProfiles && (command === "pi" || command === "sandbox")) {
|
|
35
|
+
return withoutArguments(rest, { kind: "launch", profileId: command });
|
|
36
|
+
}
|
|
26
37
|
if (command === "version")
|
|
27
|
-
return { kind: "version" };
|
|
28
|
-
if (command === "update")
|
|
29
|
-
return { kind: "update", channel: "stable" };
|
|
38
|
+
return withoutArguments(rest, { kind: "version" });
|
|
30
39
|
if (command === "update:next")
|
|
31
|
-
return { kind: "update", channel: "next" };
|
|
40
|
+
return withoutArguments(rest, { kind: "update", channel: "next" });
|
|
41
|
+
if (command === "update")
|
|
42
|
+
return parseUpdate(rest);
|
|
43
|
+
if (command === "install" || command === "remove" || command === "uninstall") {
|
|
44
|
+
return parseSourceCommand(command === "install" ? "install" : "remove", rest);
|
|
45
|
+
}
|
|
46
|
+
if (command === "list")
|
|
47
|
+
return withoutArguments(rest, { kind: "packages", request: { verb: "list", source: null } });
|
|
48
|
+
if (command === "ui")
|
|
49
|
+
return { kind: "error", message: `The ui subcommand was removed; run bare ${PRODUCT_TEXT.commandName} for the owned UI.` };
|
|
32
50
|
if (command === "agent")
|
|
33
51
|
return { kind: "error", message: `Bare ${PRODUCT_TEXT.commandName} is the ${PRODUCT_TEXT.displayName} agent experience; there is no agent subcommand.` };
|
|
34
52
|
return { kind: "error", message: PRODUCT_TEXT.diagnostic(`received an unknown command: ${command ?? ""}`) };
|
|
35
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* `update` carries both meanings pinned Pi gives it: itself by default, and the
|
|
56
|
+
* profile's packages when a target says so. Pi is refused as a target because A1
|
|
57
|
+
* certifies each release against one pinned Pi, so moving Pi underneath it would
|
|
58
|
+
* invalidate what was certified.
|
|
59
|
+
*/
|
|
60
|
+
function parseUpdate(rest) {
|
|
61
|
+
if (rest.length === 0)
|
|
62
|
+
return { kind: "update", channel: "stable" };
|
|
63
|
+
if (rest.length > 1)
|
|
64
|
+
return { kind: "error", message: PRODUCT_TEXT.diagnostic("update accepts one target.") };
|
|
65
|
+
const [target] = rest;
|
|
66
|
+
if (target === "self")
|
|
67
|
+
return { kind: "update", channel: "stable" };
|
|
68
|
+
if (target === "pi") {
|
|
69
|
+
return {
|
|
70
|
+
kind: "error",
|
|
71
|
+
message: PRODUCT_TEXT.diagnostic(`pins the Pi version it was certified against; run ${PRODUCT_TEXT.commandName} update to move ${PRODUCT_TEXT.displayName} itself.`),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
// A release channel is spelled with the colon. Taking the bare word as a package
|
|
75
|
+
// source would turn a near miss into a confident search for a package nobody has.
|
|
76
|
+
if (target === "next" || target === "stable") {
|
|
77
|
+
const form = target === "next" ? `${PRODUCT_TEXT.commandName} update:next` : `${PRODUCT_TEXT.commandName} update`;
|
|
78
|
+
return { kind: "error", message: PRODUCT_TEXT.diagnostic(`selects a release channel with a colon; run ${form}.`) };
|
|
79
|
+
}
|
|
80
|
+
if (target === "--extensions")
|
|
81
|
+
return { kind: "packages", request: { verb: "update", source: null } };
|
|
82
|
+
if (target === "--models")
|
|
83
|
+
return { kind: "packages", request: { verb: "refresh-models", source: null } };
|
|
84
|
+
if (target === undefined || target.startsWith("-"))
|
|
85
|
+
return unknownOption(target ?? "", "update");
|
|
86
|
+
if (PROFILE_WORDS.has(target))
|
|
87
|
+
return profileRejection("update");
|
|
88
|
+
return { kind: "packages", request: { verb: "update", source: target } };
|
|
89
|
+
}
|
|
90
|
+
function parseSourceCommand(verb, rest) {
|
|
91
|
+
const flag = rest.find(argument => argument.startsWith("-"));
|
|
92
|
+
if (flag !== undefined)
|
|
93
|
+
return unknownOption(flag, verb);
|
|
94
|
+
if (rest.length === 0)
|
|
95
|
+
return { kind: "error", message: PRODUCT_TEXT.diagnostic(`${verb} requires a package source.`) };
|
|
96
|
+
if (rest.length > 1)
|
|
97
|
+
return { kind: "error", message: PRODUCT_TEXT.diagnostic(`${verb} accepts one package source.`) };
|
|
98
|
+
const [source] = rest;
|
|
99
|
+
if (source === undefined)
|
|
100
|
+
return { kind: "error", message: PRODUCT_TEXT.diagnostic(`${verb} requires a package source.`) };
|
|
101
|
+
if (PROFILE_WORDS.has(source))
|
|
102
|
+
return profileRejection(verb);
|
|
103
|
+
return { kind: "packages", request: { verb, source } };
|
|
104
|
+
}
|
|
105
|
+
function withoutArguments(rest, command) {
|
|
106
|
+
if (rest.length === 0)
|
|
107
|
+
return command;
|
|
108
|
+
const [argument] = rest;
|
|
109
|
+
if (argument !== undefined && (PROFILE_WORDS.has(argument) || argument.startsWith("--profile")))
|
|
110
|
+
return profileRejection("list");
|
|
111
|
+
return { kind: "error", message: PRODUCT_TEXT.diagnostic("commands do not accept additional arguments.") };
|
|
112
|
+
}
|
|
113
|
+
function profileRejection(verb) {
|
|
114
|
+
return {
|
|
115
|
+
kind: "error",
|
|
116
|
+
message: PRODUCT_TEXT.diagnostic(`manages packages in its own profile, so ${verb} takes no profile; Pi manages Pi's own profile.`),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function unknownOption(option, verb) {
|
|
120
|
+
if (option.startsWith("--profile"))
|
|
121
|
+
return profileRejection(verb);
|
|
122
|
+
return { kind: "error", message: PRODUCT_TEXT.diagnostic(`received an unknown option for ${verb}: ${option}`) };
|
|
123
|
+
}
|
package/dist/src/cli/index.d.ts
CHANGED
package/dist/src/cli/index.js
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { initializeProductProfile } from "../features/launch/index.js";
|
|
2
|
+
import type { AgentPackageOutcome, AgentPackagesPort, AgentPackagesPortInput } from "../foundation/agent-engine-contracts/index.js";
|
|
3
|
+
/**
|
|
4
|
+
* Package commands manage A1's own profile and nothing else, so no request carries
|
|
5
|
+
* a profile: `update` with no source means every installed package, and refreshing
|
|
6
|
+
* model catalogs is its own verb rather than a flag the caller has to remember to
|
|
7
|
+
* check.
|
|
8
|
+
*/
|
|
9
|
+
export type PackageCommandVerb = "install" | "remove" | "list" | "update" | "refresh-models";
|
|
10
|
+
export interface PackageCommandRequest {
|
|
11
|
+
readonly verb: PackageCommandVerb;
|
|
12
|
+
readonly source: string | null;
|
|
13
|
+
}
|
|
14
|
+
export interface PackageCommandEnvironment {
|
|
15
|
+
readonly createPort: (input: AgentPackagesPortInput) => AgentPackagesPort;
|
|
16
|
+
readonly cwd?: string;
|
|
17
|
+
readonly environment?: NodeJS.ProcessEnv;
|
|
18
|
+
readonly stdout?: (message: string) => void;
|
|
19
|
+
readonly stderr?: (message: string) => void;
|
|
20
|
+
readonly initializeProfile?: typeof initializeProductProfile;
|
|
21
|
+
}
|
|
22
|
+
export declare function runPackageCommand(request: PackageCommandRequest, environment: PackageCommandEnvironment): Promise<number>;
|
|
23
|
+
export declare function renderPackageOutcome(outcome: AgentPackageOutcome, profileRoot: string): string;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { configurationRootForProfile, initializeProductProfile, resolveLaunchProfilePaths } from "../features/launch/index.js";
|
|
2
|
+
import { PRODUCT_TEXT } from "../product-identity.js";
|
|
3
|
+
export async function runPackageCommand(request, environment) {
|
|
4
|
+
const stdout = environment.stdout ?? (message => process.stdout.write(message));
|
|
5
|
+
const stderr = environment.stderr ?? (message => process.stderr.write(message));
|
|
6
|
+
const cwd = environment.cwd ?? process.cwd();
|
|
7
|
+
const processEnvironment = environment.environment ?? process.env;
|
|
8
|
+
const paths = resolveLaunchProfilePaths({ environment: processEnvironment });
|
|
9
|
+
const profileRoot = configurationRootForProfile("a1", paths);
|
|
10
|
+
if (profileRoot === null)
|
|
11
|
+
throw new Error(PRODUCT_TEXT.diagnostic("has no profile root for package commands"));
|
|
12
|
+
try {
|
|
13
|
+
await (environment.initializeProfile ?? initializeProductProfile)(profileRoot);
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
stderr(`${PRODUCT_TEXT.diagnostic(`could not prepare its profile at ${profileRoot}: ${message(error)}`)}\n`);
|
|
17
|
+
return 1;
|
|
18
|
+
}
|
|
19
|
+
const port = environment.createPort({
|
|
20
|
+
profileRoot,
|
|
21
|
+
cwd,
|
|
22
|
+
onProgress: progress => stdout(`${progress.message}\n`),
|
|
23
|
+
});
|
|
24
|
+
const outcome = await runVerb(port, request);
|
|
25
|
+
const rendered = renderPackageOutcome(outcome, profileRoot);
|
|
26
|
+
(outcome.status === "completed" ? stdout : stderr)(rendered);
|
|
27
|
+
return outcome.status === "completed" ? 0 : 1;
|
|
28
|
+
}
|
|
29
|
+
async function runVerb(port, request) {
|
|
30
|
+
if (request.verb === "list")
|
|
31
|
+
return await port.list();
|
|
32
|
+
if (request.verb === "refresh-models")
|
|
33
|
+
return await port.refreshModels();
|
|
34
|
+
if (request.verb === "update")
|
|
35
|
+
return await port.update(request.source ?? undefined);
|
|
36
|
+
if (request.source === null)
|
|
37
|
+
throw new Error(PRODUCT_TEXT.diagnostic(`requires a source for ${request.verb}`));
|
|
38
|
+
return request.verb === "install" ? await port.install(request.source) : await port.remove(request.source);
|
|
39
|
+
}
|
|
40
|
+
export function renderPackageOutcome(outcome, profileRoot) {
|
|
41
|
+
const name = PRODUCT_TEXT.displayName;
|
|
42
|
+
if (outcome.status === "failed") {
|
|
43
|
+
return `${PRODUCT_TEXT.diagnostic(`could not ${describeOperation(outcome.operation)}: ${outcome.detail ?? "unknown failure"}`)}\n`;
|
|
44
|
+
}
|
|
45
|
+
if (outcome.status === "not-found") {
|
|
46
|
+
return `${PRODUCT_TEXT.diagnostic(`found no package matching ${outcome.source ?? "that source"} in ${profileRoot}.`)}\n`;
|
|
47
|
+
}
|
|
48
|
+
switch (outcome.operation) {
|
|
49
|
+
case "install":
|
|
50
|
+
return `${name} installed ${outcome.source} into ${profileRoot}.\n`
|
|
51
|
+
+ `Restart ${PRODUCT_TEXT.commandName} for a running session to load it.\n`;
|
|
52
|
+
case "remove":
|
|
53
|
+
return `${name} removed ${outcome.source} from ${profileRoot}.\n`;
|
|
54
|
+
case "update":
|
|
55
|
+
return outcome.source === null
|
|
56
|
+
? `${name} updated the packages in ${profileRoot}.\n`
|
|
57
|
+
: `${name} updated ${outcome.source}.\n`;
|
|
58
|
+
case "refresh-models":
|
|
59
|
+
return `${name} refreshed the model catalogs in ${profileRoot}.\n`;
|
|
60
|
+
case "list":
|
|
61
|
+
return renderPackageList(outcome, profileRoot);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function renderPackageList(outcome, profileRoot) {
|
|
65
|
+
if (outcome.packages.length === 0)
|
|
66
|
+
return `${PRODUCT_TEXT.displayName} has no packages installed in ${profileRoot}.\n`;
|
|
67
|
+
const lines = [`Packages installed for ${PRODUCT_TEXT.commandName} in ${profileRoot}:`];
|
|
68
|
+
for (const entry of outcome.packages) {
|
|
69
|
+
lines.push(` ${entry.source}${entry.filtered ? " (partly enabled)" : ""}`);
|
|
70
|
+
if (entry.installedPath !== null)
|
|
71
|
+
lines.push(` ${entry.installedPath}`);
|
|
72
|
+
}
|
|
73
|
+
return `${lines.join("\n")}\n`;
|
|
74
|
+
}
|
|
75
|
+
function describeOperation(operation) {
|
|
76
|
+
if (operation === "refresh-models")
|
|
77
|
+
return "refresh the model catalogs";
|
|
78
|
+
if (operation === "list")
|
|
79
|
+
return "list installed packages";
|
|
80
|
+
return `${operation} the package`;
|
|
81
|
+
}
|
|
82
|
+
function message(error) {
|
|
83
|
+
return (error instanceof Error ? error.message : String(error)).replace(/\s+/g, " ").trim();
|
|
84
|
+
}
|
|
@@ -2,6 +2,7 @@ export * from "./capability-ports.js";
|
|
|
2
2
|
export * from "./domain.js";
|
|
3
3
|
export * from "./domain-validation.js";
|
|
4
4
|
export * from "./model.js";
|
|
5
|
+
export * from "./package-ports.js";
|
|
5
6
|
export * from "./ports.js";
|
|
6
7
|
export * from "./validation.js";
|
|
7
8
|
export * from "./serialization.js";
|
|
@@ -2,6 +2,7 @@ export * from "./capability-ports.js";
|
|
|
2
2
|
export * from "./domain.js";
|
|
3
3
|
export * from "./domain-validation.js";
|
|
4
4
|
export * from "./model.js";
|
|
5
|
+
export * from "./package-ports.js";
|
|
5
6
|
export * from "./ports.js";
|
|
6
7
|
export * from "./validation.js";
|
|
7
8
|
export * from "./serialization.js";
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extension packages are managed outside any session: the command that installs
|
|
3
|
+
* one runs before the runtime exists, against a profile root chosen by the caller
|
|
4
|
+
* rather than by whatever session happens to be open. So this port stands apart
|
|
5
|
+
* from the session-scoped service ports and names only what a package operation
|
|
6
|
+
* needs — a root, a source, and an outcome a caller can render without knowing
|
|
7
|
+
* which engine performed it.
|
|
8
|
+
*/
|
|
9
|
+
export type AgentPackageOperation = "install" | "remove" | "update" | "refresh-models" | "list";
|
|
10
|
+
/**
|
|
11
|
+
* Why an operation ended, kept separate from the prose so callers can branch on
|
|
12
|
+
* it. `not-found` is the one failure an engine can state structurally — the source
|
|
13
|
+
* is not configured in this profile — rather than only in a message.
|
|
14
|
+
*/
|
|
15
|
+
export type AgentPackageStatus = "completed" | "not-found" | "failed";
|
|
16
|
+
export interface AgentPackageDescriptor {
|
|
17
|
+
readonly source: string;
|
|
18
|
+
readonly installedPath: string | null;
|
|
19
|
+
/** True when the profile enables only part of what the package provides. */
|
|
20
|
+
readonly filtered: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface AgentPackageOutcome {
|
|
23
|
+
readonly operation: AgentPackageOperation;
|
|
24
|
+
readonly status: AgentPackageStatus;
|
|
25
|
+
readonly source: string | null;
|
|
26
|
+
readonly packages: readonly AgentPackageDescriptor[];
|
|
27
|
+
readonly detail: string | null;
|
|
28
|
+
}
|
|
29
|
+
export interface AgentPackageProgress {
|
|
30
|
+
readonly operation: AgentPackageOperation;
|
|
31
|
+
readonly message: string;
|
|
32
|
+
}
|
|
33
|
+
export interface AgentPackagesPort {
|
|
34
|
+
readonly capabilities: {
|
|
35
|
+
readonly install: boolean;
|
|
36
|
+
readonly remove: boolean;
|
|
37
|
+
readonly update: boolean;
|
|
38
|
+
readonly refreshModels: boolean;
|
|
39
|
+
};
|
|
40
|
+
/** Absolute profile root every operation of this port acts on. */
|
|
41
|
+
readonly profileRoot: string;
|
|
42
|
+
list(): Promise<AgentPackageOutcome>;
|
|
43
|
+
install(source: string): Promise<AgentPackageOutcome>;
|
|
44
|
+
remove(source: string): Promise<AgentPackageOutcome>;
|
|
45
|
+
update(source?: string): Promise<AgentPackageOutcome>;
|
|
46
|
+
refreshModels(): Promise<AgentPackageOutcome>;
|
|
47
|
+
}
|
|
48
|
+
export interface AgentPackagesPortInput {
|
|
49
|
+
readonly profileRoot: string;
|
|
50
|
+
readonly cwd: string;
|
|
51
|
+
readonly onProgress?: (progress: AgentPackageProgress) => void;
|
|
52
|
+
}
|
|
53
|
+
export declare function assertAgentPackagesPort(port: AgentPackagesPort): void;
|
|
54
|
+
export declare function agentPackageOutcome(operation: AgentPackageOperation, status: AgentPackageStatus, detail?: string | null, source?: string | null, packages?: readonly AgentPackageDescriptor[]): AgentPackageOutcome;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extension packages are managed outside any session: the command that installs
|
|
3
|
+
* one runs before the runtime exists, against a profile root chosen by the caller
|
|
4
|
+
* rather than by whatever session happens to be open. So this port stands apart
|
|
5
|
+
* from the session-scoped service ports and names only what a package operation
|
|
6
|
+
* needs — a root, a source, and an outcome a caller can render without knowing
|
|
7
|
+
* which engine performed it.
|
|
8
|
+
*/
|
|
9
|
+
export function assertAgentPackagesPort(port) {
|
|
10
|
+
if (typeof port?.capabilities !== "object" || port.capabilities === null)
|
|
11
|
+
throw new TypeError("packages port capabilities are required");
|
|
12
|
+
if (typeof port.profileRoot !== "string" || port.profileRoot.length === 0)
|
|
13
|
+
throw new TypeError("packages port requires an absolute profile root");
|
|
14
|
+
for (const operation of ["list", "install", "remove", "update", "refreshModels"]) {
|
|
15
|
+
if (typeof port[operation] !== "function")
|
|
16
|
+
throw new TypeError(`packages port requires ${operation}`);
|
|
17
|
+
}
|
|
18
|
+
for (const capability of ["install", "remove", "update", "refreshModels"]) {
|
|
19
|
+
if (typeof port.capabilities[capability] !== "boolean")
|
|
20
|
+
throw new TypeError(`packages capability ${capability} must be explicit`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function agentPackageOutcome(operation, status, detail, source, packages = []) {
|
|
24
|
+
return Object.freeze({
|
|
25
|
+
operation,
|
|
26
|
+
status,
|
|
27
|
+
source: source ?? null,
|
|
28
|
+
packages: Object.freeze([...packages]),
|
|
29
|
+
detail: detail ?? null,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -37,6 +37,21 @@ export declare function createPiShellChangelog(markdown: string): PiShellCompone
|
|
|
37
37
|
export declare function createPiShellHotkeys(): PiShellComponentPort;
|
|
38
38
|
export declare function createPiShellTranscriptComponent(initial: OwnedUiTranscriptBlock, cwd: string, extensions?: PiShellExtensionRendererResolver): PiShellTranscriptComponentPort;
|
|
39
39
|
export declare function renderPiShellTranscriptBlock(block: OwnedUiTranscriptBlock, width: number, cwd: string): readonly string[];
|
|
40
|
+
/**
|
|
41
|
+
* Pinned Pi's CLI prints startup diagnostics with `reportDiagnostics` before
|
|
42
|
+
* the banner: the whole line, prefix included, in chalk's basic ANSI severity
|
|
43
|
+
* colour — not the theme's tokens — with info lines dim and unprefixed.
|
|
44
|
+
*/
|
|
45
|
+
export declare function renderPiShellStartupDiagnostic(diagnostic: {
|
|
46
|
+
readonly severity: "info" | "warning" | "error";
|
|
47
|
+
readonly message: string;
|
|
48
|
+
}, width: number): readonly string[];
|
|
49
|
+
/**
|
|
50
|
+
* Pinned Pi's `showPackageUpdateNotification` banner: warning-coloured dynamic
|
|
51
|
+
* borders around a bold warning title, the muted update instruction with the
|
|
52
|
+
* accent command, and the package list.
|
|
53
|
+
*/
|
|
54
|
+
export declare function renderPiShellPackageUpdateNotice(packages: readonly string[], width: number): readonly string[];
|
|
40
55
|
type PiAssistantMessage = NonNullable<ConstructorParameters<typeof AssistantMessageComponent>[0]>;
|
|
41
56
|
export declare function validatedAssistantMessage(block: OwnedUiTranscriptBlock): PiAssistantMessage;
|
|
42
57
|
export {};
|
|
@@ -141,6 +141,39 @@ export function renderPiShellTranscriptBlock(block, width, cwd) {
|
|
|
141
141
|
ensureTheme();
|
|
142
142
|
return transcriptComponent(block, cwd, true).render(width);
|
|
143
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* Pinned Pi's CLI prints startup diagnostics with `reportDiagnostics` before
|
|
146
|
+
* the banner: the whole line, prefix included, in chalk's basic ANSI severity
|
|
147
|
+
* colour — not the theme's tokens — with info lines dim and unprefixed.
|
|
148
|
+
*/
|
|
149
|
+
export function renderPiShellStartupDiagnostic(diagnostic, width) {
|
|
150
|
+
ensureTheme();
|
|
151
|
+
const escape = String.fromCharCode(27);
|
|
152
|
+
const chalk = diagnostic.severity === "error"
|
|
153
|
+
? { open: `${escape}[31m`, close: `${escape}[39m`, prefix: "Error: " }
|
|
154
|
+
: diagnostic.severity === "warning"
|
|
155
|
+
? { open: `${escape}[33m`, close: `${escape}[39m`, prefix: "Warning: " }
|
|
156
|
+
: { open: `${escape}[2m`, close: `${escape}[22m`, prefix: "" };
|
|
157
|
+
return new Text(`${chalk.open}${chalk.prefix}${diagnostic.message}${chalk.close}`, 0, 0).render(width);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Pinned Pi's `showPackageUpdateNotification` banner: warning-coloured dynamic
|
|
161
|
+
* borders around a bold warning title, the muted update instruction with the
|
|
162
|
+
* accent command, and the package list.
|
|
163
|
+
*/
|
|
164
|
+
export function renderPiShellPackageUpdateNotice(packages, width) {
|
|
165
|
+
ensureTheme();
|
|
166
|
+
const theme = piTheme();
|
|
167
|
+
const container = new Container();
|
|
168
|
+
container.addChild(new Spacer(1));
|
|
169
|
+
container.addChild(new DynamicBorder(text => theme.fg("warning", text)));
|
|
170
|
+
container.addChild(new Text(`${theme.bold(theme.fg("warning", "Package Updates Available"))}\n`
|
|
171
|
+
+ `${theme.fg("muted", "Package updates are available. Run ")}${theme.fg("accent", "pi update --extensions")}\n`
|
|
172
|
+
+ `${theme.fg("muted", "Packages:")}\n`
|
|
173
|
+
+ packages.map(name => `- ${name}`).join("\n"), 1, 0));
|
|
174
|
+
container.addChild(new DynamicBorder(text => theme.fg("warning", text)));
|
|
175
|
+
return container.render(width);
|
|
176
|
+
}
|
|
144
177
|
function transcriptComponent(block, cwd, expanded, extensions) {
|
|
145
178
|
switch (block.kind) {
|
|
146
179
|
case "user": {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AgentSessionRuntime, type SessionInfo } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import { type AgentSessionRuntime, type AgentSessionServices, type SessionInfo } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { OWNED_UI_EXTENSION_CONTRACT_VERSION, OWNED_UI_EXTENSION_RENDER_CALLBACKS, OWNED_UI_EXTENSION_UI_CALLBACKS, OWNED_UI_EXTENSION_UI_PROPERTIES, type OwnedUiCommand, type OwnedUiCommandOutcome, type OwnedUiEvent, type OwnedUiSessionViewModel, type OwnedUiSnapshot } from "../owned-ui-contracts/index.js";
|
|
3
3
|
import { type PiAuthenticationProviderOption, type PiBashWorkflowResult, type PiPinnedSettingsCallback, type PiPinnedSettingsSnapshot, type PiWorkflowAutocompleteCommand, type PiWorkflowHost, type PiWorkflowInteractionHost, type PiWorkflowOption, type PiWorkflowRequest, type PiWorkflowResult } from "./workflows.js";
|
|
4
4
|
import type { AgentSettingsPort } from "../agent-engine-contracts/index.js";
|
|
@@ -51,6 +51,7 @@ export interface PiScopedModelsRefreshResult extends PiScopedModelsContext {
|
|
|
51
51
|
readonly status: string;
|
|
52
52
|
readonly statusKind: "success" | "warning";
|
|
53
53
|
}
|
|
54
|
+
type PiServicesApi = AgentSessionServices;
|
|
54
55
|
export type PiEngineRuntimeFactory = (input: PiEngineRuntimeFactoryInput) => Promise<AgentSessionRuntime>;
|
|
55
56
|
export interface OwnedPiResourceSummary {
|
|
56
57
|
readonly kind: "skill" | "prompt-template" | "agent-context" | "system-prompt" | "theme";
|
|
@@ -90,6 +91,11 @@ export interface PiEngineAdapterOptions {
|
|
|
90
91
|
* offering stay here.
|
|
91
92
|
*/
|
|
92
93
|
readonly availableThemes?: () => readonly string[];
|
|
94
|
+
/**
|
|
95
|
+
* Startup extension-package update probe, mirroring pinned Pi's interactive
|
|
96
|
+
* mode. Returns display names of packages with updates available.
|
|
97
|
+
*/
|
|
98
|
+
readonly checkPackageUpdates?: (settingsManager: PiServicesApi["settingsManager"]) => Promise<readonly string[]>;
|
|
93
99
|
}
|
|
94
100
|
export interface AdapterCommandResult {
|
|
95
101
|
readonly outcome: OwnedUiCommandOutcome;
|
|
@@ -155,3 +161,4 @@ export declare class PiEngineAdapter {
|
|
|
155
161
|
dispose(): Promise<void>;
|
|
156
162
|
}
|
|
157
163
|
export declare function createPiEngineAdapter(options?: PiEngineAdapterOptions): Promise<PiEngineAdapter>;
|
|
164
|
+
export {};
|
|
@@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
6
|
import { PRODUCT_IDENTITY } from "../../product-identity.js";
|
|
7
|
-
import { copyToClipboard, getAgentDir, ProjectTrustStore, SessionManager, } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { copyToClipboard, DefaultPackageManager, getAgentDir, ProjectTrustStore, SessionManager, } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import { OWNED_UI_EXTENSION_CONTRACT_VERSION, OWNED_UI_EXTENSION_RENDER_CALLBACKS, OWNED_UI_EXTENSION_UI_CALLBACKS, OWNED_UI_EXTENSION_UI_PROPERTIES, assertOwnedUiCommand, assertOwnedUiExtensionUiPort, assertOwnedUiSnapshot, } from "../owned-ui-contracts/index.js";
|
|
9
9
|
import { PINNED_PI_SETTINGS_CALLBACKS, PINNED_PI_WORKFLOW_COMMAND_NAMES, } from "./workflows.js";
|
|
10
10
|
import { createPiRuntimeIntegration } from "./runtime-integration.js";
|
|
@@ -19,6 +19,7 @@ const DEFAULT_SURFACE = {
|
|
|
19
19
|
};
|
|
20
20
|
export class PiEngineAdapter {
|
|
21
21
|
#runtimeFactory;
|
|
22
|
+
#checkPackageUpdates;
|
|
22
23
|
#cwd;
|
|
23
24
|
#agentDir;
|
|
24
25
|
#sessionId;
|
|
@@ -73,6 +74,10 @@ export class PiEngineAdapter {
|
|
|
73
74
|
this.#agentDir = options.agentDir ?? getAgentDir();
|
|
74
75
|
this.#sessionId = options.sessionId ?? "owned-session-1";
|
|
75
76
|
this.#runtimeFactory = options.createRuntime ?? createDefaultPiRuntime;
|
|
77
|
+
this.#checkPackageUpdates = options.checkPackageUpdates
|
|
78
|
+
?? (options.createRuntime
|
|
79
|
+
? async () => []
|
|
80
|
+
: settingsManager => checkDefaultPiPackageUpdates(this.#cwd, this.#agentDir, settingsManager));
|
|
76
81
|
this.#workflowHost = options.workflowHost ?? defaultWorkflowHost();
|
|
77
82
|
this.#availableThemes = options.availableThemes ?? null;
|
|
78
83
|
this.#workflowInteraction = { prompt: async () => null, notify() { } };
|
|
@@ -122,8 +127,25 @@ export class PiEngineAdapter {
|
|
|
122
127
|
this.#editor = { ...this.#editor, submitEnabled: true };
|
|
123
128
|
this.#emitEvent({ type: "session-lifecycle", lifecycle: "ready", reason: null });
|
|
124
129
|
this.#emitView();
|
|
130
|
+
void this.#announcePackageUpdates(runtime.services.settingsManager);
|
|
125
131
|
return this.view();
|
|
126
132
|
}
|
|
133
|
+
async #announcePackageUpdates(settingsManager) {
|
|
134
|
+
if (process.env.PI_OFFLINE)
|
|
135
|
+
return;
|
|
136
|
+
let updates;
|
|
137
|
+
try {
|
|
138
|
+
updates = await this.#checkPackageUpdates(settingsManager);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (this.#disposed || updates.length === 0)
|
|
144
|
+
return;
|
|
145
|
+
const packages = updates.map(name => `- ${name}`).join("\n");
|
|
146
|
+
this.#addDiagnostic("info", "package-updates", `Package updates are available. Run pi update --extensions\nPackages:\n${packages}`, true);
|
|
147
|
+
this.#emitView();
|
|
148
|
+
}
|
|
127
149
|
onEvent(listener) {
|
|
128
150
|
this.#listeners.add(listener);
|
|
129
151
|
listener(this.#event({ type: "session-view", view: this.view() }));
|
|
@@ -1876,6 +1898,11 @@ export async function createPiEngineAdapter(options = {}) {
|
|
|
1876
1898
|
async function createDefaultPiRuntime(input) {
|
|
1877
1899
|
return createPiRuntimeIntegration({ cwd: input.cwd, agentDir: input.agentDir });
|
|
1878
1900
|
}
|
|
1901
|
+
async function checkDefaultPiPackageUpdates(cwd, agentDir, settingsManager) {
|
|
1902
|
+
const packageManager = new DefaultPackageManager({ cwd, agentDir, settingsManager });
|
|
1903
|
+
const updates = await packageManager.checkForAvailableUpdates();
|
|
1904
|
+
return updates.map(update => update.displayName);
|
|
1905
|
+
}
|
|
1879
1906
|
function pinnedSessionInfoPresentation(value, sessionName, entries, modelRuntime) {
|
|
1880
1907
|
const stats = isRecord(value) ? value : {};
|
|
1881
1908
|
const tokens = dynamicObject(stats, "tokens");
|
|
@@ -4,6 +4,7 @@ export * from "./runtime-integration.js";
|
|
|
4
4
|
export * from "./session-integration.js";
|
|
5
5
|
export * from "./model-auth-integration.js";
|
|
6
6
|
export * from "./settings-integration.js";
|
|
7
|
+
export * from "./package-integration.js";
|
|
7
8
|
export * from "./resource-extension-integration.js";
|
|
8
9
|
export * from "./workflow-controllers.js";
|
|
9
10
|
export * from "./workflows.js";
|
|
@@ -4,6 +4,7 @@ export * from "./runtime-integration.js";
|
|
|
4
4
|
export * from "./session-integration.js";
|
|
5
5
|
export * from "./model-auth-integration.js";
|
|
6
6
|
export * from "./settings-integration.js";
|
|
7
|
+
export * from "./package-integration.js";
|
|
7
8
|
export * from "./resource-extension-integration.js";
|
|
8
9
|
export * from "./workflow-controllers.js";
|
|
9
10
|
export * from "./workflows.js";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type AgentPackagesPort, type AgentPackagesPortInput } from "../agent-engine-contracts/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Pinned Pi's package manager already takes the profile root as an argument, so
|
|
4
|
+
* this binds it to the root A1 chose rather than to whatever the configuration-root
|
|
5
|
+
* environment variable happens to say. Pi's own package command handler is not used:
|
|
6
|
+
* it prints Pi's command names and ends the process, and both of those belong to
|
|
7
|
+
* whichever A1 command is running.
|
|
8
|
+
*
|
|
9
|
+
* The settings manager is created with project trust withheld, which is what keeps
|
|
10
|
+
* every operation to the user scope of the selected profile — a project-local
|
|
11
|
+
* `packages` list is never read, so it can never be written either.
|
|
12
|
+
*/
|
|
13
|
+
export declare function createPiPackagesPort(input: AgentPackagesPortInput): AgentPackagesPort;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { DefaultPackageManager, ModelRuntime, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { agentPackageOutcome, } from "../agent-engine-contracts/index.js";
|
|
4
|
+
const MODEL_REFRESH_TIMEOUT_MS = 15_000;
|
|
5
|
+
/**
|
|
6
|
+
* Pinned Pi's package manager already takes the profile root as an argument, so
|
|
7
|
+
* this binds it to the root A1 chose rather than to whatever the configuration-root
|
|
8
|
+
* environment variable happens to say. Pi's own package command handler is not used:
|
|
9
|
+
* it prints Pi's command names and ends the process, and both of those belong to
|
|
10
|
+
* whichever A1 command is running.
|
|
11
|
+
*
|
|
12
|
+
* The settings manager is created with project trust withheld, which is what keeps
|
|
13
|
+
* every operation to the user scope of the selected profile — a project-local
|
|
14
|
+
* `packages` list is never read, so it can never be written either.
|
|
15
|
+
*/
|
|
16
|
+
export function createPiPackagesPort(input) {
|
|
17
|
+
const { profileRoot, cwd } = input;
|
|
18
|
+
const settingsManager = SettingsManager.create(cwd, profileRoot, { projectTrusted: false });
|
|
19
|
+
const packageManager = new DefaultPackageManager({ cwd, agentDir: profileRoot, settingsManager });
|
|
20
|
+
packageManager.setProgressCallback(event => {
|
|
21
|
+
if (event.type !== "start" || !event.message)
|
|
22
|
+
return;
|
|
23
|
+
input.onProgress?.({ operation: operationFor(event.action), message: event.message });
|
|
24
|
+
});
|
|
25
|
+
const configured = () => packageManager.listConfiguredPackages()
|
|
26
|
+
.filter(entry => entry.scope === "user")
|
|
27
|
+
.map(entry => Object.freeze({
|
|
28
|
+
source: entry.source,
|
|
29
|
+
installedPath: entry.installedPath ?? null,
|
|
30
|
+
filtered: entry.filtered,
|
|
31
|
+
}));
|
|
32
|
+
const isConfigured = (source) => configured().some(entry => entry.source === source || entry.source.startsWith(`${source}@`));
|
|
33
|
+
return Object.freeze({
|
|
34
|
+
capabilities: Object.freeze({ install: true, remove: true, update: true, refreshModels: true }),
|
|
35
|
+
profileRoot,
|
|
36
|
+
async list() {
|
|
37
|
+
return await attempt("list", null, async () => agentPackageOutcome("list", "completed", null, null, configured()));
|
|
38
|
+
},
|
|
39
|
+
async install(source) {
|
|
40
|
+
return await attempt("install", source, async () => {
|
|
41
|
+
await packageManager.installAndPersist(source);
|
|
42
|
+
return agentPackageOutcome("install", "completed", null, source, configured());
|
|
43
|
+
});
|
|
44
|
+
},
|
|
45
|
+
async remove(source) {
|
|
46
|
+
return await attempt("remove", source, async () => {
|
|
47
|
+
const removed = await packageManager.removeAndPersist(source);
|
|
48
|
+
if (!removed)
|
|
49
|
+
return agentPackageOutcome("remove", "not-found", null, source);
|
|
50
|
+
return agentPackageOutcome("remove", "completed", null, source, configured());
|
|
51
|
+
});
|
|
52
|
+
},
|
|
53
|
+
async update(source) {
|
|
54
|
+
return await attempt("update", source ?? null, async () => {
|
|
55
|
+
// Asking first keeps "that package is not installed here" a structural
|
|
56
|
+
// answer rather than a string a caller would have to recognize.
|
|
57
|
+
if (source !== undefined && !isConfigured(source))
|
|
58
|
+
return agentPackageOutcome("update", "not-found", null, source);
|
|
59
|
+
await packageManager.update(source);
|
|
60
|
+
return agentPackageOutcome("update", "completed", null, source ?? null, configured());
|
|
61
|
+
});
|
|
62
|
+
},
|
|
63
|
+
async refreshModels() {
|
|
64
|
+
return await attempt("refresh-models", null, async () => {
|
|
65
|
+
await refreshModelCatalogs(profileRoot);
|
|
66
|
+
return agentPackageOutcome("refresh-models", "completed");
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
async function attempt(operation, source, run) {
|
|
72
|
+
try {
|
|
73
|
+
return await run();
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
return agentPackageOutcome(operation, "failed", describe(error), source);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async function refreshModelCatalogs(profileRoot) {
|
|
80
|
+
const controller = new AbortController();
|
|
81
|
+
const timeout = setTimeout(() => controller.abort(), MODEL_REFRESH_TIMEOUT_MS);
|
|
82
|
+
try {
|
|
83
|
+
const modelRuntime = await ModelRuntime.create({
|
|
84
|
+
authPath: join(profileRoot, "auth.json"),
|
|
85
|
+
modelsPath: join(profileRoot, "models.json"),
|
|
86
|
+
allowModelNetwork: false,
|
|
87
|
+
signal: controller.signal,
|
|
88
|
+
});
|
|
89
|
+
const result = await modelRuntime.refresh({ allowNetwork: true, force: true, signal: controller.signal });
|
|
90
|
+
if (result.aborted)
|
|
91
|
+
throw new Error(`model catalog refresh timed out after ${MODEL_REFRESH_TIMEOUT_MS}ms`);
|
|
92
|
+
if (result.errors.size > 0) {
|
|
93
|
+
throw new Error(Array.from(result.errors, ([provider, error]) => `${provider}: ${error.message}`).join("; "));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
clearTimeout(timeout);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function operationFor(action) {
|
|
101
|
+
if (action === "install")
|
|
102
|
+
return "install";
|
|
103
|
+
if (action === "remove")
|
|
104
|
+
return "remove";
|
|
105
|
+
return "update";
|
|
106
|
+
}
|
|
107
|
+
function describe(error) {
|
|
108
|
+
const message = (error instanceof Error ? error.message : String(error)).replace(/\s+/g, " ").trim();
|
|
109
|
+
if (message.length === 0)
|
|
110
|
+
return "unknown package failure";
|
|
111
|
+
return message.length > 600 ? `${message.slice(0, 597)}...` : message;
|
|
112
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AgentSession, type AgentSessionRuntime } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import { type AgentSession, type AgentSessionRuntime, type AgentSessionServices, type ScopedModel } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
export interface PiRuntimeIntegrationOptions {
|
|
3
3
|
readonly cwd: string;
|
|
4
4
|
readonly agentDir: string;
|
|
@@ -10,9 +10,26 @@ export type PiSessionReplacement = {
|
|
|
10
10
|
readonly kind: "resume";
|
|
11
11
|
readonly sessionPath: string;
|
|
12
12
|
};
|
|
13
|
+
interface ConfiguredModelScope {
|
|
14
|
+
readonly scopedModels: readonly ScopedModel[];
|
|
15
|
+
readonly model: ScopedModel["model"] | undefined;
|
|
16
|
+
readonly thinkingLevel: ScopedModel["thinkingLevel"];
|
|
17
|
+
readonly diagnostics: readonly {
|
|
18
|
+
type: "info" | "warning" | "error";
|
|
19
|
+
message: string;
|
|
20
|
+
}[];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Mirrors pinned Pi's CLI startup: resolve the `models` patterns from settings
|
|
24
|
+
* into a scoped model list, keep the resolver's warnings (e.g. "No models match
|
|
25
|
+
* pattern ..."), and pick the same initial model pinned Pi would pick — the
|
|
26
|
+
* saved default when it is in scope, otherwise the first scoped model.
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveConfiguredModelScope(services: Pick<AgentSessionServices, "settingsManager" | "modelRuntime">): Promise<ConfiguredModelScope>;
|
|
13
29
|
export declare function createPiRuntimeIntegration(options: PiRuntimeIntegrationOptions): Promise<AgentSessionRuntime>;
|
|
14
30
|
export declare function bindPiRuntimeSession(runtime: AgentSessionRuntime, rebind: (session: AgentSession) => Promise<void>): () => void;
|
|
15
31
|
export declare function replacePiRuntimeSession(runtime: AgentSessionRuntime, replacement: PiSessionReplacement): Promise<{
|
|
16
32
|
readonly cancelled: boolean;
|
|
17
33
|
}>;
|
|
18
34
|
export declare function disposePiRuntimeIntegration(runtime: AgentSessionRuntime): Promise<void>;
|
|
35
|
+
export {};
|
|
@@ -1,14 +1,49 @@
|
|
|
1
|
-
import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, SessionManager, } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, resolveModelScopeWithDiagnostics, SessionManager, } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
/**
|
|
3
|
+
* Mirrors pinned Pi's CLI startup: resolve the `models` patterns from settings
|
|
4
|
+
* into a scoped model list, keep the resolver's warnings (e.g. "No models match
|
|
5
|
+
* pattern ..."), and pick the same initial model pinned Pi would pick — the
|
|
6
|
+
* saved default when it is in scope, otherwise the first scoped model.
|
|
7
|
+
*/
|
|
8
|
+
export async function resolveConfiguredModelScope(services) {
|
|
9
|
+
const patterns = services.settingsManager.getEnabledModels();
|
|
10
|
+
if (!patterns || patterns.length === 0) {
|
|
11
|
+
return { scopedModels: [], model: undefined, thinkingLevel: undefined, diagnostics: [] };
|
|
12
|
+
}
|
|
13
|
+
const { scopedModels, diagnostics } = await resolveModelScopeWithDiagnostics([...patterns], services.modelRuntime, { signal: AbortSignal.timeout(15_000) });
|
|
14
|
+
let selected;
|
|
15
|
+
if (scopedModels.length > 0) {
|
|
16
|
+
const savedProvider = services.settingsManager.getDefaultProvider();
|
|
17
|
+
const savedModelId = services.settingsManager.getDefaultModel();
|
|
18
|
+
const savedModel = savedProvider && savedModelId
|
|
19
|
+
? services.modelRuntime.getModel(savedProvider, savedModelId)
|
|
20
|
+
: undefined;
|
|
21
|
+
selected = (savedModel
|
|
22
|
+
? scopedModels.find(scoped => scoped.model.provider === savedModel.provider && scoped.model.id === savedModel.id)
|
|
23
|
+
: undefined) ?? scopedModels[0];
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
scopedModels,
|
|
27
|
+
model: selected?.model,
|
|
28
|
+
thinkingLevel: selected?.thinkingLevel,
|
|
29
|
+
diagnostics: diagnostics.map(diagnostic => ({ type: diagnostic.type, message: diagnostic.message })),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
2
32
|
export async function createPiRuntimeIntegration(options) {
|
|
3
33
|
const sessionManager = SessionManager.create(options.cwd, options.sessionDir ?? process.env.PI_CODING_AGENT_SESSION_DIR);
|
|
4
34
|
const createRuntime = async ({ cwd, sessionManager: targetSessionManager, sessionStartEvent, }) => {
|
|
5
35
|
const services = await createAgentSessionServices({ cwd, agentDir: options.agentDir });
|
|
36
|
+
const modelScope = await resolveConfiguredModelScope(services);
|
|
37
|
+
const hasExistingSession = targetSessionManager.buildSessionContext().messages.length > 0;
|
|
6
38
|
const created = await createAgentSessionFromServices({
|
|
7
39
|
services,
|
|
8
40
|
sessionManager: targetSessionManager,
|
|
9
41
|
...(sessionStartEvent ? { sessionStartEvent } : {}),
|
|
42
|
+
...(modelScope.model && !hasExistingSession ? { model: modelScope.model } : {}),
|
|
43
|
+
...(modelScope.thinkingLevel && !hasExistingSession ? { thinkingLevel: modelScope.thinkingLevel } : {}),
|
|
44
|
+
...(modelScope.scopedModels.length > 0 ? { scopedModels: [...modelScope.scopedModels] } : {}),
|
|
10
45
|
});
|
|
11
|
-
return { ...created, services, diagnostics:
|
|
46
|
+
return { ...created, services, diagnostics: [...modelScope.diagnostics] };
|
|
12
47
|
};
|
|
13
48
|
return createAgentSessionRuntime(createRuntime, {
|
|
14
49
|
cwd: options.cwd,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { MOUSE_TRACKING_OFF, MOUSE_TRACKING_ON, parseMouseInput } from "../ui-components/index.js";
|
|
2
2
|
import { PINNED_PI_HIDDEN_COMMAND_NAMES, PINNED_PI_WORKFLOW_COMMAND_NAMES, } from "../pi-engine-adapter/index.js";
|
|
3
|
-
import { createPiExtensionUiBridge, createPiQueuedInputStatus, createPiShellArmin, createPiShellAuthProviderSelector, createPiShellChangelog, createPiShellDaxnuts, createPiShellDialog, createPiShellEarendilAnnouncement, createPiShellEditor, createPiShellExtensionSelector, createPiShellFooter, createPiShellHeader, createPiShellHotkeys, createPiShellLoadedResources, createPiShellLoginDialog, createPiShellModelSelector, createPiShellOperationLoader, createPiShellReloadBox, createPiShellScopedModelsSelector, createPiShellSelector, createPiShellSessionInfo, createPiShellSessionSelector, createPiShellSettingsSelector, createPiShellStatus, createPiShellTranscriptComponent, createPiShellTreeSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, piTheme, renderPiShellStatusText, renderPiShellTranscriptBlock, } from "../pi-component-adapter/index.js";
|
|
3
|
+
import { createPiExtensionUiBridge, createPiQueuedInputStatus, createPiShellArmin, createPiShellAuthProviderSelector, createPiShellChangelog, createPiShellDaxnuts, createPiShellDialog, createPiShellEarendilAnnouncement, createPiShellEditor, createPiShellExtensionSelector, createPiShellFooter, createPiShellHeader, createPiShellHotkeys, createPiShellLoadedResources, createPiShellLoginDialog, createPiShellModelSelector, createPiShellOperationLoader, createPiShellReloadBox, createPiShellScopedModelsSelector, createPiShellSelector, createPiShellSessionInfo, createPiShellSessionSelector, createPiShellSettingsSelector, createPiShellStatus, createPiShellTranscriptComponent, createPiShellTreeSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, piTheme, renderPiShellPackageUpdateNotice, renderPiShellStartupDiagnostic, renderPiShellStatusText, renderPiShellTranscriptBlock, } from "../pi-component-adapter/index.js";
|
|
4
4
|
import { PiTuiRuntimeAdapter, } from "../pi-tui-runtime-adapter/index.js";
|
|
5
5
|
export class OwnedUiSessionShellRoot {
|
|
6
6
|
editor;
|
|
@@ -136,7 +136,17 @@ export class OwnedUiSessionShellRoot {
|
|
|
136
136
|
return ["", ...rows];
|
|
137
137
|
return rows;
|
|
138
138
|
});
|
|
139
|
-
const
|
|
139
|
+
const diagnostics = this.#view.diagnostics;
|
|
140
|
+
const startupRows = diagnostics
|
|
141
|
+
.filter(diagnostic => diagnostic.code === "engine-startup")
|
|
142
|
+
.flatMap(diagnostic => renderPiShellStartupDiagnostic(diagnostic, width));
|
|
143
|
+
const packageUpdateRows = diagnostics
|
|
144
|
+
.filter(diagnostic => diagnostic.code === "package-updates")
|
|
145
|
+
.flatMap(diagnostic => renderPiShellPackageUpdateNotice(diagnostic.message.split("\n").filter(line => line.startsWith("- ")).map(line => line.slice(2)), width));
|
|
146
|
+
const diagnosticRows = diagnostics
|
|
147
|
+
.filter(diagnostic => diagnostic.code !== "engine-startup" && diagnostic.code !== "package-updates")
|
|
148
|
+
.slice(-3)
|
|
149
|
+
.flatMap(diagnostic => renderPiShellTranscriptBlock({
|
|
140
150
|
id: `diagnostic-${diagnostic.sequence}`,
|
|
141
151
|
kind: diagnostic.severity === "error" ? "error" : "system",
|
|
142
152
|
status: "finalized",
|
|
@@ -149,9 +159,11 @@ export class OwnedUiSessionShellRoot {
|
|
|
149
159
|
if (resourceRows.at(-1) === "")
|
|
150
160
|
resourceRows.pop();
|
|
151
161
|
return [
|
|
162
|
+
...startupRows,
|
|
152
163
|
...(this.#extensionHeader ?? this.header).render(width),
|
|
153
164
|
...resourceRows,
|
|
154
165
|
...transcript,
|
|
166
|
+
...packageUpdateRows,
|
|
155
167
|
...diagnosticRows,
|
|
156
168
|
];
|
|
157
169
|
}
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
| `a1 pi` | Untouched vanilla Pi fallback and comparison oracle | ordinary `~/.pi/agent` |
|
|
9
9
|
| `a1 sandbox` | Unchanged isolated vanilla Pi profile for experiments | `~/.a1/sandbox` |
|
|
10
10
|
|
|
11
|
+
`a1 pi` and `a1 sandbox` are development instruments: one compares A1 against pinned Pi, the other tries resources against an isolated profile. Prerelease builds — what `a1 update:next` installs — expose them. A release build does not, and does not recognize the words: what it exposes is bare `a1` plus the maintenance and package commands. Working in this repository is unaffected, because `npm start:pi` and `npm run start:sandbox` prepare the profile and launch directly rather than through the command line.
|
|
12
|
+
|
|
11
13
|
There is no `a1 agent` command. The former `a1 ui` subcommand is removed. Bare `a1` is the owned agent product surface and remains the entry point when multi-agent UX is introduced.
|
|
12
14
|
|
|
13
15
|
## First launch
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@timurproko/a1",
|
|
3
|
-
"version": "0.1.1-dev.
|
|
3
|
+
"version": "0.1.1-dev.13",
|
|
4
4
|
"description": "Standalone terminal workspace for supervised native and managed agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "npm@11.13.0",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"start:sandbox": "npm run build && node scripts/dev-launch.mjs sandbox",
|
|
47
47
|
"prepack": "node scripts/prepack-gate.mjs",
|
|
48
48
|
"prepublishOnly": "npm run test:release",
|
|
49
|
-
"prepare": "npm run build",
|
|
49
|
+
"prepare": "node scripts/unify-pi-tui.mjs && npm run build",
|
|
50
50
|
"update:pi-settings-metadata": "node scripts/update-pi-settings-metadata.mjs"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|