@explyt/ai-agent 0.2.0 → 0.3.0-dogfood.1
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/THIRD-PARTY-NOTICES.md +34 -5
- package/agent-docs/README.md +6 -3
- package/agent-docs/updates.md +56 -0
- package/dist/cli/engine.js +102 -23
- 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 +141 -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 +9 -73
- 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 -0
- package/dist/install/installer.js +3652 -0
- package/dist/mcp-adapter.js +4 -4
- package/dist/rpiv-ask-user-question/{chunk-GAM3ZAHN.js → chunk-JNDW3P3A.js} +2 -2
- package/dist/rpiv-ask-user-question/index.js +3 -3
- package/dist/vendor/model-probe/rules.json +102 -0
- package/dist/vendor/model-probe/src/index.js +983 -0
- package/dist/vendor/model-probe/src/opencode-models-snapshot.js +8 -0
- package/dist/vendor/model-probe/src/opencode-models.js +6 -0
- package/dist/vendor/pi-ai-compat.js +2 -0
- package/dist/vendor/pi-coding-agent.js +159 -0
- package/dist/vendor/pi-web-access-deps/modelcontextprotocol__sdk__client__index.js.js +9 -0
- package/dist/vendor/pi-web-access-deps/modelcontextprotocol__sdk__client__streamableHttp.js.js +9 -0
- package/dist/vendor/pi-web-access.js +24958 -0
- package/dist/vendor/typebox/chunk-6XHASRAZ.js +4819 -0
- package/dist/vendor/typebox/index.js +703 -0
- package/dist/vendor/typebox/value.js +3736 -0
- package/package.json +16 -5
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/THIRD-PARTY-NOTICES.md
CHANGED
|
@@ -20,7 +20,8 @@ License or other written license agreements with the Company.
|
|
|
20
20
|
> depends on or redistributes. The release pipeline additionally generates a
|
|
21
21
|
> machine-collected list of license metadata for every runtime npm dependency of the
|
|
22
22
|
> artifact, marking which of them are bundled inside it. The public npm package bundles
|
|
23
|
-
> none of them: npm installs them beside it
|
|
23
|
+
> none of them as packages: npm installs them beside it; it does carry compiled copies of
|
|
24
|
+
> `model-probe` and `typebox` (section 5) and of `pi-web-access` (section 3) under `dist/vendor/`. The managed backend archive redistributes its
|
|
24
25
|
> whole installed tree. The generated list is not a substitute for reviewing the licenses
|
|
25
26
|
> and NOTICE files of all transitive dependencies before a commercial release.
|
|
26
27
|
|
|
@@ -88,6 +89,12 @@ License or other written license agreements with the Company.
|
|
|
88
89
|
the unmodified `pi-mcp-adapter` entrypoint (`index.ts`) together with the package's
|
|
89
90
|
small runtime helper files, so that Node can load it outside the engine's TypeScript
|
|
90
91
|
loader.
|
|
92
|
+
- The public npm package contains `dist/vendor/pi-web-access.js`, an unobfuscated
|
|
93
|
+
compilation of the unmodified `pi-web-access` entrypoint (`dist/index.js`) carrying the
|
|
94
|
+
package's name, version and licence in a banner, so that its imports of the engine and
|
|
95
|
+
`typebox` resolve to the product's files under `dist/vendor/`; and small generated files
|
|
96
|
+
under `dist/vendor/pi-web-access-deps/` that import the package's own installed copy of
|
|
97
|
+
`@modelcontextprotocol/sdk`. The package's own files are not altered.
|
|
91
98
|
- In the managed backend archive only, `pi-mcp-adapter` is redistributed with one
|
|
92
99
|
textual modification made at build time: in its `index.ts`, the slash-command alias
|
|
93
100
|
registered as `pi-mcp` (a second name for the package's `mcp` status command) is
|
|
@@ -97,6 +104,8 @@ License or other written license agreements with the Company.
|
|
|
97
104
|
here as required by the MIT License; the original copyright and license notice above
|
|
98
105
|
apply to the modified package unchanged. The public npm package does not carry the
|
|
99
106
|
adapter and therefore carries no modification of it.
|
|
107
|
+
- The public npm package contains `dist/vendor/pi-coding-agent.js`, a generated file that
|
|
108
|
+
re-exports the engine's own bundled entry point by name; it contains no engine code.
|
|
100
109
|
- The formerly bundled `pi-subagents` package was removed by spec 008; references to it in
|
|
101
110
|
older release history are historical and do not describe the current artifact.
|
|
102
111
|
- Seven units of `@earendil-works/pi-coding-agent` 0.84.4 are **copied** (from the TypeScript in
|
|
@@ -133,7 +142,7 @@ License or other written license agreements with the Company.
|
|
|
133
142
|
- local Explyt extension with current `@earendil-works` imports
|
|
134
143
|
- shell API-key execution removed; credential files use locked atomic writes and private permissions
|
|
135
144
|
|
|
136
|
-
### 5. model-probe, @opencode-ai/models, yaml, minimatch and
|
|
145
|
+
### 5. model-probe, @opencode-ai/models, yaml, minimatch, semver and typebox
|
|
137
146
|
|
|
138
147
|
- `model-probe` 0.6.0, MIT, https://www.npmjs.com/package/model-probe
|
|
139
148
|
- Copyright (c) 2025 ratatulieoi
|
|
@@ -146,9 +155,18 @@ License or other written license agreements with the Company.
|
|
|
146
155
|
- Copyright Isaac Z. Schlueter <i@izs.me> and minimatch contributors
|
|
147
156
|
- `semver` 7.8.5, ISC, https://www.npmjs.com/package/semver
|
|
148
157
|
- Copyright (c) Isaac Z. Schlueter and Contributors
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
158
|
+
- `typebox` (the version pinned in the development manifest, which the build compiles; the
|
|
159
|
+
build-time metadata below and each compiled file's banner name it), MIT, https://www.npmjs.com/package/typebox
|
|
160
|
+
- Copyright (c) 2017-2026 Haydn Paterson
|
|
161
|
+
|
|
162
|
+
`model-probe` and `typebox` are also redistributed compiled, unobfuscated and unmodified:
|
|
163
|
+
the public npm package contains `dist/vendor/model-probe/` (the installed `model-probe`
|
|
164
|
+
compiled from its TypeScript, with its `rules.json`) and `dist/vendor/typebox/` (the pinned
|
|
165
|
+
`typebox` in one ESM build), each file carrying the package's name, version and
|
|
166
|
+
licence in a banner that names the exact version compiled.
|
|
167
|
+
|
|
168
|
+
The MIT text below applies to the derivative, `model-probe`, `@opencode-ai/models` and
|
|
169
|
+
`typebox` with the copyright notices above. The ISC text immediately below applies to `yaml` and
|
|
152
170
|
`semver`, each with its own copyright holder listed above (the ISC terms are identical);
|
|
153
171
|
`minimatch` is covered by the separate Blue Oak Model License text further down, and its
|
|
154
172
|
copyright holder is the one listed for it above rather than the one named in the ISC text:
|
|
@@ -511,17 +529,28 @@ Consult each package for its complete license text.
|
|
|
511
529
|
- @modelcontextprotocol/client@2.0.0: license metadata = MIT (installed by npm beside this package)
|
|
512
530
|
- @modelcontextprotocol/core@2.0.0: license metadata = MIT (installed by npm beside this package)
|
|
513
531
|
- @modelcontextprotocol/ext-apps@1.7.5: license metadata = MIT (installed by npm beside this package)
|
|
532
|
+
- @mozilla/readability@0.6.0: license metadata = Apache-2.0 (installed by npm beside this package)
|
|
514
533
|
- @napi-rs/keyring@1.3.0: license metadata = MIT (installed by npm beside this package)
|
|
515
534
|
- ajv@8.20.0: license metadata = MIT (installed by npm beside this package)
|
|
516
535
|
- ajv-formats@3.0.1: license metadata = MIT (installed by npm beside this package)
|
|
517
536
|
- cross-spawn@7.0.6: license metadata = MIT (installed by npm beside this package)
|
|
537
|
+
- defuddle@0.19.3: license metadata = MIT (installed by npm beside this package)
|
|
538
|
+
- linkedom@0.16.11: license metadata = ISC (installed by npm beside this package)
|
|
518
539
|
- minimatch@10.2.6: license metadata = BlueOak-1.0.0 (installed by npm beside this package)
|
|
519
540
|
- model-probe@0.6.0: license metadata = MIT (installed by npm beside this package)
|
|
520
541
|
- open@10.2.0: license metadata = MIT (installed by npm beside this package)
|
|
542
|
+
- p-limit@6.2.0: license metadata = MIT (installed by npm beside this package)
|
|
521
543
|
- pi-mcp-adapter@2.31.0: license metadata = MIT (installed by npm beside this package)
|
|
522
544
|
- pi-web-access@0.31.0: license metadata = MIT (installed by npm beside this package)
|
|
545
|
+
- promise.try@2.0.2: license metadata = MIT (installed by npm beside this package)
|
|
523
546
|
- recheck@4.5.0: license metadata = MIT (installed by npm beside this package)
|
|
524
547
|
- semver@7.8.5: license metadata = ISC (installed by npm beside this package)
|
|
525
548
|
- smol-toml@1.9.0: license metadata = BSD-3-Clause (installed by npm beside this package)
|
|
526
549
|
- strip-json-comments@5.0.3: license metadata = MIT (installed by npm beside this package)
|
|
550
|
+
- turndown@7.2.4: license metadata = MIT (installed by npm beside this package)
|
|
551
|
+
- undici@8.11.2: license metadata = MIT (installed by npm beside this package)
|
|
552
|
+
- unpdf@1.8.1: license metadata = MIT (installed by npm beside this package)
|
|
527
553
|
- yaml@2.9.0: license metadata = ISC (installed by npm beside this package)
|
|
554
|
+
- typebox@1.3.24: license metadata = MIT (compiled into dist/vendor/typebox)
|
|
555
|
+
- model-probe@0.6.0: license metadata = MIT (compiled into dist/vendor/model-probe)
|
|
556
|
+
- pi-web-access@0.31.0: license metadata = MIT (compiled into dist/vendor/pi-web-access.js)
|
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
|
|
|
@@ -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,19 +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.
|
|
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.
|
|
263
262
|
*/
|
|
264
|
-
export function
|
|
265
|
-
|
|
266
|
-
const
|
|
267
|
-
|
|
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);
|
|
268
269
|
}
|
|
269
270
|
|
|
270
271
|
/**
|
|
@@ -361,10 +362,36 @@ function explicitAgentDir(value, home) {
|
|
|
361
362
|
* package would be walked as one. Next to it keeps it under the one parent this project
|
|
362
363
|
* owns, and moves with an explicit `PI_CODING_AGENT_DIR` (the agent directory's parent
|
|
363
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
|
|
364
376
|
*/
|
|
365
|
-
export function engineShimDir(env = process.env, home = homedir()) {
|
|
377
|
+
export function engineShimDir(env = process.env, home = homedir(), engineDir) {
|
|
366
378
|
const agentDir = withAgentDir(env, home)[AGENT_DIR_ENV];
|
|
367
|
-
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);
|
|
368
395
|
}
|
|
369
396
|
|
|
370
397
|
/**
|
|
@@ -899,7 +926,7 @@ export function withIsolation(rest, packageRoot) {
|
|
|
899
926
|
* debug artifacts, where the adapter ships next to the wrapper; the public terminal
|
|
900
927
|
* package answers it with a migration error.
|
|
901
928
|
*/
|
|
902
|
-
export const COMMANDS = new Set(["acp", "login", "logout", "tui", "update"]);
|
|
929
|
+
export const COMMANDS = new Set(["acp", "config", "install", "login", "logout", "tui", "update"]);
|
|
903
930
|
|
|
904
931
|
/**
|
|
905
932
|
* Engine flags that confine a terminal run to a single package: ours.
|
|
@@ -994,6 +1021,54 @@ export function terminalEngineArgs(entrypoint, packageRoot, extra = [], { login
|
|
|
994
1021
|
*/
|
|
995
1022
|
export const ADAPTER_LOGIN_FLAG = "--terminal-login";
|
|
996
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
|
+
|
|
997
1072
|
/**
|
|
998
1073
|
* Flags belonging to other programs that consume the NEXT argument as their value.
|
|
999
1074
|
*
|
|
@@ -1017,6 +1092,7 @@ export function parseArgs(argv) {
|
|
|
1017
1092
|
let help = false;
|
|
1018
1093
|
let version = false;
|
|
1019
1094
|
let seenPositional = false;
|
|
1095
|
+
let terminalLogin = false;
|
|
1020
1096
|
const rest = [];
|
|
1021
1097
|
|
|
1022
1098
|
for (let i = 0; i < argv.length; i += 1) {
|
|
@@ -1037,7 +1113,7 @@ export function parseArgs(argv) {
|
|
|
1037
1113
|
// to the `acp` invocation from its own config -- see ADAPTER_LOGIN_FLAG. It is also
|
|
1038
1114
|
// consumed rather than forwarded: the entrypoint re-adds it for the adapter, so
|
|
1039
1115
|
// passing it through as well would hand the adapter the same flag twice.
|
|
1040
|
-
if (arg === ADAPTER_LOGIN_FLAG) { command = "login"; continue; }
|
|
1116
|
+
if (arg === ADAPTER_LOGIN_FLAG) { command = "login"; terminalLogin = true; continue; }
|
|
1041
1117
|
|
|
1042
1118
|
// A foreign flag plus its value: forward both, and never read the value as a
|
|
1043
1119
|
// subcommand.
|
|
@@ -1058,29 +1134,32 @@ export function parseArgs(argv) {
|
|
|
1058
1134
|
rest.push(arg);
|
|
1059
1135
|
}
|
|
1060
1136
|
|
|
1061
|
-
return { command, help, version, rest };
|
|
1137
|
+
return { command, help, version, rest, terminalLogin };
|
|
1062
1138
|
}
|
|
1063
1139
|
|
|
1064
1140
|
export function helpText() {
|
|
1065
1141
|
return [
|
|
1066
1142
|
"Explyt AI Agent",
|
|
1067
1143
|
"",
|
|
1068
|
-
"Install
|
|
1144
|
+
"Install with the Explyt installer, then run explyt:",
|
|
1145
|
+
" explyt install [--channel stable|dogfood] install or update the managed installation",
|
|
1069
1146
|
"Or run a pinned version without installing:",
|
|
1070
1147
|
" npx -y @explyt/ai-agent@<version> start the terminal UI",
|
|
1071
1148
|
" npx -y @explyt/ai-agent@<version> tui start the terminal UI",
|
|
1072
1149
|
" npx -y @explyt/ai-agent@<version> login store a provider credential",
|
|
1073
1150
|
" npx -y @explyt/ai-agent@<version> logout remove stored credentials",
|
|
1074
1151
|
"",
|
|
1075
|
-
"
|
|
1076
|
-
" 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",
|
|
1077
1155
|
" -h, --help show this message",
|
|
1078
1156
|
" -v, --version print the version",
|
|
1079
1157
|
"",
|
|
1080
1158
|
"Credentials and sessions are separate from other agents: logging in to a pi you run",
|
|
1081
1159
|
"by hand does not log in here, so run login once for this agent.",
|
|
1082
1160
|
"The terminal UI notes a newer version at startup; PI_OFFLINE=1 turns the check off.",
|
|
1083
|
-
"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.",
|
|
1084
1163
|
"",
|
|
1085
1164
|
].join("\n");
|
|
1086
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,
|
|
57
|
+
setProductPath,
|
|
55
58
|
terminalEngineArgs,
|
|
56
59
|
withAgentDir,
|
|
57
|
-
withEnginePath,
|
|
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,
|