devlensio 1.0.2 → 1.0.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/config/backcompat.test.d.ts +1 -0
- package/dist/config/backcompat.test.js +188 -0
- package/dist/config/index.d.ts +5 -3
- package/dist/config/index.js +37 -54
- package/dist/config/providers/file.d.ts +3 -1
- package/dist/config/providers/file.js +47 -53
- package/dist/config/providers/providers.default.js +0 -1
- package/dist/config/providers/request.js +1 -1
- package/dist/config/types.d.ts +0 -1
- package/dist/config/types.js +2 -18
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/jobs/queue/memory.js +12 -1
- package/dist/pipeline/index.d.ts +1 -0
- package/dist/pipeline/index.js +14 -2
- package/dist/storage/fileStorage.d.ts +3 -0
- package/dist/storage/fileStorage.js +25 -1
- package/dist/storage/interface.d.ts +2 -0
- package/dist/summarizer/providers/index.js +4 -4
- package/dist/summarizer/providers/openai.js +1 -1
- package/extractors/go/bin/darwin-amd64/devlens_go_extractor +0 -0
- package/extractors/go/bin/darwin-arm64/devlens_go_extractor +0 -0
- package/extractors/go/bin/linux-amd64/devlens_go_extractor +0 -0
- package/extractors/go/bin/linux-arm64/devlens_go_extractor +0 -0
- package/extractors/go/bin/windows-amd64/devlens_go_extractor.exe +0 -0
- package/extractors/java/devlens_java_extractor.jar +0 -0
- package/package.json +10 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// Back-compat + tolerance tests for the config system (plan issue 15, M1).
|
|
2
|
+
//
|
|
3
|
+
// Each scenario runs in a SUBPROCESS with HOME pointed at a throwaway dir:
|
|
4
|
+
// CONFIG_FILE is resolved from os.homedir() at module load, so isolation only
|
|
5
|
+
// works in a fresh process. This also guarantees these tests can never touch
|
|
6
|
+
// the developer's real ~/.devlens/config.json.
|
|
7
|
+
//
|
|
8
|
+
// Run: bun test src/config/backcompat.test.ts
|
|
9
|
+
import { describe, test, expect } from "bun:test";
|
|
10
|
+
import { spawnSync } from "node:child_process";
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import os from "node:os";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
const ROOT = path.resolve(import.meta.dir, "..", ".."); // engine repo root
|
|
15
|
+
function runScenario(name, files, script) {
|
|
16
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), `devlens-cfg-${name}-`));
|
|
17
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
18
|
+
const p = path.join(home, rel);
|
|
19
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
20
|
+
fs.writeFileSync(p, content);
|
|
21
|
+
}
|
|
22
|
+
const env = { ...process.env, HOME: home };
|
|
23
|
+
// strip ambient LLM env so fixtures are hermetic
|
|
24
|
+
for (const k of Object.keys(env)) {
|
|
25
|
+
if (k.startsWith("DEVLENS_LLM") || k.startsWith("DEVLENS_EMBED") || k.startsWith("DEVLENS_BATCH")) {
|
|
26
|
+
delete env[k];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const r = spawnSync("bun", ["-e", script], {
|
|
30
|
+
cwd: ROOT,
|
|
31
|
+
env,
|
|
32
|
+
encoding: "utf8",
|
|
33
|
+
timeout: 30000,
|
|
34
|
+
});
|
|
35
|
+
if (r.error)
|
|
36
|
+
throw r.error;
|
|
37
|
+
const out = (r.stdout ?? "").trim().split("\n").pop() ?? "";
|
|
38
|
+
let result;
|
|
39
|
+
try {
|
|
40
|
+
result = JSON.parse(out);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
result = { parseError: r.stdout, status: r.status, stderr: r.stderr };
|
|
44
|
+
}
|
|
45
|
+
return { home, result, stderr: r.stderr ?? "" };
|
|
46
|
+
}
|
|
47
|
+
const CONFIG_REL = ".devlens/config.json";
|
|
48
|
+
const PROBE = `
|
|
49
|
+
const m = await import("./src/config/index.js");
|
|
50
|
+
const out = {};
|
|
51
|
+
try {
|
|
52
|
+
const c = m.resolveConfig(undefined, { validate: false });
|
|
53
|
+
out.tolerant = { provider: c.summarization.provider, providerName: c.summarization.providerName ?? null, model: c.summarization.model, hasKey: !!c.summarization.apiKey };
|
|
54
|
+
} catch (e) { out.tolerantErr = e instanceof Error ? e.message : String(e); }
|
|
55
|
+
try {
|
|
56
|
+
m.resolveConfig();
|
|
57
|
+
out.validating = "ok";
|
|
58
|
+
} catch (e) { out.validatingErr = e instanceof Error ? e.message : String(e); }
|
|
59
|
+
out.configured = m.hasSummarizationConfigured();
|
|
60
|
+
console.log(JSON.stringify(out));
|
|
61
|
+
`;
|
|
62
|
+
describe("fresh install (no config file)", () => {
|
|
63
|
+
const s = runScenario("fresh", {}, PROBE);
|
|
64
|
+
test("tolerant read resolves defaults without error", () => {
|
|
65
|
+
expect(s.result.tolerantErr).toBeUndefined();
|
|
66
|
+
expect(s.result.tolerant).toMatchObject({ provider: "anthropic", hasKey: false });
|
|
67
|
+
});
|
|
68
|
+
test("validating read fails with the actionable missing-key message", () => {
|
|
69
|
+
expect(String(s.result.validatingErr)).toContain("summarization.apiKey is required");
|
|
70
|
+
expect(String(s.result.validatingErr)).toContain("devlens init");
|
|
71
|
+
});
|
|
72
|
+
test("hasSummarizationConfigured() = false", () => {
|
|
73
|
+
expect(s.result.configured).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
describe("incomplete config (provider, no key)", () => {
|
|
77
|
+
const s = runScenario("incomplete", { [CONFIG_REL]: JSON.stringify({ summarization: { provider: "openai", providerName: "deepseek", model: "deepseek-chat" } }) }, PROBE);
|
|
78
|
+
test("tolerant read works (init/config/analyze can run)", () => {
|
|
79
|
+
expect(s.result.tolerantErr).toBeUndefined();
|
|
80
|
+
expect(s.result.tolerant).toMatchObject({ provider: "openai", providerName: "deepseek", hasKey: false });
|
|
81
|
+
});
|
|
82
|
+
test("only the validating path throws", () => {
|
|
83
|
+
expect(String(s.result.validatingErr)).toContain("apiKey is required");
|
|
84
|
+
expect(s.result.configured).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
describe("valid config (current multi-provider v2)", () => {
|
|
88
|
+
const s = runScenario("valid", {
|
|
89
|
+
[CONFIG_REL]: JSON.stringify({
|
|
90
|
+
summarization: {
|
|
91
|
+
active: "openai:deepseek",
|
|
92
|
+
providers: {
|
|
93
|
+
"openai:deepseek": { provider: "openai", providerName: "deepseek", model: "deepseek-chat", apiKey: "sk-x", batchSize: 25 },
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
}),
|
|
97
|
+
}, PROBE);
|
|
98
|
+
test("both paths resolve; configured = true", () => {
|
|
99
|
+
expect(s.result.tolerantErr).toBeUndefined();
|
|
100
|
+
expect(s.result.validating).toBe("ok");
|
|
101
|
+
expect(s.result.configured).toBe(true);
|
|
102
|
+
expect(s.result.tolerant).toMatchObject({ provider: "openai", providerName: "deepseek", hasKey: true });
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
describe("broken config JSON", () => {
|
|
106
|
+
const s = runScenario("broken", { [CONFIG_REL]: "{ not json" }, PROBE);
|
|
107
|
+
test("tolerant read STILL reports the invalid-JSON error (never silent)", () => {
|
|
108
|
+
expect(String(s.result.tolerantErr)).toContain("invalid JSON");
|
|
109
|
+
expect(String(s.result.validatingErr)).toContain("invalid JSON");
|
|
110
|
+
expect(s.result.configured).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
describe("old v1 flat config (pre multi-provider)", () => {
|
|
114
|
+
const flat = JSON.stringify({ summarization: { provider: "openai", providerName: "mybrand", model: "m1", apiKey: "sk-old", batchSize: 30 } });
|
|
115
|
+
const s = runScenario("flat", { [CONFIG_REL]: flat }, PROBE);
|
|
116
|
+
test("loads fine on both paths", () => {
|
|
117
|
+
expect(s.result.tolerantErr).toBeUndefined();
|
|
118
|
+
expect(s.result.validating).toBe("ok");
|
|
119
|
+
expect(s.result.configured).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
test("migrates the file to multi-provider format (active + providers)", () => {
|
|
122
|
+
const raw = JSON.parse(fs.readFileSync(path.join(s.home, CONFIG_REL), "utf8"));
|
|
123
|
+
expect(raw.summarization.active).toBe("openai:mybrand");
|
|
124
|
+
expect(raw.summarization.providers["openai:mybrand"].apiKey).toBe("sk-old");
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
describe("v1 config with a BRAND in provider (deepseek)", () => {
|
|
128
|
+
const s = runScenario("brand", { [CONFIG_REL]: JSON.stringify({ summarization: { provider: "deepseek", model: "m1", apiKey: "sk-b" } }) }, PROBE);
|
|
129
|
+
test("migrates brand → protocol + providerName without error", () => {
|
|
130
|
+
expect(s.result.tolerantErr).toBeUndefined();
|
|
131
|
+
expect(s.result.tolerant).toMatchObject({ provider: "openai", providerName: "deepseek", hasKey: true });
|
|
132
|
+
expect(s.result.validating).toBe("ok");
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
describe("legacy OLLAMA config (provider removed)", () => {
|
|
136
|
+
const s = runScenario("ollama", { [CONFIG_REL]: JSON.stringify({ summarization: { provider: "ollama", model: "qwen2.5-coder:3b", baseUrl: "http://localhost:11434/v1" } }) }, PROBE);
|
|
137
|
+
test("tolerant read loads (old configs must keep working)", () => {
|
|
138
|
+
expect(s.result.tolerantErr).toBeUndefined();
|
|
139
|
+
expect(s.result.tolerant).toMatchObject({ provider: "openai", providerName: "ollama", hasKey: false });
|
|
140
|
+
});
|
|
141
|
+
test("validating read gives the 'no longer supported' migration message", () => {
|
|
142
|
+
expect(String(s.result.validatingErr)).toContain("Ollama is no longer supported");
|
|
143
|
+
expect(s.result.configured).toBe(false);
|
|
144
|
+
});
|
|
145
|
+
test("stderr carries the not-in-catalog warning (once)", () => {
|
|
146
|
+
expect(s.stderr).toContain("not in the provider catalog");
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
describe("config with an embedding block but no embedding key", () => {
|
|
150
|
+
const s = runScenario("embedding", {
|
|
151
|
+
[CONFIG_REL]: JSON.stringify({
|
|
152
|
+
summarization: { provider: "openai", providerName: "deepseek", model: "m", apiKey: "sk-ok" },
|
|
153
|
+
embedding: { provider: "openai", model: "text-embedding-3-small" },
|
|
154
|
+
}),
|
|
155
|
+
}, PROBE);
|
|
156
|
+
test("validating read no longer demands an embedding key (issue 14)", () => {
|
|
157
|
+
expect(s.result.validating).toBe("ok");
|
|
158
|
+
expect(s.result.tolerantErr).toBeUndefined();
|
|
159
|
+
expect(s.result.configured).toBe(true);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
describe("provider catalog", () => {
|
|
163
|
+
test("ollama is gone from the shipped catalog", () => {
|
|
164
|
+
const s = runScenario("catalog", {}, `
|
|
165
|
+
const { loadCatalog } = await import("./src/config/providers/catalog.js");
|
|
166
|
+
const names = loadCatalog().map(p => p.name);
|
|
167
|
+
console.log(JSON.stringify({ names }));
|
|
168
|
+
`);
|
|
169
|
+
const names = (s.result.names ?? []);
|
|
170
|
+
expect(names).not.toContain("ollama");
|
|
171
|
+
expect(names).toContain("deepseek");
|
|
172
|
+
expect(names).toContain("anthropic");
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
describe("env-var key counts as configured", () => {
|
|
176
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), "devlens-cfg-env-"));
|
|
177
|
+
fs.mkdirSync(path.join(home, ".devlens"), { recursive: true });
|
|
178
|
+
const r = spawnSync("bun", ["-e", `const m = await import("./src/config/index.js"); console.log(JSON.stringify({ configured: m.hasSummarizationConfigured(), hasKey: !!m.resolveConfig(undefined,{validate:false}).summarization.apiKey }));`], {
|
|
179
|
+
cwd: ROOT,
|
|
180
|
+
encoding: "utf8",
|
|
181
|
+
timeout: 30000,
|
|
182
|
+
env: { ...process.env, HOME: home, DEVLENS_LLM_KEY: "sk-from-env" },
|
|
183
|
+
});
|
|
184
|
+
test("DEVLENS_LLM_KEY enables hasSummarizationConfigured()", () => {
|
|
185
|
+
const out = JSON.parse((r.stdout ?? "").trim().split("\n").pop() ?? "{}");
|
|
186
|
+
expect(out).toEqual({ configured: true, hasKey: true });
|
|
187
|
+
});
|
|
188
|
+
});
|
package/dist/config/index.d.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { type DevLensConfig, type ProviderConfigEntry } from "./types.js";
|
|
2
|
-
export declare function detectOllama(): Promise<boolean>;
|
|
3
2
|
export declare function initConfig(): Promise<void>;
|
|
4
|
-
export declare function resolveConfig(req?: Request
|
|
3
|
+
export declare function resolveConfig(req?: Request, opts?: {
|
|
4
|
+
validate?: boolean;
|
|
5
|
+
}): DevLensConfig;
|
|
6
|
+
export declare function hasSummarizationConfigured(req?: Request): boolean;
|
|
5
7
|
export type { DevLensConfig } from "./types.js";
|
|
6
8
|
export type { SafeConfig } from "./writer.js";
|
|
7
9
|
export { maskConfig, writeConfig, atomicWrite } from "./writer.js";
|
|
8
10
|
export { CONFIG_FILE, CONFIG_DIR, ENV } from "./providers/file.js";
|
|
9
11
|
export { sanitizeHeaders, CONFIG_HEADERS } from "./types.js";
|
|
10
|
-
export {
|
|
12
|
+
export { ANTHROPIC_DEFAULTS } from "./types.js";
|
|
11
13
|
export type { ProviderConfigEntry, MultiProviderStorage } from "./types.js";
|
|
12
14
|
export { makeProviderKey, parseProviderKey } from "./types.js";
|
|
13
15
|
export interface AllProvidersResult {
|
package/dist/config/index.js
CHANGED
|
@@ -1,72 +1,35 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ANTHROPIC_DEFAULTS, makeProviderKey } from "./types.js";
|
|
2
2
|
import { loadFileConfig } from "./providers/file.js";
|
|
3
3
|
import { applyRequestHeaders } from "./providers/request.js";
|
|
4
4
|
import { atomicWrite } from "./writer.js";
|
|
5
5
|
import { CONFIG_FILE } from "./providers/paths.js";
|
|
6
|
+
import { findProvider } from "./providers/catalog.js";
|
|
6
7
|
import fs from "fs";
|
|
7
|
-
//
|
|
8
|
+
// Startup Initialization
|
|
8
9
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// Uses a short timeout — we don't want server startup to hang for 30 seconds
|
|
15
|
-
// if Ollama is not installed. 2 seconds is enough for a local HTTP ping.
|
|
16
|
-
//
|
|
17
|
-
// Called ONCE at startup and the result is cached — see `cachedDefaults` below.
|
|
18
|
-
const OLLAMA_PING_URL = "http://localhost:11434";
|
|
19
|
-
const OLLAMA_PING_TIMEOUT_MS = 2000;
|
|
20
|
-
export async function detectOllama() {
|
|
21
|
-
try {
|
|
22
|
-
const controller = new AbortController();
|
|
23
|
-
const timeout = setTimeout(() => controller.abort(), OLLAMA_PING_TIMEOUT_MS);
|
|
24
|
-
const res = await fetch(OLLAMA_PING_URL, {
|
|
25
|
-
signal: controller.signal,
|
|
26
|
-
method: "GET",
|
|
27
|
-
});
|
|
28
|
-
clearTimeout(timeout);
|
|
29
|
-
return res.ok;
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
// Ollama not running, not installed, or timed out — all treated the same
|
|
33
|
-
return false;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
// Startup Initialization
|
|
37
|
-
//
|
|
38
|
-
// detectOllama() is called once when the server starts (in server/index.ts).
|
|
39
|
-
// The result is stored here so resolveConfig() doesn't ping Ollama on
|
|
40
|
-
// every single request — that would be slow and noisy.
|
|
41
|
-
//
|
|
42
|
-
// initConfig() must be called before any request is handled.
|
|
43
|
-
// Until it is called, resolveConfig() falls back to ANTHROPIC_DEFAULTS safely.
|
|
10
|
+
// Historically this pinged a local Ollama endpoint to pick defaults and logged
|
|
11
|
+
// the result with console.log — noise, and a real hazard for the MCP stdio
|
|
12
|
+
// transport (stdout is the JSON-RPC channel). Ollama support has been removed;
|
|
13
|
+
// initConfig() is now a quiet, idempotent shim kept for call-site
|
|
14
|
+
// compatibility (OSS `devlens serve` + MCP servers await it at startup).
|
|
44
15
|
let cachedDefaults = ANTHROPIC_DEFAULTS;
|
|
45
16
|
let initialized = false;
|
|
46
17
|
export async function initConfig() {
|
|
47
18
|
if (initialized)
|
|
48
19
|
return;
|
|
49
|
-
const ollamaRunning = await detectOllama();
|
|
50
|
-
if (ollamaRunning) {
|
|
51
|
-
cachedDefaults = OLLAMA_DEFAULTS;
|
|
52
|
-
console.log("⚡ Ollama detected — using local LLM defaults");
|
|
53
|
-
console.log(` Summarization: ${OLLAMA_DEFAULTS.summarization.model}`);
|
|
54
|
-
console.log(` Embedding: ${OLLAMA_DEFAULTS.embedding.model}`);
|
|
55
|
-
}
|
|
56
|
-
else {
|
|
57
|
-
cachedDefaults = ANTHROPIC_DEFAULTS;
|
|
58
|
-
console.log("☁️ Ollama not detected — using Anthropic defaults");
|
|
59
|
-
console.log(" Add an apiKey to ~/.devlens/config.json to enable summarization");
|
|
60
|
-
console.log(` Or set ${(await import("./providers/file.js")).ENV.LLM_KEY}=your-key`);
|
|
61
|
-
}
|
|
62
20
|
initialized = true;
|
|
63
21
|
}
|
|
64
22
|
// This function reads config.json fresh on every call —
|
|
65
23
|
// so if the user edits settings in the UI, the next job picks up the change
|
|
66
24
|
// without requiring a server restart.
|
|
67
|
-
|
|
25
|
+
//
|
|
26
|
+
// opts.validate === false → tolerant read: an incomplete summarization config
|
|
27
|
+
// (missing API key etc.) resolves fine and is only reported when summarization
|
|
28
|
+
// actually runs. Used by display paths (devlens config/doctor/init) and by
|
|
29
|
+
// structure-only analysis (GitHub issue #10).
|
|
30
|
+
export function resolveConfig(req, opts) {
|
|
68
31
|
// Step 1 — load file config merged with detected defaults + env vars
|
|
69
|
-
const fileConfig = loadFileConfig(cachedDefaults);
|
|
32
|
+
const fileConfig = loadFileConfig(cachedDefaults, opts);
|
|
70
33
|
if (!req)
|
|
71
34
|
return fileConfig;
|
|
72
35
|
// In local mode, ignore headers even if present
|
|
@@ -75,10 +38,29 @@ export function resolveConfig(req) {
|
|
|
75
38
|
// Step 4 — apply header overrides for cloud users
|
|
76
39
|
return applyRequestHeaders(fileConfig, req);
|
|
77
40
|
}
|
|
41
|
+
// True when summarization can actually run with the current config: the active
|
|
42
|
+
// provider either needs no key, or has one (file or env). Never throws on an
|
|
43
|
+
// incomplete config — callers use it to auto-skip summarization instead of
|
|
44
|
+
// failing analysis (GitHub issue #10). Returns false when config.json is
|
|
45
|
+
// unreadable: summarization must not run, but analysis may still proceed.
|
|
46
|
+
export function hasSummarizationConfigured(req) {
|
|
47
|
+
try {
|
|
48
|
+
const config = resolveConfig(req, { validate: false });
|
|
49
|
+
const providerName = config.summarization.providerName ?? config.summarization.provider;
|
|
50
|
+
if (providerName === "ollama")
|
|
51
|
+
return false; // removed provider
|
|
52
|
+
const entry = findProvider(providerName);
|
|
53
|
+
const needsKey = entry?.requiresKey ?? true;
|
|
54
|
+
return !needsKey || !!config.summarization.apiKey;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
78
60
|
export { maskConfig, writeConfig, atomicWrite } from "./writer.js";
|
|
79
61
|
export { CONFIG_FILE, CONFIG_DIR, ENV } from "./providers/file.js";
|
|
80
62
|
export { sanitizeHeaders, CONFIG_HEADERS } from "./types.js";
|
|
81
|
-
export {
|
|
63
|
+
export { ANTHROPIC_DEFAULTS } from "./types.js";
|
|
82
64
|
export { makeProviderKey, parseProviderKey } from "./types.js";
|
|
83
65
|
/** Read the raw multi-provider storage from config.json (no defaults applied). */
|
|
84
66
|
function readProviderStorage() {
|
|
@@ -107,7 +89,8 @@ export function resolveAllProviders() {
|
|
|
107
89
|
};
|
|
108
90
|
}
|
|
109
91
|
// Fallback: use the resolved active config to synthesise one entry
|
|
110
|
-
|
|
92
|
+
// (tolerant read — a missing key must not break the settings UI)
|
|
93
|
+
const config = loadFileConfig(ANTHROPIC_DEFAULTS, { validate: false });
|
|
111
94
|
const key = makeProviderKey(config.summarization.provider, config.summarization.providerName ?? config.summarization.provider);
|
|
112
95
|
return {
|
|
113
96
|
active: key,
|
|
@@ -18,4 +18,6 @@ export declare const ENV: {
|
|
|
18
18
|
};
|
|
19
19
|
/** Public — reads the raw config.json as a plain object. Used by multi-provider helpers. */
|
|
20
20
|
export declare function readRawConfigFile(): Record<string, unknown>;
|
|
21
|
-
export declare function loadFileConfig(defaults?: DevLensConfig
|
|
21
|
+
export declare function loadFileConfig(defaults?: DevLensConfig, opts?: {
|
|
22
|
+
validate?: boolean;
|
|
23
|
+
}): DevLensConfig;
|
|
@@ -6,9 +6,9 @@ export { CONFIG_DIR, CONFIG_FILE } from "./paths.js";
|
|
|
6
6
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
7
7
|
//
|
|
8
8
|
// For Docker users who prefer env vars over config files.
|
|
9
|
-
// A Docker user
|
|
10
|
-
// DEVLENS_LLM_PROVIDER=
|
|
11
|
-
// DEVLENS_LLM_BASE_URL=
|
|
9
|
+
// A Docker user with their own endpoint would set:
|
|
10
|
+
// DEVLENS_LLM_PROVIDER=openai
|
|
11
|
+
// DEVLENS_LLM_BASE_URL=https://my-endpoint.example/v1
|
|
12
12
|
//
|
|
13
13
|
// Priority: config file wins over env vars.
|
|
14
14
|
// Env vars only fill fields that the config file left empty.
|
|
@@ -67,16 +67,24 @@ function migrateProviderConfig(config) {
|
|
|
67
67
|
const p = config.summarization.provider;
|
|
68
68
|
if (p === "openai" || p === "anthropic")
|
|
69
69
|
return config;
|
|
70
|
+
// Known brand → its catalog protocol. Unknown brand (including providers
|
|
71
|
+
// removed from the catalog, e.g. "ollama") → treat as a custom
|
|
72
|
+
// OpenAI-compatible entry instead of throwing: configs written by older CLI
|
|
73
|
+
// versions must keep loading. The actionable error ("no longer supported" /
|
|
74
|
+
// missing key) fires at SUMMARIZE time, never on a plain config read.
|
|
70
75
|
const entry = findProvider(p);
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
76
|
+
const protocol = entry?.protocol ?? "openai";
|
|
77
|
+
const name = entry?.name ?? p;
|
|
78
|
+
if (!entry) {
|
|
79
|
+
console.warn(`DevLens: "${p}" is not in the provider catalog — ` +
|
|
80
|
+
`treating it as a custom OpenAI-compatible provider.`);
|
|
81
|
+
}
|
|
82
|
+
const migrated = { ...config, summarization: { ...config.summarization, provider: protocol, providerName: name } };
|
|
75
83
|
try {
|
|
76
84
|
const raw = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
|
|
77
|
-
raw.summarization = { ...raw.summarization, provider:
|
|
85
|
+
raw.summarization = { ...raw.summarization, provider: protocol };
|
|
78
86
|
if (!raw.summarization.providerName)
|
|
79
|
-
raw.summarization.providerName =
|
|
87
|
+
raw.summarization.providerName = name;
|
|
80
88
|
const tmp = CONFIG_FILE + ".tmp";
|
|
81
89
|
fs.writeFileSync(tmp, JSON.stringify(raw, null, 2));
|
|
82
90
|
fs.renameSync(tmp, CONFIG_FILE);
|
|
@@ -85,7 +93,7 @@ function migrateProviderConfig(config) {
|
|
|
85
93
|
return migrated;
|
|
86
94
|
}
|
|
87
95
|
const VALID_LLM_PROTOCOLS = new Set(["openai", "anthropic"]);
|
|
88
|
-
const VALID_EMBED_PROTOCOLS = new Set(["openai", "anthropic", "openrouter", "gemini"
|
|
96
|
+
const VALID_EMBED_PROTOCOLS = new Set(["openai", "anthropic", "openrouter", "gemini"]);
|
|
89
97
|
function sanitizeProviderEnv(raw) {
|
|
90
98
|
if (!raw)
|
|
91
99
|
return undefined;
|
|
@@ -176,45 +184,35 @@ function buildNeo4jFromEnv() {
|
|
|
176
184
|
return undefined;
|
|
177
185
|
return { url, username, password, storeRawCode };
|
|
178
186
|
}
|
|
179
|
-
// Validation
|
|
187
|
+
// Validation //
|
|
180
188
|
//
|
|
181
|
-
// Only validates what cannot have a sensible default.
|
|
189
|
+
// Only validates what cannot have a sensible default. Called from
|
|
190
|
+
// loadFileConfig() ONLY when validation is requested (summarize paths);
|
|
191
|
+
// display/init/analyze paths resolve tolerantly — see loadFileConfig opts.
|
|
182
192
|
// Key requirement is resolved from the catalog's `requiresKey` per provider.
|
|
183
193
|
// If a providerName isn't in the catalog (custom), we default to requiring a key.
|
|
184
194
|
//
|
|
185
195
|
// Error messages are actionable — they tell the user exactly how to fix the problem.
|
|
186
|
-
const EMBEDDING_PROVIDERS_NEEDING_KEY = new Set([
|
|
187
|
-
"anthropic",
|
|
188
|
-
"openai",
|
|
189
|
-
"openrouter",
|
|
190
|
-
"gemini",
|
|
191
|
-
]);
|
|
192
196
|
function validate(config) {
|
|
193
|
-
const { summarization
|
|
197
|
+
const { summarization } = config;
|
|
198
|
+
// Ollama support was removed — fail with a migration hint, never a generic
|
|
199
|
+
// missing-key error. (Tolerant reads never reach here; this fires only when
|
|
200
|
+
// actually summarizing.)
|
|
201
|
+
if (summarization.providerName === "ollama") {
|
|
202
|
+
throw new Error(`DevLens config error: Ollama is no longer supported.\n` +
|
|
203
|
+
` Fix: run "devlens init" to configure a cloud provider,\n` +
|
|
204
|
+
` or switch the active provider with "devlens config --active <provider:key>".`);
|
|
205
|
+
}
|
|
194
206
|
// Summarization apiKey — resolved from catalog
|
|
195
207
|
const entry = findProvider(summarization.providerName ?? "");
|
|
196
208
|
const needsKey = entry?.requiresKey ?? true; // unknown/custom → require key
|
|
197
209
|
if (needsKey && !summarization.apiKey) {
|
|
198
210
|
throw new Error(`DevLens config error: summarization.apiKey is required for "${summarization.providerName ?? summarization.provider}".\n` +
|
|
199
|
-
` Fix option 1 —
|
|
211
|
+
` Fix option 1 — run "devlens init" (or "devlens config --set") to configure a provider.\n` +
|
|
212
|
+
` Fix option 2 — add to ${CONFIG_FILE}:\n` +
|
|
200
213
|
` { "summarization": { "apiKey": "your-key-here" } }\n` +
|
|
201
|
-
` Fix option
|
|
202
|
-
`
|
|
203
|
-
`Fix option 3 - Skip Summarization`);
|
|
204
|
-
}
|
|
205
|
-
// Embedding apiKey — only validate if the user explicitly configured embedding.
|
|
206
|
-
// If the user only set summarization, embedding may still be at default (openai
|
|
207
|
-
// with no key) which is fine — embedding is only needed for vector search (cloud).
|
|
208
|
-
const rawFile = readFileConfig();
|
|
209
|
-
const userSetEmbedding = !!rawFile.embedding?.provider;
|
|
210
|
-
if (userSetEmbedding &&
|
|
211
|
-
EMBEDDING_PROVIDERS_NEEDING_KEY.has(embedding.provider) &&
|
|
212
|
-
!embedding.apiKey) {
|
|
213
|
-
throw new Error(`DevLens config error: embedding.apiKey is required when provider is "${embedding.provider}".\n` +
|
|
214
|
-
` Fix option 1 — add to ${CONFIG_FILE}:\n` +
|
|
215
|
-
` { "embedding": { "apiKey": "your-key-here" } }\n` +
|
|
216
|
-
` Fix option 2 — set environment variable:\n` +
|
|
217
|
-
` ${ENV.EMBED_KEY}=your-key-here`);
|
|
214
|
+
` Fix option 3 — set environment variable ${ENV.LLM_KEY}=your-key-here\n` +
|
|
215
|
+
` Fix option 4 — run analyze WITHOUT --summarize (structure-only needs no key).`);
|
|
218
216
|
}
|
|
219
217
|
// Neo4j — if any field is provided, all three must be present
|
|
220
218
|
if (config.neo4j) {
|
|
@@ -230,14 +228,6 @@ function validate(config) {
|
|
|
230
228
|
` ${ENV.NEO4J_PASSWORD}=your-password`);
|
|
231
229
|
}
|
|
232
230
|
}
|
|
233
|
-
// Ollama baseUrl format
|
|
234
|
-
if (summarization.providerName === "ollama") {
|
|
235
|
-
const base = summarization.baseUrl ?? "http://localhost:11434/v1";
|
|
236
|
-
if (!base.startsWith("http://") && !base.startsWith("https://")) {
|
|
237
|
-
throw new Error(`DevLens config error: summarization.baseUrl must start with http:// or https://.\n` +
|
|
238
|
-
` Got: "${base}"`);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
231
|
}
|
|
242
232
|
// readFileConfig
|
|
243
233
|
//
|
|
@@ -308,21 +298,24 @@ function extractActiveProvider(storage) {
|
|
|
308
298
|
batchSize: entry.batchSize,
|
|
309
299
|
};
|
|
310
300
|
}
|
|
311
|
-
// loadFileConfig
|
|
301
|
+
// loadFileConfig
|
|
312
302
|
//
|
|
313
303
|
// Public entry point — called by resolveConfig() in config/index.ts.
|
|
314
304
|
//
|
|
315
|
-
// Takes the active defaults (chosen by detectOllama() in index.ts):
|
|
316
|
-
// - OLLAMA_DEFAULTS if Ollama is running at startup
|
|
317
|
-
// - ANTHROPIC_DEFAULTS if Ollama is not detected
|
|
318
|
-
//
|
|
319
305
|
// Steps:
|
|
320
306
|
// 1. Read ~/.devlens/config.json (partial — only what user set)
|
|
321
307
|
// 2. Deep merge onto provided defaults
|
|
322
308
|
// 3. Apply env vars for any still-missing fields
|
|
323
|
-
// 4. Validate
|
|
309
|
+
// 4. Validate (only when opts.validate !== false) — see opts below
|
|
324
310
|
// 5. Return fully resolved DevLensConfig — never partial, never undefined fields
|
|
325
|
-
|
|
311
|
+
//
|
|
312
|
+
// opts.validate:
|
|
313
|
+
// - default (true): summarize paths — throws actionable errors for missing
|
|
314
|
+
// keys / incomplete neo4j / removed providers.
|
|
315
|
+
// - false: tolerant reads for display, init, config editing and
|
|
316
|
+
// structure-only analysis — an incomplete summarization config is NOT an
|
|
317
|
+
// error here (GitHub issue #10). Invalid JSON in the file still throws.
|
|
318
|
+
export function loadFileConfig(defaults = ANTHROPIC_DEFAULTS, opts = {}) {
|
|
326
319
|
let partial = readFileConfig();
|
|
327
320
|
// ── Multi-provider detection & migration ─────────────────────────────────
|
|
328
321
|
if (partial.summarization) {
|
|
@@ -376,6 +369,7 @@ export function loadFileConfig(defaults = ANTHROPIC_DEFAULTS) {
|
|
|
376
369
|
const merged = deepMerge(defaults, partial);
|
|
377
370
|
const migrated = migrateProviderConfig(merged);
|
|
378
371
|
const withEnv = applyEnvVars(migrated);
|
|
379
|
-
validate
|
|
372
|
+
if (opts.validate !== false)
|
|
373
|
+
validate(withEnv);
|
|
380
374
|
return withEnv;
|
|
381
375
|
}
|
|
@@ -7,6 +7,5 @@ export const DEFAULT_PROVIDERS = [
|
|
|
7
7
|
{ name: "mistral", label: "Mistral", protocol: "openai", baseUrl: "https://api.mistral.ai/v1", requiresKey: true },
|
|
8
8
|
{ name: "xai", label: "xAI Grok", protocol: "openai", baseUrl: "https://api.x.ai/v1", requiresKey: true },
|
|
9
9
|
{ name: "openrouter", label: "OpenRouter", protocol: "openai", baseUrl: "https://openrouter.ai/api/v1", requiresKey: true },
|
|
10
|
-
{ name: "ollama", label: "Ollama (local)", protocol: "openai", baseUrl: "http://localhost:11434/v1", requiresKey: false },
|
|
11
10
|
];
|
|
12
11
|
export const CATALOG_VERSION = 2;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CONFIG_HEADERS } from "../types.js";
|
|
2
2
|
const VALID_LLM_PROTOCOLS = new Set(["openai", "anthropic"]);
|
|
3
|
-
const VALID_EMBED_PROTOCOLS = new Set(["openai", "anthropic", "openrouter", "gemini"
|
|
3
|
+
const VALID_EMBED_PROTOCOLS = new Set(["openai", "anthropic", "openrouter", "gemini"]);
|
|
4
4
|
function rejectPlaceholder(v) {
|
|
5
5
|
if (!v)
|
|
6
6
|
return undefined;
|
package/dist/config/types.d.ts
CHANGED
|
@@ -48,7 +48,6 @@ export interface DevLensConfig {
|
|
|
48
48
|
embedding: EmbeddingConfig;
|
|
49
49
|
neo4j?: Neo4jConfig;
|
|
50
50
|
}
|
|
51
|
-
export declare const OLLAMA_DEFAULTS: DevLensConfig;
|
|
52
51
|
export declare const ANTHROPIC_DEFAULTS: DevLensConfig;
|
|
53
52
|
export declare const CONFIG_HEADERS: {
|
|
54
53
|
readonly PROVIDER: "x-llm-provider";
|
package/dist/config/types.js
CHANGED
|
@@ -12,22 +12,6 @@ export function parseProviderKey(key) {
|
|
|
12
12
|
// Note: apiKey is intentionally absent from all defaults.
|
|
13
13
|
// If a user reaches the defaults with no key configured anywhere,
|
|
14
14
|
// the system fails clearly at the LLM call — never silently sends an empty key.
|
|
15
|
-
export const OLLAMA_DEFAULTS = {
|
|
16
|
-
deploymentMode: "local",
|
|
17
|
-
summarization: {
|
|
18
|
-
provider: "openai",
|
|
19
|
-
providerName: "ollama",
|
|
20
|
-
model: "qwen2.5-coder:3b", // code-aware, 3B params, runs on ~2GB RAM
|
|
21
|
-
baseUrl: "http://localhost:11434/v1",
|
|
22
|
-
batchSize: 50,
|
|
23
|
-
},
|
|
24
|
-
embedding: {
|
|
25
|
-
provider: "ollama",
|
|
26
|
-
model: "nomic-embed-text", // best local embedding model, 768 dims
|
|
27
|
-
baseUrl: "http://localhost:11434/v1",
|
|
28
|
-
},
|
|
29
|
-
// neo4j absent — file-only mode is the safe default
|
|
30
|
-
};
|
|
31
15
|
export const ANTHROPIC_DEFAULTS = {
|
|
32
16
|
deploymentMode: "local",
|
|
33
17
|
summarization: {
|
|
@@ -58,12 +42,12 @@ export const CONFIG_HEADERS = {
|
|
|
58
42
|
PROVIDER_NAME: "x-llm-provider-name", // e.g. "deepseek" — brand identity
|
|
59
43
|
MODEL: "x-llm-model", // e.g. "claude-haiku-4-5"
|
|
60
44
|
API_KEY: "x-llm-key",
|
|
61
|
-
BASE_URL: "x-llm-base-url", //
|
|
45
|
+
BASE_URL: "x-llm-base-url", // e.g. "https://api.deepseek.com"
|
|
62
46
|
BATCH_SIZE: "x-batch-size", // e.g. "30"
|
|
63
47
|
EMBED_PROVIDER: "x-embed-provider",
|
|
64
48
|
EMBED_MODEL: "x-embed-model",
|
|
65
49
|
EMBED_KEY: "x-embed-key",
|
|
66
|
-
EMBED_BASE_URL: "x-embed-base-url", //
|
|
50
|
+
EMBED_BASE_URL: "x-embed-base-url", // cloud embedding endpoint
|
|
67
51
|
NEO4J_URL: "x-neo4j-url",
|
|
68
52
|
NEO4J_USER: "x-neo4j-user",
|
|
69
53
|
NEO4J_PASSWORD: "x-neo4j-password",
|
package/dist/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export * from "./pipeline/index.js";
|
|
|
9
9
|
export * from "./config/types.js";
|
|
10
10
|
export { queue } from "./jobs/index.js";
|
|
11
11
|
export { storage } from "./storage/index.js";
|
|
12
|
-
export { resolveConfig, initConfig, maskConfig, writeConfig, resolveAllProviders, setActiveProvider, removeProvider as removeProviderConfig, } from "./config/index.js";
|
|
12
|
+
export { resolveConfig, hasSummarizationConfigured, initConfig, maskConfig, writeConfig, resolveAllProviders, setActiveProvider, removeProvider as removeProviderConfig, } from "./config/index.js";
|
|
13
13
|
export type { AllProvidersResult } from "./config/index.js";
|
|
14
14
|
export { readPackageDependencies, categorizeLibrary } from "./graph/thirdPartyLibs.js";
|
|
15
15
|
export { analyzePipeline } from "./pipeline/index.js";
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ export * from "./config/types.js";
|
|
|
11
11
|
export { queue } from "./jobs/index.js";
|
|
12
12
|
export { storage } from "./storage/index.js";
|
|
13
13
|
// Config helpers
|
|
14
|
-
export { resolveConfig, initConfig, maskConfig, writeConfig, resolveAllProviders, setActiveProvider, removeProvider as removeProviderConfig, } from "./config/index.js";
|
|
14
|
+
export { resolveConfig, hasSummarizationConfigured, initConfig, maskConfig, writeConfig, resolveAllProviders, setActiveProvider, removeProvider as removeProviderConfig, } from "./config/index.js";
|
|
15
15
|
// Pre-scan helpers
|
|
16
16
|
export { readPackageDependencies, categorizeLibrary } from "./graph/thirdPartyLibs.js";
|
|
17
17
|
// Pipeline & analysis
|
|
@@ -116,7 +116,18 @@ export class InMemoryQueue {
|
|
|
116
116
|
this._markCancelled(jobId, true);
|
|
117
117
|
return true;
|
|
118
118
|
}
|
|
119
|
-
//
|
|
119
|
+
// Paused: NO runner loop is active (the summarizer returned at the
|
|
120
|
+
// pause checkpoint), so a cancelRequested flag would sit forever —
|
|
121
|
+
// resumeJob() even clears it. Mark cancelled right here. The
|
|
122
|
+
// checkpoint file stays on disk (cleanedUp=false), so a later
|
|
123
|
+
// summarize resumes from where it stopped.
|
|
124
|
+
if (job.status === "paused") {
|
|
125
|
+
job.cancelRequested = true;
|
|
126
|
+
job.pauseRequested = false;
|
|
127
|
+
this._markCancelled(jobId, false);
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
// Running — signal the runner (checked between batches)
|
|
120
131
|
job.cancelRequested = true;
|
|
121
132
|
job.pauseRequested = false; // clear pause signal if both were set
|
|
122
133
|
console.log(`🚫 Cancel requested for job ${jobId}`);
|
package/dist/pipeline/index.d.ts
CHANGED
|
@@ -46,6 +46,7 @@ export interface PipelineResult {
|
|
|
46
46
|
isGithubRepo: boolean;
|
|
47
47
|
gitInfo: GitInfo;
|
|
48
48
|
}
|
|
49
|
+
export declare function worktreeCommitHash(nodes: CodeNode[]): string;
|
|
49
50
|
export declare function routesToCodeNodes(routes: (RouteNode | BackendRouteNode)[], repoPath: string): CodeNode[];
|
|
50
51
|
export declare function analyzePipeline(repoPath: string, isGithubRepo: boolean, options?: PipelineOptions): Promise<PipelineResult>;
|
|
51
52
|
export declare function refilterPipeline(stored: PipelineResult, thresholds: FilterThresholds): Pick<PipelineResult, "nodes" | "edges" | "stats">;
|
package/dist/pipeline/index.js
CHANGED
|
@@ -15,6 +15,12 @@ function generateGraphId(repoPath, isGithubRepo) {
|
|
|
15
15
|
.digest("hex")
|
|
16
16
|
.slice(0, 16);
|
|
17
17
|
}
|
|
18
|
+
// Stable id for repos without git: derived from node ids + code hashes, so the
|
|
19
|
+
// same content overwrites one snapshot in place instead of accumulating timestamps
|
|
20
|
+
export function worktreeCommitHash(nodes) {
|
|
21
|
+
const parts = nodes.map((n) => `${n.id}:${n.codeHash ?? ""}`).sort();
|
|
22
|
+
return "worktree-" + createHash("sha256").update(parts.join("|")).digest("hex").slice(0, 12);
|
|
23
|
+
}
|
|
18
24
|
// Gets current git state of the repo
|
|
19
25
|
// Falls back gracefully if git is not initialized
|
|
20
26
|
function getGitInfo(repoPath) {
|
|
@@ -28,9 +34,11 @@ function getGitInfo(repoPath) {
|
|
|
28
34
|
return { commitHash, branch, message, hasGit: true };
|
|
29
35
|
}
|
|
30
36
|
catch {
|
|
31
|
-
// No git, or no commits yet
|
|
37
|
+
// No git, or no commits yet. Placeholder resolved to a content-derived
|
|
38
|
+
// id after extraction (see worktreeCommitHash) so unchanged repos reuse
|
|
39
|
+
// one snapshot instead of growing a new timestamped one per analyze
|
|
32
40
|
return {
|
|
33
|
-
commitHash:
|
|
41
|
+
commitHash: "worktree-pending",
|
|
34
42
|
branch: "unknown",
|
|
35
43
|
message: "no git history",
|
|
36
44
|
hasGit: false,
|
|
@@ -221,6 +229,10 @@ export async function analyzePipeline(repoPath, isGithubRepo, options) {
|
|
|
221
229
|
}
|
|
222
230
|
const allNodes = extractorResult.nodes;
|
|
223
231
|
const allEdges = extractorResult.edges;
|
|
232
|
+
if (!gitInfo.hasGit) {
|
|
233
|
+
gitInfo.commitHash = worktreeCommitHash(allNodes);
|
|
234
|
+
gitInfo.message = "worktree snapshot";
|
|
235
|
+
}
|
|
224
236
|
// Step 5: Score and filter
|
|
225
237
|
console.log("\n[5/5] Scoring and filtering...");
|
|
226
238
|
const scoringResult = scoreAndFilter(allNodes, allEdges, options?.thresholds);
|
|
@@ -21,6 +21,7 @@ export interface CommitSummary {
|
|
|
21
21
|
edgeCount: number;
|
|
22
22
|
hasGit: boolean;
|
|
23
23
|
isSummarized?: boolean;
|
|
24
|
+
isIndexed?: boolean;
|
|
24
25
|
}
|
|
25
26
|
export interface GraphMeta {
|
|
26
27
|
graphId: string;
|
|
@@ -101,6 +102,8 @@ export declare function deleteCommit(graphId: string, commitHash: string): boole
|
|
|
101
102
|
export declare function diffCommits(graphId: string, fromHash: string, toHash: string): NodeDiff | undefined;
|
|
102
103
|
export declare function markCommitSummarized(graphId: string, commitHash: string): void;
|
|
103
104
|
export declare function isCommitSummarized(graphId: string, commitHash: string): boolean;
|
|
105
|
+
export declare function markCommitIndexed(graphId: string, commitHash: string): void;
|
|
106
|
+
export declare function isCommitIndexed(graphId: string, commitHash: string): boolean;
|
|
104
107
|
export declare function findLastSummarizedAncestor(graphId: string, commitHash: string, repoPath: string): Promise<string | undefined>;
|
|
105
108
|
export declare function saveNodeSummaries(graphId: string, commitHash: string, nodeUpdates: Map<string, {
|
|
106
109
|
technicalSummary: string;
|
|
@@ -182,9 +182,14 @@ export function saveGraph(result, options) {
|
|
|
182
182
|
commits: [],
|
|
183
183
|
summarizedCommits: [],
|
|
184
184
|
};
|
|
185
|
-
// Replace if same commit already exists, else append
|
|
185
|
+
// Replace if same commit already exists, else append.
|
|
186
|
+
// Carry over flags the rebuilt summary does not know about (same hazard as
|
|
187
|
+
// isSummarized: buildCommitSummary starts from scratch)
|
|
186
188
|
const existingCommit = meta.commits.findIndex((c) => c.commitHash === commitHash);
|
|
187
189
|
if (existingCommit >= 0) {
|
|
190
|
+
const prev = meta.commits[existingCommit];
|
|
191
|
+
if (prev.isIndexed)
|
|
192
|
+
newSummary.isIndexed = true;
|
|
188
193
|
meta.commits[existingCommit] = newSummary;
|
|
189
194
|
}
|
|
190
195
|
else {
|
|
@@ -500,6 +505,23 @@ export function isCommitSummarized(graphId, commitHash) {
|
|
|
500
505
|
return false;
|
|
501
506
|
return meta.summarizedCommits?.includes(commitHash);
|
|
502
507
|
}
|
|
508
|
+
// Marks a commit as having a derived .search.json index
|
|
509
|
+
export function markCommitIndexed(graphId, commitHash) {
|
|
510
|
+
const meta = readMeta(graphId);
|
|
511
|
+
if (!meta)
|
|
512
|
+
return;
|
|
513
|
+
const commit = meta.commits.find((c) => c.commitHash === commitHash);
|
|
514
|
+
if (!commit || commit.isIndexed)
|
|
515
|
+
return;
|
|
516
|
+
commit.isIndexed = true;
|
|
517
|
+
writeMeta(meta);
|
|
518
|
+
}
|
|
519
|
+
export function isCommitIndexed(graphId, commitHash) {
|
|
520
|
+
const meta = readMeta(graphId);
|
|
521
|
+
if (!meta)
|
|
522
|
+
return false;
|
|
523
|
+
return meta.commits.find((c) => c.commitHash === commitHash)?.isIndexed === true;
|
|
524
|
+
}
|
|
503
525
|
// Finds the most recent ancestor commit that has been summarized.
|
|
504
526
|
// Uses simple-git to walk the commit history of the repo.
|
|
505
527
|
// Returns undefined if no summarized ancestor exists (= full summarization needed).
|
|
@@ -601,6 +623,8 @@ export const fileStorage = {
|
|
|
601
623
|
diffCommits,
|
|
602
624
|
markCommitSummarized,
|
|
603
625
|
isCommitSummarized,
|
|
626
|
+
markCommitIndexed,
|
|
627
|
+
isCommitIndexed,
|
|
604
628
|
findLastSummarizedAncestor,
|
|
605
629
|
saveNodeSummaries,
|
|
606
630
|
getCheckpointPath,
|
|
@@ -11,6 +11,8 @@ export interface GraphStorage {
|
|
|
11
11
|
diffCommits(graphId: string, fromHash: string, toHash: string): NodeDiff | undefined;
|
|
12
12
|
markCommitSummarized(graphId: string, commitHash: string): void;
|
|
13
13
|
isCommitSummarized(graphId: string, commitHash: string): boolean;
|
|
14
|
+
markCommitIndexed(graphId: string, commitHash: string): void;
|
|
15
|
+
isCommitIndexed(graphId: string, commitHash: string): boolean;
|
|
14
16
|
findLastSummarizedAncestor(graphId: string, commitHash: string, repoPath: string): Promise<string | undefined>;
|
|
15
17
|
saveNodeSummaries(graphId: string, commitHash: string, nodeUpdates: Map<string, {
|
|
16
18
|
technicalSummary: string;
|
|
@@ -12,13 +12,13 @@ export function createLLMClient(config) {
|
|
|
12
12
|
const effectiveBase = baseUrl ?? entry?.baseUrl;
|
|
13
13
|
switch (provider) {
|
|
14
14
|
case "openai": {
|
|
15
|
-
if (
|
|
16
|
-
throw new Error(`
|
|
17
|
-
return new OpenAIClient(apiKey
|
|
15
|
+
if (!apiKey)
|
|
16
|
+
throw new Error(`No API key configured for "${providerName}" — run "devlens init" to set one up.`);
|
|
17
|
+
return new OpenAIClient(apiKey, model, effectiveBase, providerName);
|
|
18
18
|
}
|
|
19
19
|
case "anthropic": {
|
|
20
20
|
if (!apiKey)
|
|
21
|
-
throw new Error(`
|
|
21
|
+
throw new Error(`No API key configured for "${providerName}" — run "devlens init" to set one up.`);
|
|
22
22
|
return new AnthropicClient(apiKey, model, effectiveBase, providerName);
|
|
23
23
|
}
|
|
24
24
|
default:
|
|
@@ -26,7 +26,7 @@ function parseResponse(raw) {
|
|
|
26
26
|
tokensUsed: 0,
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
|
-
// Also used as the base for
|
|
29
|
+
// Also used as the base for other OpenAI-compatible providers —
|
|
30
30
|
// both expose OpenAI-compatible APIs, just with a different baseURL.
|
|
31
31
|
export class OpenAIClient {
|
|
32
32
|
constructor(apiKey, model, baseURL, providerName) {
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devlensio",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Codebase intelligence engine for TypeScript/JavaScript/React/Next.js, Python, Java, Go, and Rust repositories.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -21,7 +21,14 @@
|
|
|
21
21
|
"prepack": "node extractors/java/build.mjs && node extractors/go/build.mjs && node extractors/rust/build.mjs",
|
|
22
22
|
"postinstall": "node extractors/python/setup.mjs"
|
|
23
23
|
},
|
|
24
|
-
"keywords": [
|
|
24
|
+
"keywords": [
|
|
25
|
+
"codebase",
|
|
26
|
+
"graph",
|
|
27
|
+
"ast",
|
|
28
|
+
"typescript",
|
|
29
|
+
"javascript",
|
|
30
|
+
"devtools"
|
|
31
|
+
],
|
|
25
32
|
"author": "",
|
|
26
33
|
"license": "AGPL-3.0",
|
|
27
34
|
"files": [
|
|
@@ -63,4 +70,4 @@
|
|
|
63
70
|
"ts-node-dev": "^2.0.0",
|
|
64
71
|
"typescript": "^5.9.3"
|
|
65
72
|
}
|
|
66
|
-
}
|
|
73
|
+
}
|