jev-agent-tools 0.1.3 → 0.1.4
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/CHANGELOG.md +19 -3
- package/README.md +26 -4
- package/package.json +1 -1
- package/src/configuration.ts +267 -0
- package/src/host-tui.d.ts +14 -0
- package/src/index.ts +13 -5
- package/src/secret-input.ts +222 -0
- package/src/setup.ts +162 -0
- package/src/texts/configuration.ts +1 -1
- package/src/tools/ask-files.ts +3 -6
- package/src/tools/ask.ts +3 -6
- package/src/tools/check-diff.ts +3 -6
- package/src/tools/find.ts +3 -6
- package/src/tools/locate.ts +3 -1
- package/src/tools/select-tests.ts +3 -6
package/CHANGELOG.md
CHANGED
|
@@ -9,10 +9,25 @@ modules are not a stable library API.
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## [0.1.4] - 2026-10-02
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- Interactive configuration for pi and omp: first-launch offer, `/jev-setup`, masked
|
|
17
|
+
key entry, `--jev-skip-setup`, `--jev-url` and `--jev-model`. Configuration applies
|
|
18
|
+
immediately; optional saving uses a private file outside the repository and stores the
|
|
19
|
+
key in plaintext. No dialog appears in print, JSON, RPC or sub-agent sessions.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- Release publication uses trusted publishing (OIDC) without a temporary npm token.
|
|
24
|
+
- Document the npm release's verified pi 1.0.0 integration, simulated-service smoke
|
|
25
|
+
scope and non-blocking `@sinclair/typebox` packaging warning.
|
|
26
|
+
|
|
12
27
|
## [0.1.3] - 2026-10-02
|
|
13
28
|
|
|
14
|
-
|
|
15
|
-
|
|
29
|
+
First npm release, published as `jev-agent-tools`. Versions 0.1.0, 0.1.1 and 0.1.2
|
|
30
|
+
were tagged but never published; their outcomes are recorded below.
|
|
16
31
|
|
|
17
32
|
### Added
|
|
18
33
|
|
|
@@ -59,7 +74,8 @@ Tagged but never published to npm: the unscoped package name was rejected.
|
|
|
59
74
|
|
|
60
75
|
Tagged but never published to npm: the publish workflow failed before upload.
|
|
61
76
|
|
|
62
|
-
[Unreleased]: https://github.com/NomenAK/jev-tools/compare/v0.1.
|
|
77
|
+
[Unreleased]: https://github.com/NomenAK/jev-tools/compare/v0.1.4...HEAD
|
|
78
|
+
[0.1.4]: https://github.com/NomenAK/jev-tools/compare/v0.1.3...v0.1.4
|
|
63
79
|
[0.1.3]: https://github.com/NomenAK/jev-tools/compare/v0.1.0...v0.1.3
|
|
64
80
|
[0.1.2]: https://github.com/NomenAK/jev-tools/tree/v0.1.2
|
|
65
81
|
[0.1.1]: https://github.com/NomenAK/jev-tools/tree/v0.1.1
|
package/README.md
CHANGED
|
@@ -9,23 +9,45 @@ Install through your host's package manager. The npm package is `jev-agent-tools
|
|
|
9
9
|
### pi
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
|
-
pi install npm:jev-agent-tools@0.1.
|
|
12
|
+
pi install npm:jev-agent-tools@0.1.4
|
|
13
13
|
# Project-local installation:
|
|
14
|
-
pi install -l npm:jev-agent-tools@0.1.
|
|
14
|
+
pi install -l npm:jev-agent-tools@0.1.4
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
### omp
|
|
18
18
|
|
|
19
19
|
```sh
|
|
20
|
-
omp plugin install jev-agent-tools@0.1.
|
|
20
|
+
omp plugin install jev-agent-tools@0.1.4
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
### Requirements and compatibility
|
|
24
24
|
|
|
25
25
|
Requires Node.js 24 or later. Supported host baselines are pi 0.87.1 and omp 18.4.10. These are support baselines, not claims that every later version has been individually validated. omp uses its Bun runtime; Node.js is also required for Node-based project checks. Git and, for optional command evidence, Bash must be available. Optional native parsers and file-search acceleration may be unavailable on some platforms; affected tools report their limitations.
|
|
26
26
|
|
|
27
|
+
The published `jev-agent-tools@0.1.3` package was also checked with **pi 1.0.0** on Linux under Node.js 24.15.0: npm installation, TypeScript checking against the host types, all six tools in the actual CLI, and reading-guide injection passed. The CLI smoke used a simulated conversation provider and Jev endpoint; it verifies host integration, not live model accuracy or every tool scenario.
|
|
28
|
+
|
|
29
|
+
**pi 1.0.0 packaging warning:** the package declares `@sinclair/typebox` in `dependencies`, while pi expects host-provided extension modules in `peerDependencies` with a `"*"` range. pi warns that separately installed copies can create duplicate runtime modules. The warning did not prevent the smoke from completing; it remains a packaging issue, not a claim of warning-free compatibility.
|
|
30
|
+
|
|
27
31
|
## Configure
|
|
28
32
|
|
|
33
|
+
### Interactive setup
|
|
34
|
+
|
|
35
|
+
In the main interactive terminal of pi or omp, a first launch without configuration offers **Configure now / Later**. Run `/jev-setup` at any time to change the endpoint, model or key. The key is typed in a masked field and is never passed as a command-line argument. Choose **This session only** (nothing is written) or **Save for future sessions**. Cancelling at any step changes nothing.
|
|
36
|
+
|
|
37
|
+
| Flag | Effect |
|
|
38
|
+
|---|---|
|
|
39
|
+
| `--jev-skip-setup` | Suppress the first-launch offer for this run; `/jev-setup` still works. |
|
|
40
|
+
| `--jev-url <url>` | Endpoint for this run. |
|
|
41
|
+
| `--jev-model <name>` | Jev model for this run, not the conversation model. |
|
|
42
|
+
|
|
43
|
+
No dialog appears in print, JSON or RPC modes, or in sub-agents; configure those with the environment variables below. Changes apply immediately, without restarting the host.
|
|
44
|
+
|
|
45
|
+
**Precedence per field:** environment variable, then launch flag, then this session's setup, then saved configuration, then the default model `openjev`. A field set by the environment or a flag is shown as controlled and is never saved.
|
|
46
|
+
|
|
47
|
+
**Saved configuration** lives outside the repository at `$XDG_CONFIG_HOME/jev-agent-tools/config.json` (default `~/.config/jev-agent-tools/config.json`), in a private directory (`0700`) with a private file (`0600`). **The key is stored in plaintext, not encrypted.** Storage that is a symlink, group/world-accessible, not owned by you or malformed is refused rather than overwritten.
|
|
48
|
+
|
|
49
|
+
### Environment variables
|
|
50
|
+
|
|
29
51
|
Set these before starting the host, using your own endpoint and credentials:
|
|
30
52
|
|
|
31
53
|
```sh
|
|
@@ -35,7 +57,7 @@ export JEV_TOOLS_API_KEY="${YOUR_JEV_API_KEY}"
|
|
|
35
57
|
export JEV_TOOLS_MODEL="openjev"
|
|
36
58
|
```
|
|
37
59
|
|
|
38
|
-
`YOUR_*` placeholders are inputs you supply, not additional product settings.
|
|
60
|
+
`YOUR_*` placeholders are inputs you supply, not additional product settings. Environment variables are read when the extension loads and take precedence over everything else.
|
|
39
61
|
|
|
40
62
|
| Variable | Meaning |
|
|
41
63
|
|---|---|
|
package/package.json
CHANGED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { lstat, mkdir, open, rename, unlink } from "node:fs/promises";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
6
|
+
import { createJevClient } from "./jev/client.ts";
|
|
7
|
+
import type { JevClient } from "./jev/types.ts";
|
|
8
|
+
|
|
9
|
+
export interface ConfigurationValues {
|
|
10
|
+
url: string;
|
|
11
|
+
apiKey: string;
|
|
12
|
+
model: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type SavedConfiguration = Partial<ConfigurationValues>;
|
|
16
|
+
const fields = ["url", "apiKey", "model"] as const;
|
|
17
|
+
const defaults: ConfigurationValues = { url: "", apiKey: "", model: "openjev" };
|
|
18
|
+
const storageError = () =>
|
|
19
|
+
new Error(
|
|
20
|
+
"Cannot read or save Jev configuration. Check that the configuration directory and config.json are owned by you, private, regular storage without symlinks, and contain valid configuration JSON.",
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
function nonempty(value: string | undefined): value is string {
|
|
24
|
+
return value !== undefined && value.trim().length > 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function validate(values: ConfigurationValues): void {
|
|
28
|
+
let validUrl = false;
|
|
29
|
+
try {
|
|
30
|
+
const url = new URL(values.url);
|
|
31
|
+
validUrl =
|
|
32
|
+
/^https?:\/\//i.test(values.url) &&
|
|
33
|
+
(url.protocol === "http:" || url.protocol === "https:") &&
|
|
34
|
+
url.hostname !== "" &&
|
|
35
|
+
url.username === "" &&
|
|
36
|
+
url.password === "";
|
|
37
|
+
} catch {
|
|
38
|
+
// Keep user-controlled URL and credentials out of error messages.
|
|
39
|
+
}
|
|
40
|
+
if (
|
|
41
|
+
!validUrl ||
|
|
42
|
+
!nonempty(values.apiKey) ||
|
|
43
|
+
/[^\x20-\x7e\x80-\xff]/.test(values.apiKey) ||
|
|
44
|
+
!nonempty(values.model)
|
|
45
|
+
) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
"Jev configuration requires a full HTTP(S) URL without embedded credentials, a nonempty HTTP-header-compatible API key, and a nonempty model.",
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Effective configuration and its client move together, only after a save succeeds. */
|
|
53
|
+
export class ConfigController {
|
|
54
|
+
private readonly environment: SavedConfiguration = {};
|
|
55
|
+
private flags: SavedConfiguration = {};
|
|
56
|
+
private saved: SavedConfiguration = {};
|
|
57
|
+
private session: SavedConfiguration = {};
|
|
58
|
+
private readonly directory: string;
|
|
59
|
+
private initialization: Promise<void> | undefined;
|
|
60
|
+
private currentClient: JevClient | undefined;
|
|
61
|
+
|
|
62
|
+
constructor(options: { env?: NodeJS.ProcessEnv; directory?: string } = {}) {
|
|
63
|
+
const env = options.env ?? process.env;
|
|
64
|
+
for (const [field, name] of [
|
|
65
|
+
["url", "JEV_TOOLS_URL"],
|
|
66
|
+
["apiKey", "JEV_TOOLS_API_KEY"],
|
|
67
|
+
["model", "JEV_TOOLS_MODEL"],
|
|
68
|
+
] as const) {
|
|
69
|
+
if (nonempty(env[name])) this.environment[field] = env[name];
|
|
70
|
+
}
|
|
71
|
+
const base =
|
|
72
|
+
nonempty(env.XDG_CONFIG_HOME) && isAbsolute(env.XDG_CONFIG_HOME)
|
|
73
|
+
? env.XDG_CONFIG_HOME
|
|
74
|
+
: join(homedir(), ".config");
|
|
75
|
+
this.directory = resolve(
|
|
76
|
+
options.directory ?? join(base, "jev-agent-tools"),
|
|
77
|
+
);
|
|
78
|
+
if (nonempty(this.environment.url) && nonempty(this.environment.apiKey)) {
|
|
79
|
+
const effective = this.values();
|
|
80
|
+
validate(effective);
|
|
81
|
+
this.currentClient = createJevClient(effective);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
get client(): JevClient | undefined {
|
|
86
|
+
return this.currentClient;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
initialize(flags: { url?: string; model?: string }): Promise<void> {
|
|
90
|
+
if (this.initialization) return this.initialization;
|
|
91
|
+
this.initialization = this.load(flags).catch((error: unknown) => {
|
|
92
|
+
this.initialization = undefined;
|
|
93
|
+
throw error;
|
|
94
|
+
});
|
|
95
|
+
return this.initialization;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
values(): ConfigurationValues {
|
|
99
|
+
return {
|
|
100
|
+
...defaults,
|
|
101
|
+
...this.saved,
|
|
102
|
+
...this.session,
|
|
103
|
+
...this.flags,
|
|
104
|
+
...this.environment,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
locked(field: keyof ConfigurationValues): boolean {
|
|
109
|
+
return (
|
|
110
|
+
this.environment[field] !== undefined || this.flags[field] !== undefined
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async apply(values: ConfigurationValues, persist: boolean): Promise<void> {
|
|
115
|
+
if (!this.initialization) {
|
|
116
|
+
throw new Error("Initialize Jev configuration before applying changes.");
|
|
117
|
+
}
|
|
118
|
+
await this.initialization;
|
|
119
|
+
const session = { ...this.session };
|
|
120
|
+
for (const field of fields) {
|
|
121
|
+
if (!this.locked(field)) session[field] = values[field];
|
|
122
|
+
}
|
|
123
|
+
const effective = {
|
|
124
|
+
...defaults,
|
|
125
|
+
...this.saved,
|
|
126
|
+
...session,
|
|
127
|
+
...this.flags,
|
|
128
|
+
...this.environment,
|
|
129
|
+
};
|
|
130
|
+
validate(effective);
|
|
131
|
+
const saved = { ...this.saved, ...session };
|
|
132
|
+
if (persist) await this.save(saved);
|
|
133
|
+
const previous = this.values();
|
|
134
|
+
this.session = session;
|
|
135
|
+
if (persist) this.saved = saved;
|
|
136
|
+
if (fields.some((field) => previous[field] !== effective[field])) {
|
|
137
|
+
this.currentClient?.clearCache();
|
|
138
|
+
this.currentClient = createJevClient(effective);
|
|
139
|
+
} else {
|
|
140
|
+
this.currentClient ??= createJevClient(effective);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private async load(flags: { url?: string; model?: string }): Promise<void> {
|
|
145
|
+
const saved = await this.readSaved();
|
|
146
|
+
const supplied: SavedConfiguration = {};
|
|
147
|
+
if (nonempty(flags.url)) supplied.url = flags.url;
|
|
148
|
+
if (nonempty(flags.model)) supplied.model = flags.model;
|
|
149
|
+
const effective = {
|
|
150
|
+
...defaults,
|
|
151
|
+
...saved,
|
|
152
|
+
...supplied,
|
|
153
|
+
...this.environment,
|
|
154
|
+
};
|
|
155
|
+
if (nonempty(effective.url) && nonempty(effective.apiKey)) {
|
|
156
|
+
validate(effective);
|
|
157
|
+
this.currentClient?.clearCache();
|
|
158
|
+
this.currentClient = createJevClient(effective);
|
|
159
|
+
}
|
|
160
|
+
this.saved = saved;
|
|
161
|
+
this.flags = supplied;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private async checkDirectory(): Promise<boolean> {
|
|
165
|
+
// Check ancestors too: recursive mkdir and path-based reads must not follow symlinks.
|
|
166
|
+
let path = this.directory;
|
|
167
|
+
let exists = true;
|
|
168
|
+
while (true) {
|
|
169
|
+
try {
|
|
170
|
+
const stat = await lstat(path);
|
|
171
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw storageError();
|
|
172
|
+
if (
|
|
173
|
+
path === this.directory &&
|
|
174
|
+
((stat.mode & 0o077) !== 0 ||
|
|
175
|
+
(process.getuid && stat.uid !== process.getuid()))
|
|
176
|
+
) {
|
|
177
|
+
throw storageError();
|
|
178
|
+
}
|
|
179
|
+
} catch (error) {
|
|
180
|
+
if ((error as NodeJS.ErrnoException)?.code !== "ENOENT")
|
|
181
|
+
throw storageError();
|
|
182
|
+
if (path === this.directory) exists = false;
|
|
183
|
+
}
|
|
184
|
+
const parent = dirname(path);
|
|
185
|
+
if (parent === path) break;
|
|
186
|
+
path = parent;
|
|
187
|
+
}
|
|
188
|
+
return exists;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private async readSaved(): Promise<SavedConfiguration> {
|
|
192
|
+
try {
|
|
193
|
+
if (!(await this.checkDirectory())) return {};
|
|
194
|
+
const path = join(this.directory, "config.json");
|
|
195
|
+
try {
|
|
196
|
+
const stat = await lstat(path);
|
|
197
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw storageError();
|
|
198
|
+
} catch (error) {
|
|
199
|
+
if ((error as NodeJS.ErrnoException)?.code === "ENOENT") return {};
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
203
|
+
try {
|
|
204
|
+
const stat = await file.stat();
|
|
205
|
+
if (
|
|
206
|
+
!stat.isFile() ||
|
|
207
|
+
stat.nlink !== 1 ||
|
|
208
|
+
(stat.mode & 0o077) !== 0 ||
|
|
209
|
+
(process.getuid && stat.uid !== process.getuid())
|
|
210
|
+
) {
|
|
211
|
+
throw storageError();
|
|
212
|
+
}
|
|
213
|
+
const value: unknown = JSON.parse(await file.readFile("utf8"));
|
|
214
|
+
if (
|
|
215
|
+
value === null ||
|
|
216
|
+
typeof value !== "object" ||
|
|
217
|
+
Array.isArray(value) ||
|
|
218
|
+
Object.entries(value).some(
|
|
219
|
+
([key, entry]) =>
|
|
220
|
+
!fields.some((field) => field === key) ||
|
|
221
|
+
typeof entry !== "string",
|
|
222
|
+
)
|
|
223
|
+
) {
|
|
224
|
+
throw storageError();
|
|
225
|
+
}
|
|
226
|
+
return value as SavedConfiguration;
|
|
227
|
+
} finally {
|
|
228
|
+
await file.close();
|
|
229
|
+
}
|
|
230
|
+
} catch {
|
|
231
|
+
throw storageError();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private async save(values: SavedConfiguration): Promise<void> {
|
|
236
|
+
let temporary: string | undefined;
|
|
237
|
+
try {
|
|
238
|
+
if (!(await this.checkDirectory())) {
|
|
239
|
+
await mkdir(this.directory, { recursive: true, mode: 0o700 });
|
|
240
|
+
}
|
|
241
|
+
// Refuse to replace malformed, insecure or newly introduced storage.
|
|
242
|
+
await this.readSaved();
|
|
243
|
+
temporary = join(this.directory, `.config-${randomUUID()}.tmp`);
|
|
244
|
+
const file = await open(
|
|
245
|
+
temporary,
|
|
246
|
+
constants.O_WRONLY |
|
|
247
|
+
constants.O_CREAT |
|
|
248
|
+
constants.O_EXCL |
|
|
249
|
+
constants.O_NOFOLLOW,
|
|
250
|
+
0o600,
|
|
251
|
+
);
|
|
252
|
+
try {
|
|
253
|
+
await file.writeFile(`${JSON.stringify(values, null, 2)}\n`, "utf8");
|
|
254
|
+
await file.sync();
|
|
255
|
+
} finally {
|
|
256
|
+
await file.close();
|
|
257
|
+
}
|
|
258
|
+
await this.readSaved();
|
|
259
|
+
await rename(temporary, join(this.directory, "config.json"));
|
|
260
|
+
temporary = undefined;
|
|
261
|
+
} catch {
|
|
262
|
+
throw storageError();
|
|
263
|
+
} finally {
|
|
264
|
+
if (temporary) await unlink(temporary).catch(() => {});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Runtime implementations are supplied by the pi/omp extension loader.
|
|
2
|
+
declare module "@earendil-works/pi-tui" {
|
|
3
|
+
export function matchesKey(
|
|
4
|
+
data: string,
|
|
5
|
+
key: "enter" | "escape" | "backspace" | "ctrl+c" | "ctrl+u",
|
|
6
|
+
): boolean;
|
|
7
|
+
export const decodePrintableKey:
|
|
8
|
+
| ((data: string) => string | undefined)
|
|
9
|
+
| undefined;
|
|
10
|
+
export const decodeKittyPrintable:
|
|
11
|
+
| ((data: string) => string | undefined)
|
|
12
|
+
| undefined;
|
|
13
|
+
export function isKeyRelease(data: string): boolean;
|
|
14
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { ConfigController } from "./configuration.ts";
|
|
2
3
|
import { Guide } from "./guide.ts";
|
|
3
4
|
import { detectHost } from "./host.ts";
|
|
4
|
-
import { createJevClient, readConfig } from "./jev/client.ts";
|
|
5
5
|
import { RunEnd, type RunEndHost } from "./run-end.ts";
|
|
6
6
|
import { readSessionLimits, Session } from "./session.ts";
|
|
7
|
+
import { Setup } from "./setup.ts";
|
|
7
8
|
import { createAskTool } from "./tools/ask.ts";
|
|
8
9
|
import { createAskFilesTool } from "./tools/ask-files.ts";
|
|
9
10
|
import { createCheckDiffTool } from "./tools/check-diff.ts";
|
|
@@ -12,8 +13,7 @@ import { createLocateTool } from "./tools/locate.ts";
|
|
|
12
13
|
import { createSelectTestsTool } from "./tools/select-tests.ts";
|
|
13
14
|
export default function extension(api: ExtensionAPI & { pi?: unknown }): void {
|
|
14
15
|
const host = detectHost(api);
|
|
15
|
-
const config =
|
|
16
|
-
const client = config ? createJevClient(config) : undefined;
|
|
16
|
+
const config = new ConfigController();
|
|
17
17
|
const runtime = {
|
|
18
18
|
session: new Session(readSessionLimits(process.env)),
|
|
19
19
|
guide: new Guide(host),
|
|
@@ -22,14 +22,22 @@ export default function extension(api: ExtensionAPI & { pi?: unknown }): void {
|
|
|
22
22
|
api.on("session_start", async () => {
|
|
23
23
|
runtime.session.reset();
|
|
24
24
|
runtime.guide.reset();
|
|
25
|
-
client?.clearCache();
|
|
25
|
+
config.client?.clearCache();
|
|
26
26
|
if (host.isOmp) {
|
|
27
27
|
const active = api.getActiveTools();
|
|
28
28
|
if (!active.includes("find"))
|
|
29
29
|
await api.setActiveTools([...active, "jev_find_files"]);
|
|
30
30
|
}
|
|
31
31
|
});
|
|
32
|
-
|
|
32
|
+
new Setup(config).attach(api);
|
|
33
|
+
const dependencies = {
|
|
34
|
+
get client() {
|
|
35
|
+
return config.client;
|
|
36
|
+
},
|
|
37
|
+
host,
|
|
38
|
+
runtime,
|
|
39
|
+
exec: api.exec.bind(api),
|
|
40
|
+
};
|
|
33
41
|
new RunEnd(dependencies).attach(api as unknown as RunEndHost);
|
|
34
42
|
api.registerTool(createAskTool(dependencies));
|
|
35
43
|
api.registerTool(createSelectTestsTool(dependencies));
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
type SecretKey = "enter" | "escape" | "ctrl+c" | "backspace";
|
|
2
|
+
|
|
3
|
+
export interface SecretKeyHelpers {
|
|
4
|
+
matchesKey(data: string, key: SecretKey): boolean;
|
|
5
|
+
decodePrintableKey(data: string): string | undefined;
|
|
6
|
+
isKeyRelease(data: string): boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface SecretUIContext {
|
|
10
|
+
ui: {
|
|
11
|
+
custom<T>(
|
|
12
|
+
factory: (
|
|
13
|
+
tui: { requestRender(): void },
|
|
14
|
+
theme: unknown,
|
|
15
|
+
keybindings: unknown,
|
|
16
|
+
done: (result: T) => void,
|
|
17
|
+
) => MaskedSecretInput,
|
|
18
|
+
): Promise<T>;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const pasteStart = "\x1b[200~";
|
|
23
|
+
const pasteEnd = "\x1b[201~";
|
|
24
|
+
|
|
25
|
+
/** A dedicated input: no editor, clipboard, history, undo stack or kill ring. */
|
|
26
|
+
export class MaskedSecretInput {
|
|
27
|
+
#characters: number[] = [];
|
|
28
|
+
#pasting = false;
|
|
29
|
+
#pasteLength = 0;
|
|
30
|
+
#invalidPaste = false;
|
|
31
|
+
#pasteRejected = false;
|
|
32
|
+
#pasteMarker = "";
|
|
33
|
+
#closed = false;
|
|
34
|
+
#required = false;
|
|
35
|
+
#title: string;
|
|
36
|
+
#existing: boolean;
|
|
37
|
+
#keys: SecretKeyHelpers;
|
|
38
|
+
#done: (result: string | undefined) => void;
|
|
39
|
+
#requestRender: () => void;
|
|
40
|
+
|
|
41
|
+
constructor(
|
|
42
|
+
title: string,
|
|
43
|
+
existing: boolean,
|
|
44
|
+
keys: SecretKeyHelpers,
|
|
45
|
+
done: (result: string | undefined) => void,
|
|
46
|
+
requestRender: () => void,
|
|
47
|
+
) {
|
|
48
|
+
this.#title = title.replace(/[^\x20-\x7e]/g, "?");
|
|
49
|
+
this.#existing = existing;
|
|
50
|
+
this.#keys = keys;
|
|
51
|
+
this.#done = done;
|
|
52
|
+
this.#requestRender = requestRender;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
render(width: number): string[] {
|
|
56
|
+
const columns = Number.isFinite(width) ? Math.max(0, Math.floor(width)) : 0;
|
|
57
|
+
return [
|
|
58
|
+
this.#title,
|
|
59
|
+
`Key: ${"*".repeat(Math.min(this.#characters.length, columns))}`,
|
|
60
|
+
this.#existing ? "Enter: save (empty keeps current key)" : "Enter: save",
|
|
61
|
+
"Esc / Ctrl+C: cancel",
|
|
62
|
+
this.#pasteRejected
|
|
63
|
+
? "Paste rejected: use a single line without controls."
|
|
64
|
+
: "",
|
|
65
|
+
this.#required ? "A key is required." : "",
|
|
66
|
+
].map((line) => line.slice(0, columns));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
handleInput(data: string): void {
|
|
70
|
+
if (this.#closed) return;
|
|
71
|
+
if (this.#pasting) {
|
|
72
|
+
// Keep only a possible delimiter prefix, never a second secret buffer.
|
|
73
|
+
let offset = 0;
|
|
74
|
+
for (const character of data) {
|
|
75
|
+
offset += character.length;
|
|
76
|
+
while (
|
|
77
|
+
this.#pasteMarker &&
|
|
78
|
+
character !== pasteEnd[this.#pasteMarker.length]
|
|
79
|
+
) {
|
|
80
|
+
this.#append(this.#pasteMarker);
|
|
81
|
+
this.#pasteMarker = "";
|
|
82
|
+
}
|
|
83
|
+
if (character === pasteEnd[this.#pasteMarker.length]) {
|
|
84
|
+
this.#pasteMarker += character;
|
|
85
|
+
if (this.#pasteMarker === pasteEnd) {
|
|
86
|
+
this.#pasteMarker = "";
|
|
87
|
+
if (this.#invalidPaste) {
|
|
88
|
+
this.#characters.fill(0, this.#pasteLength);
|
|
89
|
+
this.#characters.length = this.#pasteLength;
|
|
90
|
+
this.#pasteRejected = true;
|
|
91
|
+
}
|
|
92
|
+
this.#invalidPaste = false;
|
|
93
|
+
this.#pasting = false;
|
|
94
|
+
this.handleInput(data.slice(offset));
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
} else if (this.#pasting) {
|
|
98
|
+
this.#append(character);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
this.#requestRender();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const start = data.indexOf(pasteStart);
|
|
105
|
+
if (start !== -1) {
|
|
106
|
+
if (start > 0) this.handleInput(data.slice(0, start));
|
|
107
|
+
if (this.#closed) return;
|
|
108
|
+
this.#pasting = true;
|
|
109
|
+
this.#pasteLength = this.#characters.length;
|
|
110
|
+
this.#invalidPaste = false;
|
|
111
|
+
this.#pasteRejected = false;
|
|
112
|
+
this.handleInput(data.slice(start + pasteStart.length));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (this.#keys.isKeyRelease(data)) return;
|
|
116
|
+
if (
|
|
117
|
+
this.#keys.matchesKey(data, "escape") ||
|
|
118
|
+
this.#keys.matchesKey(data, "ctrl+c")
|
|
119
|
+
) {
|
|
120
|
+
this.#finish(undefined);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (this.#keys.matchesKey(data, "enter")) {
|
|
124
|
+
if (!this.#characters.length && !this.#existing) {
|
|
125
|
+
this.#required = true;
|
|
126
|
+
this.#requestRender();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
// Strings supplied by the terminal and returned to the caller are immutable;
|
|
130
|
+
// JavaScript cannot guarantee their erasure. Our retained numeric buffer can.
|
|
131
|
+
const result = this.#characters
|
|
132
|
+
.map((codepoint) => String.fromCodePoint(codepoint))
|
|
133
|
+
.join("");
|
|
134
|
+
this.#finish(result);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (this.#keys.matchesKey(data, "backspace")) {
|
|
138
|
+
if (this.#characters.length) {
|
|
139
|
+
this.#characters[this.#characters.length - 1] = 0;
|
|
140
|
+
this.#characters.pop();
|
|
141
|
+
}
|
|
142
|
+
this.#required = false;
|
|
143
|
+
this.#requestRender();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const printable = this.#keys.decodePrintableKey(data);
|
|
147
|
+
if (printable !== undefined) this.#append(printable);
|
|
148
|
+
else if (
|
|
149
|
+
[...data].every((character) => {
|
|
150
|
+
const code = character.codePointAt(0) as number;
|
|
151
|
+
return code >= 0x20 && (code < 0x7f || code > 0x9f);
|
|
152
|
+
})
|
|
153
|
+
)
|
|
154
|
+
this.#append(data);
|
|
155
|
+
this.#requestRender();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
#append(text: string): void {
|
|
159
|
+
for (const character of text) {
|
|
160
|
+
const code = character.codePointAt(0) as number;
|
|
161
|
+
if (this.#pasting && (code < 0x20 || (code >= 0x7f && code <= 0x9f))) {
|
|
162
|
+
this.#invalidPaste = true;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
this.#characters.push(character.codePointAt(0) as number);
|
|
166
|
+
}
|
|
167
|
+
this.#required = false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
#finish(result: string | undefined): void {
|
|
171
|
+
this.dispose();
|
|
172
|
+
this.#done(result);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
invalidate(): void {
|
|
176
|
+
// Rendering has no cache or theme-dependent state.
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
dispose(): void {
|
|
180
|
+
this.#characters.fill(0);
|
|
181
|
+
this.#characters.length = 0;
|
|
182
|
+
this.#pasteMarker = "";
|
|
183
|
+
this.#pasting = false;
|
|
184
|
+
this.#pasteLength = 0;
|
|
185
|
+
this.#invalidPaste = false;
|
|
186
|
+
this.#pasteRejected = false;
|
|
187
|
+
this.#required = false;
|
|
188
|
+
this.#closed = true;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Caller must restrict this to the host's interactive TUI mode. */
|
|
193
|
+
export async function readSecret(
|
|
194
|
+
ctx: SecretUIContext,
|
|
195
|
+
title: string,
|
|
196
|
+
existing: boolean,
|
|
197
|
+
): Promise<string | undefined> {
|
|
198
|
+
// Both hosts provide this module at runtime; pi exports decodeKittyPrintable and
|
|
199
|
+
// omp exports decodePrintableKey, so static package resolution cannot be used.
|
|
200
|
+
const hostKeys = await import("@earendil-works/pi-tui");
|
|
201
|
+
const decodePrintableKey =
|
|
202
|
+
hostKeys.decodeKittyPrintable ?? hostKeys.decodePrintableKey;
|
|
203
|
+
if (!decodePrintableKey) {
|
|
204
|
+
throw new Error(
|
|
205
|
+
"This host cannot decode printable keys for masked Jev setup; use environment variables.",
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
const keys: SecretKeyHelpers = { ...hostKeys, decodePrintableKey };
|
|
209
|
+
let input: MaskedSecretInput | undefined;
|
|
210
|
+
try {
|
|
211
|
+
return await ctx.ui.custom<string | undefined>(
|
|
212
|
+
(tui, _theme, _keybindings, done) => {
|
|
213
|
+
input = new MaskedSecretInput(title, existing, keys, done, () =>
|
|
214
|
+
tui.requestRender(),
|
|
215
|
+
);
|
|
216
|
+
return input;
|
|
217
|
+
},
|
|
218
|
+
);
|
|
219
|
+
} finally {
|
|
220
|
+
input?.dispose();
|
|
221
|
+
}
|
|
222
|
+
}
|
package/src/setup.ts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import type { ConfigController, ConfigurationValues } from "./configuration.ts";
|
|
6
|
+
import { readSecret } from "./secret-input.ts";
|
|
7
|
+
|
|
8
|
+
type SetupContext = Pick<ExtensionContext, "ui" | "hasUI"> & {
|
|
9
|
+
mode?: string;
|
|
10
|
+
agent?: { kind: string };
|
|
11
|
+
};
|
|
12
|
+
type SetupAPI = Pick<
|
|
13
|
+
ExtensionAPI,
|
|
14
|
+
"on" | "registerFlag" | "getFlag" | "registerCommand"
|
|
15
|
+
>;
|
|
16
|
+
|
|
17
|
+
/** Setup never sends credentials to the conversation or starts a model turn. */
|
|
18
|
+
export class Setup {
|
|
19
|
+
private offered = false;
|
|
20
|
+
private busy = false;
|
|
21
|
+
private readonly config: ConfigController;
|
|
22
|
+
private readonly promptSecret: typeof readSecret;
|
|
23
|
+
constructor(
|
|
24
|
+
config: ConfigController,
|
|
25
|
+
promptSecret: typeof readSecret = readSecret,
|
|
26
|
+
) {
|
|
27
|
+
this.config = config;
|
|
28
|
+
this.promptSecret = promptSecret;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
attach(api: SetupAPI): void {
|
|
32
|
+
api.registerFlag("jev-skip-setup", {
|
|
33
|
+
type: "boolean",
|
|
34
|
+
default: false,
|
|
35
|
+
description: "Skip the interactive Jev configuration offer",
|
|
36
|
+
});
|
|
37
|
+
api.registerFlag("jev-url", {
|
|
38
|
+
type: "string",
|
|
39
|
+
description:
|
|
40
|
+
"Jev endpoint URL for this run (environment takes precedence)",
|
|
41
|
+
});
|
|
42
|
+
api.registerFlag("jev-model", {
|
|
43
|
+
type: "string",
|
|
44
|
+
description: "Jev model for this run, not the conversation model",
|
|
45
|
+
});
|
|
46
|
+
const initialize = async () => {
|
|
47
|
+
const url = api.getFlag("jev-url");
|
|
48
|
+
const model = api.getFlag("jev-model");
|
|
49
|
+
await this.config.initialize({
|
|
50
|
+
...(typeof url === "string" ? { url } : {}),
|
|
51
|
+
...(typeof model === "string" ? { model } : {}),
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
api.on("session_start", async (_event, ctx) => {
|
|
55
|
+
try {
|
|
56
|
+
await initialize();
|
|
57
|
+
if (
|
|
58
|
+
!this.interactive(ctx) ||
|
|
59
|
+
this.offered ||
|
|
60
|
+
this.config.client ||
|
|
61
|
+
api.getFlag("jev-skip-setup")
|
|
62
|
+
)
|
|
63
|
+
return;
|
|
64
|
+
this.offered = true;
|
|
65
|
+
const choice = await ctx.ui.select(
|
|
66
|
+
"Jev Tools needs an endpoint and API key",
|
|
67
|
+
["Configure now", "Later"],
|
|
68
|
+
);
|
|
69
|
+
if (choice === "Configure now") await this.configure(ctx);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (this.interactive(ctx)) ctx.ui.notify(this.message(error), "error");
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
api.registerCommand("jev-setup", {
|
|
75
|
+
description: "Configure the Jev endpoint, model and API key",
|
|
76
|
+
handler: async (_args, ctx) => {
|
|
77
|
+
if (!this.interactive(ctx)) {
|
|
78
|
+
ctx.ui.notify(
|
|
79
|
+
"Jev setup requires the main interactive terminal. Use JEV_TOOLS_URL, JEV_TOOLS_MODEL and JEV_TOOLS_API_KEY for headless runs.",
|
|
80
|
+
"warning",
|
|
81
|
+
);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
await initialize();
|
|
86
|
+
await this.configure(ctx);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
ctx.ui.notify(this.message(error), "error");
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private interactive(ctx: SetupContext): boolean {
|
|
95
|
+
return ctx.mode === "tui" && ctx.hasUI && ctx.agent?.kind !== "sub";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private message(error: unknown): string {
|
|
99
|
+
// Configuration errors are deliberately credential-free.
|
|
100
|
+
return error instanceof Error
|
|
101
|
+
? error.message
|
|
102
|
+
: "Jev configuration failed; the previous configuration was kept.";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private async configure(ctx: SetupContext): Promise<void> {
|
|
106
|
+
if (this.busy) return;
|
|
107
|
+
this.busy = true;
|
|
108
|
+
try {
|
|
109
|
+
const values: ConfigurationValues = this.config.values();
|
|
110
|
+
for (const field of ["url", "model"] as const) {
|
|
111
|
+
if (this.config.locked(field)) {
|
|
112
|
+
ctx.ui.notify(
|
|
113
|
+
`Jev ${field} is controlled by the environment or a launch flag; change it before restarting.`,
|
|
114
|
+
"info",
|
|
115
|
+
);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const label =
|
|
119
|
+
field === "url"
|
|
120
|
+
? "Complete Jev endpoint URL"
|
|
121
|
+
: "Jev model (not the conversation model)";
|
|
122
|
+
const answer = await ctx.ui.input(
|
|
123
|
+
label,
|
|
124
|
+
values[field] ||
|
|
125
|
+
(field === "model"
|
|
126
|
+
? "openjev"
|
|
127
|
+
: "https://your-service.example/v1/systemone"),
|
|
128
|
+
);
|
|
129
|
+
if (answer === undefined) return;
|
|
130
|
+
values[field] = answer.trim() || values[field];
|
|
131
|
+
}
|
|
132
|
+
if (this.config.locked("apiKey")) {
|
|
133
|
+
ctx.ui.notify(
|
|
134
|
+
"The Jev API key is supplied by the environment and will not be saved.",
|
|
135
|
+
"info",
|
|
136
|
+
);
|
|
137
|
+
} else {
|
|
138
|
+
const key = await this.promptSecret(
|
|
139
|
+
ctx,
|
|
140
|
+
"Jev API key",
|
|
141
|
+
Boolean(values.apiKey),
|
|
142
|
+
);
|
|
143
|
+
if (key === undefined) return;
|
|
144
|
+
values.apiKey = key || values.apiKey;
|
|
145
|
+
}
|
|
146
|
+
const storage = await ctx.ui.select("Apply Jev configuration", [
|
|
147
|
+
"This session only",
|
|
148
|
+
"Save for future sessions (editable values and plaintext key in a private global file)",
|
|
149
|
+
]);
|
|
150
|
+
if (storage === undefined) return;
|
|
151
|
+
await this.config.apply(values, storage !== "This session only");
|
|
152
|
+
ctx.ui.notify(
|
|
153
|
+
storage === "This session only"
|
|
154
|
+
? "Jev configuration applied for this session; no file was written."
|
|
155
|
+
: "Jev configuration applied and saved outside the repository. The stored key is plaintext, not encrypted.",
|
|
156
|
+
"info",
|
|
157
|
+
);
|
|
158
|
+
} finally {
|
|
159
|
+
this.busy = false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const NOT_CONFIGURED =
|
|
2
|
-
"jev-tools is not configured: set JEV_TOOLS_URL and JEV_TOOLS_API_KEY.";
|
|
2
|
+
"jev-tools is not configured: set JEV_TOOLS_URL and JEV_TOOLS_API_KEY, or run /jev-setup in the main interactive terminal.";
|
package/src/tools/ask-files.ts
CHANGED
|
@@ -42,12 +42,8 @@ export interface AskFilesDetails {
|
|
|
42
42
|
skipped: string[];
|
|
43
43
|
envelope: Envelope;
|
|
44
44
|
}
|
|
45
|
-
export function createAskFilesTool({
|
|
46
|
-
|
|
47
|
-
host,
|
|
48
|
-
runtime,
|
|
49
|
-
exec: execute,
|
|
50
|
-
}: ToolDependencies) {
|
|
45
|
+
export function createAskFilesTool(dependencies: ToolDependencies) {
|
|
46
|
+
const { host, runtime, exec: execute } = dependencies;
|
|
51
47
|
return {
|
|
52
48
|
name: "jev_ask_files",
|
|
53
49
|
label: "Jev ask files",
|
|
@@ -70,6 +66,7 @@ export function createAskFilesTool({
|
|
|
70
66
|
_update: unknown,
|
|
71
67
|
ctx: { cwd: string } & GuideContext,
|
|
72
68
|
) {
|
|
69
|
+
const client = dependencies.client;
|
|
73
70
|
const started = performance.now();
|
|
74
71
|
const exec = shareGitInventory(execute);
|
|
75
72
|
const totals = {
|
package/src/tools/ask.ts
CHANGED
|
@@ -67,12 +67,8 @@ export const askParameters = Type.Object(
|
|
|
67
67
|
);
|
|
68
68
|
export type AskArgs = Static<typeof askParameters>;
|
|
69
69
|
|
|
70
|
-
export function createAskTool({
|
|
71
|
-
|
|
72
|
-
host,
|
|
73
|
-
runtime,
|
|
74
|
-
exec: execute,
|
|
75
|
-
}: ToolDependencies) {
|
|
70
|
+
export function createAskTool(dependencies: ToolDependencies) {
|
|
71
|
+
const { host, runtime, exec: execute } = dependencies;
|
|
76
72
|
const { isOmp, names } = host;
|
|
77
73
|
const allowCommand = process.env.JEV_TOOLS_ALLOW_COMMAND !== "0";
|
|
78
74
|
const parameters = allowCommand
|
|
@@ -120,6 +116,7 @@ export function createAskTool({
|
|
|
120
116
|
};
|
|
121
117
|
};
|
|
122
118
|
}> {
|
|
119
|
+
const client = dependencies.client;
|
|
123
120
|
const started = performance.now();
|
|
124
121
|
const exec = shareGitInventory(execute);
|
|
125
122
|
const finish = (
|
package/src/tools/check-diff.ts
CHANGED
|
@@ -83,12 +83,8 @@ function unitLabel(unit: EvidenceUnit): string {
|
|
|
83
83
|
.slice(0, 8);
|
|
84
84
|
return `${unit.file}${range ? `:${range.start}-${range.end}` : ""} ${unit.name} [${fingerprint}]`;
|
|
85
85
|
}
|
|
86
|
-
export function createCheckDiffTool({
|
|
87
|
-
|
|
88
|
-
host,
|
|
89
|
-
runtime,
|
|
90
|
-
exec: execute,
|
|
91
|
-
}: ToolDependencies) {
|
|
86
|
+
export function createCheckDiffTool(dependencies: ToolDependencies) {
|
|
87
|
+
const { host, runtime, exec: execute } = dependencies;
|
|
92
88
|
return {
|
|
93
89
|
name: "jev_check_diff",
|
|
94
90
|
label: "Jev check diff",
|
|
@@ -110,6 +106,7 @@ export function createCheckDiffTool({
|
|
|
110
106
|
_update: unknown,
|
|
111
107
|
ctx: { cwd: string } & GuideContext,
|
|
112
108
|
) {
|
|
109
|
+
const client = dependencies.client;
|
|
113
110
|
const exec = shareGitInventory(execute);
|
|
114
111
|
if (args.check === "docs" || args.check === "spec") {
|
|
115
112
|
const deps = { client, host, runtime, exec };
|
package/src/tools/find.ts
CHANGED
|
@@ -46,12 +46,8 @@ export const findParameters = Type.Object(
|
|
|
46
46
|
{ additionalProperties: false },
|
|
47
47
|
);
|
|
48
48
|
export type FindArgs = Static<typeof findParameters>;
|
|
49
|
-
export function createFindFilesTool({
|
|
50
|
-
|
|
51
|
-
host,
|
|
52
|
-
runtime,
|
|
53
|
-
exec: execute,
|
|
54
|
-
}: ToolDependencies) {
|
|
49
|
+
export function createFindFilesTool(dependencies: ToolDependencies) {
|
|
50
|
+
const { host, runtime, exec: execute } = dependencies;
|
|
55
51
|
return {
|
|
56
52
|
name: "jev_find_files",
|
|
57
53
|
label: "Jev find files",
|
|
@@ -77,6 +73,7 @@ export function createFindFilesTool({
|
|
|
77
73
|
_update: unknown,
|
|
78
74
|
ctx: { cwd: string } & GuideContext,
|
|
79
75
|
) {
|
|
76
|
+
const client = dependencies.client;
|
|
80
77
|
const started = performance.now();
|
|
81
78
|
const exec = shareGitInventory(execute);
|
|
82
79
|
const totals = {
|
package/src/tools/locate.ts
CHANGED
|
@@ -43,9 +43,10 @@ const parameters = Type.Object(
|
|
|
43
43
|
type LocateArgs = Static<typeof parameters>;
|
|
44
44
|
|
|
45
45
|
export function createLocateTool(
|
|
46
|
-
|
|
46
|
+
dependencies: ToolDependencies,
|
|
47
47
|
loadParser: () => Promise<SyntaxParser | undefined> = loadSyntaxParser,
|
|
48
48
|
) {
|
|
49
|
+
const { host, runtime, exec: execute } = dependencies;
|
|
49
50
|
return {
|
|
50
51
|
name: "jev_locate_in_file",
|
|
51
52
|
label: "Jev locate in file",
|
|
@@ -72,6 +73,7 @@ export function createLocateTool(
|
|
|
72
73
|
_update: unknown,
|
|
73
74
|
ctx: { cwd: string } & GuideContext,
|
|
74
75
|
) {
|
|
76
|
+
const client = dependencies.client;
|
|
75
77
|
const started = performance.now();
|
|
76
78
|
const exec = shareGitInventory(execute);
|
|
77
79
|
const results: Judgment[] = [];
|
|
@@ -104,12 +104,8 @@ function makeCandidate(
|
|
|
104
104
|
};
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
export function createSelectTestsTool({
|
|
108
|
-
|
|
109
|
-
host,
|
|
110
|
-
runtime,
|
|
111
|
-
exec: execute,
|
|
112
|
-
}: ToolDependencies) {
|
|
107
|
+
export function createSelectTestsTool(dependencies: ToolDependencies) {
|
|
108
|
+
const { host, runtime, exec: execute } = dependencies;
|
|
113
109
|
return {
|
|
114
110
|
name: "jev_select_tests",
|
|
115
111
|
label: "Jev select tests",
|
|
@@ -128,6 +124,7 @@ export function createSelectTestsTool({
|
|
|
128
124
|
_update: unknown,
|
|
129
125
|
ctx: { cwd: string } & GuideContext,
|
|
130
126
|
) {
|
|
127
|
+
const client = dependencies.client;
|
|
131
128
|
const started = performance.now();
|
|
132
129
|
const exec = shareGitInventory(execute);
|
|
133
130
|
const totals = {
|