@rigour-labs/core 6.2.3 → 6.2.5

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.
Files changed (100) hide show
  1. package/dist/deep/code-context.d.ts +28 -0
  2. package/dist/deep/code-context.js +93 -0
  3. package/dist/deep/code-context.test.d.ts +1 -0
  4. package/dist/deep/code-context.test.js +48 -0
  5. package/dist/deep/code-pass.d.ts +15 -0
  6. package/dist/deep/code-pass.js +44 -0
  7. package/dist/deep/code-review-prompt.d.ts +17 -0
  8. package/dist/deep/code-review-prompt.js +30 -0
  9. package/dist/deep/code-review-prompt.test.d.ts +1 -0
  10. package/dist/deep/code-review-prompt.test.js +20 -0
  11. package/dist/deep/code-verifier.d.ts +19 -0
  12. package/dist/deep/code-verifier.js +48 -0
  13. package/dist/deep/code-verifier.test.d.ts +1 -0
  14. package/dist/deep/code-verifier.test.js +46 -0
  15. package/dist/deep/fact-extractor.d.ts +7 -1
  16. package/dist/deep/fact-extractor.js +16 -8
  17. package/dist/deep/fact-extractor.test.js +34 -1
  18. package/dist/deep/facts-pass.d.ts +24 -0
  19. package/dist/deep/facts-pass.js +69 -0
  20. package/dist/deep/index.d.ts +6 -1
  21. package/dist/deep/index.js +5 -1
  22. package/dist/deep/parse-findings.d.ts +12 -0
  23. package/dist/deep/parse-findings.js +73 -0
  24. package/dist/deep/parse-findings.test.d.ts +1 -0
  25. package/dist/deep/parse-findings.test.js +25 -0
  26. package/dist/deep/prompts.d.ts +1 -1
  27. package/dist/deep/prompts.js +7 -8
  28. package/dist/deep/prompts.test.js +15 -0
  29. package/dist/gates/context.js +7 -0
  30. package/dist/gates/deep-analysis.d.ts +31 -8
  31. package/dist/gates/deep-analysis.js +165 -306
  32. package/dist/gates/deep-analysis.test.d.ts +1 -0
  33. package/dist/gates/deep-analysis.test.js +86 -0
  34. package/dist/gates/deep-runner.d.ts +15 -0
  35. package/dist/gates/deep-runner.js +65 -0
  36. package/dist/gates/dependency.js +4 -0
  37. package/dist/gates/hallucinated-imports/index.js +13 -9
  38. package/dist/gates/hallucinated-imports.test.js +10 -0
  39. package/dist/gates/js-style-context.d.ts +3 -0
  40. package/dist/gates/js-style-context.js +65 -0
  41. package/dist/gates/js-style-context.test.d.ts +1 -0
  42. package/dist/gates/js-style-context.test.js +35 -0
  43. package/dist/gates/logic-drift-git-base.d.ts +7 -0
  44. package/dist/gates/logic-drift-git-base.js +41 -0
  45. package/dist/gates/logic-drift.d.ts +3 -3
  46. package/dist/gates/logic-drift.js +39 -7
  47. package/dist/gates/logic-drift.test.d.ts +1 -0
  48. package/dist/gates/logic-drift.test.js +45 -0
  49. package/dist/gates/runner.js +5 -40
  50. package/dist/gates/runner.test.js +37 -25
  51. package/dist/gates/scoped-gates.test.d.ts +1 -0
  52. package/dist/gates/scoped-gates.test.js +52 -0
  53. package/dist/gates/security-command-execution.d.ts +5 -0
  54. package/dist/gates/security-command-execution.js +123 -0
  55. package/dist/gates/security-patterns-data.js +1 -17
  56. package/dist/gates/security-patterns.d.ts +0 -5
  57. package/dist/gates/security-patterns.js +11 -19
  58. package/dist/gates/security-patterns.test.js +37 -2
  59. package/dist/gates/style-drift.d.ts +4 -4
  60. package/dist/gates/style-drift.js +15 -16
  61. package/dist/gates/style-drift.test.d.ts +1 -0
  62. package/dist/gates/style-drift.test.js +26 -0
  63. package/dist/index.d.ts +6 -1
  64. package/dist/index.js +6 -1
  65. package/dist/inference/http-download.d.ts +22 -0
  66. package/dist/inference/http-download.js +132 -0
  67. package/dist/inference/http-download.test.d.ts +1 -0
  68. package/dist/inference/http-download.test.js +109 -0
  69. package/dist/inference/index.d.ts +5 -1
  70. package/dist/inference/index.js +2 -1
  71. package/dist/inference/llama-engine.d.ts +27 -0
  72. package/dist/inference/llama-engine.js +152 -0
  73. package/dist/inference/llama-engine.test.d.ts +1 -0
  74. package/dist/inference/llama-engine.test.js +51 -0
  75. package/dist/inference/llama-process.d.ts +72 -0
  76. package/dist/inference/llama-process.js +126 -0
  77. package/dist/inference/llama-process.test.d.ts +1 -0
  78. package/dist/inference/llama-process.test.js +61 -0
  79. package/dist/inference/model-download.test.d.ts +1 -0
  80. package/dist/inference/model-download.test.js +73 -0
  81. package/dist/inference/model-manager.d.ts +14 -1
  82. package/dist/inference/model-manager.js +65 -80
  83. package/dist/inference/sidecar-provider.d.ts +19 -10
  84. package/dist/inference/sidecar-provider.js +115 -343
  85. package/dist/templates/universal-config.js +0 -2
  86. package/dist/types/index.d.ts +58 -14
  87. package/dist/types/index.js +9 -2
  88. package/dist/utils/deep-status.d.ts +7 -0
  89. package/dist/utils/deep-status.js +11 -0
  90. package/dist/utils/diff.d.ts +3 -0
  91. package/dist/utils/diff.js +52 -0
  92. package/dist/utils/diff.test.d.ts +1 -0
  93. package/dist/utils/diff.test.js +38 -0
  94. package/dist/utils/scanner.js +13 -2
  95. package/dist/utils/scanner.test.js +22 -8
  96. package/dist/utils/scope.d.ts +15 -0
  97. package/dist/utils/scope.js +51 -0
  98. package/dist/utils/scope.test.d.ts +1 -0
  99. package/dist/utils/scope.test.js +38 -0
  100. package/package.json +6 -6
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Streaming HTTPS download for large binaries (models, engine archives).
3
+ *
4
+ * Uses node:https rather than global fetch: on large Hugging Face downloads
5
+ * undici's fetch measured ~1.7 MB/s where node:https reached ~17 MB/s on the
6
+ * same host and byte range, which made the first-run model download exceed
7
+ * any reasonable setup window.
8
+ *
9
+ * - follows at most MAX_REDIRECTS redirects, https only
10
+ * - resumes a kept partial file with a Range request (the prefix is re-hashed
11
+ * so the SHA-256 still covers the whole file)
12
+ * - aborts when no bytes arrive for `stallTimeoutMs` instead of imposing a
13
+ * wall-clock limit that a slow but healthy connection cannot meet
14
+ */
15
+ import https from 'https';
16
+ import fs from 'fs-extra';
17
+ import { createHash } from 'crypto';
18
+ import { Transform } from 'stream';
19
+ import { pipeline } from 'stream/promises';
20
+ const MAX_REDIRECTS = 5;
21
+ const DEFAULT_STALL_TIMEOUT_MS = 30_000;
22
+ export async function downloadToFile(url, destPath, options = {}) {
23
+ const stallMs = options.stallTimeoutMs ?? DEFAULT_STALL_TIMEOUT_MS;
24
+ const get = options.get ?? httpsGet;
25
+ let existing = options.resume === false ? 0 : await partialSize(destPath);
26
+ let { response, etag } = await followRedirects(url, rangeFrom(existing), get, stallMs);
27
+ if (response.statusCode === 416 && existing > 0) {
28
+ // The partial is already at or past the end: start over once, from zero.
29
+ response.body.resume();
30
+ await fs.remove(destPath);
31
+ existing = 0;
32
+ ({ response, etag } = await followRedirects(url, rangeFrom(0), get, stallMs));
33
+ }
34
+ if (response.statusCode < 200 || response.statusCode >= 300) {
35
+ response.body.resume();
36
+ throw new Error(`HTTP ${response.statusCode} for ${url}`);
37
+ }
38
+ const resumed = response.statusCode === 206 && existing > 0;
39
+ const hash = createHash('sha256');
40
+ if (resumed)
41
+ await hashFilePrefix(destPath, hash);
42
+ const startBytes = resumed ? existing : 0;
43
+ const total = contentLength(response.headers, startBytes);
44
+ const bytes = await writeBody(response.body, destPath, hash, {
45
+ append: resumed, startBytes, total, stallMs, onProgress: options.onProgress,
46
+ });
47
+ return { sha256: hash.digest('hex'), bytes, etag };
48
+ }
49
+ function rangeFrom(offset) {
50
+ return offset > 0 ? { Range: `bytes=${offset}-` } : {};
51
+ }
52
+ async function partialSize(filePath) {
53
+ try {
54
+ return (await fs.stat(filePath)).size;
55
+ }
56
+ catch {
57
+ return 0;
58
+ }
59
+ }
60
+ async function followRedirects(url, headers, get, timeoutMs) {
61
+ let current = url;
62
+ let etag = null;
63
+ for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
64
+ if (new URL(current).protocol !== 'https:') {
65
+ throw new Error(`Refusing non-https download URL: ${current}`);
66
+ }
67
+ const response = await get(current, headers, timeoutMs);
68
+ etag = etag ?? headerValue(response.headers, 'x-linked-etag') ?? headerValue(response.headers, 'etag');
69
+ const location = headerValue(response.headers, 'location');
70
+ if (response.statusCode >= 300 && response.statusCode < 400 && location) {
71
+ response.body.resume();
72
+ current = new URL(location, current).toString();
73
+ continue;
74
+ }
75
+ return { response, etag };
76
+ }
77
+ throw new Error(`Too many redirects downloading ${url}`);
78
+ }
79
+ async function writeBody(body, destPath, hash, opts) {
80
+ let downloaded = opts.startBytes;
81
+ let stallTimer;
82
+ const armStall = () => {
83
+ clearTimeout(stallTimer);
84
+ stallTimer = setTimeout(() => {
85
+ body.destroy(new Error(`Download stalled: no data for ${opts.stallMs / 1000}s`));
86
+ }, opts.stallMs);
87
+ };
88
+ const tap = new Transform({
89
+ transform(chunk, _encoding, callback) {
90
+ armStall();
91
+ hash.update(chunk);
92
+ downloaded += chunk.length;
93
+ opts.onProgress?.(downloaded, opts.total);
94
+ callback(null, chunk);
95
+ },
96
+ });
97
+ armStall();
98
+ try {
99
+ await pipeline(body, tap, fs.createWriteStream(destPath, { flags: opts.append ? 'a' : 'w' }));
100
+ }
101
+ finally {
102
+ clearTimeout(stallTimer);
103
+ }
104
+ return downloaded;
105
+ }
106
+ async function hashFilePrefix(filePath, hash) {
107
+ const stream = fs.createReadStream(filePath);
108
+ try {
109
+ for await (const chunk of stream)
110
+ hash.update(chunk);
111
+ }
112
+ finally {
113
+ stream.destroy();
114
+ }
115
+ }
116
+ function contentLength(headers, offset) {
117
+ const value = Number(headerValue(headers, 'content-length'));
118
+ return Number.isFinite(value) && value > 0 ? value + offset : null;
119
+ }
120
+ function headerValue(headers, name) {
121
+ const value = headers[name];
122
+ if (Array.isArray(value))
123
+ return value[0] ?? null;
124
+ return value ?? null;
125
+ }
126
+ const httpsGet = (url, headers, timeoutMs) => new Promise((resolve, reject) => {
127
+ const request = https.get(url, { headers: { 'user-agent': 'rigour-cli', ...headers } }, (res) => {
128
+ resolve({ statusCode: res.statusCode ?? 0, headers: res.headers, body: res });
129
+ });
130
+ request.setTimeout(timeoutMs, () => request.destroy(new Error(`No response from ${new URL(url).host} after ${timeoutMs / 1000}s`)));
131
+ request.on('error', reject);
132
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,109 @@
1
+ import fs from 'fs-extra';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { createHash } from 'crypto';
5
+ import { Readable, PassThrough } from 'stream';
6
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
7
+ import { downloadToFile } from './http-download.js';
8
+ const PAYLOAD = Buffer.from('0123456789abcdefghijklmnopqrstuvwxyz');
9
+ const sha = (b) => createHash('sha256').update(b).digest('hex');
10
+ function response(statusCode, body = Buffer.alloc(0), headers = {}) {
11
+ return { statusCode, headers, body: Buffer.isBuffer(body) ? Readable.from([body]) : body };
12
+ }
13
+ /** Serves PAYLOAD, honouring Range unless told not to. */
14
+ function server(opts = {}) {
15
+ const calls = [];
16
+ const get = async (url, headers) => {
17
+ calls.push({ url, headers });
18
+ const range = headers.Range?.match(/bytes=(\d+)-/);
19
+ if (range && opts.honourRange !== false) {
20
+ const start = Number(range[1]);
21
+ if (start >= PAYLOAD.length)
22
+ return response(416);
23
+ return response(206, PAYLOAD.subarray(start), { 'content-length': String(PAYLOAD.length - start) });
24
+ }
25
+ return response(200, PAYLOAD, { 'content-length': String(PAYLOAD.length), etag: opts.etag });
26
+ };
27
+ return { get, calls };
28
+ }
29
+ describe('downloadToFile', () => {
30
+ let dir;
31
+ let dest;
32
+ beforeEach(async () => {
33
+ dir = await fs.mkdtemp(path.join(os.tmpdir(), 'rigour-dl-'));
34
+ dest = path.join(dir, 'model.gguf.download');
35
+ });
36
+ afterEach(async () => { await fs.remove(dir); });
37
+ it('downloads the full body and hashes it', async () => {
38
+ const { get } = server({ etag: '"abc"' });
39
+ const result = await downloadToFile('https://host/model', dest, { get });
40
+ expect(await fs.readFile(dest)).toEqual(PAYLOAD);
41
+ expect(result).toEqual({ sha256: sha(PAYLOAD), bytes: PAYLOAD.length, etag: '"abc"' });
42
+ });
43
+ it('follows redirects and keeps the x-linked-etag from the first hop', async () => {
44
+ const { get: origin, calls } = server();
45
+ const get = async (url, headers, timeout) => {
46
+ if (url === 'https://hub/model') {
47
+ return response(302, Buffer.alloc(0), { location: 'https://cdn/model', 'x-linked-etag': '"lfs-sha"' });
48
+ }
49
+ return origin(url, headers, timeout);
50
+ };
51
+ const result = await downloadToFile('https://hub/model', dest, { get });
52
+ expect(calls.map(c => c.url)).toEqual(['https://cdn/model']);
53
+ expect(result.etag).toBe('"lfs-sha"');
54
+ });
55
+ it('resumes a partial file with a Range request and hashes the whole file', async () => {
56
+ await fs.writeFile(dest, PAYLOAD.subarray(0, 10));
57
+ const { get, calls } = server();
58
+ const result = await downloadToFile('https://host/model', dest, { get });
59
+ expect(calls[0].headers.Range).toBe('bytes=10-');
60
+ expect(await fs.readFile(dest)).toEqual(PAYLOAD);
61
+ expect(result.sha256).toBe(sha(PAYLOAD));
62
+ expect(result.bytes).toBe(PAYLOAD.length);
63
+ });
64
+ it('restarts from zero when the server ignores Range', async () => {
65
+ await fs.writeFile(dest, Buffer.from('stale-bytes'));
66
+ const { get } = server({ honourRange: false });
67
+ const result = await downloadToFile('https://host/model', dest, { get });
68
+ expect(await fs.readFile(dest)).toEqual(PAYLOAD);
69
+ expect(result.sha256).toBe(sha(PAYLOAD));
70
+ });
71
+ it('restarts when the partial is already past the end (416)', async () => {
72
+ await fs.writeFile(dest, Buffer.concat([PAYLOAD, Buffer.from('extra')]));
73
+ const { get, calls } = server();
74
+ await downloadToFile('https://host/model', dest, { get });
75
+ expect(calls).toHaveLength(2);
76
+ expect(await fs.readFile(dest)).toEqual(PAYLOAD);
77
+ });
78
+ it('rejects non-success responses without creating the file', async () => {
79
+ const get = async () => response(401);
80
+ await expect(downloadToFile('https://host/model', dest, { get })).rejects.toThrow('HTTP 401');
81
+ expect(await fs.pathExists(dest)).toBe(false);
82
+ });
83
+ it('refuses non-https URLs, including redirects to them', async () => {
84
+ const { get } = server();
85
+ await expect(downloadToFile('http://host/model', dest, { get })).rejects.toThrow('non-https');
86
+ const redirecting = async () => response(302, Buffer.alloc(0), { location: 'http://evil/model' });
87
+ await expect(downloadToFile('https://host/model', dest, { get: redirecting })).rejects.toThrow('non-https');
88
+ });
89
+ it('stops after too many redirects', async () => {
90
+ const loop = async (url) => response(302, Buffer.alloc(0), { location: `${url}x` });
91
+ await expect(downloadToFile('https://host/m', dest, { get: loop })).rejects.toThrow('Too many redirects');
92
+ });
93
+ it('aborts a stalled body and keeps the partial for the next resume', async () => {
94
+ const body = new PassThrough();
95
+ body.write(PAYLOAD.subarray(0, 5));
96
+ const get = async () => response(200, body, { 'content-length': String(PAYLOAD.length) });
97
+ await expect(downloadToFile('https://host/model', dest, { get, stallTimeoutMs: 50 })).rejects.toThrow('stalled');
98
+ // The partial file is kept for the next resume (how many bytes reached disk is up to the OS).
99
+ expect(await fs.pathExists(dest)).toBe(true);
100
+ expect((await fs.stat(dest)).size).toBeLessThanOrEqual(5);
101
+ });
102
+ it('reports progress against the full size when resuming', async () => {
103
+ await fs.writeFile(dest, PAYLOAD.subarray(0, 10));
104
+ const { get } = server();
105
+ const seen = [];
106
+ await downloadToFile('https://host/model', dest, { get, onProgress: (d, t) => seen.push([d, t]) });
107
+ expect(seen.at(-1)).toEqual([PAYLOAD.length, PAYLOAD.length]);
108
+ });
109
+ });
@@ -4,8 +4,12 @@
4
4
  export type { InferenceProvider, InferenceOptions, DeepFinding, DeepAnalysisResult, ModelTier, ModelInfo } from './types.js';
5
5
  export { MODELS } from './types.js';
6
6
  export { SidecarProvider } from './sidecar-provider.js';
7
+ export type { ActiveModel } from './sidecar-provider.js';
8
+ export { installLlamaEngine, probeBinary, managedEnginePath, LLAMA_RELEASE_TAG } from './llama-engine.js';
9
+ export type { ProbeResult } from './llama-engine.js';
7
10
  export { CloudProvider } from './cloud-provider.js';
8
- export { ensureModel, isModelCached, getModelPath, getModelInfo, downloadModel, getModelsDir } from './model-manager.js';
11
+ export { ensureModel, isModelCached, getModelPath, getModelInfo, downloadModel, getModelsDir, getCachedModel } from './model-manager.js';
12
+ export type { CachedModel } from './model-manager.js';
9
13
  import type { InferenceProvider } from './types.js';
10
14
  import type { DeepOptions } from '../types/index.js';
11
15
  /**
@@ -1,7 +1,8 @@
1
1
  export { MODELS } from './types.js';
2
2
  export { SidecarProvider } from './sidecar-provider.js';
3
+ export { installLlamaEngine, probeBinary, managedEnginePath, LLAMA_RELEASE_TAG } from './llama-engine.js';
3
4
  export { CloudProvider } from './cloud-provider.js';
4
- export { ensureModel, isModelCached, getModelPath, getModelInfo, downloadModel, getModelsDir } from './model-manager.js';
5
+ export { ensureModel, isModelCached, getModelPath, getModelInfo, downloadModel, getModelsDir, getCachedModel } from './model-manager.js';
5
6
  import { SidecarProvider } from './sidecar-provider.js';
6
7
  import { CloudProvider } from './cloud-provider.js';
7
8
  /**
@@ -0,0 +1,27 @@
1
+ import { type HttpGet } from './http-download.js';
2
+ export declare const LLAMA_RELEASE_TAG = "b5604";
3
+ export declare function llamaBinaryName(): string;
4
+ /** Install directory for the pinned release: ~/.rigour/bin/llama-<tag>. */
5
+ export declare function managedEngineDir(): string;
6
+ export declare function managedEnginePath(): string;
7
+ export declare function releaseAssetFor(platformKey: string): string | undefined;
8
+ export interface ProbeResult {
9
+ ok: boolean;
10
+ version?: string;
11
+ error?: string;
12
+ }
13
+ /**
14
+ * Run `<binary> --version` and accept it only if it exits 0. A placeholder
15
+ * script (such as an unbundled brain package) or a binary missing its shared
16
+ * libraries fails here instead of on every inference call.
17
+ */
18
+ export declare function probeBinary(binaryPath: string): Promise<ProbeResult>;
19
+ /** Forget cached probe results, e.g. after installing a new engine. */
20
+ export declare function resetProbeCache(): void;
21
+ /**
22
+ * Download the pinned release for this platform and install its whole bin
23
+ * folder into managedEngineDir(). Returns the verified binary path.
24
+ */
25
+ export declare function installLlamaEngine(onProgress?: (message: string) => void, get?: HttpGet): Promise<string>;
26
+ /** Copy the folder that contains llama-cli (binary + shared libraries) into place. */
27
+ export declare function installBinFolder(extractedRoot: string, installDir: string): Promise<void>;
@@ -0,0 +1,152 @@
1
+ /**
2
+ * llama.cpp engine: install from the pinned GitHub release and verify that a
3
+ * candidate binary actually runs.
4
+ *
5
+ * The release archives link llama-cli against shared libraries next to it
6
+ * (@rpath → @loader_path on macOS), so the whole bin folder is installed; a
7
+ * lone llama-cli fails with "Library not loaded: @rpath/libllama.dylib".
8
+ */
9
+ import os from 'os';
10
+ import path from 'path';
11
+ import fs from 'fs-extra';
12
+ import { RIGOUR_DIR } from '../storage/db.js';
13
+ import { downloadToFile } from './http-download.js';
14
+ import { runProcess } from './llama-process.js';
15
+ export const LLAMA_RELEASE_TAG = 'b5604';
16
+ const LLAMA_RELEASE_BASE = `https://github.com/ggml-org/llama.cpp/releases/download/${LLAMA_RELEASE_TAG}`;
17
+ const PROBE_TIMEOUT_MS = 5_000;
18
+ const EXTRACT_TIMEOUT_MS = 120_000;
19
+ /** Platform → llama.cpp release asset. linux-arm64 has no published build. */
20
+ const LLAMA_RELEASE_ASSETS = {
21
+ 'darwin-arm64': `llama-${LLAMA_RELEASE_TAG}-bin-macos-arm64.zip`,
22
+ 'darwin-x64': `llama-${LLAMA_RELEASE_TAG}-bin-macos-x64.zip`,
23
+ 'linux-x64': `llama-${LLAMA_RELEASE_TAG}-bin-ubuntu-x64.zip`,
24
+ 'win32-x64': `llama-${LLAMA_RELEASE_TAG}-bin-win-cpu-x64.zip`,
25
+ };
26
+ export function llamaBinaryName() {
27
+ return os.platform() === 'win32' ? 'llama-cli.exe' : 'llama-cli';
28
+ }
29
+ /** Install directory for the pinned release: ~/.rigour/bin/llama-<tag>. */
30
+ export function managedEngineDir() {
31
+ return path.join(RIGOUR_DIR, 'bin', `llama-${LLAMA_RELEASE_TAG}`);
32
+ }
33
+ export function managedEnginePath() {
34
+ return path.join(managedEngineDir(), llamaBinaryName());
35
+ }
36
+ export function releaseAssetFor(platformKey) {
37
+ return LLAMA_RELEASE_ASSETS[platformKey];
38
+ }
39
+ const probeCache = new Map();
40
+ /**
41
+ * Run `<binary> --version` and accept it only if it exits 0. A placeholder
42
+ * script (such as an unbundled brain package) or a binary missing its shared
43
+ * libraries fails here instead of on every inference call.
44
+ */
45
+ export function probeBinary(binaryPath) {
46
+ let cached = probeCache.get(binaryPath);
47
+ if (!cached) {
48
+ cached = runProbe(binaryPath);
49
+ probeCache.set(binaryPath, cached);
50
+ }
51
+ return cached;
52
+ }
53
+ /** Forget cached probe results, e.g. after installing a new engine. */
54
+ export function resetProbeCache() {
55
+ probeCache.clear();
56
+ }
57
+ async function runProbe(binaryPath) {
58
+ if (!(await fs.pathExists(binaryPath)))
59
+ return { ok: false, error: 'not found' };
60
+ try {
61
+ const { code, stdout, stderr } = await runProcess(binaryPath, ['--version'], { timeoutMs: PROBE_TIMEOUT_MS });
62
+ const output = `${stdout}\n${stderr}`;
63
+ if (code !== 0)
64
+ return { ok: false, error: lastLine(output) || `exit code ${code}` };
65
+ const version = output.match(/version:\s*([^\s]+)/)?.[1];
66
+ return { ok: true, version };
67
+ }
68
+ catch (error) {
69
+ return { ok: false, error: error?.message ?? String(error) };
70
+ }
71
+ }
72
+ function lastLine(text) {
73
+ return text.trim().split(/\r?\n/).pop()?.trim() ?? '';
74
+ }
75
+ /**
76
+ * Download the pinned release for this platform and install its whole bin
77
+ * folder into managedEngineDir(). Returns the verified binary path.
78
+ */
79
+ export async function installLlamaEngine(onProgress, get) {
80
+ const platformKey = `${os.platform()}-${os.arch()}`;
81
+ const asset = releaseAssetFor(platformKey);
82
+ if (!asset) {
83
+ throw new Error(`No llama.cpp ${LLAMA_RELEASE_TAG} build is published for ${platformKey}. Install llama-cli on PATH or use a cloud provider.`);
84
+ }
85
+ const installDir = managedEngineDir();
86
+ const workDir = `${installDir}.partial`;
87
+ const zipPath = path.join(path.dirname(installDir), asset);
88
+ await fs.ensureDir(path.dirname(installDir));
89
+ await fs.remove(workDir);
90
+ onProgress?.(`⬇ Downloading inference engine (llama.cpp ${LLAMA_RELEASE_TAG})...`);
91
+ try {
92
+ await downloadToFile(`${LLAMA_RELEASE_BASE}/${asset}`, zipPath, { get });
93
+ onProgress?.(' Extracting...');
94
+ await extractZip(zipPath, workDir);
95
+ await installBinFolder(workDir, installDir);
96
+ }
97
+ finally {
98
+ await fs.remove(zipPath).catch(() => undefined);
99
+ await fs.remove(workDir).catch(() => undefined);
100
+ }
101
+ resetProbeCache();
102
+ const binary = managedEnginePath();
103
+ const probe = await probeBinary(binary);
104
+ if (!probe.ok) {
105
+ throw new Error(`Installed llama-cli does not run: ${probe.error}`);
106
+ }
107
+ onProgress?.(`✓ Inference engine ready (llama.cpp ${probe.version ?? LLAMA_RELEASE_TAG})`);
108
+ return binary;
109
+ }
110
+ async function extractZip(zipPath, destDir) {
111
+ await fs.ensureDir(destDir);
112
+ const result = os.platform() === 'win32'
113
+ ? await runProcess('powershell', ['-NoProfile', '-Command', `Expand-Archive -LiteralPath ${psQuote(zipPath)} -DestinationPath ${psQuote(destDir)} -Force`], { timeoutMs: EXTRACT_TIMEOUT_MS })
114
+ : await runProcess('unzip', ['-q', '-o', zipPath, '-d', destDir], { timeoutMs: EXTRACT_TIMEOUT_MS });
115
+ if (result.code !== 0) {
116
+ throw new Error(`Could not extract ${path.basename(zipPath)}: ${lastLine(result.stderr) || `exit code ${result.code}`}`);
117
+ }
118
+ }
119
+ /** PowerShell single-quoted literal: a quote inside is escaped by doubling it. */
120
+ function psQuote(value) {
121
+ return `'${value.replace(/'/g, "''")}'`;
122
+ }
123
+ /** Copy the folder that contains llama-cli (binary + shared libraries) into place. */
124
+ export async function installBinFolder(extractedRoot, installDir) {
125
+ const binary = await findFileRecursive(extractedRoot, llamaBinaryName());
126
+ if (!binary)
127
+ throw new Error(`${llamaBinaryName()} not found in the llama.cpp release archive`);
128
+ await fs.remove(installDir);
129
+ await fs.copy(path.dirname(binary), installDir);
130
+ if (os.platform() !== 'win32') {
131
+ await fs.chmod(path.join(installDir, llamaBinaryName()), 0o755);
132
+ }
133
+ }
134
+ /** Release archives nest the binary at most a few folders deep (build/bin/). */
135
+ const MAX_ARCHIVE_DEPTH = 6;
136
+ async function findFileRecursive(dir, filename, depth = 0) {
137
+ if (depth > MAX_ARCHIVE_DEPTH)
138
+ return null;
139
+ const entries = await fs.readdir(dir, { withFileTypes: true });
140
+ for (const entry of entries) {
141
+ const fullPath = path.join(dir, entry.name);
142
+ if (entry.isDirectory()) {
143
+ const found = await findFileRecursive(fullPath, filename, depth + 1);
144
+ if (found)
145
+ return found;
146
+ }
147
+ else if (entry.name === filename) {
148
+ return fullPath;
149
+ }
150
+ }
151
+ return null;
152
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,51 @@
1
+ import fs from 'fs-extra';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
+ import { installBinFolder, llamaBinaryName, probeBinary, resetProbeCache } from './llama-engine.js';
6
+ const isWindows = process.platform === 'win32';
7
+ async function writeScript(file, body) {
8
+ await fs.outputFile(file, `#!/bin/sh\n${body}\n`);
9
+ await fs.chmod(file, 0o755);
10
+ }
11
+ describe.skipIf(isWindows)('probeBinary', () => {
12
+ let dir;
13
+ beforeEach(async () => {
14
+ dir = await fs.mkdtemp(path.join(os.tmpdir(), 'rigour-engine-'));
15
+ resetProbeCache();
16
+ });
17
+ afterEach(async () => { await fs.remove(dir); });
18
+ it('accepts a binary that reports its version', async () => {
19
+ const bin = path.join(dir, 'llama-cli');
20
+ await writeScript(bin, 'echo "version: 5604 (228f34c9)" >&2; exit 0');
21
+ expect(await probeBinary(bin)).toEqual({ ok: true, version: '5604' });
22
+ });
23
+ it('rejects the placeholder brain script', async () => {
24
+ const bin = path.join(dir, 'rigour-brain');
25
+ await writeScript(bin, 'echo "rigour-brain is a placeholder." >&2; exit 1');
26
+ const result = await probeBinary(bin);
27
+ expect(result.ok).toBe(false);
28
+ expect(result.error).toContain('placeholder');
29
+ });
30
+ it('rejects a missing binary', async () => {
31
+ expect((await probeBinary(path.join(dir, 'nope'))).ok).toBe(false);
32
+ });
33
+ });
34
+ describe('installBinFolder', () => {
35
+ let dir;
36
+ beforeEach(async () => { dir = await fs.mkdtemp(path.join(os.tmpdir(), 'rigour-engine-')); });
37
+ afterEach(async () => { await fs.remove(dir); });
38
+ it('installs the binary together with the shared libraries next to it', async () => {
39
+ const extracted = path.join(dir, 'extract', 'build', 'bin');
40
+ await fs.outputFile(path.join(extracted, llamaBinaryName()), 'bin');
41
+ await fs.outputFile(path.join(extracted, 'libllama.dylib'), 'lib');
42
+ await fs.outputFile(path.join(extracted, 'ggml-metal.metal'), 'metal');
43
+ const installDir = path.join(dir, 'llama-b5604');
44
+ await installBinFolder(path.join(dir, 'extract'), installDir);
45
+ expect((await fs.readdir(installDir)).sort()).toEqual(['ggml-metal.metal', 'libllama.dylib', llamaBinaryName()].sort());
46
+ });
47
+ it('fails clearly when the archive has no llama-cli', async () => {
48
+ await fs.outputFile(path.join(dir, 'extract', 'README.md'), 'x');
49
+ await expect(installBinFolder(path.join(dir, 'extract'), path.join(dir, 'out'))).rejects.toThrow('not found');
50
+ });
51
+ });
@@ -0,0 +1,72 @@
1
+ export interface ProcessResult {
2
+ code: number | null;
3
+ stdout: string;
4
+ stderr: string;
5
+ }
6
+ export interface RunProcessOptions {
7
+ timeoutMs: number;
8
+ env?: NodeJS.ProcessEnv;
9
+ }
10
+ export declare class ProcessTimeoutError extends Error {
11
+ constructor(timeoutMs: number);
12
+ }
13
+ /**
14
+ * Run a binary with stdin closed, collecting stdout/stderr. Rejects on spawn
15
+ * errors and timeouts; a non-zero exit resolves with its code.
16
+ */
17
+ export declare function runProcess(command: string, args: string[], options: RunProcessOptions): Promise<ProcessResult>;
18
+ export interface LlamaArgsInput {
19
+ modelPath: string;
20
+ prompt: string;
21
+ maxTokens: number;
22
+ threads: number;
23
+ temperature: number;
24
+ schemaPath?: string;
25
+ }
26
+ export declare function buildLlamaArgs(input: LlamaArgsInput): string[];
27
+ /**
28
+ * Context window large enough for prompt + output, rounded up to 1024 and
29
+ * clamped to what Qwen2.5-Coder was trained on. The b5604 default is 4096,
30
+ * which silently truncates code-bearing prompts.
31
+ */
32
+ export declare function contextSizeFor(prompt: string, maxTokens: number): number;
33
+ /** Remove llama-cli's end-of-generation marker from stdout. */
34
+ export declare function cleanLlamaOutput(stdout: string): string;
35
+ /** JSON schema matching DeepFinding, used to constrain local generation. */
36
+ export declare const FINDINGS_JSON_SCHEMA: {
37
+ readonly type: "object";
38
+ readonly properties: {
39
+ readonly findings: {
40
+ readonly type: "array";
41
+ readonly items: {
42
+ readonly type: "object";
43
+ readonly properties: {
44
+ readonly category: {
45
+ readonly type: "string";
46
+ };
47
+ readonly severity: {
48
+ readonly type: "string";
49
+ readonly enum: readonly ["critical", "high", "medium", "low", "info"];
50
+ };
51
+ readonly file: {
52
+ readonly type: "string";
53
+ };
54
+ readonly line: {
55
+ readonly type: readonly ["integer", "null"];
56
+ };
57
+ readonly description: {
58
+ readonly type: "string";
59
+ };
60
+ readonly suggestion: {
61
+ readonly type: "string";
62
+ };
63
+ readonly confidence: {
64
+ readonly type: "number";
65
+ };
66
+ };
67
+ readonly required: readonly ["category", "severity", "file", "description", "suggestion", "confidence"];
68
+ };
69
+ };
70
+ };
71
+ readonly required: readonly ["findings"];
72
+ };