jev-agent-tools 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +88 -3
- package/CONTRIBUTING.md +40 -0
- package/README.md +64 -9
- package/SECURITY.md +27 -0
- package/dist/adapters/analysis-context.js +75 -0
- package/dist/adapters/ask-files.js +189 -0
- package/dist/adapters/ask-proof.js +144 -0
- package/dist/adapters/ask-syntax.js +385 -0
- package/dist/adapters/canonical-path.js +17 -0
- package/dist/adapters/command.js +181 -0
- package/dist/adapters/docs.js +172 -0
- package/dist/adapters/exec.js +207 -0
- package/dist/adapters/files.js +293 -0
- package/dist/adapters/find.js +122 -0
- package/dist/adapters/git-base.js +26 -0
- package/dist/adapters/git-inventory.js +71 -0
- package/dist/adapters/git.js +439 -0
- package/dist/adapters/locate-file.js +159 -0
- package/dist/adapters/output-lines.js +46 -0
- package/dist/adapters/private-storage.js +98 -0
- package/dist/adapters/risk-callers.js +426 -0
- package/dist/adapters/runner-version.js +78 -0
- package/dist/adapters/shell.js +76 -0
- package/dist/adapters/syntax.js +187 -0
- package/dist/adapters/test-inventory.js +131 -0
- package/dist/adapters/usage.js +20 -0
- package/dist/adapters/utf8.js +47 -0
- package/dist/configuration.js +257 -0
- package/dist/constants.js +119 -0
- package/dist/core/ask-closure.js +282 -0
- package/dist/core/ask-proof.js +1 -0
- package/dist/core/ask-references.js +194 -0
- package/dist/core/asks.js +436 -0
- package/dist/core/batches.js +65 -0
- package/dist/core/command-output.js +224 -0
- package/dist/core/diff.js +178 -0
- package/dist/core/docs.js +302 -0
- package/dist/core/find.js +108 -0
- package/dist/core/git.js +1 -0
- package/dist/core/imports.js +550 -0
- package/dist/core/integrity.js +45 -0
- package/dist/core/lexical.js +132 -0
- package/dist/core/locate.js +169 -0
- package/dist/core/output.js +120 -0
- package/dist/core/pointer.js +29 -0
- package/dist/core/risk-callers.js +851 -0
- package/dist/core/runner-version.js +45 -0
- package/dist/core/sections.js +230 -0
- package/dist/core/state.js +44 -0
- package/dist/core/syntax.js +1 -0
- package/dist/core/test-commands.js +334 -0
- package/dist/core/test-coverage.js +74 -0
- package/dist/core/test-discovery.js +1382 -0
- package/dist/core/test-evidence.js +527 -0
- package/dist/core/test-state.js +81 -0
- package/dist/core/truncate.js +12 -0
- package/dist/core/units.js +349 -0
- package/dist/describe.js +23 -0
- package/dist/guide.js +33 -0
- package/dist/host.js +24 -0
- package/dist/jev/client.js +434 -0
- package/dist/jev/pool.js +54 -0
- package/dist/jev/types.js +1 -0
- package/dist/mcp/main.js +124 -0
- package/dist/mcp/protocol.js +187 -0
- package/dist/mcp/tools.js +116 -0
- package/dist/presets/docs.js +62 -0
- package/dist/presets/risk.js +179 -0
- package/dist/presets/spec.js +81 -0
- package/dist/presets/witnesses.js +249 -0
- package/dist/render.js +42 -0
- package/dist/result.js +3 -0
- package/dist/runtime.js +1 -0
- package/dist/session.js +147 -0
- package/dist/texts/ask-files.js +1 -0
- package/dist/texts/ask.js +2 -0
- package/dist/texts/check-diff.js +17 -0
- package/dist/texts/configuration.js +1 -0
- package/dist/texts/find.js +14 -0
- package/dist/texts/guide.js +16 -0
- package/dist/texts/locate.js +10 -0
- package/dist/texts/select-tests.js +2 -0
- package/dist/tools/ask-files.js +217 -0
- package/dist/tools/ask-schema.js +70 -0
- package/dist/tools/ask.js +686 -0
- package/dist/tools/check-diff.js +402 -0
- package/dist/tools/docs-check.js +299 -0
- package/dist/tools/find.js +389 -0
- package/dist/tools/locate.js +303 -0
- package/dist/tools/select-tests.js +567 -0
- package/dist/tools/spec-check.js +166 -0
- package/docs/adr/0001-strict-typescript-pure-core-offline-tests.md +31 -0
- package/docs/adr/0002-one-http-protocol-across-hosts.md +17 -0
- package/docs/adr/0003-explicit-scope-conservative-automation.md +19 -0
- package/docs/adr/0004-compiled-typed-intents.md +19 -0
- package/docs/adr/0005-evidence-construction-before-judgment.md +19 -0
- package/docs/adr/0006-visible-uncertainty-constrained-controls.md +21 -0
- package/docs/adr/0007-bounded-evidence-visible-limits.md +21 -0
- package/docs/adr/0008-static-test-discovery-conservative-plans.md +19 -0
- package/docs/adr/0009-session-cache-requested-model-identity.md +17 -0
- package/docs/adr/0010-mcp-server-thin-host.md +23 -0
- package/docs/agent-instructions.md +91 -0
- package/docs/design.md +3 -3
- package/docs/mcp.md +231 -0
- package/package.json +19 -4
- package/server.json +57 -0
- package/src/adapters/canonical-path.ts +18 -0
- package/src/adapters/command.ts +7 -4
- package/src/adapters/exec.ts +226 -0
- package/src/adapters/private-storage.ts +143 -0
- package/src/adapters/risk-callers.ts +4 -2
- package/src/adapters/shell.ts +97 -0
- package/src/configuration.ts +294 -0
- package/src/constants.ts +11 -0
- package/src/core/command-output.ts +17 -1
- package/src/host-tui.d.ts +14 -0
- package/src/host.ts +11 -0
- package/src/index.ts +13 -5
- package/src/jev/client.ts +12 -0
- package/src/jev/types.ts +6 -0
- package/src/mcp/main.ts +135 -0
- package/src/mcp/protocol.ts +282 -0
- package/src/mcp/tools.ts +166 -0
- package/src/secret-input.ts +222 -0
- package/src/session.ts +59 -0
- package/src/setup.ts +170 -0
- package/src/texts/configuration.ts +1 -1
- package/src/tools/ask-files.ts +8 -13
- package/src/tools/ask.ts +29 -28
- package/src/tools/check-diff.ts +11 -11
- package/src/tools/docs-check.ts +1 -0
- package/src/tools/find.ts +8 -8
- package/src/tools/locate.ts +8 -9
- package/src/tools/select-tests.ts +10 -10
- package/src/tools/spec-check.ts +1 -0
|
@@ -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/session.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Envelope } from "./core/output.ts";
|
|
2
|
+
import type { JudgmentOptions } from "./jev/types.ts";
|
|
2
3
|
import type { Result } from "./result.ts";
|
|
3
4
|
|
|
4
5
|
export interface SessionLimits {
|
|
@@ -53,6 +54,10 @@ export class Session {
|
|
|
53
54
|
cacheHits: 0,
|
|
54
55
|
};
|
|
55
56
|
private readonly limits: SessionLimits;
|
|
57
|
+
// USD gate: at most one admitted request in flight under a USD limit.
|
|
58
|
+
private gateHeld = false;
|
|
59
|
+
private gateEpoch = 0;
|
|
60
|
+
private gateWaiters = new Set<() => void>();
|
|
56
61
|
constructor(limits: SessionLimits) {
|
|
57
62
|
this.limits = limits;
|
|
58
63
|
}
|
|
@@ -75,6 +80,56 @@ export class Session {
|
|
|
75
80
|
this.counters.questions += questions;
|
|
76
81
|
return { ok: true };
|
|
77
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Under a USD limit, admit one request at a time so each admission sees the
|
|
85
|
+
* cost reported by every earlier request; without it, concurrent batches are
|
|
86
|
+
* all admitted before any cost arrives and overshoot the limit. Only the
|
|
87
|
+
* final admitted request can then exceed the limit, as documented.
|
|
88
|
+
*
|
|
89
|
+
* `awaitAdmission` reserves the gate atomically: the availability check and
|
|
90
|
+
* `gateHeld = true` run with no await between them, so two callers cannot
|
|
91
|
+
* both pass. It resolves to a release function that the client calls on
|
|
92
|
+
* every exit path (response, refusal, failure, abort); release is
|
|
93
|
+
* idempotent and ignores a gate that `reset()` already replaced.
|
|
94
|
+
*/
|
|
95
|
+
requestGate(): Pick<JudgmentOptions, "awaitAdmission"> {
|
|
96
|
+
return {
|
|
97
|
+
awaitAdmission: async (signal) => {
|
|
98
|
+
if (this.limits.maxUsd === undefined) return undefined;
|
|
99
|
+
while (this.gateHeld) {
|
|
100
|
+
signal?.throwIfAborted();
|
|
101
|
+
await new Promise<void>((resolve, reject) => {
|
|
102
|
+
const onAbort = () => {
|
|
103
|
+
this.gateWaiters.delete(wake);
|
|
104
|
+
reject(signal?.reason);
|
|
105
|
+
};
|
|
106
|
+
const wake = () => {
|
|
107
|
+
signal?.removeEventListener("abort", onAbort);
|
|
108
|
+
resolve();
|
|
109
|
+
};
|
|
110
|
+
this.gateWaiters.add(wake);
|
|
111
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
signal?.throwIfAborted();
|
|
115
|
+
this.gateHeld = true;
|
|
116
|
+
const epoch = this.gateEpoch;
|
|
117
|
+
let released = false;
|
|
118
|
+
return () => {
|
|
119
|
+
if (released || epoch !== this.gateEpoch) return;
|
|
120
|
+
released = true;
|
|
121
|
+
this.gateHeld = false;
|
|
122
|
+
this.wakeGateWaiters();
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
// Every waiter rechecks; the first to run reserves, the rest wait again.
|
|
128
|
+
private wakeGateWaiters(): void {
|
|
129
|
+
const waiters = [...this.gateWaiters];
|
|
130
|
+
this.gateWaiters.clear();
|
|
131
|
+
for (const wake of waiters) wake();
|
|
132
|
+
}
|
|
78
133
|
recordUsage(usage: { costUsd: number }): void {
|
|
79
134
|
this.counters.costUsd += usage.costUsd;
|
|
80
135
|
}
|
|
@@ -93,6 +148,10 @@ export class Session {
|
|
|
93
148
|
return { ...this.counters };
|
|
94
149
|
}
|
|
95
150
|
reset(): void {
|
|
151
|
+
// A new session generation: outstanding release functions become no-ops.
|
|
152
|
+
this.gateEpoch++;
|
|
153
|
+
this.gateHeld = false;
|
|
154
|
+
this.wakeGateWaiters();
|
|
96
155
|
this.counters = {
|
|
97
156
|
calls: 0,
|
|
98
157
|
questions: 0,
|
package/src/setup.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
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
|
+
// Host event deadlines bound work, not the time needed to find credentials.
|
|
66
|
+
// Return before waiting for input so omp clears the handler's UI abort timer.
|
|
67
|
+
void this.offer(ctx).catch((error: unknown) => {
|
|
68
|
+
ctx.ui.notify(this.message(error), "error");
|
|
69
|
+
});
|
|
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 offer(ctx: SetupContext): Promise<void> {
|
|
106
|
+
const choice = await ctx.ui.select(
|
|
107
|
+
"Jev Tools needs an endpoint and API key",
|
|
108
|
+
["Configure now", "Later"],
|
|
109
|
+
);
|
|
110
|
+
if (choice === "Configure now") await this.configure(ctx);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private async configure(ctx: SetupContext): Promise<void> {
|
|
114
|
+
if (this.busy) return;
|
|
115
|
+
this.busy = true;
|
|
116
|
+
try {
|
|
117
|
+
const values: ConfigurationValues = this.config.values();
|
|
118
|
+
for (const field of ["url", "model"] as const) {
|
|
119
|
+
if (this.config.locked(field)) {
|
|
120
|
+
ctx.ui.notify(
|
|
121
|
+
`Jev ${field} is controlled by the environment or a launch flag; change it before restarting.`,
|
|
122
|
+
"info",
|
|
123
|
+
);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const label =
|
|
127
|
+
field === "url"
|
|
128
|
+
? "Complete Jev endpoint URL"
|
|
129
|
+
: "Jev model (not the conversation model)";
|
|
130
|
+
const answer = await ctx.ui.input(
|
|
131
|
+
label,
|
|
132
|
+
values[field] ||
|
|
133
|
+
(field === "model"
|
|
134
|
+
? "openjev"
|
|
135
|
+
: "https://your-service.example/v1/systemone"),
|
|
136
|
+
);
|
|
137
|
+
if (answer === undefined) return;
|
|
138
|
+
values[field] = answer.trim() || values[field];
|
|
139
|
+
}
|
|
140
|
+
if (this.config.locked("apiKey")) {
|
|
141
|
+
ctx.ui.notify(
|
|
142
|
+
"The Jev API key is supplied by the environment and will not be saved.",
|
|
143
|
+
"info",
|
|
144
|
+
);
|
|
145
|
+
} else {
|
|
146
|
+
const key = await this.promptSecret(
|
|
147
|
+
ctx,
|
|
148
|
+
"Jev API key",
|
|
149
|
+
Boolean(values.apiKey),
|
|
150
|
+
);
|
|
151
|
+
if (key === undefined) return;
|
|
152
|
+
values.apiKey = key || values.apiKey;
|
|
153
|
+
}
|
|
154
|
+
const storage = await ctx.ui.select("Apply Jev configuration", [
|
|
155
|
+
"This session only",
|
|
156
|
+
"Save for future sessions (editable values and plaintext key in a private global file)",
|
|
157
|
+
]);
|
|
158
|
+
if (storage === undefined) return;
|
|
159
|
+
await this.config.apply(values, storage !== "This session only");
|
|
160
|
+
ctx.ui.notify(
|
|
161
|
+
storage === "This session only"
|
|
162
|
+
? "Jev configuration applied for this session; no file was written."
|
|
163
|
+
: "Jev configuration applied and saved outside the repository. The stored key is plaintext, not encrypted.",
|
|
164
|
+
"info",
|
|
165
|
+
);
|
|
166
|
+
} finally {
|
|
167
|
+
this.busy = false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -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
|
@@ -3,6 +3,7 @@ import { resolve } from "node:path";
|
|
|
3
3
|
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { type Static, Type } from "@sinclair/typebox";
|
|
5
5
|
import { collectAskFiles } from "../adapters/ask-files.ts";
|
|
6
|
+
import { canonicalPath } from "../adapters/canonical-path.ts";
|
|
6
7
|
import { shareGitInventory } from "../adapters/git-inventory.ts";
|
|
7
8
|
import { hostUsage } from "../adapters/usage.ts";
|
|
8
9
|
import { compileAsks, readAsks, reverseQuestions } from "../core/asks.ts";
|
|
@@ -42,12 +43,8 @@ export interface AskFilesDetails {
|
|
|
42
43
|
skipped: string[];
|
|
43
44
|
envelope: Envelope;
|
|
44
45
|
}
|
|
45
|
-
export function createAskFilesTool({
|
|
46
|
-
|
|
47
|
-
host,
|
|
48
|
-
runtime,
|
|
49
|
-
exec: execute,
|
|
50
|
-
}: ToolDependencies) {
|
|
46
|
+
export function createAskFilesTool(dependencies: ToolDependencies) {
|
|
47
|
+
const { host, runtime, exec: execute } = dependencies;
|
|
51
48
|
return {
|
|
52
49
|
name: "jev_ask_files",
|
|
53
50
|
label: "Jev ask files",
|
|
@@ -70,6 +67,8 @@ export function createAskFilesTool({
|
|
|
70
67
|
_update: unknown,
|
|
71
68
|
ctx: { cwd: string } & GuideContext,
|
|
72
69
|
) {
|
|
70
|
+
const client = dependencies.client;
|
|
71
|
+
const cwd = await canonicalPath(ctx.cwd);
|
|
73
72
|
const started = performance.now();
|
|
74
73
|
const exec = shareGitInventory(execute);
|
|
75
74
|
const totals = {
|
|
@@ -102,12 +101,7 @@ export function createAskFilesTool({
|
|
|
102
101
|
if (!client) return finish({ refusal: NOT_CONFIGURED });
|
|
103
102
|
const plan = compileAsks(args.asks, { surface: "files" });
|
|
104
103
|
if (!plan.ok) return finish({ refusal: plan.error });
|
|
105
|
-
const collected = await collectAskFiles(
|
|
106
|
-
ctx.cwd,
|
|
107
|
-
args.paths,
|
|
108
|
-
signal,
|
|
109
|
-
exec,
|
|
110
|
-
);
|
|
104
|
+
const collected = await collectAskFiles(cwd, args.paths, signal, exec);
|
|
111
105
|
if (!collected.ok) return finish({ refusal: collected.error });
|
|
112
106
|
skipped = collected.skipped;
|
|
113
107
|
if (!collected.files.length && skipped.length)
|
|
@@ -118,6 +112,7 @@ export function createAskFilesTool({
|
|
|
118
112
|
let budget: BudgetRefusal | undefined;
|
|
119
113
|
const options = {
|
|
120
114
|
signal,
|
|
115
|
+
...runtime.session.requestGate(),
|
|
121
116
|
beforeRequest: (count: number) => {
|
|
122
117
|
if (args.max_calls !== undefined && sent >= args.max_calls) {
|
|
123
118
|
budget = {
|
|
@@ -151,7 +146,7 @@ export function createAskFilesTool({
|
|
|
151
146
|
const integrity = checkIntegrity(state, plan.asks, [
|
|
152
147
|
{
|
|
153
148
|
...file.identity,
|
|
154
|
-
insertedPath: resolve(
|
|
149
|
+
insertedPath: resolve(cwd, state.path),
|
|
155
150
|
content: state.content,
|
|
156
151
|
insertedSha256: createHash("sha256")
|
|
157
152
|
.update(state.content)
|