@manishbht/helpcode 0.2.3 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/helpcode.js +0 -0
- package/dist/src/commands/ask.js +15 -1
- package/dist/src/commands/ask.js.map +1 -1
- package/dist/src/commands/cockpit.d.ts +13 -0
- package/dist/src/commands/cockpit.js +33 -0
- package/dist/src/commands/cockpit.js.map +1 -0
- package/dist/src/commands/plan.d.ts +14 -0
- package/dist/src/commands/plan.js +117 -0
- package/dist/src/commands/plan.js.map +1 -0
- package/dist/src/commands/run.js +55 -4
- package/dist/src/commands/run.js.map +1 -1
- package/dist/src/core/cockpit.d.ts +27 -0
- package/dist/src/core/cockpit.js +109 -0
- package/dist/src/core/cockpit.js.map +1 -0
- package/dist/src/core/cockpitHtml.d.ts +52 -0
- package/dist/src/core/cockpitHtml.js +395 -0
- package/dist/src/core/cockpitHtml.js.map +1 -0
- package/dist/src/core/consent.d.ts +20 -0
- package/dist/src/core/consent.js +34 -0
- package/dist/src/core/consent.js.map +1 -0
- package/dist/src/core/decompose.d.ts +45 -0
- package/dist/src/core/decompose.js +86 -0
- package/dist/src/core/decompose.js.map +1 -0
- package/dist/src/core/gemini.d.ts +41 -0
- package/dist/src/core/gemini.js +91 -0
- package/dist/src/core/gemini.js.map +1 -0
- package/dist/src/core/keys.d.ts +23 -0
- package/dist/src/core/keys.js +68 -0
- package/dist/src/core/keys.js.map +1 -0
- package/dist/src/core/llmSelector.d.ts +6 -0
- package/dist/src/core/llmSelector.js +22 -7
- package/dist/src/core/llmSelector.js.map +1 -1
- package/dist/src/core/selector.d.ts +9 -4
- package/dist/src/core/selector.js +88 -29
- package/dist/src/core/selector.js.map +1 -1
- package/dist/src/core/souschef.d.ts +63 -0
- package/dist/src/core/souschef.js +55 -0
- package/dist/src/core/souschef.js.map +1 -0
- package/dist/src/core/state.d.ts +13 -1
- package/dist/src/core/state.js +47 -5
- package/dist/src/core/state.js.map +1 -1
- package/dist/src/core/triage.d.ts +14 -1
- package/dist/src/core/triage.js +23 -5
- package/dist/src/core/triage.js.map +1 -1
- package/dist/src/index.d.ts +8 -7
- package/dist/src/index.js +21 -8
- package/dist/src/index.js.map +1 -1
- package/dist/src/types.d.ts +64 -1
- package/package.json +1 -1
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal client for the Google Gemini API (free tier), via REST.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the Ollama client's discipline:
|
|
5
|
+
* - Zero runtime dependencies: plain fetch, no SDK.
|
|
6
|
+
* - fetch is injectable so tests never hit the live API and CI passes offline.
|
|
7
|
+
* - Every failure becomes a GeminiError; callers fall back to a cheaper
|
|
8
|
+
* worker (local) or deterministic path. The remote path can NEVER break the
|
|
9
|
+
* core flow.
|
|
10
|
+
*
|
|
11
|
+
* PRIVACY: the free tier may use inputs/outputs to train Google's models. The
|
|
12
|
+
* sous-chef privacy gate (core/souschef.ts) ensures only non-code task input
|
|
13
|
+
* (decomposition) reaches here unless the user opts into allowRemoteCode. This
|
|
14
|
+
* client does not enforce that — it's enforced upstream at routing time.
|
|
15
|
+
*/
|
|
16
|
+
export declare class GeminiError extends Error {
|
|
17
|
+
/** True if the failure was a 429 RESOURCE_EXHAUSTED (free quota used up). */
|
|
18
|
+
readonly quotaExhausted: boolean;
|
|
19
|
+
constructor(message: string, quotaExhausted?: boolean);
|
|
20
|
+
}
|
|
21
|
+
type FetchImpl = (url: string, init?: RequestInit) => Promise<Response>;
|
|
22
|
+
export interface GeminiCallOptions {
|
|
23
|
+
fetchImpl?: FetchImpl;
|
|
24
|
+
}
|
|
25
|
+
export interface GeminiConfig {
|
|
26
|
+
apiKey: string;
|
|
27
|
+
/** Model id, e.g. "gemini-2.5-flash-lite". */
|
|
28
|
+
model: string;
|
|
29
|
+
timeoutMs: number;
|
|
30
|
+
}
|
|
31
|
+
/** Build the generateContent endpoint URL for a model + key. */
|
|
32
|
+
export declare function buildGeminiUrl(model: string, apiKey: string): string;
|
|
33
|
+
/** Extract concatenated text from a generateContent response body. */
|
|
34
|
+
export declare function parseGeminiResponse(body: any): string;
|
|
35
|
+
/**
|
|
36
|
+
* Generate text from Gemini. Returns the model's text on success; throws a
|
|
37
|
+
* GeminiError on any failure (with quotaExhausted set for 429s so the router
|
|
38
|
+
* can mark the provider throttled).
|
|
39
|
+
*/
|
|
40
|
+
export declare function geminiGenerate(prompt: string, config: GeminiConfig, opts?: GeminiCallOptions): Promise<string>;
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal client for the Google Gemini API (free tier), via REST.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the Ollama client's discipline:
|
|
5
|
+
* - Zero runtime dependencies: plain fetch, no SDK.
|
|
6
|
+
* - fetch is injectable so tests never hit the live API and CI passes offline.
|
|
7
|
+
* - Every failure becomes a GeminiError; callers fall back to a cheaper
|
|
8
|
+
* worker (local) or deterministic path. The remote path can NEVER break the
|
|
9
|
+
* core flow.
|
|
10
|
+
*
|
|
11
|
+
* PRIVACY: the free tier may use inputs/outputs to train Google's models. The
|
|
12
|
+
* sous-chef privacy gate (core/souschef.ts) ensures only non-code task input
|
|
13
|
+
* (decomposition) reaches here unless the user opts into allowRemoteCode. This
|
|
14
|
+
* client does not enforce that — it's enforced upstream at routing time.
|
|
15
|
+
*/
|
|
16
|
+
export class GeminiError extends Error {
|
|
17
|
+
/** True if the failure was a 429 RESOURCE_EXHAUSTED (free quota used up). */
|
|
18
|
+
quotaExhausted;
|
|
19
|
+
constructor(message, quotaExhausted = false) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = 'GeminiError';
|
|
22
|
+
this.quotaExhausted = quotaExhausted;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const DEFAULT_GENERATE_TIMEOUT = 20000;
|
|
26
|
+
const API_BASE = 'https://generativelanguage.googleapis.com/v1beta/models';
|
|
27
|
+
/** Build the generateContent endpoint URL for a model + key. */
|
|
28
|
+
export function buildGeminiUrl(model, apiKey) {
|
|
29
|
+
return `${API_BASE}/${model}:generateContent?key=${apiKey}`;
|
|
30
|
+
}
|
|
31
|
+
/** Extract concatenated text from a generateContent response body. */
|
|
32
|
+
export function parseGeminiResponse(body) {
|
|
33
|
+
const cand = body?.candidates?.[0];
|
|
34
|
+
const parts = cand?.content?.parts;
|
|
35
|
+
if (!Array.isArray(parts))
|
|
36
|
+
return '';
|
|
37
|
+
return parts.map((p) => (typeof p?.text === 'string' ? p.text : '')).join('');
|
|
38
|
+
}
|
|
39
|
+
function getFetch(opts) {
|
|
40
|
+
return opts?.fetchImpl ?? globalThis.fetch;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Generate text from Gemini. Returns the model's text on success; throws a
|
|
44
|
+
* GeminiError on any failure (with quotaExhausted set for 429s so the router
|
|
45
|
+
* can mark the provider throttled).
|
|
46
|
+
*/
|
|
47
|
+
export async function geminiGenerate(prompt, config, opts) {
|
|
48
|
+
const fetchImpl = getFetch(opts);
|
|
49
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_GENERATE_TIMEOUT;
|
|
50
|
+
const url = buildGeminiUrl(config.model, config.apiKey);
|
|
51
|
+
const requestBody = {
|
|
52
|
+
contents: [{ parts: [{ text: prompt }] }],
|
|
53
|
+
};
|
|
54
|
+
const controller = new AbortController();
|
|
55
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
56
|
+
let response;
|
|
57
|
+
try {
|
|
58
|
+
response = await fetchImpl(url, {
|
|
59
|
+
method: 'POST',
|
|
60
|
+
headers: { 'content-type': 'application/json' },
|
|
61
|
+
body: JSON.stringify(requestBody),
|
|
62
|
+
signal: controller.signal,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
catch (e) {
|
|
66
|
+
clearTimeout(timer);
|
|
67
|
+
const why = e?.name === 'AbortError' ? 'timed out' : e.message;
|
|
68
|
+
throw new GeminiError(`request failed: ${why}`);
|
|
69
|
+
}
|
|
70
|
+
clearTimeout(timer);
|
|
71
|
+
// Parse the body (may be an error envelope).
|
|
72
|
+
let body;
|
|
73
|
+
try {
|
|
74
|
+
body = await response.json();
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
throw new GeminiError(`bad response (status ${response.status}, non-JSON body)`);
|
|
78
|
+
}
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
const status = body?.error?.status ?? '';
|
|
81
|
+
const msg = body?.error?.message ?? `HTTP ${response.status}`;
|
|
82
|
+
const isQuota = response.status === 429 || status === 'RESOURCE_EXHAUSTED';
|
|
83
|
+
throw new GeminiError(`Gemini error: ${msg}`, isQuota);
|
|
84
|
+
}
|
|
85
|
+
const text = parseGeminiResponse(body).trim();
|
|
86
|
+
if (!text) {
|
|
87
|
+
throw new GeminiError('Gemini returned no usable text');
|
|
88
|
+
}
|
|
89
|
+
return text;
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=gemini.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gemini.js","sourceRoot":"","sources":["../../../src/core/gemini.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API key loading for remote sous-chefs (v0.3.2).
|
|
3
|
+
*
|
|
4
|
+
* Keys are secrets and must never touch project.json (which is committed).
|
|
5
|
+
* Resolution order:
|
|
6
|
+
* 1. GEMINI_API_KEY environment variable (precedence; nothing on disk)
|
|
7
|
+
* 2. .helpcode/keys.json { "gemini": "..." } (gitignored by init)
|
|
8
|
+
*
|
|
9
|
+
* Returns null if no key is configured — in which case the remote sous-chef
|
|
10
|
+
* simply doesn't exist and helpcode behaves exactly as v0.3.1 (local only).
|
|
11
|
+
* Never throws: a missing or malformed keys.json yields null, not an error.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Resolve the Gemini API key. `env` is injectable for testing (defaults to
|
|
15
|
+
* process.env). Returns the key string, or null if none is configured.
|
|
16
|
+
*/
|
|
17
|
+
export declare function loadGeminiKey(cwd?: string, env?: Record<string, string | undefined>): string | null;
|
|
18
|
+
/**
|
|
19
|
+
* Whether a keys.json exists but is NOT gitignored — a footgun worth warning
|
|
20
|
+
* about (the user could commit their key). Best-effort: checks for a literal
|
|
21
|
+
* keys.json or .helpcode/ entry in .gitignore.
|
|
22
|
+
*/
|
|
23
|
+
export declare function keysFileAtRiskOfCommit(cwd?: string): boolean;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API key loading for remote sous-chefs (v0.3.2).
|
|
3
|
+
*
|
|
4
|
+
* Keys are secrets and must never touch project.json (which is committed).
|
|
5
|
+
* Resolution order:
|
|
6
|
+
* 1. GEMINI_API_KEY environment variable (precedence; nothing on disk)
|
|
7
|
+
* 2. .helpcode/keys.json { "gemini": "..." } (gitignored by init)
|
|
8
|
+
*
|
|
9
|
+
* Returns null if no key is configured — in which case the remote sous-chef
|
|
10
|
+
* simply doesn't exist and helpcode behaves exactly as v0.3.1 (local only).
|
|
11
|
+
* Never throws: a missing or malformed keys.json yields null, not an error.
|
|
12
|
+
*/
|
|
13
|
+
import * as fs from 'fs';
|
|
14
|
+
import * as path from 'path';
|
|
15
|
+
const KEYS_FILE = path.join('.helpcode', 'keys.json');
|
|
16
|
+
/**
|
|
17
|
+
* Resolve the Gemini API key. `env` is injectable for testing (defaults to
|
|
18
|
+
* process.env). Returns the key string, or null if none is configured.
|
|
19
|
+
*/
|
|
20
|
+
export function loadGeminiKey(cwd = process.cwd(), env = process.env) {
|
|
21
|
+
// 1. Environment variable wins (and leaves nothing on disk).
|
|
22
|
+
const fromEnv = env.GEMINI_API_KEY;
|
|
23
|
+
if (typeof fromEnv === 'string' && fromEnv.trim().length > 0) {
|
|
24
|
+
return fromEnv.trim();
|
|
25
|
+
}
|
|
26
|
+
// 2. .helpcode/keys.json
|
|
27
|
+
const file = path.join(cwd, KEYS_FILE);
|
|
28
|
+
if (!fs.existsSync(file))
|
|
29
|
+
return null;
|
|
30
|
+
try {
|
|
31
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
32
|
+
const k = parsed?.gemini;
|
|
33
|
+
if (typeof k === 'string' && k.trim().length > 0)
|
|
34
|
+
return k.trim();
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// Malformed file: behave as if no key (never throw).
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Whether a keys.json exists but is NOT gitignored — a footgun worth warning
|
|
44
|
+
* about (the user could commit their key). Best-effort: checks for a literal
|
|
45
|
+
* keys.json or .helpcode/ entry in .gitignore.
|
|
46
|
+
*/
|
|
47
|
+
export function keysFileAtRiskOfCommit(cwd = process.cwd()) {
|
|
48
|
+
const keysPath = path.join(cwd, KEYS_FILE);
|
|
49
|
+
if (!fs.existsSync(keysPath))
|
|
50
|
+
return false;
|
|
51
|
+
const gitignorePath = path.join(cwd, '.gitignore');
|
|
52
|
+
if (!fs.existsSync(gitignorePath))
|
|
53
|
+
return true; // exists, nothing ignoring it
|
|
54
|
+
try {
|
|
55
|
+
const ignore = fs.readFileSync(gitignorePath, 'utf-8');
|
|
56
|
+
const lines = ignore.split(/\r?\n/).map(l => l.trim());
|
|
57
|
+
const covered = lines.some(l => l === '.helpcode/' ||
|
|
58
|
+
l === '.helpcode' ||
|
|
59
|
+
l === '.helpcode/keys.json' ||
|
|
60
|
+
l === 'keys.json' ||
|
|
61
|
+
l === '.helpcode/*');
|
|
62
|
+
return !covered;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=keys.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keys.js","sourceRoot":"","sources":["../../../src/core/keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAE7B,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;AAEtD;;;GAGG;AACH,MAAM,UAAU,aAAa,CAC3B,MAAc,OAAO,CAAC,GAAG,EAAE,EAC3B,MAA0C,OAAO,CAAC,GAAG;IAErD,6DAA6D;IAC7D,MAAM,OAAO,GAAG,GAAG,CAAC,cAAc,CAAC;IACnC,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC7D,OAAO,OAAO,CAAC,IAAI,EAAE,CAAC;IACxB,CAAC;IAED,yBAAyB;IACzB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;IACvC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACtC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;QAC1D,MAAM,CAAC,GAAG,MAAM,EAAE,MAAM,CAAC;QACzB,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;QAClE,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,qDAAqD;QACrD,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,sBAAsB,CAAC,MAAc,OAAO,CAAC,GAAG,EAAE;IAChE,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;IAC3C,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO,KAAK,CAAC;IAE3C,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;IACnD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,aAAa,CAAC;QAAE,OAAO,IAAI,CAAC,CAAC,8BAA8B;IAE9E,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,EAAE,CAAC,YAAY,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC;QACvD,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QACvD,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAC7B,CAAC,KAAK,YAAY;YAClB,CAAC,KAAK,WAAW;YACjB,CAAC,KAAK,qBAAqB;YAC3B,CAAC,KAAK,WAAW;YACjB,CAAC,KAAK,aAAa,CACpB,CAAC;QACF,OAAO,CAAC,OAAO,CAAC;IAClB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
|
|
@@ -56,6 +56,12 @@ export declare function parseSelectionResponse(response: string, candidates: Can
|
|
|
56
56
|
* "model ran but found nothing" (empty array) from "couldn't reach model"
|
|
57
57
|
* (throw) — both lead to fallback, but the caller may log them differently.
|
|
58
58
|
*/
|
|
59
|
+
/**
|
|
60
|
+
* Backend-agnostic file selection: builds the prompt, calls the provided
|
|
61
|
+
* generate function (Ollama or Gemini or any text model), parses + validates.
|
|
62
|
+
* The empty-file filter and hallucination guard apply regardless of backend.
|
|
63
|
+
*/
|
|
64
|
+
export declare function selectFilesWithGenerate(task: string, candidatePaths: string[], projectRoot: string, count: number, generateFn: (prompt: string) => Promise<string>): Promise<Selection[]>;
|
|
59
65
|
export declare function llmSelectFiles(task: string, candidatePaths: string[], projectRoot: string, opts: {
|
|
60
66
|
host: string;
|
|
61
67
|
model: string;
|
|
@@ -71,7 +71,8 @@ export function buildSelectionPrompt(task, candidates, count) {
|
|
|
71
71
|
lines.push(`- ${c.path}: ${c.signature}`);
|
|
72
72
|
}
|
|
73
73
|
lines.push('');
|
|
74
|
-
lines.push(`Return
|
|
74
|
+
lines.push(`Return UP TO ${count} files that are genuinely relevant to the task, most relevant first.`);
|
|
75
|
+
lines.push('Include FEWER than ' + count + ' if fewer are truly relevant — do not pad the list with files that are empty or unrelated.');
|
|
75
76
|
lines.push('Format each line exactly as:');
|
|
76
77
|
lines.push(' PATH | one-line reason');
|
|
77
78
|
lines.push('Only use paths from the candidate list above. No prose, no other text.');
|
|
@@ -118,17 +119,31 @@ export function parseSelectionResponse(response, candidates) {
|
|
|
118
119
|
* "model ran but found nothing" (empty array) from "couldn't reach model"
|
|
119
120
|
* (throw) — both lead to fallback, but the caller may log them differently.
|
|
120
121
|
*/
|
|
121
|
-
|
|
122
|
-
|
|
122
|
+
/**
|
|
123
|
+
* Backend-agnostic file selection: builds the prompt, calls the provided
|
|
124
|
+
* generate function (Ollama or Gemini or any text model), parses + validates.
|
|
125
|
+
* The empty-file filter and hallucination guard apply regardless of backend.
|
|
126
|
+
*/
|
|
127
|
+
export async function selectFilesWithGenerate(task, candidatePaths, projectRoot, count, generateFn) {
|
|
128
|
+
const meaningful = candidatePaths.filter(p => {
|
|
129
|
+
try {
|
|
130
|
+
return fs.readFileSync(p, 'utf-8').trim().length >= 10;
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
const candidates = meaningful.map(p => ({
|
|
123
137
|
path: path.relative(projectRoot, p),
|
|
124
138
|
signature: buildSignature(p),
|
|
125
139
|
}));
|
|
126
|
-
const prompt = buildSelectionPrompt(task, candidates,
|
|
127
|
-
const response = await
|
|
128
|
-
timeoutMs: opts.timeoutMs,
|
|
129
|
-
});
|
|
140
|
+
const prompt = buildSelectionPrompt(task, candidates, count);
|
|
141
|
+
const response = await generateFn(prompt);
|
|
130
142
|
return parseSelectionResponse(response, candidates);
|
|
131
143
|
}
|
|
144
|
+
export async function llmSelectFiles(task, candidatePaths, projectRoot, opts) {
|
|
145
|
+
return selectFilesWithGenerate(task, candidatePaths, projectRoot, opts.count, (prompt) => generate(opts.host, opts.model, prompt, { timeoutMs: opts.timeoutMs }));
|
|
146
|
+
}
|
|
132
147
|
// Re-export for callers that want to detect transport errors specifically.
|
|
133
148
|
export { OllamaError };
|
|
134
149
|
//# sourceMappingURL=llmSelector.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"llmSelector.js","sourceRoot":"","sources":["../../../src/core/llmSelector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAcpD,MAAM,mBAAmB,GAAG,CAAC,CAAC,CAAK,mCAAmC;AACtE,MAAM,4BAA4B,GAAG,CAAC,CAAC,CAAE,2BAA2B;AAEpE;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,QAAgB;IAC7C,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QACH,IAAI,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAC5C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,cAAc,CAAC;IACxB,CAAC;IACD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAElC,MAAM,MAAM,GAAG,0HAA0H,CAAC;IAC1I,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACtB,kEAAkE;YAClE,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACnE,IAAI,OAAO;gBAAE,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACjC,IAAI,KAAK,CAAC,MAAM,IAAI,mBAAmB;gBAAE,MAAM;QACjD,CAAC;IACH,CAAC;IAED,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrB,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;IAED,mDAAmD;IACnD,MAAM,UAAU,GAAG,KAAK;SACrB,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SAClB,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;SACtE,KAAK,CAAC,CAAC,EAAE,4BAA4B,CAAC,CAAC;IAC1C,OAAO,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,SAAS,CAAC;AAC3D,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAClC,IAAY,EACZ,UAAuB,EACvB,KAAa;IAEb,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,CAAC,IAAI,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC;IAC5B,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC;IAC/B,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;QAC3B,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC;IAC5C,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"llmSelector.js","sourceRoot":"","sources":["../../../src/core/llmSelector.ts"],"names":[],"mappings":"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"}
|
|
@@ -23,14 +23,19 @@ export interface SelectedFile {
|
|
|
23
23
|
export interface SelectionResult {
|
|
24
24
|
files: SelectedFile[];
|
|
25
25
|
/** Which strategy actually produced the result. */
|
|
26
|
-
strategy: 'llm' | 'heuristic';
|
|
26
|
+
strategy: 'llm' | 'remote' | 'heuristic';
|
|
27
27
|
/** If the LLM was attempted but fell back, why. Empty otherwise. */
|
|
28
28
|
fallbackReason: string;
|
|
29
|
+
/** For the remote strategy: the model used (for the cockpit). */
|
|
30
|
+
remoteModel?: string;
|
|
29
31
|
}
|
|
30
32
|
/**
|
|
31
|
-
* The entry point used by `ask`. Chooses
|
|
32
|
-
*
|
|
33
|
-
* to the
|
|
33
|
+
* The entry point used by `ask`. Chooses, cheapest-first:
|
|
34
|
+
* local LLM (Ollama) → remote free-tier (if opted in) → keyword heuristic.
|
|
35
|
+
* Never throws — any model failure degrades to the next option with a reason.
|
|
36
|
+
*
|
|
37
|
+
* The remote branch only runs when allowRemoteCode is on AND a key is present:
|
|
38
|
+
* file selection sends file signatures (code), so it's privacy-gated.
|
|
34
39
|
*/
|
|
35
40
|
export declare function selectFilesWithStrategy(taskDescription: string, config: ProjectConfig, opts?: {
|
|
36
41
|
forceHeuristic?: boolean;
|
|
@@ -14,8 +14,32 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import * as fs from 'fs';
|
|
16
16
|
import * as path from 'path';
|
|
17
|
-
import { isOllamaReachable
|
|
18
|
-
import { llmSelectFiles } from './llmSelector.js';
|
|
17
|
+
import { isOllamaReachable } from './ollama.js';
|
|
18
|
+
import { llmSelectFiles, selectFilesWithGenerate } from './llmSelector.js';
|
|
19
|
+
import { geminiGenerate } from './gemini.js';
|
|
20
|
+
import { loadGeminiKey } from './keys.js';
|
|
21
|
+
import { shouldShowRemoteCodeNotice, remoteCodeNoticeText } from './consent.js';
|
|
22
|
+
import { loadState, saveState } from './state.js';
|
|
23
|
+
const REMOTE_SELECTION_MODEL = 'gemini-2.5-flash-lite';
|
|
24
|
+
/**
|
|
25
|
+
* Show the one-time "code is leaving the machine to a free tier" notice if it
|
|
26
|
+
* hasn't been shown before, and persist that it has. Best-effort; never throws.
|
|
27
|
+
*/
|
|
28
|
+
function maybeShowRemoteCodeNotice(task, model) {
|
|
29
|
+
try {
|
|
30
|
+
const state = loadState();
|
|
31
|
+
const alreadyShown = state.flags?.remoteCodeNoticeShown === true;
|
|
32
|
+
if (shouldShowRemoteCodeNotice({ allowRemoteCode: true, task, alreadyShown })) {
|
|
33
|
+
// eslint-disable-next-line no-console
|
|
34
|
+
console.error('\n' + remoteCodeNoticeText(model) + '\n');
|
|
35
|
+
state.flags = { ...(state.flags ?? {}), remoteCodeNoticeShown: true };
|
|
36
|
+
saveState(state);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// never block selection on the notice
|
|
41
|
+
}
|
|
42
|
+
}
|
|
19
43
|
const IGNORE_DIRS = new Set([
|
|
20
44
|
'.git', '.venv', 'venv', 'env', 'node_modules', '__pycache__',
|
|
21
45
|
'.pytest_cache', '.mypy_cache', 'dist', 'build', '.next',
|
|
@@ -29,42 +53,77 @@ const MAX_RESULTS = 6;
|
|
|
29
53
|
const RECENCY_WINDOW_DAYS = 14;
|
|
30
54
|
const LLM_CANDIDATE_CAP = 60; // cap files sent to the model (design §3.1)
|
|
31
55
|
/**
|
|
32
|
-
* The entry point used by `ask`. Chooses
|
|
33
|
-
*
|
|
34
|
-
* to the
|
|
56
|
+
* The entry point used by `ask`. Chooses, cheapest-first:
|
|
57
|
+
* local LLM (Ollama) → remote free-tier (if opted in) → keyword heuristic.
|
|
58
|
+
* Never throws — any model failure degrades to the next option with a reason.
|
|
59
|
+
*
|
|
60
|
+
* The remote branch only runs when allowRemoteCode is on AND a key is present:
|
|
61
|
+
* file selection sends file signatures (code), so it's privacy-gated.
|
|
35
62
|
*/
|
|
36
63
|
export async function selectFilesWithStrategy(taskDescription, config, opts = {}) {
|
|
37
64
|
const ollama = config.ollama;
|
|
38
65
|
const wantLlm = !opts.forceHeuristic && ollama?.enabled === true;
|
|
39
66
|
if (wantLlm && ollama) {
|
|
40
67
|
const reachable = await isOllamaReachable(ollama.host, { timeoutMs: 1000 });
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
68
|
+
if (reachable) {
|
|
69
|
+
try {
|
|
70
|
+
const allFiles = walkAllSourceFiles(config).slice(0, LLM_CANDIDATE_CAP);
|
|
71
|
+
const selections = await llmSelectFiles(taskDescription, allFiles, config.root, {
|
|
72
|
+
host: ollama.host,
|
|
73
|
+
model: ollama.model,
|
|
74
|
+
count: MAX_RESULTS,
|
|
75
|
+
timeoutMs: ollama.timeoutMs,
|
|
76
|
+
});
|
|
77
|
+
if (selections.length > 0) {
|
|
78
|
+
const files = selections.map(s => ({
|
|
79
|
+
filepath: path.join(config.root, s.path),
|
|
80
|
+
reason: s.reason,
|
|
81
|
+
}));
|
|
82
|
+
return { files, strategy: 'llm', fallbackReason: '' };
|
|
83
|
+
}
|
|
84
|
+
// fall through to remote/heuristic
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// fall through to remote/heuristic
|
|
54
88
|
}
|
|
55
|
-
// Map relative paths back to absolute for the caller
|
|
56
|
-
const files = selections.map(s => ({
|
|
57
|
-
filepath: path.join(config.root, s.path),
|
|
58
|
-
reason: s.reason,
|
|
59
|
-
}));
|
|
60
|
-
return { files, strategy: 'llm', fallbackReason: '' };
|
|
61
|
-
}
|
|
62
|
-
catch (e) {
|
|
63
|
-
const why = e instanceof OllamaError ? e.message : e.message;
|
|
64
|
-
return heuristicResult(taskDescription, config, why);
|
|
65
89
|
}
|
|
66
90
|
}
|
|
67
|
-
|
|
91
|
+
// Remote branch: only if NOT forcing heuristic, the project opted into
|
|
92
|
+
// sending code remotely, and a key is configured. Selection sends code
|
|
93
|
+
// (file signatures), so the privacy gate requires allowRemoteCode.
|
|
94
|
+
if (!opts.forceHeuristic && config.remote?.allowRemoteCode === true) {
|
|
95
|
+
const remote = await tryRemoteSelection(taskDescription, config);
|
|
96
|
+
if (remote)
|
|
97
|
+
return remote;
|
|
98
|
+
}
|
|
99
|
+
return heuristicResult(taskDescription, config, wantLlm ? 'local model unavailable; used keyword heuristic' : '');
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Attempt remote file selection via Gemini. Returns null on any failure (caller
|
|
103
|
+
* falls back to heuristic). Shows the one-time code-consent notice on first use.
|
|
104
|
+
*/
|
|
105
|
+
async function tryRemoteSelection(taskDescription, config) {
|
|
106
|
+
const key = loadGeminiKey();
|
|
107
|
+
if (!key)
|
|
108
|
+
return null;
|
|
109
|
+
// One-time consent notice (code is about to leave the machine to a free tier).
|
|
110
|
+
maybeShowRemoteCodeNotice('file_selection', REMOTE_SELECTION_MODEL);
|
|
111
|
+
try {
|
|
112
|
+
const allFiles = walkAllSourceFiles(config).slice(0, LLM_CANDIDATE_CAP);
|
|
113
|
+
const selections = await selectFilesWithGenerate(taskDescription, allFiles, config.root, MAX_RESULTS, (prompt) => geminiGenerate(prompt, {
|
|
114
|
+
apiKey: key, model: REMOTE_SELECTION_MODEL, timeoutMs: 30000,
|
|
115
|
+
}));
|
|
116
|
+
if (selections.length === 0)
|
|
117
|
+
return null;
|
|
118
|
+
const files = selections.map(s => ({
|
|
119
|
+
filepath: path.join(config.root, s.path),
|
|
120
|
+
reason: s.reason,
|
|
121
|
+
}));
|
|
122
|
+
return { files, strategy: 'remote', fallbackReason: '', remoteModel: REMOTE_SELECTION_MODEL };
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
68
127
|
}
|
|
69
128
|
function heuristicResult(taskDescription, config, fallbackReason) {
|
|
70
129
|
const files = selectFiles(taskDescription, config).map(filepath => ({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"selector.js","sourceRoot":"","sources":["../../../src/core/selector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAE7B,OAAO,EAAE,iBAAiB,EAAE,
|
|
1
|
+
{"version":3,"file":"selector.js","sourceRoot":"","sources":["../../../src/core/selector.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sous-chef layer (v0.3.2) — makes local and remote workers interchangeable
|
|
3
|
+
* behind one interface, with a privacy gate and cheapest-capable-first routing.
|
|
4
|
+
*
|
|
5
|
+
* Design doc: docs/v0.3.2-remote-souschef-design.md
|
|
6
|
+
*
|
|
7
|
+
* The privacy gate is the load-bearing rule: a REMOTE free-tier worker may only
|
|
8
|
+
* receive data the user has accepted leaving the machine. By default that means
|
|
9
|
+
* decomposition only (input is the task description, not code). A per-project
|
|
10
|
+
* `allowRemoteCode` opt-in lets consenting users route code-bearing tasks too.
|
|
11
|
+
*
|
|
12
|
+
* Routing is cheapest-capable-first: local before remote. A remote worker is
|
|
13
|
+
* only chosen when (a) the privacy gate permits the task, (b) no local worker
|
|
14
|
+
* can handle it, and (c) the worker's own canHandle() says yes (e.g. has quota).
|
|
15
|
+
*/
|
|
16
|
+
import { SousChefTask } from '../types.js';
|
|
17
|
+
export interface QuotaStatus {
|
|
18
|
+
/** Free requests used in the current day, if tracked. */
|
|
19
|
+
usedToday: number;
|
|
20
|
+
/** Daily limit, if known. */
|
|
21
|
+
limitPerDay: number | null;
|
|
22
|
+
/** True if the provider is currently throttled (e.g. after a 429). */
|
|
23
|
+
throttled: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface WorkerContext {
|
|
26
|
+
/** Project opt-in: may code-bearing tasks go to remote free tiers? */
|
|
27
|
+
allowRemoteCode: boolean;
|
|
28
|
+
}
|
|
29
|
+
export interface SousChef {
|
|
30
|
+
/** Stable id, e.g. "local:qwen2.5-coder:7b" or "gemini:2.5-flash-lite". */
|
|
31
|
+
id: string;
|
|
32
|
+
kind: 'local' | 'remote';
|
|
33
|
+
/**
|
|
34
|
+
* Whether this worker can take this task right now, ignoring privacy (that's
|
|
35
|
+
* enforced separately in canWorkerHandle). Used for quota/availability vetoes.
|
|
36
|
+
*/
|
|
37
|
+
canHandle: (task: SousChefTask, ctx: WorkerContext) => boolean;
|
|
38
|
+
/** Do the work. Throws on failure; callers fall back to a cheaper worker. */
|
|
39
|
+
run: (prompt: string, opts?: {
|
|
40
|
+
timeoutMs?: number;
|
|
41
|
+
}) => Promise<string>;
|
|
42
|
+
/** Remote workers report quota for the cockpit + routing; local returns null. */
|
|
43
|
+
quota: () => QuotaStatus | null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The privacy gate. Which tasks may a REMOTE worker receive?
|
|
47
|
+
* - decomposition: always (input is the user's task description, no code)
|
|
48
|
+
* - everything else: only if the project opted into allowRemoteCode
|
|
49
|
+
*
|
|
50
|
+
* Local workers are never gated by this — code never leaves the machine.
|
|
51
|
+
*/
|
|
52
|
+
export declare function isRemoteAllowedForTask(task: SousChefTask, allowRemoteCode: boolean): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Can this specific worker handle this task, accounting for privacy AND the
|
|
55
|
+
* worker's own availability veto?
|
|
56
|
+
*/
|
|
57
|
+
export declare function canWorkerHandle(worker: SousChef, task: SousChefTask, ctx: WorkerContext): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Pick the cheapest capable worker. Local workers are cheaper than remote, so
|
|
60
|
+
* they're preferred; within a kind, the first in the list wins (stable order).
|
|
61
|
+
* Returns null if nothing can handle the task.
|
|
62
|
+
*/
|
|
63
|
+
export declare function pickWorker(workers: SousChef[], task: SousChefTask, ctx: WorkerContext): SousChef | null;
|