jev-agent-tools 0.1.4 → 0.3.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 +106 -1
- package/CONTRIBUTING.md +43 -0
- package/README.md +58 -17
- package/SECURITY.md +43 -0
- package/dist/adapters/analysis-context.js +75 -0
- package/dist/adapters/ask-files.js +198 -0
- package/dist/adapters/ask-proof.js +200 -0
- package/dist/adapters/ask-syntax.js +385 -0
- package/dist/adapters/canonical-path.js +17 -0
- package/dist/adapters/command.js +234 -0
- package/dist/adapters/docs.js +192 -0
- package/dist/adapters/evidence-context.js +119 -0
- package/dist/adapters/exec.js +207 -0
- package/dist/adapters/files.js +418 -0
- package/dist/adapters/find.js +150 -0
- package/dist/adapters/git-base.js +32 -0
- package/dist/adapters/git-inventory.js +71 -0
- package/dist/adapters/git.js +483 -0
- package/dist/adapters/locate-file.js +197 -0
- package/dist/adapters/output-lines.js +46 -0
- package/dist/adapters/private-storage.js +106 -0
- package/dist/adapters/risk-callers.js +429 -0
- package/dist/adapters/runner-version.js +78 -0
- package/dist/adapters/shell.js +92 -0
- package/dist/adapters/syntax.js +187 -0
- package/dist/adapters/test-inventory.js +139 -0
- package/dist/adapters/usage.js +20 -0
- package/dist/adapters/utf8.js +47 -0
- package/dist/configuration.js +267 -0
- package/dist/constants.js +140 -0
- package/dist/core/ask-closure.js +282 -0
- package/dist/core/ask-proof.js +1 -0
- package/dist/core/ask-references.js +278 -0
- package/dist/core/asks.js +507 -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 +137 -0
- package/dist/core/pointer.js +29 -0
- package/dist/core/result-report.js +302 -0
- package/dist/core/risk-callers.js +851 -0
- package/dist/core/runner-version.js +45 -0
- package/dist/core/secret-path.js +34 -0
- package/dist/core/sections.js +230 -0
- package/dist/core/state.js +51 -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 +456 -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 +210 -0
- package/dist/mcp/tools.js +129 -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 +114 -0
- package/dist/report-schema.js +1356 -0
- package/dist/result-types.js +1 -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 +3 -0
- package/dist/texts/ask.js +4 -0
- package/dist/texts/check-diff.js +20 -0
- package/dist/texts/configuration.js +1 -0
- package/dist/texts/find.js +19 -0
- package/dist/texts/guide.js +3 -0
- package/dist/texts/instructions.js +72 -0
- package/dist/texts/locate.js +15 -0
- package/dist/texts/select-tests.js +4 -0
- package/dist/tools/ask-files.js +450 -0
- package/dist/tools/ask-schema.js +70 -0
- package/dist/tools/ask.js +1147 -0
- package/dist/tools/check-diff.js +594 -0
- package/dist/tools/docs-check.js +408 -0
- package/dist/tools/find.js +682 -0
- package/dist/tools/locate.js +602 -0
- package/dist/tools/review-report.js +230 -0
- package/dist/tools/select-tests.js +821 -0
- package/dist/tools/spec-check.js +263 -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 +120 -0
- package/docs/design.md +16 -4
- package/docs/mcp.md +233 -0
- package/docs/tools/jev_ask.md +8 -5
- package/docs/tools/jev_ask_files.md +2 -1
- package/docs/tools/jev_check_diff.md +4 -1
- package/docs/tools/jev_find_files.md +2 -1
- package/docs/tools/jev_locate_in_file.md +5 -0
- package/docs/tools/jev_select_tests.md +4 -1
- package/package.json +19 -4
- package/rules/jev-ask.md +22 -1
- package/server.json +57 -0
- package/src/adapters/ask-files.ts +11 -3
- package/src/adapters/ask-proof.ts +69 -11
- package/src/adapters/canonical-path.ts +18 -0
- package/src/adapters/command.ts +102 -36
- package/src/adapters/docs.ts +33 -14
- package/src/adapters/evidence-context.ts +169 -0
- package/src/adapters/exec.ts +226 -0
- package/src/adapters/files.ts +146 -16
- package/src/adapters/find.ts +37 -7
- package/src/adapters/git-base.ts +7 -1
- package/src/adapters/git.ts +61 -8
- package/src/adapters/locate-file.ts +51 -9
- package/src/adapters/private-storage.ts +155 -0
- package/src/adapters/risk-callers.ts +7 -2
- package/src/adapters/shell.ts +113 -0
- package/src/adapters/test-inventory.ts +12 -4
- package/src/configuration.ts +55 -14
- package/src/constants.ts +37 -5
- package/src/core/ask-references.ts +262 -146
- package/src/core/asks.ts +79 -7
- package/src/core/command-output.ts +17 -1
- package/src/core/import-boundaries.ts +8 -3
- package/src/core/locate.ts +8 -5
- package/src/core/output.ts +34 -0
- package/src/core/result-report.ts +410 -0
- package/src/core/secret-path.ts +37 -0
- package/src/core/state.ts +8 -1
- package/src/core/units.ts +3 -2
- package/src/host.ts +11 -0
- package/src/index.ts +3 -0
- package/src/jev/client.ts +66 -16
- package/src/jev/types.ts +24 -3
- package/src/mcp/main.ts +135 -0
- package/src/mcp/protocol.ts +332 -0
- package/src/mcp/tools.ts +179 -0
- package/src/render.ts +109 -0
- package/src/report-schema.ts +1380 -0
- package/src/result-types.ts +234 -0
- package/src/result.ts +4 -1
- package/src/runtime.ts +6 -0
- package/src/session.ts +59 -0
- package/src/setup.ts +13 -5
- package/src/texts/ask-files.ts +4 -1
- package/src/texts/ask.ts +8 -1
- package/src/texts/check-diff.ts +7 -4
- package/src/texts/find.ts +8 -2
- package/src/texts/guide.ts +8 -16
- package/src/texts/instructions.ts +98 -0
- package/src/texts/locate.ts +8 -2
- package/src/texts/run-end.ts +2 -2
- package/src/texts/select-tests.ts +4 -1
- package/src/tools/ask-files.ts +311 -18
- package/src/tools/ask.ts +722 -95
- package/src/tools/check-diff.ts +337 -31
- package/src/tools/docs-check.ts +241 -38
- package/src/tools/find.ts +389 -29
- package/src/tools/locate.ts +387 -25
- package/src/tools/review-report.ts +308 -0
- package/src/tools/select-tests.ts +484 -23
- package/src/tools/spec-check.ts +194 -19
|
@@ -34,6 +34,7 @@ export async function readLocateFile(
|
|
|
34
34
|
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(path))
|
|
35
35
|
return {
|
|
36
36
|
ok: false,
|
|
37
|
+
cause: "forbidden_path",
|
|
37
38
|
error: `Internal URLs are not files; use read for ${path}.`,
|
|
38
39
|
};
|
|
39
40
|
const opened = await openRepoFile(cwd, path, { exec, signal });
|
|
@@ -41,7 +42,12 @@ export async function readLocateFile(
|
|
|
41
42
|
const handle = opened.handle;
|
|
42
43
|
try {
|
|
43
44
|
const stat = await handle.stat();
|
|
44
|
-
if (!stat.isFile())
|
|
45
|
+
if (!stat.isFile())
|
|
46
|
+
return {
|
|
47
|
+
ok: false,
|
|
48
|
+
cause: "file_unavailable",
|
|
49
|
+
error: `Not a file: ${path}.`,
|
|
50
|
+
};
|
|
45
51
|
const buffer = Buffer.alloc(
|
|
46
52
|
Math.min(stat.size + 1, LOCATE_WHOLE_MAX_BYTES + 1),
|
|
47
53
|
);
|
|
@@ -58,10 +64,19 @@ export async function readLocateFile(
|
|
|
58
64
|
count += read.bytesRead;
|
|
59
65
|
}
|
|
60
66
|
if (buffer.subarray(0, count).includes(0))
|
|
61
|
-
return {
|
|
67
|
+
return {
|
|
68
|
+
ok: false,
|
|
69
|
+
cause: "binary_or_non_utf8",
|
|
70
|
+
error: `Not a text file: ${path}.`,
|
|
71
|
+
};
|
|
62
72
|
if (count < buffer.length) {
|
|
63
73
|
const decoded = decodeUtf8(buffer.subarray(0, count));
|
|
64
|
-
if (!decoded.ok)
|
|
74
|
+
if (!decoded.ok)
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
cause: "binary_or_non_utf8",
|
|
78
|
+
error: `${path}: ${decoded.error}`,
|
|
79
|
+
};
|
|
65
80
|
const text = decoded.text;
|
|
66
81
|
if (text.length <= LOCATE_WHOLE_MAX_CHARS)
|
|
67
82
|
return {
|
|
@@ -85,7 +100,11 @@ export async function readLocateFile(
|
|
|
85
100
|
}
|
|
86
101
|
: scan;
|
|
87
102
|
} catch (error) {
|
|
88
|
-
return {
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
cause: signal?.aborted ? "cancelled" : "file_unavailable",
|
|
106
|
+
error: `Cannot read ${path}: ${String(error)}`,
|
|
107
|
+
};
|
|
89
108
|
} finally {
|
|
90
109
|
await handle.close();
|
|
91
110
|
}
|
|
@@ -171,26 +190,49 @@ export async function scanRange(
|
|
|
171
190
|
break;
|
|
172
191
|
}
|
|
173
192
|
if (buffer.subarray(0, read.bytesRead).includes(0))
|
|
174
|
-
return {
|
|
193
|
+
return {
|
|
194
|
+
ok: false,
|
|
195
|
+
cause: "binary_or_non_utf8",
|
|
196
|
+
error: `Not a text file: ${path}.`,
|
|
197
|
+
};
|
|
175
198
|
bytes += read.bytesRead;
|
|
176
199
|
const decoded = decode(buffer.subarray(0, read.bytesRead));
|
|
177
|
-
if (!decoded.ok)
|
|
200
|
+
if (!decoded.ok)
|
|
201
|
+
return {
|
|
202
|
+
ok: false,
|
|
203
|
+
cause: "binary_or_non_utf8",
|
|
204
|
+
error: `${path}: ${decoded.error}`,
|
|
205
|
+
};
|
|
178
206
|
if (!consume(decoded.text))
|
|
179
207
|
return {
|
|
180
208
|
ok: false,
|
|
209
|
+
cause: "evidence_too_large",
|
|
181
210
|
error: `Selected range exceeds ${STATE_MAX_CHARS} chars; read a narrower range directly.`,
|
|
182
211
|
};
|
|
183
212
|
if (range && line > range.end) break;
|
|
184
213
|
}
|
|
185
214
|
const end = reachedEof ? decode() : { ok: true as const, text: "" };
|
|
186
|
-
if (!end.ok)
|
|
215
|
+
if (!end.ok)
|
|
216
|
+
return {
|
|
217
|
+
ok: false,
|
|
218
|
+
cause: "binary_or_non_utf8",
|
|
219
|
+
error: `${path}: ${end.error}`,
|
|
220
|
+
};
|
|
187
221
|
if (!consume(end.text))
|
|
188
|
-
return {
|
|
222
|
+
return {
|
|
223
|
+
ok: false,
|
|
224
|
+
cause: "evidence_too_large",
|
|
225
|
+
error: "Selected range too large.",
|
|
226
|
+
};
|
|
189
227
|
const lines = line - Number(!pending);
|
|
190
228
|
if (lines > 0 && (windows.at(-1)?.end ?? 0) < lines) finishWindow(lines);
|
|
191
229
|
return { ok: true, bytes, lines, windows, text };
|
|
192
230
|
} catch (error) {
|
|
193
|
-
return {
|
|
231
|
+
return {
|
|
232
|
+
ok: false,
|
|
233
|
+
cause: signal?.aborted ? "cancelled" : "file_unavailable",
|
|
234
|
+
error: `Cannot read ${path}: ${String(error)}`,
|
|
235
|
+
};
|
|
194
236
|
} finally {
|
|
195
237
|
await handle.close();
|
|
196
238
|
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import type { Stats } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { PRIVATE_STORAGE_TIMEOUT_MS } from "../constants.ts";
|
|
5
|
+
import { withoutApiKey } from "./shell.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Owner-only storage, checked with each operating system's own model.
|
|
9
|
+
*
|
|
10
|
+
* POSIX: no group/other mode bits and owned by the current user.
|
|
11
|
+
* Windows: mode bits are synthesized (always 0o666), so the real ACL is read.
|
|
12
|
+
* Storage is private when every allow entry belongs to the current user,
|
|
13
|
+
* SYSTEM or the Administrators group (the Windows equivalent of root), and
|
|
14
|
+
* the owner is the current user or Administrators. Entries are compared as
|
|
15
|
+
* SIDs, so localized account names do not matter.
|
|
16
|
+
*/
|
|
17
|
+
export interface PrivateStorage {
|
|
18
|
+
/** True when every path is private. One check for all paths. */
|
|
19
|
+
isPrivate(
|
|
20
|
+
entries: readonly { path: string; stat: Stats }[],
|
|
21
|
+
): Promise<boolean>;
|
|
22
|
+
/** Restrict a directory the current user owns so new files inherit privacy. */
|
|
23
|
+
restrictDirectory(path: string): Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const SYSTEM = "S-1-5-18";
|
|
27
|
+
const ADMINISTRATORS = "S-1-5-32-544";
|
|
28
|
+
const CREATOR_OWNER = "S-1-3-0";
|
|
29
|
+
|
|
30
|
+
export const posixStorage: PrivateStorage = {
|
|
31
|
+
async isPrivate(entries) {
|
|
32
|
+
return entries.every(
|
|
33
|
+
({ stat }) =>
|
|
34
|
+
(stat.mode & 0o077) === 0 &&
|
|
35
|
+
(!process.getuid || stat.uid === process.getuid()),
|
|
36
|
+
);
|
|
37
|
+
},
|
|
38
|
+
async restrictDirectory() {
|
|
39
|
+
// mkdir(..., { mode: 0o700 }) already creates POSIX directories privately.
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Paths arrive as JSON in an environment variable, never in the command text.
|
|
44
|
+
const CHECK = `$ErrorActionPreference = 'Stop'
|
|
45
|
+
$me = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value
|
|
46
|
+
$trusted = @($me, '${SYSTEM}', '${ADMINISTRATORS}')
|
|
47
|
+
$sid = [System.Security.Principal.SecurityIdentifier]
|
|
48
|
+
$results = foreach ($path in (ConvertFrom-Json $env:JEV_PRIVATE_PATHS)) {
|
|
49
|
+
try {
|
|
50
|
+
$acl = Get-Acl -LiteralPath $path
|
|
51
|
+
$owner = $acl.GetOwner($sid).Value
|
|
52
|
+
$private = ($owner -eq $me) -or ($owner -eq '${ADMINISTRATORS}')
|
|
53
|
+
foreach ($rule in $acl.GetAccessRules($true, $true, $sid)) {
|
|
54
|
+
if ($rule.AccessControlType -ne 'Allow') { continue }
|
|
55
|
+
$id = $rule.IdentityReference.Value
|
|
56
|
+
$inheritOnly = ($rule.PropagationFlags -band [System.Security.AccessControl.PropagationFlags]::InheritOnly) -ne 0
|
|
57
|
+
if ($id -eq '${CREATOR_OWNER}' -and $inheritOnly) { continue }
|
|
58
|
+
if ($trusted -notcontains $id) { $private = $false }
|
|
59
|
+
}
|
|
60
|
+
$private
|
|
61
|
+
} catch { $false }
|
|
62
|
+
}
|
|
63
|
+
ConvertTo-Json -Compress @($results)`;
|
|
64
|
+
|
|
65
|
+
const RESTRICT = `$ErrorActionPreference = 'Stop'
|
|
66
|
+
$path = $env:JEV_PRIVATE_PATH
|
|
67
|
+
$me = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
|
|
68
|
+
$acl = Get-Acl -LiteralPath $path
|
|
69
|
+
if ($acl.GetOwner([System.Security.Principal.SecurityIdentifier]).Value -ne $me.Value) { throw 'not owner' }
|
|
70
|
+
$acl.SetAccessRuleProtection($true, $false)
|
|
71
|
+
foreach ($rule in @($acl.Access)) { [void]$acl.RemoveAccessRuleAll($rule) }
|
|
72
|
+
$inherit = [System.Security.AccessControl.InheritanceFlags]'ContainerInherit, ObjectInherit'
|
|
73
|
+
foreach ($id in @($me.Value, '${SYSTEM}', '${ADMINISTRATORS}')) {
|
|
74
|
+
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
|
|
75
|
+
(New-Object System.Security.Principal.SecurityIdentifier($id)),
|
|
76
|
+
'FullControl', $inherit, 'None', 'Allow')
|
|
77
|
+
$acl.AddAccessRule($rule)
|
|
78
|
+
}
|
|
79
|
+
Set-Acl -LiteralPath $path -AclObject $acl`;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Host environment for the ACL helper, without the Jev API key. PowerShell 7
|
|
83
|
+
* parents export PSModulePath, which stops Windows PowerShell 5.1 from loading
|
|
84
|
+
* its own Get-Acl module; 5.1 rebuilds it when unset.
|
|
85
|
+
*/
|
|
86
|
+
export function powershellEnv(
|
|
87
|
+
host: NodeJS.ProcessEnv,
|
|
88
|
+
env: Record<string, string>,
|
|
89
|
+
): NodeJS.ProcessEnv {
|
|
90
|
+
const childEnv = withoutApiKey({ ...host, ...env });
|
|
91
|
+
for (const key of Object.keys(childEnv))
|
|
92
|
+
if (key.toLowerCase() === "psmodulepath") delete childEnv[key];
|
|
93
|
+
return childEnv;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function powershell(
|
|
97
|
+
script: string,
|
|
98
|
+
env: Record<string, string>,
|
|
99
|
+
): Promise<string> {
|
|
100
|
+
const root = process.env.SystemRoot ?? "C:\\Windows";
|
|
101
|
+
// A fixed system path, so a powershell.exe earlier on PATH is never used.
|
|
102
|
+
const executable = join(
|
|
103
|
+
root,
|
|
104
|
+
"System32",
|
|
105
|
+
"WindowsPowerShell",
|
|
106
|
+
"v1.0",
|
|
107
|
+
"powershell.exe",
|
|
108
|
+
);
|
|
109
|
+
const childEnv = powershellEnv(process.env, env);
|
|
110
|
+
const { promise, resolve, reject } = Promise.withResolvers<string>();
|
|
111
|
+
execFile(
|
|
112
|
+
executable,
|
|
113
|
+
[
|
|
114
|
+
"-NoProfile",
|
|
115
|
+
"-NonInteractive",
|
|
116
|
+
"-ExecutionPolicy",
|
|
117
|
+
"Bypass",
|
|
118
|
+
// UTF-16LE base64 runs the script as one unit; stdin runs line by line.
|
|
119
|
+
"-EncodedCommand",
|
|
120
|
+
Buffer.from(script, "utf16le").toString("base64"),
|
|
121
|
+
],
|
|
122
|
+
{ env: childEnv, windowsHide: true, timeout: PRIVATE_STORAGE_TIMEOUT_MS },
|
|
123
|
+
(error, stdout) => (error ? reject(error) : resolve(stdout)),
|
|
124
|
+
);
|
|
125
|
+
return promise;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export const windowsStorage: PrivateStorage = {
|
|
129
|
+
async isPrivate(entries) {
|
|
130
|
+
if (!entries.length) return true;
|
|
131
|
+
try {
|
|
132
|
+
const output = await powershell(CHECK, {
|
|
133
|
+
JEV_PRIVATE_PATHS: JSON.stringify(entries.map(({ path }) => path)),
|
|
134
|
+
});
|
|
135
|
+
const results: unknown = JSON.parse(output.trim());
|
|
136
|
+
return (
|
|
137
|
+
Array.isArray(results) &&
|
|
138
|
+
results.length === entries.length &&
|
|
139
|
+
results.every((result) => result === true)
|
|
140
|
+
);
|
|
141
|
+
} catch {
|
|
142
|
+
// An ACL that cannot be read is never treated as private.
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
async restrictDirectory(path) {
|
|
147
|
+
await powershell(RESTRICT, { JEV_PRIVATE_PATH: path });
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export function privateStorage(
|
|
152
|
+
platform: NodeJS.Platform = process.platform,
|
|
153
|
+
): PrivateStorage {
|
|
154
|
+
return platform === "win32" ? windowsStorage : posixStorage;
|
|
155
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type FileHandle, lstat } from "node:fs/promises";
|
|
2
|
-
import {
|
|
2
|
+
import { posix } from "node:path";
|
|
3
3
|
import type { SgNode } from "@ast-grep/napi";
|
|
4
4
|
import { CONCURRENCY, STATE_MAX_CHARS, TIMEOUT_MS } from "../constants.ts";
|
|
5
5
|
import type { GitExec } from "../core/git.ts";
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
type CallerSource,
|
|
10
10
|
prepareRiskCallers,
|
|
11
11
|
} from "../core/risk-callers.ts";
|
|
12
|
+
import { SECRET_NAME_GLOBS } from "../core/secret-path.ts";
|
|
12
13
|
import type { SyntaxRootParser } from "../core/syntax.ts";
|
|
13
14
|
import type { EvidenceUnit, SourceFile } from "../core/units.ts";
|
|
14
15
|
import {
|
|
@@ -201,6 +202,8 @@ export async function collectRiskCallers(
|
|
|
201
202
|
ref,
|
|
202
203
|
"--",
|
|
203
204
|
...extensions.map((extension) => `*.${extension}`),
|
|
205
|
+
// Base-tree secret-named files are never searched or read.
|
|
206
|
+
...SECRET_NAME_GLOBS.map((glob) => `:(exclude,icase,glob)**/${glob}`),
|
|
204
207
|
],
|
|
205
208
|
{ cwd, timeout: TIMEOUT_MS, signal: options.signal },
|
|
206
209
|
).catch((error: unknown) => ({
|
|
@@ -460,8 +463,10 @@ export async function collectRiskCallers(
|
|
|
460
463
|
typeof source.beforePath === "string"
|
|
461
464
|
? source.beforePath
|
|
462
465
|
: source.path;
|
|
466
|
+
// Repository paths are always "/"-separated; the platform
|
|
467
|
+
// resolve would yield "C:\\src\\..." on Windows.
|
|
463
468
|
const basePath = specifier.startsWith(".")
|
|
464
|
-
? resolve("/", sourcePath, "..", specifier).slice(1)
|
|
469
|
+
? posix.resolve("/", sourcePath, "..", specifier).slice(1)
|
|
465
470
|
: specifier.replaceAll(".", "/");
|
|
466
471
|
const runtimeSource = basePath.replace(
|
|
467
472
|
/\.(js|jsx|mjs|cjs)$/,
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { accessSync, constants } from "node:fs";
|
|
2
|
+
// Candidates are Windows paths whatever the host OS, so the helpers use win32
|
|
3
|
+
// semantics explicitly; the host-native `node:path` splits PATH on ":" on POSIX.
|
|
4
|
+
import { win32 } from "node:path";
|
|
5
|
+
|
|
6
|
+
export interface ShellInvocation {
|
|
7
|
+
executable: string;
|
|
8
|
+
/** Arguments placed before the `-c` script. */
|
|
9
|
+
prefix: string[];
|
|
10
|
+
/** Exports prepended to the script when the launcher cannot set them. */
|
|
11
|
+
scriptPrefix: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type ShellResolution =
|
|
15
|
+
| ({ ok: true } & ShellInvocation)
|
|
16
|
+
| { ok: false; error: string };
|
|
17
|
+
|
|
18
|
+
function executable(path: string): boolean {
|
|
19
|
+
try {
|
|
20
|
+
accessSync(path, constants.X_OK);
|
|
21
|
+
return true;
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Windows ships `bash.exe` launchers for WSL in System32 (and SysWOW64,
|
|
29
|
+
* Sysnative) and in WindowsApps. They run in a Linux VM that cannot see Windows
|
|
30
|
+
* temporary paths, so they are never used for command evidence, including
|
|
31
|
+
* when named explicitly in JEV_TOOLS_BASH.
|
|
32
|
+
*/
|
|
33
|
+
export function isWslLauncher(path: string): boolean {
|
|
34
|
+
const lower = win32.normalize(path).toLowerCase();
|
|
35
|
+
return (
|
|
36
|
+
/\\windows\\(system32|syswow64|sysnative)\\/.test(lower) ||
|
|
37
|
+
lower.includes("\\microsoft\\windowsapps\\")
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Permitted Git for Windows bash candidates, most specific first. */
|
|
42
|
+
export function windowsBashCandidates(env: NodeJS.ProcessEnv): string[] {
|
|
43
|
+
const candidates: string[] = [];
|
|
44
|
+
if (env.JEV_TOOLS_BASH?.trim()) candidates.push(env.JEV_TOOLS_BASH.trim());
|
|
45
|
+
const pathEntries = (env.PATH ?? env.Path ?? "")
|
|
46
|
+
.split(win32.delimiter)
|
|
47
|
+
.filter(Boolean);
|
|
48
|
+
for (const entry of pathEntries) {
|
|
49
|
+
candidates.push(win32.join(entry, "bash.exe"));
|
|
50
|
+
// Git\cmd\git.exe or Git\bin\git.exe on PATH implies Git\bin\bash.exe.
|
|
51
|
+
if (/[\\/](cmd|bin)$/i.test(entry))
|
|
52
|
+
candidates.push(win32.join(win32.dirname(entry), "bin", "bash.exe"));
|
|
53
|
+
}
|
|
54
|
+
for (const root of [
|
|
55
|
+
env.ProgramFiles,
|
|
56
|
+
env["ProgramFiles(x86)"],
|
|
57
|
+
env.LOCALAPPDATA && win32.join(env.LOCALAPPDATA, "Programs"),
|
|
58
|
+
])
|
|
59
|
+
if (root) candidates.push(win32.join(root, "Git", "bin", "bash.exe"));
|
|
60
|
+
// Filter every source, so no candidate can reintroduce a WSL launcher.
|
|
61
|
+
return [...new Set(candidates)].filter(
|
|
62
|
+
(candidate) => win32.isAbsolute(candidate) && !isWslLauncher(candidate),
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const UNAVAILABLE =
|
|
67
|
+
"No permitted bash found: install Git for Windows or set JEV_TOOLS_BASH to the full path of a bash that is not the WSL launcher.";
|
|
68
|
+
/** Never handed to command children: the Jev API key stays with the extension. */
|
|
69
|
+
export const API_KEY_VARIABLE = "JEV_TOOLS_API_KEY";
|
|
70
|
+
|
|
71
|
+
/** Copy of `env` without the Jev API key (any case, for Windows). */
|
|
72
|
+
export function withoutApiKey(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|
73
|
+
const copy = { ...env };
|
|
74
|
+
for (const key of Object.keys(copy))
|
|
75
|
+
if (key.toUpperCase() === API_KEY_VARIABLE) delete copy[key];
|
|
76
|
+
return copy;
|
|
77
|
+
}
|
|
78
|
+
let cached: ShellResolution | undefined;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* POSIX hosts run `env -u JEV_TOOLS_API_KEY CI=1 bash -c`. Windows has no
|
|
82
|
+
* `env` and its default `bash` is the WSL launcher, so resolve Git for Windows
|
|
83
|
+
* bash (or JEV_TOOLS_BASH) to an absolute, permitted path, and unset the key
|
|
84
|
+
* and export CI inside the script. When none is found this fails closed: no
|
|
85
|
+
* bare name is returned, because spawning one would search PATH again without
|
|
86
|
+
* the WSL exclusion.
|
|
87
|
+
*/
|
|
88
|
+
export function resolveShell(
|
|
89
|
+
platform: NodeJS.Platform = process.platform,
|
|
90
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
91
|
+
isExecutable: (path: string) => boolean = executable,
|
|
92
|
+
): ShellResolution {
|
|
93
|
+
if (platform !== "win32")
|
|
94
|
+
return {
|
|
95
|
+
ok: true,
|
|
96
|
+
executable: "env",
|
|
97
|
+
prefix: ["-u", API_KEY_VARIABLE, "CI=1", "bash"],
|
|
98
|
+
scriptPrefix: "",
|
|
99
|
+
};
|
|
100
|
+
const useCache = isExecutable === executable && env === process.env;
|
|
101
|
+
if (useCache && cached) return cached;
|
|
102
|
+
const found = windowsBashCandidates(env).find((path) => isExecutable(path));
|
|
103
|
+
const resolved: ShellResolution = found
|
|
104
|
+
? {
|
|
105
|
+
ok: true,
|
|
106
|
+
executable: found,
|
|
107
|
+
prefix: [],
|
|
108
|
+
scriptPrefix: `unset ${API_KEY_VARIABLE}; export CI=1; `,
|
|
109
|
+
}
|
|
110
|
+
: { ok: false, error: UNAVAILABLE };
|
|
111
|
+
if (useCache && found) cached = resolved;
|
|
112
|
+
return resolved;
|
|
113
|
+
}
|
|
@@ -2,6 +2,7 @@ import { type FileHandle, lstat } from "node:fs/promises";
|
|
|
2
2
|
import { TEST_SOURCE_MAX_BYTES, TIMEOUT_MS } from "../constants.ts";
|
|
3
3
|
import type { GitExec } from "../core/git.ts";
|
|
4
4
|
import type { ImportSource } from "../core/imports.ts";
|
|
5
|
+
import { isSecretPath } from "../core/secret-path.ts";
|
|
5
6
|
import type { Result } from "../result.ts";
|
|
6
7
|
import {
|
|
7
8
|
checkFileAdmission,
|
|
@@ -21,7 +22,7 @@ export async function collectTestInventory(
|
|
|
21
22
|
read(path: string): Promise<ImportSource | undefined>;
|
|
22
23
|
limits: readonly {
|
|
23
24
|
path: string;
|
|
24
|
-
kind: "unreadable" | "too_large" | "not UTF-8 text";
|
|
25
|
+
kind: "unreadable" | "too_large" | "not UTF-8 text" | "secret_pattern";
|
|
25
26
|
}[];
|
|
26
27
|
}>
|
|
27
28
|
> {
|
|
@@ -33,6 +34,7 @@ export async function collectTestInventory(
|
|
|
33
34
|
if (root.code !== 0 || root.killed)
|
|
34
35
|
return {
|
|
35
36
|
ok: false,
|
|
37
|
+
cause: signal?.aborted ? "cancelled" : "git_failure",
|
|
36
38
|
error: root.stderr || "Cannot identify repository root.",
|
|
37
39
|
};
|
|
38
40
|
cwd = root.stdout.replace(/\n$/, "");
|
|
@@ -51,6 +53,7 @@ export async function collectTestInventory(
|
|
|
51
53
|
if (listed.code !== 0 || listed.killed)
|
|
52
54
|
return {
|
|
53
55
|
ok: false,
|
|
56
|
+
cause: signal?.aborted ? "cancelled" : "git_failure",
|
|
54
57
|
error: listed.stderr || "Cannot inventory repository files.",
|
|
55
58
|
};
|
|
56
59
|
const ignored = await exec(
|
|
@@ -61,10 +64,11 @@ export async function collectTestInventory(
|
|
|
61
64
|
if (ignored.code !== 0 || ignored.killed)
|
|
62
65
|
return {
|
|
63
66
|
ok: false,
|
|
67
|
+
cause: signal?.aborted ? "cancelled" : "git_failure",
|
|
64
68
|
error: ignored.stderr || "Cannot identify ignored tracked files.",
|
|
65
69
|
};
|
|
66
70
|
const excluded = new Set(ignored.stdout.split("\0").filter(Boolean));
|
|
67
|
-
const
|
|
71
|
+
const listedPaths = listed.stdout
|
|
68
72
|
.split("\0")
|
|
69
73
|
.filter(
|
|
70
74
|
(path) =>
|
|
@@ -72,11 +76,15 @@ export async function collectTestInventory(
|
|
|
72
76
|
(!excluded.has(path) ||
|
|
73
77
|
/(?:^|\/)(?:package|tsconfig[^/]*)\.json$/.test(path)),
|
|
74
78
|
);
|
|
79
|
+
// Secret-named files are named exclusions, never inventory members to read.
|
|
80
|
+
const paths = listedPaths.filter((path) => !isSecretPath(path));
|
|
75
81
|
const admitted = new Set(paths);
|
|
76
82
|
const limits: {
|
|
77
83
|
path: string;
|
|
78
|
-
kind: "unreadable" | "too_large" | "not UTF-8 text";
|
|
79
|
-
}[] =
|
|
84
|
+
kind: "unreadable" | "too_large" | "not UTF-8 text" | "secret_pattern";
|
|
85
|
+
}[] = listedPaths
|
|
86
|
+
.filter(isSecretPath)
|
|
87
|
+
.map((path) => ({ path, kind: "secret_pattern" as const }));
|
|
80
88
|
const cache = new Map<string, Promise<ImportSource | undefined>>();
|
|
81
89
|
for (let offset = 0; offset < paths.length; offset += 16) {
|
|
82
90
|
const inspected = await Promise.all(
|
package/src/configuration.ts
CHANGED
|
@@ -3,6 +3,10 @@ import { constants } from "node:fs";
|
|
|
3
3
|
import { lstat, mkdir, open, rename, unlink } from "node:fs/promises";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
6
|
+
import {
|
|
7
|
+
type PrivateStorage,
|
|
8
|
+
privateStorage,
|
|
9
|
+
} from "./adapters/private-storage.ts";
|
|
6
10
|
import { createJevClient } from "./jev/client.ts";
|
|
7
11
|
import type { JevClient } from "./jev/types.ts";
|
|
8
12
|
|
|
@@ -26,17 +30,31 @@ function nonempty(value: string | undefined): value is string {
|
|
|
26
30
|
|
|
27
31
|
function validate(values: ConfigurationValues): void {
|
|
28
32
|
let validUrl = false;
|
|
33
|
+
let insecureTransport = false;
|
|
29
34
|
try {
|
|
30
35
|
const url = new URL(values.url);
|
|
31
|
-
|
|
36
|
+
const host = url.hostname.toLowerCase();
|
|
37
|
+
const loopback =
|
|
38
|
+
host === "127.0.0.1" ||
|
|
39
|
+
host === "localhost" ||
|
|
40
|
+
host === "::1" ||
|
|
41
|
+
host === "[::1]";
|
|
42
|
+
const wellFormed =
|
|
32
43
|
/^https?:\/\//i.test(values.url) &&
|
|
33
44
|
(url.protocol === "http:" || url.protocol === "https:") &&
|
|
34
45
|
url.hostname !== "" &&
|
|
35
46
|
url.username === "" &&
|
|
36
47
|
url.password === "";
|
|
48
|
+
insecureTransport = wellFormed && url.protocol === "http:" && !loopback;
|
|
49
|
+
validUrl = wellFormed && (url.protocol === "https:" || loopback);
|
|
37
50
|
} catch {
|
|
38
51
|
// Keep user-controlled URL and credentials out of error messages.
|
|
39
52
|
}
|
|
53
|
+
if (insecureTransport) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
"Jev configuration refuses plain http: to a non-loopback host; use https: or an http: loopback URL (127.0.0.1, ::1 or localhost).",
|
|
56
|
+
);
|
|
57
|
+
}
|
|
40
58
|
if (
|
|
41
59
|
!validUrl ||
|
|
42
60
|
!nonempty(values.apiKey) ||
|
|
@@ -44,7 +62,7 @@ function validate(values: ConfigurationValues): void {
|
|
|
44
62
|
!nonempty(values.model)
|
|
45
63
|
) {
|
|
46
64
|
throw new Error(
|
|
47
|
-
"Jev configuration requires a full
|
|
65
|
+
"Jev configuration requires a full https: URL (or http: to 127.0.0.1, ::1 or localhost) without embedded credentials, a nonempty HTTP-header-compatible API key, and a nonempty model.",
|
|
48
66
|
);
|
|
49
67
|
}
|
|
50
68
|
}
|
|
@@ -56,11 +74,19 @@ export class ConfigController {
|
|
|
56
74
|
private saved: SavedConfiguration = {};
|
|
57
75
|
private session: SavedConfiguration = {};
|
|
58
76
|
private readonly directory: string;
|
|
77
|
+
private readonly storage: PrivateStorage;
|
|
59
78
|
private initialization: Promise<void> | undefined;
|
|
60
79
|
private currentClient: JevClient | undefined;
|
|
61
80
|
|
|
62
|
-
constructor(
|
|
81
|
+
constructor(
|
|
82
|
+
options: {
|
|
83
|
+
env?: NodeJS.ProcessEnv;
|
|
84
|
+
directory?: string;
|
|
85
|
+
storage?: PrivateStorage;
|
|
86
|
+
} = {},
|
|
87
|
+
) {
|
|
63
88
|
const env = options.env ?? process.env;
|
|
89
|
+
this.storage = options.storage ?? privateStorage();
|
|
64
90
|
for (const [field, name] of [
|
|
65
91
|
["url", "JEV_TOOLS_URL"],
|
|
66
92
|
["apiKey", "JEV_TOOLS_API_KEY"],
|
|
@@ -163,19 +189,14 @@ export class ConfigController {
|
|
|
163
189
|
|
|
164
190
|
private async checkDirectory(): Promise<boolean> {
|
|
165
191
|
// Check ancestors too: recursive mkdir and path-based reads must not follow symlinks.
|
|
192
|
+
// Privacy of the directory itself is checked with its file in readSaved,
|
|
193
|
+
// using the operating system's own permission model.
|
|
166
194
|
let path = this.directory;
|
|
167
195
|
let exists = true;
|
|
168
196
|
while (true) {
|
|
169
197
|
try {
|
|
170
198
|
const stat = await lstat(path);
|
|
171
199
|
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
200
|
} catch (error) {
|
|
180
201
|
if ((error as NodeJS.ErrnoException)?.code !== "ENOENT")
|
|
181
202
|
throw storageError();
|
|
@@ -191,12 +212,20 @@ export class ConfigController {
|
|
|
191
212
|
private async readSaved(): Promise<SavedConfiguration> {
|
|
192
213
|
try {
|
|
193
214
|
if (!(await this.checkDirectory())) return {};
|
|
215
|
+
const directory = {
|
|
216
|
+
path: this.directory,
|
|
217
|
+
stat: await lstat(this.directory),
|
|
218
|
+
};
|
|
194
219
|
const path = join(this.directory, "config.json");
|
|
195
220
|
try {
|
|
196
221
|
const stat = await lstat(path);
|
|
197
222
|
if (!stat.isFile() || stat.isSymbolicLink()) throw storageError();
|
|
198
223
|
} catch (error) {
|
|
199
|
-
if ((error as NodeJS.ErrnoException)?.code === "ENOENT")
|
|
224
|
+
if ((error as NodeJS.ErrnoException)?.code === "ENOENT") {
|
|
225
|
+
if (!(await this.storage.isPrivate([directory])))
|
|
226
|
+
throw storageError();
|
|
227
|
+
return {};
|
|
228
|
+
}
|
|
200
229
|
throw error;
|
|
201
230
|
}
|
|
202
231
|
const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
@@ -205,8 +234,7 @@ export class ConfigController {
|
|
|
205
234
|
if (
|
|
206
235
|
!stat.isFile() ||
|
|
207
236
|
stat.nlink !== 1 ||
|
|
208
|
-
(
|
|
209
|
-
(process.getuid && stat.uid !== process.getuid())
|
|
237
|
+
!(await this.storage.isPrivate([directory, { path, stat }]))
|
|
210
238
|
) {
|
|
211
239
|
throw storageError();
|
|
212
240
|
}
|
|
@@ -235,9 +263,22 @@ export class ConfigController {
|
|
|
235
263
|
private async save(values: SavedConfiguration): Promise<void> {
|
|
236
264
|
let temporary: string | undefined;
|
|
237
265
|
try {
|
|
238
|
-
|
|
266
|
+
const existed = await this.checkDirectory();
|
|
267
|
+
if (!existed) {
|
|
239
268
|
await mkdir(this.directory, { recursive: true, mode: 0o700 });
|
|
240
269
|
}
|
|
270
|
+
// Windows ignores mkdir's mode, so restrict the ACL before any secret is
|
|
271
|
+
// written: always for a new directory, and for an existing one only while
|
|
272
|
+
// it holds no configuration (e.g. left behind by an earlier failed save).
|
|
273
|
+
// Existing configuration is never re-permissioned; readSaved refuses it.
|
|
274
|
+
if (
|
|
275
|
+
!existed ||
|
|
276
|
+
!(await lstat(join(this.directory, "config.json")).then(
|
|
277
|
+
() => true,
|
|
278
|
+
() => false,
|
|
279
|
+
))
|
|
280
|
+
)
|
|
281
|
+
await this.storage.restrictDirectory(this.directory);
|
|
241
282
|
// Refuse to replace malformed, insecure or newly introduced storage.
|
|
242
283
|
await this.readSaved();
|
|
243
284
|
temporary = join(this.directory, `.config-${randomUUID()}.tmp`);
|
package/src/constants.ts
CHANGED
|
@@ -5,6 +5,17 @@ export const ASK_NOTE_MAX_CHARS = 8_000;
|
|
|
5
5
|
export const ASK_MAX_FILES = 20;
|
|
6
6
|
export const ASK_TIMEOUT_S = 60;
|
|
7
7
|
export const ASK_TIMEOUT_MAX_S = 300;
|
|
8
|
+
export const PROCESS_KILL_GRACE_MS = 2_000;
|
|
9
|
+
export const PROCESS_PIPE_GRACE_MS = 1_000;
|
|
10
|
+
export const PROCESS_TREE_TIMEOUT_MS = 5_000;
|
|
11
|
+
export const MCP_SHUTDOWN_FLUSH_TIMEOUT_MS = 1_000;
|
|
12
|
+
export const PRIVATE_STORAGE_TIMEOUT_MS = 20_000;
|
|
13
|
+
export const MCP_VALIDATION_MAX_ERRORS = 5;
|
|
14
|
+
export const MCP_PACKAGE_REPLY_TIMEOUT_MS = 20_000;
|
|
15
|
+
export const MCP_PACKAGE_EXIT_TIMEOUT_MS = 10_000;
|
|
16
|
+
export const MCP_PACKAGE_KILL_TIMEOUT_MS = 2_000;
|
|
17
|
+
export const MCP_PACKAGE_INSTALL_TIMEOUT_MS = 300_000;
|
|
18
|
+
export const MCP_PACKAGE_STDERR_MAX_CHARS = 2_000;
|
|
8
19
|
export const OUTPUT_REPEAT_MIN = 5;
|
|
9
20
|
export const OUTPUT_CHUNK_CHARS = 2_500;
|
|
10
21
|
export const OUTPUT_FIND_MAX_CALLS = 40;
|
|
@@ -99,11 +110,32 @@ export const RUNNER_PACKAGE_MAX_BYTES = 64 * 1024;
|
|
|
99
110
|
|
|
100
111
|
// ADR 0001: allowed local dependencies, including erased type imports.
|
|
101
112
|
export const IMPORT_LAYERS = {
|
|
102
|
-
core: [
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
113
|
+
core: [
|
|
114
|
+
"core",
|
|
115
|
+
"constants.ts",
|
|
116
|
+
"result.ts",
|
|
117
|
+
"result-types.ts",
|
|
118
|
+
"jev/types.ts",
|
|
119
|
+
],
|
|
120
|
+
presets: [
|
|
121
|
+
"presets",
|
|
122
|
+
"core",
|
|
123
|
+
"constants.ts",
|
|
124
|
+
"result.ts",
|
|
125
|
+
"result-types.ts",
|
|
126
|
+
"jev/types.ts",
|
|
127
|
+
],
|
|
128
|
+
adapters: [
|
|
129
|
+
"adapters",
|
|
130
|
+
"core",
|
|
131
|
+
"constants.ts",
|
|
132
|
+
"result.ts",
|
|
133
|
+
"result-types.ts",
|
|
134
|
+
"jev/types.ts",
|
|
135
|
+
],
|
|
136
|
+
jev: ["jev", "core", "constants.ts", "result.ts", "result-types.ts"],
|
|
106
137
|
texts: ["texts", "constants.ts", "presets"],
|
|
107
138
|
"constants.ts": [],
|
|
108
|
-
"result.ts": [],
|
|
139
|
+
"result.ts": ["result-types.ts"],
|
|
140
|
+
"result-types.ts": [],
|
|
109
141
|
} as const;
|