dsh-codebase-chat 0.25.0 → 0.25.2
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/cli.js +118 -3
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +64 -0
- package/dist/index.js.map +1 -1
- package/lib/index.js +1 -1
- package/package.json +4 -1
package/dist/cli.js
CHANGED
|
@@ -2528,6 +2528,68 @@ function buildToolPrompt(tool, opts) {
|
|
|
2528
2528
|
return normalizeLabels(builder(opts), opts.lang || "fr");
|
|
2529
2529
|
}
|
|
2530
2530
|
|
|
2531
|
+
// src/local-llm.ts
|
|
2532
|
+
import { existsSync as existsSync2 } from "fs";
|
|
2533
|
+
import { join as join8 } from "path";
|
|
2534
|
+
var DEFAULT_MODEL_URI = "hf:Qwen/Qwen2.5-1.5B-Instruct-GGUF:Q4_K_M";
|
|
2535
|
+
var LOCAL_CONTEXT_SIZE = 8192;
|
|
2536
|
+
var LOCAL_MAX_TOKENS = 1024;
|
|
2537
|
+
function isLocalLlmEnabled() {
|
|
2538
|
+
return !!(process.env.CODEBASE_LOCAL_LLM || "").trim();
|
|
2539
|
+
}
|
|
2540
|
+
function configuredModel() {
|
|
2541
|
+
const v = (process.env.CODEBASE_LOCAL_LLM || "").trim();
|
|
2542
|
+
if (!v || v === "1" || v.toLowerCase() === "true") return DEFAULT_MODEL_URI;
|
|
2543
|
+
return v;
|
|
2544
|
+
}
|
|
2545
|
+
async function importLlama() {
|
|
2546
|
+
try {
|
|
2547
|
+
return await import("node-llama-cpp");
|
|
2548
|
+
} catch {
|
|
2549
|
+
throw new Error("Local LLM needs the optional dependency: npm i node-llama-cpp");
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
var modelPromise = null;
|
|
2553
|
+
function loadLocalModel() {
|
|
2554
|
+
if (!modelPromise) {
|
|
2555
|
+
modelPromise = (async () => {
|
|
2556
|
+
const spec = configuredModel();
|
|
2557
|
+
let modelPath = spec;
|
|
2558
|
+
if (spec.startsWith("hf:")) {
|
|
2559
|
+
const { createModelDownloader } = await importLlama();
|
|
2560
|
+
const downloader = await createModelDownloader({
|
|
2561
|
+
modelUri: spec,
|
|
2562
|
+
dirPath: join8(getCacheDir(), "models")
|
|
2563
|
+
});
|
|
2564
|
+
modelPath = await downloader.download();
|
|
2565
|
+
} else if (!existsSync2(spec)) {
|
|
2566
|
+
throw new Error(`Local model not found: ${spec}`);
|
|
2567
|
+
}
|
|
2568
|
+
const { getLlama } = await importLlama();
|
|
2569
|
+
const llama = await getLlama();
|
|
2570
|
+
return llama.loadModel({ modelPath });
|
|
2571
|
+
})();
|
|
2572
|
+
modelPromise.catch(() => {
|
|
2573
|
+
modelPromise = null;
|
|
2574
|
+
});
|
|
2575
|
+
}
|
|
2576
|
+
return modelPromise;
|
|
2577
|
+
}
|
|
2578
|
+
async function callLocalLlm(prompt, lang = "fr") {
|
|
2579
|
+
const model = await loadLocalModel();
|
|
2580
|
+
const { LlamaChatSession } = await importLlama();
|
|
2581
|
+
const context = await model.createContext({ contextSize: LOCAL_CONTEXT_SIZE });
|
|
2582
|
+
try {
|
|
2583
|
+
const session = new LlamaChatSession({
|
|
2584
|
+
contextSequence: context.getSequence(),
|
|
2585
|
+
systemPrompt: lang === "en" ? "You are a senior codebase analyst. Be precise and cite files with [source: path:line]." : "Tu es un analyste codebase senior. Sois precis et cite les fichiers avec [source: chemin:ligne]."
|
|
2586
|
+
});
|
|
2587
|
+
return await session.prompt(prompt, { temperature: 0.2, maxTokens: LOCAL_MAX_TOKENS });
|
|
2588
|
+
} finally {
|
|
2589
|
+
await context.dispose();
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2531
2593
|
// src/cli.ts
|
|
2532
2594
|
var ExitSignal = class {
|
|
2533
2595
|
constructor(code) {
|
|
@@ -2555,6 +2617,8 @@ Usage:
|
|
|
2555
2617
|
npx dsh-codebase-chat --project <path> --health --diff main
|
|
2556
2618
|
npx dsh-codebase-chat --project <path> --watch
|
|
2557
2619
|
npx dsh-codebase-chat --project <path> --prompt intelligence
|
|
2620
|
+
npx dsh-codebase-chat --project <path> --prompt intelligence --call # answered via DEEPSEEK_API_KEY
|
|
2621
|
+
npx dsh-codebase-chat --project <path> --prompt intelligence --local # answered 100% offline
|
|
2558
2622
|
|
|
2559
2623
|
Options:
|
|
2560
2624
|
-p, --project <path> Project directory (default: current directory)
|
|
@@ -2572,6 +2636,11 @@ Options:
|
|
|
2572
2636
|
Pipe it to any LLM (e.g. ... --prompt intelligence | dsh).
|
|
2573
2637
|
Query modes read --ask/--search/--file; --focus and
|
|
2574
2638
|
--style (ouf|punchy|dense|pedagogique|minimal) apply.
|
|
2639
|
+
--call With --prompt: send it to the API (needs DEEPSEEK_API_KEY
|
|
2640
|
+
or OPENAI_API_KEY; DEEPSEEK_BASE_URL / CODEBASE_MODEL
|
|
2641
|
+
customize endpoint/model) instead of printing it.
|
|
2642
|
+
--local With --prompt: answer with the embedded local model
|
|
2643
|
+
(node-llama-cpp, ~1 GB download on first use, offline).
|
|
2575
2644
|
-w, --watch Keep the index hot \u2014 rebuild incrementally on file changes
|
|
2576
2645
|
-e, --embed Enable local semantic embeddings (slower, more relevant)
|
|
2577
2646
|
--lang <en|fr> Language for headings (default: .codebase-chat.json lang, else fr)
|
|
@@ -2600,6 +2669,8 @@ async function main() {
|
|
|
2600
2669
|
prompt: { type: "string" },
|
|
2601
2670
|
focus: { type: "string" },
|
|
2602
2671
|
style: { type: "string" },
|
|
2672
|
+
call: { type: "boolean", default: false },
|
|
2673
|
+
local: { type: "boolean", default: false },
|
|
2603
2674
|
watch: { type: "boolean", short: "w", default: false },
|
|
2604
2675
|
embed: { type: "boolean", short: "e", default: false },
|
|
2605
2676
|
lang: { type: "string" },
|
|
@@ -2697,6 +2768,7 @@ async function main() {
|
|
|
2697
2768
|
console.error(lang === "en" ? `--prompt ${mode} needs a query: add --ask/--search/--file (or --focus)` : `--prompt ${mode} n\xE9cessite une requ\xEAte : ajoute --ask/--search/--file (ou --focus)`);
|
|
2698
2769
|
exit(1);
|
|
2699
2770
|
}
|
|
2771
|
+
const useLocal = values.local || isLocalLlmEnabled();
|
|
2700
2772
|
const result = await buildContext({
|
|
2701
2773
|
project: values.project,
|
|
2702
2774
|
query,
|
|
@@ -2704,7 +2776,8 @@ async function main() {
|
|
|
2704
2776
|
filePath: values.file,
|
|
2705
2777
|
lang,
|
|
2706
2778
|
embed: values.embed,
|
|
2707
|
-
diff: values.diff
|
|
2779
|
+
diff: values.diff,
|
|
2780
|
+
maxTokens: useLocal ? 3500 : void 0
|
|
2708
2781
|
});
|
|
2709
2782
|
let staticSection = "";
|
|
2710
2783
|
if ((/* @__PURE__ */ new Set(["intelligence", "report", "audit", "tasks", "ceo"])).has(mode)) {
|
|
@@ -2718,8 +2791,13 @@ ${formatHealthReport(report, lang)}`;
|
|
|
2718
2791
|
}
|
|
2719
2792
|
}
|
|
2720
2793
|
const projectName = basename3(result.absProject);
|
|
2794
|
+
let prompt;
|
|
2721
2795
|
try {
|
|
2722
|
-
|
|
2796
|
+
prompt = useLocal ? `${result.context}${staticSection}
|
|
2797
|
+
|
|
2798
|
+
${lang === "en" ? `Answer based only on the codebase context above (${mode} mode). Be concise and cite file paths and lines.${query ? ` Question: ${query}` : ""}` : `R\xE9ponds en t'appuyant uniquement sur le contexte du codebase ci-dessus (mode ${mode}). Sois concis et cite les chemins de fichiers et lignes.${query ? ` Question : ${query}` : ""}`}
|
|
2799
|
+
|
|
2800
|
+
${lang === "en" ? "Answer" : "R\xE9ponse"} :` : buildToolPrompt(tool, {
|
|
2723
2801
|
context: `${result.context}${staticSection}`,
|
|
2724
2802
|
projectName,
|
|
2725
2803
|
lang,
|
|
@@ -2728,11 +2806,48 @@ ${formatHealthReport(report, lang)}`;
|
|
|
2728
2806
|
focus: values.focus ?? query,
|
|
2729
2807
|
filePath: values.file ?? "",
|
|
2730
2808
|
description: query
|
|
2731
|
-
})
|
|
2809
|
+
});
|
|
2732
2810
|
} catch {
|
|
2733
2811
|
console.error(lang === "en" ? `unknown prompt mode "${mode}" \u2014 expected: intelligence, report, audit, tasks, ceo, player, chat, search, explain, refactor, crea` : `mode de prompt inconnu "${mode}" \u2014 attendu : intelligence, report, audit, tasks, ceo, player, chat, search, explain, refactor, crea`);
|
|
2734
2812
|
exit(1);
|
|
2735
2813
|
}
|
|
2814
|
+
if (values.local || isLocalLlmEnabled()) {
|
|
2815
|
+
console.error(lang === "en" ? "Answering with the embedded local model (first run downloads ~1 GB)..." : "R\xE9ponse via le mod\xE8le local embarqu\xE9 (premier lancement : ~1 Go de t\xE9l\xE9chargement)...");
|
|
2816
|
+
console.log(await callLocalLlm(prompt, lang));
|
|
2817
|
+
exit(0);
|
|
2818
|
+
}
|
|
2819
|
+
if (values.call) {
|
|
2820
|
+
const apiKey = process.env.DEEPSEEK_API_KEY || process.env.OPENAI_API_KEY || "";
|
|
2821
|
+
if (!apiKey) {
|
|
2822
|
+
console.error(lang === "en" ? "--call needs DEEPSEEK_API_KEY or OPENAI_API_KEY in the environment" : "--call n\xE9cessite DEEPSEEK_API_KEY ou OPENAI_API_KEY dans l\u2019environnement");
|
|
2823
|
+
exit(1);
|
|
2824
|
+
}
|
|
2825
|
+
const baseUrl = process.env.DEEPSEEK_BASE_URL || process.env.OPENAI_BASE_URL || "https://api.deepseek.com/v1";
|
|
2826
|
+
const model = process.env.CODEBASE_MODEL || "deepseek-chat";
|
|
2827
|
+
const res = await fetch(`${baseUrl}/chat/completions`, {
|
|
2828
|
+
method: "POST",
|
|
2829
|
+
signal: AbortSignal.timeout(12e4),
|
|
2830
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
2831
|
+
body: JSON.stringify({
|
|
2832
|
+
model,
|
|
2833
|
+
messages: [
|
|
2834
|
+
{ role: "system", content: lang === "en" ? "You are a senior codebase analyst. Be precise and cite files." : "Tu es un analyste codebase senior. Sois pr\xE9cis et cite les fichiers." },
|
|
2835
|
+
{ role: "user", content: prompt }
|
|
2836
|
+
],
|
|
2837
|
+
temperature: 0.3,
|
|
2838
|
+
max_tokens: 8192
|
|
2839
|
+
})
|
|
2840
|
+
});
|
|
2841
|
+
if (!res.ok) {
|
|
2842
|
+
const text = await res.text().catch(() => "");
|
|
2843
|
+
console.error(`API error ${res.status}: ${text}`);
|
|
2844
|
+
exit(1);
|
|
2845
|
+
}
|
|
2846
|
+
const data = await res.json();
|
|
2847
|
+
console.log(data.choices?.[0]?.message?.content || "");
|
|
2848
|
+
exit(0);
|
|
2849
|
+
}
|
|
2850
|
+
console.log(prompt);
|
|
2736
2851
|
exit(0);
|
|
2737
2852
|
}
|
|
2738
2853
|
if (values.ask || values.search || values.file) {
|