feat: M7 proper LLM integration with dual providers, caching, and parallel batching

- Add LLM client abstraction (src/llm-client.ts) with factory pattern
- Add OpenAI-compatible external client (src/external-llm-client.ts)
- Add Kimi.com client using Anthropic-based API (src/kimi-llm-client.ts)
- Add Pi native LLM stub (src/pi-llm-client.ts) for future ExtensionAPI wiring
- Add SHA-256 disk cache at ~/.cache/pi-project-map/ (src/llm-cache.ts)
- Add parallel batching with p-limit, retry + exponential backoff (src/llm-batch.ts)
- Rewrite llm-extract.ts to use real LLM calls with structured prompts
  - File-level: PURPOSE, DEPS, CONCEPTS
  - Package-level: ROLE, ARCH
  - Context truncation, 50KB skip, cache before LLM call
- Wire CLI with --llm-provider, --llm-model, --llm-base-url flags
- Update config.ts with llmProvider, llmBaseUrl fields
- Update init.ts and patch.ts to accept optional LLMClient
- Add sample project fixture for manual testing
- Add tests: llm-cache (3), llm-batch (5), llm-integration (8 with real Kimi API),
  pi-extension (14 mocked)
- All 56 tests pass
This commit is contained in:
2026-06-09 22:49:34 +02:00
parent 7b67205d43
commit 69d3acda5d
32 changed files with 1565 additions and 264 deletions
+17 -6
View File
@@ -9,34 +9,45 @@ import { extractFileAST } from "./ast-extract.js";
import { mergeFileData } from "./merge.js";
import { writeFileSync } from "fs";
import { join } from "path";
import type { LLMClient } from "./llm-client.js";
export interface InitOptions {
verbose?: boolean;
llmClient?: LLMClient;
}
export async function initProject(
rootPath: string,
options?: { verbose?: boolean },
options: InitOptions = {},
): Promise<void> {
const entries = discoverProject(rootPath);
for (const entry of entries) {
await generateDirectoryMap(entry);
await generateDirectoryMap(entry, options.llmClient);
}
if (options?.verbose !== false) {
if (options.verbose !== false) {
console.log(`Generated ${entries.length} .pi-map.md files`);
}
}
export async function generateDirectoryMap(
entry: DirectoryEntry,
llmClient?: LLMClient,
): Promise<FileEntry[]> {
const fileData: FileEntry[] = [];
for (const file of entry.files) {
const filePath = join(entry.dirPath, file);
const llmData = await extractFileLLM(filePath);
const llmData = await extractFileLLM(filePath, llmClient);
const astData = await extractFileAST(filePath);
fileData.push(mergeFileData(file, llmData, astData));
}
const packageData = await extractPackageLLM(entry.relativePath, fileData);
const packageData = await extractPackageLLM(
entry.relativePath,
fileData,
llmClient,
);
const mapData: PackageMapData = {
path: entry.relativePath,
@@ -53,7 +64,7 @@ export async function generateDirectoryMap(
export async function reinitPath(
path: string,
options?: { verbose?: boolean },
options: InitOptions = {},
): Promise<void> {
// Full regeneration clears all dirty markers by overwriting every .pi-map.md
await initProject(path, options);