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:
+64
-10
@@ -4,6 +4,8 @@ import { patchFile } from "./patch.js";
|
||||
import { validateMaps } from "./validate.js";
|
||||
import { reinitPath } from "./init.js";
|
||||
import { discoverProject } from "./discover.js";
|
||||
import { createLLMClient, LLMError } from "./llm-client.js";
|
||||
import { loadConfig } from "./config.js";
|
||||
import pc from "picocolors";
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
@@ -31,11 +33,22 @@ function printUsage() {
|
||||
console.log(
|
||||
` project-map ${pc.cyan("--version")} Show version\n`,
|
||||
);
|
||||
console.log(`${pc.bold("Options:")}`);
|
||||
console.log(
|
||||
` --llm-provider=openai|kimi LLM provider (default: config or openai)`,
|
||||
);
|
||||
console.log(
|
||||
` --llm-model=<model> LLM model name (or set LLM_MODEL env var)`,
|
||||
);
|
||||
console.log(` --llm-base-url=<url> Custom base URL for LLM API\n`);
|
||||
console.log(`${pc.bold("Examples:")}`);
|
||||
console.log(` project-map init`);
|
||||
console.log(` project-map patch src/components/Button.tsx`);
|
||||
console.log(` project-map validate --fix`);
|
||||
console.log(` project-map reinit`);
|
||||
console.log(
|
||||
` project-map init --llm-provider=kimi --llm-model=kimi-k2-6`,
|
||||
);
|
||||
}
|
||||
|
||||
function printVersion() {
|
||||
@@ -50,17 +63,49 @@ function formatCount(count: number, label: string): string {
|
||||
return `${pc.bold(String(count))} ${count === 1 ? label : plural}`;
|
||||
}
|
||||
|
||||
function parseValidateArgs(args: string[]): { path: string; fix: boolean } {
|
||||
function parseArgs(args: string[]): {
|
||||
path: string;
|
||||
fix: boolean;
|
||||
llmProvider?: string;
|
||||
llmModel?: string;
|
||||
llmBaseUrl?: string;
|
||||
positional: string[];
|
||||
} {
|
||||
let path = ".";
|
||||
let fix = false;
|
||||
let llmProvider: string | undefined;
|
||||
let llmModel: string | undefined;
|
||||
let llmBaseUrl: string | undefined;
|
||||
const positional: string[] = [];
|
||||
|
||||
for (const arg of args.slice(1)) {
|
||||
if (arg === "--fix") {
|
||||
fix = true;
|
||||
} else if (arg.startsWith("--llm-provider=")) {
|
||||
llmProvider = arg.slice("--llm-provider=".length);
|
||||
} else if (arg.startsWith("--llm-model=")) {
|
||||
llmModel = arg.slice("--llm-model=".length);
|
||||
} else if (arg.startsWith("--llm-base-url=")) {
|
||||
llmBaseUrl = arg.slice("--llm-base-url=".length);
|
||||
} else if (!arg.startsWith("-")) {
|
||||
positional.push(arg);
|
||||
path = arg;
|
||||
}
|
||||
}
|
||||
return { path, fix };
|
||||
|
||||
return { path, fix, llmProvider, llmModel, llmBaseUrl, positional };
|
||||
}
|
||||
|
||||
function createClientFromArgs(args: ReturnType<typeof parseArgs>) {
|
||||
const config = loadConfig();
|
||||
const provider = (args.llmProvider || config.llmProvider) as
|
||||
| "openai"
|
||||
| "kimi"
|
||||
| "pi";
|
||||
return createLLMClient(provider, {
|
||||
model: args.llmModel || config.llmModel || process.env.LLM_MODEL,
|
||||
baseUrl: args.llmBaseUrl || config.llmBaseUrl,
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
@@ -74,13 +119,16 @@ async function main() {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const parsed = parseArgs(args);
|
||||
|
||||
switch (command) {
|
||||
case "init": {
|
||||
const targetPath = args[1] || ".";
|
||||
const targetPath = parsed.positional[0] || ".";
|
||||
const start = Date.now();
|
||||
const entries = discoverProject(targetPath);
|
||||
console.log(`Scanning ${formatCount(entries.length, "directory")}...`);
|
||||
await initProject(targetPath, { verbose: false });
|
||||
const client = createClientFromArgs(parsed);
|
||||
await initProject(targetPath, { verbose: false, llmClient: client });
|
||||
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
||||
console.log(
|
||||
`${pc.green("✓")} Generated ${formatCount(entries.length, ".pi-map.md file")} in ${elapsed}s`,
|
||||
@@ -88,18 +136,19 @@ async function main() {
|
||||
break;
|
||||
}
|
||||
case "patch": {
|
||||
if (!args[1]) {
|
||||
if (!parsed.positional[0]) {
|
||||
console.error(
|
||||
`${pc.red("Error:")} Missing file path. Usage: project-map patch <file>`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
await patchFile(args[1]);
|
||||
const client = createClientFromArgs(parsed);
|
||||
await patchFile(parsed.positional[0], client);
|
||||
console.log(`${pc.green("✓")} Patched`);
|
||||
break;
|
||||
}
|
||||
case "validate": {
|
||||
const { path, fix } = parseValidateArgs(args);
|
||||
const { path, fix } = parsed;
|
||||
const result = await validateMaps(path, { fix, verbose: true });
|
||||
if (result.clean) {
|
||||
console.log(`${pc.green("✓")} All .pi-map.md files are clean.`);
|
||||
@@ -123,13 +172,14 @@ async function main() {
|
||||
break;
|
||||
}
|
||||
case "reinit": {
|
||||
const targetPath = args[1] || ".";
|
||||
const targetPath = parsed.positional[0] || ".";
|
||||
const start = Date.now();
|
||||
const entries = discoverProject(targetPath);
|
||||
console.log(
|
||||
`Regenerating ${formatCount(entries.length, ".pi-map.md file")}...`,
|
||||
);
|
||||
await reinitPath(targetPath, { verbose: false });
|
||||
const client = createClientFromArgs(parsed);
|
||||
await reinitPath(targetPath, { verbose: false, llmClient: client });
|
||||
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
||||
console.log(`${pc.green("✓")} Regenerated in ${elapsed}s`);
|
||||
break;
|
||||
@@ -142,6 +192,10 @@ async function main() {
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(`${pc.red("Error:")} ${err.message}`);
|
||||
if (err instanceof LLMError) {
|
||||
console.error(`${pc.red("LLM Error:")} ${err.message}`);
|
||||
} else {
|
||||
console.error(`${pc.red("Error:")} ${err.message}`);
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user