llm-lean-log-cli
Advanced tools
Sorry, the diff of this file is too big to display
| #!/usr/bin/env bun | ||
| /** | ||
| * CLI tool for llm-lean-log | ||
| */ | ||
| /** | ||
| * Main function for the CLI | ||
| */ | ||
| export declare function main(): Promise<void>; | ||
| //# sourceMappingURL=index.d.ts.map |
| {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";AACA;;GAEG;AA4FH;;GAEG;AACH,wBAAsB,IAAI,kBAuJzB"} |
+12
-6
| { | ||
| "name": "llm-lean-log-cli", | ||
| "version": "0.2.2", | ||
| "version": "0.2.4", | ||
| "description": "CLI tool for llm-lean-log", | ||
| "scripts": { | ||
| "start": "bun run src/index.ts" | ||
| "start": "bun run src/index.ts", | ||
| "build": "bun build src/index.ts --outfile dist/index.js --target node --minify && tsc --emitDeclarationOnly", | ||
| "prepublishOnly": "bun run build", | ||
| "type": "tsc --noEmit" | ||
| }, | ||
@@ -17,10 +20,10 @@ "repository": { | ||
| "homepage": "https://github.com/loclv/llm-lean-log/tree/main/packages/cli#readme", | ||
| "main": "src/index.ts", | ||
| "module": "src/index.ts", | ||
| "main": "dist/index.js", | ||
| "module": "dist/index.js", | ||
| "type": "module", | ||
| "bin": { | ||
| "l-log": "src/index.ts" | ||
| "l-log": "dist/index.js" | ||
| }, | ||
| "files": [ | ||
| "src", | ||
| "dist", | ||
| "README.md", | ||
@@ -38,2 +41,5 @@ "LICENSE" | ||
| }, | ||
| "peerDependencies": { | ||
| "typescript": "^5.9.3" | ||
| }, | ||
| "keywords": [ | ||
@@ -40,0 +46,0 @@ "llm", |
| import { | ||
| afterEach, | ||
| beforeEach, | ||
| describe, | ||
| expect, | ||
| it, | ||
| mock, | ||
| spyOn, | ||
| } from "bun:test"; | ||
| import * as core from "llm-lean-log-core"; | ||
| import pkg from "../package.json"; | ||
| import { main } from "./index"; | ||
| // Mock core functions | ||
| mock.module("llm-lean-log-core", () => ({ | ||
| loadLogs: mock(() => Promise.resolve([])), | ||
| saveLogs: mock(() => Promise.resolve()), | ||
| addLogEntry: mock((entries, entry) => [ | ||
| ...entries, | ||
| { ...entry, id: "test-id", "created-at": "2024-01-01" }, | ||
| ]), | ||
| filterByTags: mock(() => []), | ||
| searchLogs: mock(() => []), | ||
| visualizeEntry: mock(() => "visualized entry"), | ||
| visualizeStats: mock(() => "visualized stats"), | ||
| visualizeTable: mock(() => "visualized table"), | ||
| })); | ||
| describe("CLI", () => { | ||
| let consoleLogSpy: any; | ||
| let consoleErrorSpy: any; | ||
| let processExitSpy: any; | ||
| let bunFileSpy: any; | ||
| let bunWriteSpy: any; | ||
| let originalArgv: string[]; | ||
| beforeEach(() => { | ||
| consoleLogSpy = spyOn(console, "log").mockImplementation(() => {}); | ||
| consoleErrorSpy = spyOn(console, "error").mockImplementation(() => {}); | ||
| processExitSpy = spyOn(process, "exit").mockImplementation( | ||
| (code?: string | number | null | undefined) => { | ||
| throw new Error(`process.exit(${code})`); | ||
| }, | ||
| ); | ||
| bunFileSpy = spyOn(Bun, "file").mockImplementation( | ||
| () => | ||
| ({ | ||
| exists: () => Promise.resolve(true), | ||
| text: () => Promise.resolve(""), | ||
| }) as any, | ||
| ); | ||
| bunWriteSpy = spyOn(Bun, "write").mockImplementation(() => | ||
| Promise.resolve(0), | ||
| ); | ||
| originalArgv = process.argv; | ||
| }); | ||
| afterEach(() => { | ||
| consoleLogSpy.mockRestore(); | ||
| consoleErrorSpy.mockRestore(); | ||
| processExitSpy.mockRestore(); | ||
| bunFileSpy.mockRestore(); | ||
| bunWriteSpy.mockRestore(); | ||
| process.argv = originalArgv; | ||
| }); | ||
| const runCommand = async (args: string[]) => { | ||
| process.argv = ["bun", "index.ts", ...args]; | ||
| await main(); | ||
| }; | ||
| it("should show help message with 'help' command", async () => { | ||
| await runCommand(["help"]); | ||
| expect(consoleLogSpy).toHaveBeenCalled(); | ||
| expect(consoleLogSpy.mock.calls[0][0]).toContain("l-log CLI"); | ||
| }); | ||
| it("should show help message with '--help' flag", async () => { | ||
| await runCommand(["--help"]); | ||
| expect(consoleLogSpy).toHaveBeenCalled(); | ||
| expect(consoleLogSpy.mock.calls[0][0]).toContain("l-log CLI"); | ||
| }); | ||
| it("should show version with '--version' flag", async () => { | ||
| await runCommand(["--version"]); | ||
| expect(consoleLogSpy).toHaveBeenCalledWith(pkg.version); | ||
| }); | ||
| it("should show version with '-v' flag", async () => { | ||
| await runCommand(["-v"]); | ||
| expect(consoleLogSpy).toHaveBeenCalledWith(pkg.version); | ||
| }); | ||
| it("should show version with '-V' flag", async () => { | ||
| await runCommand(["-V"]); | ||
| expect(consoleLogSpy).toHaveBeenCalledWith(pkg.version); | ||
| }); | ||
| it("should call loadLogs and visualizeTable for 'list' command", async () => { | ||
| const { loadLogs, visualizeTable } = core as any; | ||
| loadLogs.mockResolvedValueOnce([ | ||
| { id: "1", name: "test", problem: "p", "created-at": "t" }, | ||
| ]); | ||
| await runCommand(["list"]); | ||
| expect(loadLogs).toHaveBeenCalled(); | ||
| expect(visualizeTable).toHaveBeenCalled(); | ||
| expect(consoleLogSpy).toHaveBeenCalledWith("visualized table"); | ||
| }); | ||
| it("should work with 'ls' alias", async () => { | ||
| const { loadLogs, visualizeTable } = core as any; | ||
| await runCommand(["ls"]); | ||
| expect(loadLogs).toHaveBeenCalled(); | ||
| expect(visualizeTable).toHaveBeenCalled(); | ||
| }); | ||
| it("should call visualizeStats for 'stats' command", async () => { | ||
| const { visualizeStats } = core as any; | ||
| await runCommand(["stats"]); | ||
| expect(visualizeStats).toHaveBeenCalled(); | ||
| expect(consoleLogSpy).toHaveBeenCalledWith("visualized stats"); | ||
| }); | ||
| it("should add a new log entry with 'add' command", async () => { | ||
| const { saveLogs, addLogEntry } = core; | ||
| await runCommand([ | ||
| "add", | ||
| "New Log", | ||
| "--problem=Test Problem", | ||
| "--tags=test,cli", | ||
| ]); | ||
| expect(addLogEntry).toHaveBeenCalled(); | ||
| expect(saveLogs).toHaveBeenCalled(); | ||
| expect(consoleLogSpy).toHaveBeenCalledWith("Log entry added successfully"); | ||
| // Verify that saveLogs was called with the expected entries | ||
| const savedEntries = (saveLogs as any).mock.calls[0][1]; | ||
| const lastEntry = savedEntries[savedEntries.length - 1]; | ||
| expect(lastEntry.name).toBe("New Log"); | ||
| expect(lastEntry.problem).toBe("Test Problem"); | ||
| expect(lastEntry.tags).toBe("test,cli"); | ||
| expect(lastEntry["created-at"]).toBeDefined(); | ||
| expect(lastEntry.id).toBeDefined(); | ||
| }); | ||
| it("should show error and exit if 'add' is missing problem", async () => { | ||
| try { | ||
| await runCommand(["add", "New Log"]); | ||
| } catch (e: any) { | ||
| expect(e.message).toBe("process.exit(1)"); | ||
| } | ||
| expect(consoleErrorSpy).toHaveBeenCalledWith( | ||
| expect.stringContaining("Please provide a problem description"), | ||
| ); | ||
| }); | ||
| it("should show error and exit if 'add' is missing name", async () => { | ||
| try { | ||
| await runCommand(["add"]); | ||
| } catch (e: any) { | ||
| expect(e.message).toBe("process.exit(1)"); | ||
| } | ||
| expect(consoleErrorSpy).toHaveBeenCalledWith( | ||
| expect.stringContaining("Please provide a log name"), | ||
| ); | ||
| }); | ||
| it("should search logs with 'search' command", async () => { | ||
| const { searchLogs, visualizeTable } = core as any; | ||
| searchLogs.mockReturnValueOnce([]); | ||
| await runCommand(["search", "query"]); | ||
| expect(searchLogs).toHaveBeenCalled(); | ||
| expect(visualizeTable).toHaveBeenCalled(); | ||
| }); | ||
| it("should show error and exit if 'search' is missing query", async () => { | ||
| try { | ||
| await runCommand(["search"]); | ||
| } catch (e: any) { | ||
| expect(e.message).toBe("process.exit(1)"); | ||
| } | ||
| expect(consoleErrorSpy).toHaveBeenCalledWith( | ||
| expect.stringContaining("Please provide a search query"), | ||
| ); | ||
| }); | ||
| it("should filter logs by tags with 'tags' command", async () => { | ||
| const { filterByTags, visualizeTable } = core as any; | ||
| filterByTags.mockReturnValueOnce([]); | ||
| await runCommand(["tags", "tag1", "tag2"]); | ||
| expect(filterByTags).toHaveBeenCalled(); | ||
| expect(visualizeTable).toHaveBeenCalled(); | ||
| }); | ||
| it("should show error and exit if 'tags' is missing tags", async () => { | ||
| try { | ||
| await runCommand(["tags"]); | ||
| } catch (e: any) { | ||
| expect(e.message).toBe("process.exit(1)"); | ||
| } | ||
| expect(consoleErrorSpy).toHaveBeenCalledWith( | ||
| expect.stringContaining("Please provide at least one tag"), | ||
| ); | ||
| }); | ||
| it("should view entry at index", async () => { | ||
| const { loadLogs, visualizeEntry } = core as any; | ||
| loadLogs.mockResolvedValueOnce([ | ||
| { id: "1", name: "test", problem: "p", "created-at": "t" }, | ||
| ]); | ||
| await runCommand(["view", "0"]); | ||
| expect(visualizeEntry).toHaveBeenCalled(); | ||
| expect(consoleLogSpy).toHaveBeenCalledWith("visualized entry"); | ||
| }); | ||
| it("should view last entry with --last flag", async () => { | ||
| const { loadLogs, visualizeEntry } = core as any; | ||
| loadLogs.mockResolvedValueOnce([ | ||
| { id: "1", name: "test1", problem: "p1", "created-at": "t1" }, | ||
| { id: "2", name: "test2", problem: "p2", "created-at": "t2" }, | ||
| ]); | ||
| await runCommand(["view", "--last"]); | ||
| expect(visualizeEntry).toHaveBeenCalledWith( | ||
| expect.objectContaining({ id: "2" }), | ||
| expect.any(Object), | ||
| ); | ||
| }); | ||
| it("should show error and exit if 'view' has no logs", async () => { | ||
| const { loadLogs } = core as any; | ||
| loadLogs.mockResolvedValueOnce([]); | ||
| try { | ||
| await runCommand(["view", "0"]); | ||
| } catch (e: any) { | ||
| expect(e.message).toBe("process.exit(1)"); | ||
| } | ||
| expect(consoleErrorSpy).toHaveBeenCalledWith( | ||
| expect.stringContaining("No log entries found"), | ||
| ); | ||
| }); | ||
| it("should show error and exit if 'view' index is NaN", async () => { | ||
| const { loadLogs } = core as any; | ||
| loadLogs.mockResolvedValueOnce([{ id: "1" }]); | ||
| try { | ||
| await runCommand(["view", "abc"]); | ||
| } catch (e: any) { | ||
| expect(e.message).toBe("process.exit(1)"); | ||
| } | ||
| expect(consoleErrorSpy).toHaveBeenCalledWith( | ||
| expect.stringContaining("Please provide a valid entry index"), | ||
| ); | ||
| }); | ||
| it("should show error and exit if 'view' index is out of range", async () => { | ||
| const { loadLogs } = core as any; | ||
| loadLogs.mockResolvedValueOnce([{ id: "1" }]); | ||
| try { | ||
| await runCommand(["view", "10"]); | ||
| } catch (e: any) { | ||
| expect(e.message).toBe("process.exit(1)"); | ||
| } | ||
| expect(consoleErrorSpy).toHaveBeenCalledWith( | ||
| expect.stringContaining("out of range"), | ||
| ); | ||
| }); | ||
| it("should show error and exit if entry is missing at index", async () => { | ||
| const { loadLogs } = core as any; | ||
| // Create an array with a hole or undefined | ||
| loadLogs.mockResolvedValueOnce([undefined]); | ||
| try { | ||
| await runCommand(["view", "0"]); | ||
| } catch (e: any) { | ||
| expect(e.message).toBe("process.exit(1)"); | ||
| } | ||
| expect(consoleErrorSpy).toHaveBeenCalledWith( | ||
| expect.stringContaining("Entry not found at index 0"), | ||
| ); | ||
| }); | ||
| it("should show error for unknown command", async () => { | ||
| try { | ||
| await runCommand(["unknown"]); | ||
| } catch (e: any) { | ||
| expect(e.message).toBe("process.exit(1)"); | ||
| } | ||
| expect(consoleErrorSpy).toHaveBeenCalledWith( | ||
| expect.stringContaining('Unknown command "unknown"'), | ||
| ); | ||
| }); | ||
| it("should show help when no command is provided", async () => { | ||
| await runCommand([]); | ||
| expect(consoleLogSpy).toHaveBeenCalled(); | ||
| expect(consoleLogSpy.mock.calls[0][0]).toContain("l-log CLI"); | ||
| }); | ||
| it("should use custom log file if provided", async () => { | ||
| const { loadLogs } = core as any; | ||
| await runCommand(["list", "custom.csv"]); | ||
| expect(loadLogs).toHaveBeenCalledWith("custom.csv"); | ||
| }); | ||
| }); |
-230
| #!/usr/bin/env bun | ||
| /** | ||
| * CLI tool for llm-lean-log | ||
| */ | ||
| import { | ||
| addLogEntry, | ||
| filterByTags, | ||
| loadLogs, | ||
| saveLogs, | ||
| searchLogs, | ||
| visualizeEntry, | ||
| visualizeStats, | ||
| visualizeTable, | ||
| } from "llm-lean-log-core"; | ||
| import pkg from "../package.json"; | ||
| /** | ||
| * Help text for LLMs, not human! | ||
| */ | ||
| const helpText = `l-log CLI | ||
| Usage: l-log <command> [log-file] [options] | ||
| Commands: | ||
| list, ls List all log entries | ||
| --compact, -c Show compact view | ||
| --human Show human-readable output (with colors) | ||
| stats Show log statistics | ||
| --human Show human-readable output (with colors) | ||
| view <index> View detailed entry at index | ||
| --last Show the last log entry | ||
| --human Show human-readable output (with colors) | ||
| search <query> Search logs by name, problem, or solution | ||
| --human Show human-readable output (with colors) | ||
| tags <tag1> [tag2] Filter logs by tags | ||
| --human Show human-readable output (with colors) | ||
| add <name> Add a new log entry | ||
| --tags=<tags> Comma-separated tags | ||
| --problem=<text> Problem description | ||
| --solution=<text> Solution description | ||
| --action=<text> Action taken | ||
| --files=<files> Comma-separated files paths | ||
| --tech-stack=<tech> Comma-separated tech stack | ||
| --model=<name> Model name | ||
| --causeIds=<ids> Comma-separated cause log row UUIDs | ||
| --effectIds=<ids> Comma-separated effect log row UUIDs | ||
| --last-commit-short-sha=<sha> Last git commit short SHA | ||
| --created-at=<time> Creation time (ISO 8601 formatted string) | ||
| --updated-at=<time> Update time (ISO 8601 formatted string) | ||
| --created-by-agent=<name> Agent model name | ||
| help, -h, --help Show this help message | ||
| -v, -V, --version Show version number | ||
| Examples for LLMs: | ||
| l-log list ./logs/example.csv | ||
| l-log stats | ||
| l-log view 0 | ||
| l-log view --last | ||
| l-log search "memory" | ||
| l-log tags error api | ||
| l-log add ./logs/chat1.csv "Fix bug" --tags=bug,fix --problem="Bug description" --files="file1.ts,src/file2.ts" --tech-stack="ts,react" --causeIds="721ace2b-5e73-4901-bef9-97de16bf170f" --last-commit-short-sha="a1b2c3d" --model="gpt-4o" | ||
| `; | ||
| /** | ||
| * Main function for the CLI | ||
| */ | ||
| export async function main() { | ||
| const args = process.argv.slice(2); | ||
| const command = args[0]; | ||
| // Check if second argument is a file (ends with .csv) or a parameter | ||
| const isLogFile = (arg: string) => arg?.endsWith(".csv"); | ||
| const secondArg = args[1]; | ||
| const hasLogFile = secondArg && isLogFile(secondArg); | ||
| const logFile: string = hasLogFile ? secondArg : "./logs/example.csv"; | ||
| // Get the index where actual parameters start | ||
| const paramStart = hasLogFile ? 2 : 1; | ||
| let entries = await loadLogs(logFile); | ||
| const isHuman = args.includes("--human"); | ||
| const llm = !isHuman; | ||
| switch (command) { | ||
| case "list": | ||
| case "ls": { | ||
| const compact = args.includes("--compact") || args.includes("-c") || llm; | ||
| console.log(visualizeTable(entries, { compact, llm })); | ||
| break; | ||
| } | ||
| case "stats": { | ||
| console.log(visualizeStats(entries, { llm })); | ||
| break; | ||
| } | ||
| case "view": { | ||
| const isLast = args.includes("--last"); | ||
| const index = isLast | ||
| ? entries.length - 1 | ||
| : parseInt(args[paramStart] || "0", 10); | ||
| if (entries.length === 0) { | ||
| console.error("Error: No log entries found"); | ||
| process.exit(1); | ||
| } | ||
| if (Number.isNaN(index)) { | ||
| console.error("Error: Please provide a valid entry index"); | ||
| process.exit(1); | ||
| } | ||
| if (index >= entries.length || index < 0) { | ||
| console.error( | ||
| `Error: Index ${index} out of range (0-${entries.length - 1})`, | ||
| ); | ||
| process.exit(1); | ||
| } | ||
| const entry = entries[index]; | ||
| if (!entry) { | ||
| console.error(`Error: Entry not found at index ${index}`); | ||
| process.exit(1); | ||
| } | ||
| console.log(visualizeEntry(entry, { llm })); | ||
| break; | ||
| } | ||
| case "search": { | ||
| const query = args[paramStart]; | ||
| if (!query) { | ||
| console.error("Error: Please provide a search query"); | ||
| process.exit(1); | ||
| } | ||
| const results = searchLogs(entries, query); | ||
| const compact = args.includes("--compact") || args.includes("-c") || llm; | ||
| console.log(visualizeTable(results, { compact, llm })); | ||
| break; | ||
| } | ||
| case "tags": { | ||
| const tagsList = args.slice(paramStart).filter((a) => a !== "--human"); | ||
| if (tagsList.length === 0) { | ||
| console.error("Error: Please provide at least one tag"); | ||
| process.exit(1); | ||
| } | ||
| const results = filterByTags(entries, tagsList); | ||
| const compact = args.includes("--compact") || args.includes("-c") || llm; | ||
| console.log(visualizeTable(results, { compact, llm })); | ||
| break; | ||
| } | ||
| case "add": { | ||
| const name = args[paramStart]; | ||
| if (!name) { | ||
| console.error("Error: Please provide a log name"); | ||
| process.exit(1); | ||
| } | ||
| const findFlag = (flag: string): string | undefined => { | ||
| const arg = args.find((a) => a.startsWith(`${flag}=`)); | ||
| return arg ? arg.split("=")[1] : undefined; | ||
| }; | ||
| const problem = findFlag("--problem"); | ||
| if (!problem) { | ||
| console.error( | ||
| "Error: Please provide a problem description with --problem=<text>", | ||
| ); | ||
| process.exit(1); | ||
| } | ||
| entries = addLogEntry(entries, { | ||
| name, | ||
| problem, | ||
| tags: findFlag("--tags"), | ||
| solution: findFlag("--solution"), | ||
| action: findFlag("--action"), | ||
| files: findFlag("--files"), | ||
| "tech-stack": findFlag("--tech-stack"), | ||
| model: findFlag("--model"), | ||
| id: findFlag("--id"), | ||
| causeIds: findFlag("--causeIds"), | ||
| effectIds: findFlag("--effectIds"), | ||
| "last-commit-short-sha": findFlag("--last-commit-short-sha"), | ||
| "created-at": findFlag("--created-at"), | ||
| "updated-at": findFlag("--updated-at"), | ||
| "created-by-agent": findFlag("--created-by-agent"), | ||
| }); | ||
| await saveLogs(logFile, entries); | ||
| console.log("Log entry added successfully"); | ||
| break; | ||
| } | ||
| case "help": | ||
| case "--help": | ||
| case "-h": | ||
| console.log(helpText); | ||
| break; | ||
| case "-v": | ||
| case "-V": | ||
| case "--version": | ||
| console.log(pkg.version); | ||
| break; | ||
| default: | ||
| if (command) { | ||
| console.error(`Error: Unknown command "${command}"`); | ||
| console.log(helpText); | ||
| process.exit(1); | ||
| } else { | ||
| console.log(helpText); | ||
| } | ||
| break; | ||
| } | ||
| } | ||
| if (import.meta.main) { | ||
| main().catch((error) => { | ||
| console.error("Error:", error.message); | ||
| process.exit(1); | ||
| }); | ||
| } |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
1148831
5504.33%6
20%4372
824.31%0
-100%2
100%1
Infinity%