@explyt/ai-agent 0.2.1 → 0.3.0-dogfood.2
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 +3 -3
- package/agent-docs/README.md +6 -3
- package/agent-docs/settings.md +43 -6
- package/agent-docs/updates.md +56 -0
- package/dist/cli/engine.js +102 -26
- package/dist/cli/index.js +112 -31
- package/dist/cli/install-auto.js +170 -0
- package/dist/cli/install-cleanup.js +187 -0
- package/dist/cli/install-command.js +135 -0
- package/dist/cli/install-config.js +33 -0
- package/dist/cli/install-file-retry.js +17 -0
- package/dist/cli/install-health.js +148 -0
- package/dist/cli/install-lock.js +203 -0
- package/dist/cli/install-migrate.js +60 -0
- package/dist/cli/install-node.js +73 -0
- package/dist/cli/install-npm.js +62 -0
- package/dist/cli/install-output.js +47 -0
- package/dist/cli/install-process.js +144 -0
- package/dist/cli/install-resolve.js +104 -0
- package/dist/cli/install-roots.js +34 -0
- package/dist/cli/install-run.js +341 -0
- package/dist/cli/install-settings.js +116 -0
- package/dist/cli/install-switch.js +233 -0
- package/dist/cli/install-winpath.js +52 -0
- package/dist/cli/installer-main.js +8 -0
- package/dist/cli/launch.js +161 -0
- package/dist/cli/update.js +2 -70
- package/dist/extensions/ask-user-question.js +1 -1
- package/dist/extensions/bash.js +1 -1
- package/dist/extensions/custom-provider.js +1 -1
- package/dist/extensions/hello.js +1 -1
- package/dist/extensions/identity.js +1 -1
- package/dist/extensions/mcp.js +1 -1
- package/dist/extensions/misc-hacks.js +1 -1
- package/dist/extensions/policy.js +1 -1
- package/dist/extensions/rules.js +1 -1
- package/dist/extensions/session-title.js +1 -1
- package/dist/extensions/skills.js +1 -1
- package/dist/extensions/subagents.js +1 -1
- package/dist/extensions/tui.js +1 -1
- package/dist/extensions/update-notice.js +1 -1
- package/dist/extensions/web-access.js +1 -1
- package/dist/extensions/write.js +1 -1
- package/dist/install/installer.js +3652 -0
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -17,8 +17,8 @@ explyt update # install the newer latest, then restart open sessi
|
|
|
17
17
|
For a one-off pinned run:
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
npx -y "@explyt/ai-agent@0.
|
|
21
|
-
npx -y "@explyt/ai-agent@0.
|
|
20
|
+
npx -y "@explyt/ai-agent@0.3.0" # start the terminal UI
|
|
21
|
+
npx -y "@explyt/ai-agent@0.3.0" login # store a provider credential, once
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
The agent runs with exactly the extensions this build ships with and ignores any
|
|
@@ -28,4 +28,4 @@ you get.
|
|
|
28
28
|
IDE (ACP) integration is not part of this package: it is provided by the Explyt
|
|
29
29
|
managed backend, which the Neo plugin downloads and manages.
|
|
30
30
|
|
|
31
|
-
Run `npx -y @explyt/ai-agent@0.
|
|
31
|
+
Run `npx -y @explyt/ai-agent@0.3.0 --help` for the available options.
|
package/agent-docs/README.md
CHANGED
|
@@ -9,6 +9,7 @@ it. Read the whole file before editing anything it describes.
|
|
|
9
9
|
| File | Covers |
|
|
10
10
|
|---|---|
|
|
11
11
|
| `settings.md` | `settings.json`, its two layers, and every setting that exists |
|
|
12
|
+
| `updates.md` | the managed installation, automatic updates, `updateChannel` and `autoUpdate` |
|
|
12
13
|
| `rules.md` | persistent instructions: where rule files live, their frontmatter, how to disable one |
|
|
13
14
|
| `skills.md` | Skills: where they are found, the `SKILL.md` frontmatter, how to disable one |
|
|
14
15
|
| `mcp.md` | MCP servers: the two `mcp.json` files, the entry shape, disabling a server or a tool |
|
|
@@ -39,9 +40,11 @@ not under `.explyt/`. `web-search.md` says where that directory is and what goes
|
|
|
39
40
|
|
|
40
41
|
## Updates of the agent itself
|
|
41
42
|
|
|
42
|
-
The terminal UI
|
|
43
|
-
|
|
44
|
-
`PI_OFFLINE=1
|
|
43
|
+
The agent updates itself: the terminal UI checks its channel at most once a day and installs a newer
|
|
44
|
+
version in the background, used from the next start. `explyt update` updates at once, and
|
|
45
|
+
`PI_OFFLINE=1` turns the check off. The update channel and the automatic update switch
|
|
46
|
+
(`updateChannel`, `autoUpdate`) are not `.explyt/` settings: they live in the engine's own
|
|
47
|
+
`settings.json` and are changed with `explyt config`. `updates.md` has the details.
|
|
45
48
|
|
|
46
49
|
## Do not create or edit `.pi/` files
|
|
47
50
|
|
package/agent-docs/settings.md
CHANGED
|
@@ -46,7 +46,10 @@ rest of the file as it is.
|
|
|
46
46
|
| `sessionTitle.enabled` | boolean | `true` | whether a new chat gets a short title generated by the model after its first turn succeeds |
|
|
47
47
|
| `subagents.maxConcurrency` | positive integer | `20` | Maximum simultaneously running or waiting children |
|
|
48
48
|
| `subagents.permission` | `inherit` \| `yolo` \| `ask-permissions` | `inherit` | Permission mode resolved for child tools |
|
|
49
|
-
| `tui.mascot` | string | `"cat"` | the animated mascot in the terminal's working row: `cat`, `snake`, `pacman`, `tetris`, `fishing`, `robot`, `pong` or `
|
|
49
|
+
| `tui.mascot` | string | `"cat"` | the animated mascot in the terminal's working row: `cat`, `snake`, `pacman`, `tetris`, `fishing`, `robot`, `pong`, `crab`, or `custom` for the user's own |
|
|
50
|
+
| `tui.customMascotFrames` | array of strings | `[]` | the frames of the custom mascot, one string per frame, played in order |
|
|
51
|
+
| `tui.customMascotText` | string | `"Working…"` | the breathing word in front of the custom mascot |
|
|
52
|
+
| `tui.tips` | boolean | `true` | whether a usage tip is shown under the working row while the agent works |
|
|
50
53
|
|
|
51
54
|
The rule and Skill list settings take exact, case-sensitive entries. Neither supports globs or
|
|
52
55
|
prefixes: `"pdf-*"` matches nothing. An entry matching nothing is kept, not an error.
|
|
@@ -66,11 +69,45 @@ project's `.explyt/settings.json` is ignored, so write it to `~/.explyt/settings
|
|
|
66
69
|
turns generation off everywhere; the chat then keeps the truncated first prompt as its title. A
|
|
67
70
|
name the user sets with `/name` is never replaced either way.
|
|
68
71
|
|
|
69
|
-
`tui
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
The four `tui.*` keys are also read from the **global** layer only; write them to
|
|
73
|
+
`~/.explyt/settings.json`. An edit applies from the user's next prompt; the terminal command
|
|
74
|
+
`/mascot` writes `tui.mascot` and switches the mascot at once. None of them has an effect in an IDE,
|
|
75
|
+
which has no working row.
|
|
76
|
+
|
|
77
|
+
`tui.mascot` is one of the nine ids above, lowercase and exact. Any other value, or a value that is
|
|
78
|
+
not a string, shows the cat.
|
|
79
|
+
|
|
80
|
+
### Drawing a custom mascot
|
|
81
|
+
|
|
82
|
+
When the user asks for a mascot of their own, write all three keys: the frames, the word, and
|
|
83
|
+
`tui.mascot: "custom"`. The custom keys stay in the file when the user later picks another
|
|
84
|
+
mascot, and `/mascot custom` brings it back.
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"tui": {
|
|
89
|
+
"mascot": "custom",
|
|
90
|
+
"customMascotText": "Quacking…",
|
|
91
|
+
"customMascotFrames": ["🦆 ", " 🦆 ", " 🦆 ", " 🦆 "]
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
- Each string is one frame, shown for 120 ms, in order, then the list repeats. Draw a loop: the
|
|
97
|
+
last frame should lead back into the first.
|
|
98
|
+
- Keep every frame the same width, measured in terminal columns; an emoji or a CJK character is
|
|
99
|
+
two columns. Shorter frames are padded with spaces at their end.
|
|
100
|
+
- A frame or a text wider than **40 columns** is cut to 40.
|
|
101
|
+
- Plain characters only: escape sequences, colour codes, tabs and line breaks are removed. The
|
|
102
|
+
product paints the frames orange and the word in a breathing orange itself.
|
|
103
|
+
- Without `customMascotText` the word is `Working…`. Use one short word with `…` (U+2026), like
|
|
104
|
+
the built-in mascots.
|
|
105
|
+
- With no frames, an empty list, or only blank frames, `custom` shows the cat.
|
|
106
|
+
|
|
107
|
+
### Tips
|
|
108
|
+
|
|
109
|
+
`tui.tips: false` hides the tip line under the working row. The tips themselves are part of the
|
|
110
|
+
product and cannot be edited.
|
|
74
111
|
|
|
75
112
|
## How the two layers combine
|
|
76
113
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Installing and updating the agent
|
|
2
|
+
|
|
3
|
+
The agent lives in one **managed installation** per user, shared by the terminal (`explyt`) and the
|
|
4
|
+
IDE plugin. The Explyt installer creates and updates it; the user never installs it with `npm`.
|
|
5
|
+
|
|
6
|
+
## Where it is
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
<root> = $XDG_DATA_HOME/explyt-ai-agent (default: ~/.local/share/explyt-ai-agent,
|
|
10
|
+
on Windows %USERPROFILE%\.local\share\explyt-ai-agent)
|
|
11
|
+
<root>/agent/ credentials, sessions, the engine's settings.json
|
|
12
|
+
<root>/node/<version>/ the Node the installed release runs with
|
|
13
|
+
<root>/install/current.json which installed release is current
|
|
14
|
+
<root>/install/launch.js the fixed entry the terminal command and the IDE start
|
|
15
|
+
~/.local/bin/explyt the terminal command (explyt.cmd on Windows)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Do not edit anything under `<root>/install/` or `<root>/node/` by hand; the installer owns it.
|
|
19
|
+
A shell and an IDE that see different `XDG_DATA_HOME` values use two different roots; the installer
|
|
20
|
+
and `explyt --version` warn when a second one exists.
|
|
21
|
+
|
|
22
|
+
## Installing
|
|
23
|
+
|
|
24
|
+
- Linux, macOS: `curl -fsSL https://raw.githubusercontent.com/explyt/explyt-ai-agent-distribution/main/install.sh | sh`
|
|
25
|
+
- Windows (PowerShell): `irm https://raw.githubusercontent.com/explyt/explyt-ai-agent-distribution/main/install.ps1 | iex`
|
|
26
|
+
- From an existing installation: `explyt install`.
|
|
27
|
+
|
|
28
|
+
`npm i -g @explyt/ai-agent` is not an install path. A global npm copy found on `PATH` is reported as a
|
|
29
|
+
conflict, and the installer offers to remove it.
|
|
30
|
+
|
|
31
|
+
## Updates
|
|
32
|
+
|
|
33
|
+
Updates are automatic: the terminal UI checks its channel at most once a day and installs a newer
|
|
34
|
+
version in the background. The new version is used from the next start; the terminal UI then says so,
|
|
35
|
+
and `explyt --continue` resumes the session on it. A running session keeps its version until it exits.
|
|
36
|
+
`explyt update` updates at once. `PI_OFFLINE=1` turns every check off.
|
|
37
|
+
|
|
38
|
+
## The two update settings
|
|
39
|
+
|
|
40
|
+
They live in the engine's settings file, `<root>/agent/settings.json`, **not** in `.explyt/`. Change them
|
|
41
|
+
with `explyt config`, which keeps every other key of that file:
|
|
42
|
+
|
|
43
|
+
| Key | Values | Default | Meaning |
|
|
44
|
+
|---|---|---|---|
|
|
45
|
+
| `updateChannel` | `"stable"`, `"dogfood"` | `"stable"` | which versions the installer takes; `dogfood` gets prereleases first |
|
|
46
|
+
| `autoUpdate` | `true`, `false` | `true` | `false` stops the background check; `explyt update` still works |
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
explyt config set updateChannel dogfood
|
|
50
|
+
explyt config set autoUpdate false
|
|
51
|
+
explyt config get updateChannel
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Any other value is refused by `explyt config` and read as the default, with a warning. Moving from
|
|
55
|
+
`dogfood` back to `stable` keeps the current (newer) version until stable catches up;
|
|
56
|
+
`explyt update --channel stable --allow-downgrade` installs the older stable version at once.
|
package/dist/cli/engine.js
CHANGED
|
@@ -19,10 +19,10 @@ import {
|
|
|
19
19
|
symlinkSync,
|
|
20
20
|
writeFileSync,
|
|
21
21
|
} from "node:fs";
|
|
22
|
-
import { basename, delimiter, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
22
|
+
import { basename, delimiter, dirname, isAbsolute, join, posix, resolve, sep, win32 } from "node:path";
|
|
23
23
|
import { homedir, platform, tmpdir } from "node:os";
|
|
24
24
|
import { fileURLToPath } from "node:url";
|
|
25
|
-
import { randomBytes } from "node:crypto";
|
|
25
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
26
26
|
|
|
27
27
|
export const ENGINE_PACKAGE = "@earendil-works/pi-coding-agent";
|
|
28
28
|
|
|
@@ -252,22 +252,20 @@ export function isUsableShim(path, os = platform()) {
|
|
|
252
252
|
}
|
|
253
253
|
|
|
254
254
|
/**
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
* to a backend and never environment variables.
|
|
263
|
-
*
|
|
264
|
-
* In place, and nothing at all without a `binDir`: assigning a missing value to
|
|
265
|
-
* `process.env` stores the string "undefined". `env.PATH` is read on the env itself,
|
|
266
|
-
* which on Windows finds `Path` too; a copy of it would not.
|
|
255
|
+
* Leave `env` with exactly one PATH key: `dirs` (in order, empty ones skipped) in front
|
|
256
|
+
* of the inherited value (RQ-6). In place, and never a missing value: the inherited
|
|
257
|
+
* search path is read under any spelling (`PATH`, `Path`, ...), every spelling is then
|
|
258
|
+
* removed and one `PATH` is written. On Windows `process.env` is case-insensitive, so
|
|
259
|
+
* this rewrites `Path`; on POSIX a caller that hands us both spellings stops leaving it
|
|
260
|
+
* unspecified which one a child sees. Assigning a missing value to `process.env` would
|
|
261
|
+
* store the string "undefined" (#219), so the value written is always a string.
|
|
267
262
|
*/
|
|
268
|
-
export function
|
|
269
|
-
|
|
270
|
-
|
|
263
|
+
export function setProductPath(env, dirs) {
|
|
264
|
+
const spellings = Object.keys(env).filter((key) => key.toUpperCase() === "PATH");
|
|
265
|
+
const inherited = env.PATH ?? (spellings.length > 0 ? env[spellings[0]] : undefined);
|
|
266
|
+
for (const key of spellings) delete env[key];
|
|
267
|
+
const parts = [...dirs.filter(Boolean), ...(inherited ? [inherited] : [])];
|
|
268
|
+
env.PATH = parts.join(delimiter);
|
|
271
269
|
}
|
|
272
270
|
|
|
273
271
|
/**
|
|
@@ -364,10 +362,36 @@ function explicitAgentDir(value, home) {
|
|
|
364
362
|
* package would be walked as one. Next to it keeps it under the one parent this project
|
|
365
363
|
* owns, and moves with an explicit `PI_CODING_AGENT_DIR` (the agent directory's parent
|
|
366
364
|
* is what a developer relocating the state relocates).
|
|
365
|
+
*
|
|
366
|
+
* The name is KEYED by the engine root of the running install (spec 019, RQ-47): the
|
|
367
|
+
* terminal and the IDE share one state root, and with one unkeyed directory two installs
|
|
368
|
+
* with different engines rebuilt it on every alternate start -- and removed it while the
|
|
369
|
+
* other one's engine still read its docs and version through it. The key is a hash of
|
|
370
|
+
* the engine root's real path; on Windows the path is normalised and lower-cased first,
|
|
371
|
+
* because the same directory has many spellings there.
|
|
372
|
+
*
|
|
373
|
+
* @param {object} env the environment the agent directory is read from
|
|
374
|
+
* @param {string} [home] home directory, injectable for tests
|
|
375
|
+
* @param {string} engineDir the engine root of the running install
|
|
367
376
|
*/
|
|
368
|
-
export function engineShimDir(env = process.env, home = homedir()) {
|
|
377
|
+
export function engineShimDir(env = process.env, home = homedir(), engineDir) {
|
|
369
378
|
const agentDir = withAgentDir(env, home)[AGENT_DIR_ENV];
|
|
370
|
-
return join(dirname(agentDir),
|
|
379
|
+
return join(dirname(agentDir), `engine-${engineShimKey(engineDir)}`);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** The per-install part of the shim directory name: a short hash of the engine root. */
|
|
383
|
+
export function engineShimKey(engineDir, os = platform()) {
|
|
384
|
+
if (typeof engineDir !== "string" || engineDir === "") throw new Error("engineShimDir needs the engine root of the running install");
|
|
385
|
+
let real = engineDir;
|
|
386
|
+
try {
|
|
387
|
+
real = realpathSync(engineDir);
|
|
388
|
+
} catch {
|
|
389
|
+
// A root that cannot be resolved still gets a stable key from its spelling.
|
|
390
|
+
}
|
|
391
|
+
// The target platform's own path rules, not the host's: a win32 key computed on
|
|
392
|
+
// Linux (tests, cross-packed trees) must equal the one Windows computes.
|
|
393
|
+
const key = os === "win32" ? win32.resolve(real).toLowerCase() : posix.resolve(real);
|
|
394
|
+
return createHash("sha256").update(key).digest("hex").slice(0, 16);
|
|
371
395
|
}
|
|
372
396
|
|
|
373
397
|
/**
|
|
@@ -902,7 +926,7 @@ export function withIsolation(rest, packageRoot) {
|
|
|
902
926
|
* debug artifacts, where the adapter ships next to the wrapper; the public terminal
|
|
903
927
|
* package answers it with a migration error.
|
|
904
928
|
*/
|
|
905
|
-
export const COMMANDS = new Set(["acp", "login", "logout", "tui", "update"]);
|
|
929
|
+
export const COMMANDS = new Set(["acp", "config", "install", "login", "logout", "tui", "update"]);
|
|
906
930
|
|
|
907
931
|
/**
|
|
908
932
|
* Engine flags that confine a terminal run to a single package: ours.
|
|
@@ -997,6 +1021,54 @@ export function terminalEngineArgs(entrypoint, packageRoot, extra = [], { login
|
|
|
997
1021
|
*/
|
|
998
1022
|
export const ADAPTER_LOGIN_FLAG = "--terminal-login";
|
|
999
1023
|
|
|
1024
|
+
/**
|
|
1025
|
+
* The launch file of the installed release `packageRoot` belongs to, or null outside one.
|
|
1026
|
+
* An installed release is `<root>/install/releases/<cli>+<adapter>/`, and its launch file
|
|
1027
|
+
* is `<root>/install/launch.js` (spec 019, RQ-42, RQ-43): derived from the path, so no
|
|
1028
|
+
* variable has to carry it.
|
|
1029
|
+
*/
|
|
1030
|
+
export function launchFileFor(packageRoot) {
|
|
1031
|
+
let dir = packageRoot;
|
|
1032
|
+
for (;;) {
|
|
1033
|
+
const parent = dirname(dir);
|
|
1034
|
+
if (parent === dir) return null;
|
|
1035
|
+
if (basename(parent) === "releases" && basename(dirname(parent)) === "install") {
|
|
1036
|
+
return join(dirname(parent), "launch.js");
|
|
1037
|
+
}
|
|
1038
|
+
dir = parent;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/**
|
|
1043
|
+
* The positionals a `--terminal-login` launch forwards to `login`, without the repeated
|
|
1044
|
+
* launch path (spec 019, RQ-28).
|
|
1045
|
+
*
|
|
1046
|
+
* A client that follows the protocol appends the terminal-auth method's `args` -- the
|
|
1047
|
+
* launch file and `--terminal-login` -- to its own launch command, so what runs is
|
|
1048
|
+
* `node <entry> acp <entry> --terminal-login`. `parseArgs` keeps the second `<entry>` as
|
|
1049
|
+
* a positional, and the engine would take it as the user's first prompt. A positional is
|
|
1050
|
+
* dropped when it names the same file as this process's `argv[1]` or as the launch file
|
|
1051
|
+
* of the installed release this CLI runs from (`launchFileFor`); every other argument is
|
|
1052
|
+
* kept.
|
|
1053
|
+
*
|
|
1054
|
+
* @param {string[]} rest the positionals `parseArgs` returned
|
|
1055
|
+
* @param {{ argv1?: string, launchFile?: string | null, cwd?: string }} self
|
|
1056
|
+
*/
|
|
1057
|
+
export function withoutLaunchPath(rest, { argv1, launchFile, cwd = process.cwd() } = {}) {
|
|
1058
|
+
const same = (a, b) => {
|
|
1059
|
+
if (!a || !b) return false;
|
|
1060
|
+
const left = resolve(cwd, a);
|
|
1061
|
+
const right = resolve(cwd, b);
|
|
1062
|
+
if (left === right) return true;
|
|
1063
|
+
try {
|
|
1064
|
+
return realpathSync(left) === realpathSync(right);
|
|
1065
|
+
} catch {
|
|
1066
|
+
return false;
|
|
1067
|
+
}
|
|
1068
|
+
};
|
|
1069
|
+
return rest.filter((arg) => arg.startsWith("-") || !(same(arg, argv1) || same(arg, launchFile)));
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1000
1072
|
/**
|
|
1001
1073
|
* Flags belonging to other programs that consume the NEXT argument as their value.
|
|
1002
1074
|
*
|
|
@@ -1020,6 +1092,7 @@ export function parseArgs(argv) {
|
|
|
1020
1092
|
let help = false;
|
|
1021
1093
|
let version = false;
|
|
1022
1094
|
let seenPositional = false;
|
|
1095
|
+
let terminalLogin = false;
|
|
1023
1096
|
const rest = [];
|
|
1024
1097
|
|
|
1025
1098
|
for (let i = 0; i < argv.length; i += 1) {
|
|
@@ -1040,7 +1113,7 @@ export function parseArgs(argv) {
|
|
|
1040
1113
|
// to the `acp` invocation from its own config -- see ADAPTER_LOGIN_FLAG. It is also
|
|
1041
1114
|
// consumed rather than forwarded: the entrypoint re-adds it for the adapter, so
|
|
1042
1115
|
// passing it through as well would hand the adapter the same flag twice.
|
|
1043
|
-
if (arg === ADAPTER_LOGIN_FLAG) { command = "login"; continue; }
|
|
1116
|
+
if (arg === ADAPTER_LOGIN_FLAG) { command = "login"; terminalLogin = true; continue; }
|
|
1044
1117
|
|
|
1045
1118
|
// A foreign flag plus its value: forward both, and never read the value as a
|
|
1046
1119
|
// subcommand.
|
|
@@ -1061,29 +1134,32 @@ export function parseArgs(argv) {
|
|
|
1061
1134
|
rest.push(arg);
|
|
1062
1135
|
}
|
|
1063
1136
|
|
|
1064
|
-
return { command, help, version, rest };
|
|
1137
|
+
return { command, help, version, rest, terminalLogin };
|
|
1065
1138
|
}
|
|
1066
1139
|
|
|
1067
1140
|
export function helpText() {
|
|
1068
1141
|
return [
|
|
1069
1142
|
"Explyt AI Agent",
|
|
1070
1143
|
"",
|
|
1071
|
-
"Install
|
|
1144
|
+
"Install with the Explyt installer, then run explyt:",
|
|
1145
|
+
" explyt install [--channel stable|dogfood] install or update the managed installation",
|
|
1072
1146
|
"Or run a pinned version without installing:",
|
|
1073
1147
|
" npx -y @explyt/ai-agent@<version> start the terminal UI",
|
|
1074
1148
|
" npx -y @explyt/ai-agent@<version> tui start the terminal UI",
|
|
1075
1149
|
" npx -y @explyt/ai-agent@<version> login store a provider credential",
|
|
1076
1150
|
" npx -y @explyt/ai-agent@<version> logout remove stored credentials",
|
|
1077
1151
|
"",
|
|
1078
|
-
"
|
|
1079
|
-
" explyt update
|
|
1152
|
+
"Commands: explyt tui | login | logout | install | update | config",
|
|
1153
|
+
" explyt update update the managed installation now (same as install)",
|
|
1154
|
+
" explyt config set updateChannel stable|dogfood | autoUpdate true|false",
|
|
1080
1155
|
" -h, --help show this message",
|
|
1081
1156
|
" -v, --version print the version",
|
|
1082
1157
|
"",
|
|
1083
1158
|
"Credentials and sessions are separate from other agents: logging in to a pi you run",
|
|
1084
1159
|
"by hand does not log in here, so run login once for this agent.",
|
|
1085
1160
|
"The terminal UI notes a newer version at startup; PI_OFFLINE=1 turns the check off.",
|
|
1086
|
-
"IDE (ACP) integration
|
|
1161
|
+
"IDE (ACP) integration is set up by the Explyt installer, which places the adapter",
|
|
1162
|
+
"next to this package; it is not part of this package.",
|
|
1087
1163
|
"",
|
|
1088
1164
|
].join("\n");
|
|
1089
1165
|
}
|
package/dist/cli/index.js
CHANGED
|
@@ -8,12 +8,11 @@
|
|
|
8
8
|
// npx @explyt/ai-agent login a human, once: store a provider credential
|
|
9
9
|
// npx @explyt/ai-agent logout a human: remove the stored credentials
|
|
10
10
|
//
|
|
11
|
-
// ACP is NOT part of
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// there `acp` works exactly as before.
|
|
11
|
+
// ACP is NOT part of this package: the adapter is the separate package
|
|
12
|
+
// `@explyt/acp-adapter` (spec 019). `acp` loads it when the installer placed it next to
|
|
13
|
+
// this CLI in an installed release, or from the backend and debug trees, which carry it
|
|
14
|
+
// inside this package root; otherwise `acp` refuses and names the installer. The CLI
|
|
15
|
+
// never depends on the adapter (RQ-1).
|
|
17
16
|
//
|
|
18
17
|
// There is deliberately no `install`. The agent registers nothing and writes to no
|
|
19
18
|
// user configuration: every run isolates the engine to the extensions WE ship, so
|
|
@@ -30,11 +29,14 @@
|
|
|
30
29
|
// only. Any stray write corrupts the stream and the client drops the connection, so
|
|
31
30
|
// all diagnostics in this file go to stderr.
|
|
32
31
|
|
|
33
|
-
import {
|
|
32
|
+
import { installationRoot, secondRootWarning } from "./install-roots.js";
|
|
33
|
+
import { signalEnv, updateArgsError, UPDATE_ENV } from "./update.js";
|
|
34
|
+
import { runInstaller } from "./install-run.js";
|
|
35
|
+
import { configCommand } from "./install-config.js";
|
|
34
36
|
import { spawnSync } from "node:child_process";
|
|
35
|
-
import { closeSync, existsSync, openSync } from "node:fs";
|
|
37
|
+
import { closeSync, existsSync, openSync, readFileSync, realpathSync } from "node:fs";
|
|
36
38
|
import { readFile, rm } from "node:fs/promises";
|
|
37
|
-
import { dirname, join } from "node:path";
|
|
39
|
+
import { basename, dirname, join, sep } from "node:path";
|
|
38
40
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
39
41
|
import {
|
|
40
42
|
AGENT_DIR_ENV,
|
|
@@ -52,15 +54,22 @@ import {
|
|
|
52
54
|
helpText,
|
|
53
55
|
interactiveStdin,
|
|
54
56
|
parseArgs,
|
|
55
|
-
|
|
57
|
+
setProductPath,
|
|
56
58
|
terminalEngineArgs,
|
|
57
59
|
withAgentDir,
|
|
58
60
|
withIsolation,
|
|
61
|
+
withoutLaunchPath,
|
|
62
|
+
launchFileFor,
|
|
59
63
|
} from "./engine.js";
|
|
60
64
|
|
|
61
65
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
62
66
|
const packageRoot = findPackageRoot(here);
|
|
63
67
|
const args = parseArgs(process.argv.slice(2));
|
|
68
|
+
// A terminal-auth launch repeats the launch path as a positional (spec 019, RQ-28);
|
|
69
|
+
// without this it would reach the engine as the user's first prompt.
|
|
70
|
+
if (args.terminalLogin) {
|
|
71
|
+
args.rest = withoutLaunchPath(args.rest, { argv1: process.argv[1], launchFile: packageRoot ? launchFileFor(packageRoot) : null });
|
|
72
|
+
}
|
|
64
73
|
|
|
65
74
|
function fail(message, code = 1) {
|
|
66
75
|
process.stderr.write(`explyt: ${message}\n`);
|
|
@@ -95,7 +104,7 @@ async function findEngine() {
|
|
|
95
104
|
// The engine presents itself under our name from a manifest copy in a directory we
|
|
96
105
|
// own, handed to it through PI_PACKAGE_DIR; see `ensureEngineShim` for why that and not
|
|
97
106
|
// the installed manifest. A failed shim is reported and the engine runs unbranded.
|
|
98
|
-
const shim = ensureEngineShim(engineDir, engineShimDir(process.env));
|
|
107
|
+
const shim = ensureEngineShim(engineDir, engineShimDir(process.env, undefined, engineDir));
|
|
99
108
|
if (shim.status === "failed") {
|
|
100
109
|
process.stderr.write(`explyt: engine keeps its own name: ${shim.detail}\n`);
|
|
101
110
|
}
|
|
@@ -142,20 +151,28 @@ if (args.version) {
|
|
|
142
151
|
const version = await selfVersion();
|
|
143
152
|
if (!version) fail("cannot read the package version");
|
|
144
153
|
process.stdout.write(`${version}\n`);
|
|
154
|
+
// RQ-46: a second installation root is named on stderr; stdout stays the version.
|
|
155
|
+
const secondRoot = secondRootWarning({ root: installationRoot() });
|
|
156
|
+
if (secondRoot) process.stderr.write(`explyt: warning: ${secondRoot}\n`);
|
|
145
157
|
process.exit(0);
|
|
146
158
|
}
|
|
147
159
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
160
|
+
// `install` and `update` run the installer of the managed installation (spec 019, RQ-18,
|
|
161
|
+
// RQ-33, RQ-36): `update` never runs `npm install -g`, also from a global npm install.
|
|
162
|
+
if (args.command === "install" || args.command === "update") {
|
|
163
|
+
// The engine's `update --extensions` is explained, not treated as an installer argument.
|
|
164
|
+
const engineUpdate = updateArgsError(args.rest);
|
|
165
|
+
if (engineUpdate && engineUpdate !== "update takes no arguments") fail(engineUpdate, 2);
|
|
166
|
+
process.exit(await runInstaller(args.rest));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (args.command === "config") {
|
|
170
|
+
process.exit(await configCommand(args.rest, { agentDir: withAgentDir(process.env)[AGENT_DIR_ENV] }));
|
|
154
171
|
}
|
|
155
172
|
|
|
156
173
|
// A bare invocation is the terminal UI. It used to mean "ACP when stdin is a pipe";
|
|
157
|
-
// that heuristic is gone
|
|
158
|
-
//
|
|
174
|
+
// that heuristic is gone -- an IDE that wants ACP passes `acp` explicitly, through the
|
|
175
|
+
// installation's launch file or a backend/debug tree (spec 019, RQ-5).
|
|
159
176
|
const wantsAcp = args.command === "acp";
|
|
160
177
|
const wantsLogin = args.command === "login";
|
|
161
178
|
const wantsLogout = args.command === "logout";
|
|
@@ -239,24 +256,76 @@ if (wantsTui || wantsLogin) {
|
|
|
239
256
|
process.exit(child.status ?? 1);
|
|
240
257
|
}
|
|
241
258
|
|
|
242
|
-
// ACP MODE.
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
259
|
+
// ACP MODE (spec 019, RQ-5). The CLI prepares the engine environment and then loads
|
|
260
|
+
// the adapter, which starts the server on import. The adapter is accepted from exactly
|
|
261
|
+
// these places, in this order:
|
|
262
|
+
// 1. the candidate paths inside this package root -- the backend and debug trees:
|
|
263
|
+
// backend artifact: dist/cli/index.js -> dist/acp/index.js
|
|
264
|
+
// dev source: cli/index.mjs -> vendor/pi-acp/dist/index.js
|
|
265
|
+
// 2. an installed release npm produced from `@explyt/acp-adapter`:
|
|
266
|
+
// hoisted <modules>/@explyt/acp-adapter next to <modules>/@explyt/ai-agent
|
|
267
|
+
// nested the package whose node_modules holds this CLI
|
|
268
|
+
// Never NODE_PATH, the working directory, PATH, a workspace link or a global prefix:
|
|
269
|
+
// plain Node resolution would honour NODE_PATH and would follow this checkout's
|
|
270
|
+
// workspace link to the unobfuscated submodule.
|
|
271
|
+
const ADAPTER_PACKAGE = "@explyt/acp-adapter";
|
|
272
|
+
|
|
273
|
+
/** The adapter's `main` entry in `dir`, or null when `dir` is not the adapter package. */
|
|
274
|
+
function adapterMain(dir) {
|
|
275
|
+
try {
|
|
276
|
+
const manifest = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
|
|
277
|
+
if (manifest.name !== ADAPTER_PACKAGE || typeof manifest.main !== "string" || manifest.main === "") return null;
|
|
278
|
+
const main = join(dir, manifest.main);
|
|
279
|
+
return existsSync(main) ? main : null;
|
|
280
|
+
} catch {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* The adapter of an installed release, found from this package's own real location. A
|
|
287
|
+
* directory that is a symlink (a workspace link) is not an installed release.
|
|
288
|
+
*/
|
|
289
|
+
function installedReleaseAdapter(root) {
|
|
290
|
+
let real;
|
|
291
|
+
try {
|
|
292
|
+
real = realpathSync(root);
|
|
293
|
+
} catch {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
const scope = dirname(real);
|
|
297
|
+
const modules = dirname(scope);
|
|
298
|
+
if (basename(scope) !== "@explyt" || basename(modules) !== "node_modules") return null;
|
|
299
|
+
const accept = (dir) => {
|
|
300
|
+
try {
|
|
301
|
+
// The package directory itself must be real: npm installs packages, a link is a workspace.
|
|
302
|
+
if (realpathSync(dir) !== dir) return null;
|
|
303
|
+
} catch {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
return adapterMain(dir);
|
|
307
|
+
};
|
|
308
|
+
// hoisted: a sibling in the same node_modules
|
|
309
|
+
const hoisted = accept(join(modules, "@explyt", "acp-adapter"));
|
|
310
|
+
if (hoisted) return hoisted;
|
|
311
|
+
// nested: the package whose node_modules holds this CLI
|
|
312
|
+
const owner = dirname(modules);
|
|
313
|
+
if (owner.split(sep).includes("node_modules")) return accept(owner);
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
|
|
246
317
|
const adapterCandidates = [
|
|
247
318
|
join(here, "..", "acp", "index.js"),
|
|
248
319
|
join(packageRoot, "dist", "acp", "index.js"),
|
|
249
320
|
join(packageRoot, "vendor", "pi-acp", "dist", "index.js"),
|
|
250
321
|
];
|
|
251
|
-
const adapter = adapterCandidates.find((candidate) => existsSync(candidate));
|
|
322
|
+
const adapter = adapterCandidates.find((candidate) => existsSync(candidate)) ?? installedReleaseAdapter(packageRoot);
|
|
252
323
|
if (!adapter) {
|
|
253
|
-
//
|
|
254
|
-
// "unknown command", because this is exactly what an old IDE entry runs.
|
|
324
|
+
// What an old IDE entry, or a terminal-only install, runs (RQ-27).
|
|
255
325
|
fail(
|
|
256
|
-
"ACP is
|
|
257
|
-
" IDE integration is
|
|
258
|
-
"
|
|
259
|
-
" ACP-capable version of this package.",
|
|
326
|
+
"no ACP adapter is installed next to this package.\n" +
|
|
327
|
+
" IDE integration is set up by the Explyt installer, which installs the adapter\n" +
|
|
328
|
+
" together with this CLI; the Neo plugin runs it for you. See `explyt --help`.",
|
|
260
329
|
);
|
|
261
330
|
}
|
|
262
331
|
|
|
@@ -265,7 +334,19 @@ if (!adapter) {
|
|
|
265
334
|
// one place the `.bin` shim is still needed, and the adapter handles the Windows
|
|
266
335
|
// `.cmd` case itself.
|
|
267
336
|
const engine = await requireEngine();
|
|
268
|
-
|
|
337
|
+
// The adapter's own `PI_ACP_PI_COMMAND` names the engine command explicitly (the Neo
|
|
338
|
+
// plugin sets it for the managed backend, whose win32 tree is packed on Linux and has
|
|
339
|
+
// no `pi.cmd`, #219). A missing shim is excused only when that variable names a file
|
|
340
|
+
// that exists: subagents and bash tools pass the variable on to child processes, so a
|
|
341
|
+
// value inherited from an unrelated session must not mask a broken install (RQ-6, RQ-50).
|
|
342
|
+
const explicitEngineCommand = process.env.PI_ACP_PI_COMMAND;
|
|
343
|
+
if (!engine.binDir && !(explicitEngineCommand && existsSync(explicitEngineCommand))) {
|
|
344
|
+
fail(`the agent engine's pi bin is missing (looked for node_modules/.bin/pi next to ${engine.engineDir}); reinstall with the Explyt installer`);
|
|
345
|
+
}
|
|
346
|
+
// Exactly one PATH key (RQ-6): the engine's `.bin` first, then this Node's directory, so
|
|
347
|
+
// the engine's `#!/usr/bin/env node` finds the same Node even when the caller's PATH has
|
|
348
|
+
// none, then the inherited value. Never a missing value (#219).
|
|
349
|
+
setProductPath(process.env, [engine.binDir, dirname(process.execPath)]);
|
|
269
350
|
|
|
270
351
|
// Point the engine at THIS agent's configuration directory rather than the
|
|
271
352
|
// `~/.pi/agent` of a pi the user may also run by hand. Set here, in our own process,
|