clippy-graph 1.0.0

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.
@@ -0,0 +1,9 @@
1
+ ---
2
+ active: true
3
+ iteration: 1
4
+ max_iterations: 13
5
+ completion_promise: null
6
+ started_at: "2026-02-03T19:46:57Z"
7
+ ---
8
+
9
+ Crie uma CLI em bun + ollama api cloud para traduzir linguagem natural para query de linguagem de grafo para o neo4j --complete-promise DONE
package/bun.lock ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "lockfileVersion": 1,
3
+ "workspaces": {
4
+ "": {
5
+ "name": "clippy-graph",
6
+ "dependencies": {
7
+ "commander": "^14.0.3",
8
+ },
9
+ "devDependencies": {
10
+ "@types/bun": "latest",
11
+ },
12
+ "peerDependencies": {
13
+ "typescript": "^5",
14
+ },
15
+ },
16
+ },
17
+ "packages": {
18
+ "@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="],
19
+
20
+ "@types/node": ["@types/node@25.2.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w=="],
21
+
22
+ "bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="],
23
+
24
+ "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
25
+
26
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
27
+
28
+ "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
29
+ }
30
+ }
package/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { translate } from "./src/translate";
2
+ export { chatCompletion } from "./src/provider";
3
+ export type * from "./src/types";
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "clippy-graph",
3
+ "version": "1.0.0",
4
+ "description": "Translate natural language to Neo4j Cypher queries using LLMs (Ollama / OpenRouter)",
5
+ "type": "module",
6
+ "module": "index.ts",
7
+ "bin": {
8
+ "clippy-graph": "src/cli.ts"
9
+ },
10
+ "scripts": {
11
+ "start": "bun run src/cli.ts"
12
+ },
13
+ "keywords": ["neo4j", "cypher", "natural-language", "llm", "ollama", "openrouter", "graph-database"],
14
+ "license": "MIT",
15
+ "devDependencies": {
16
+ "@types/bun": "latest"
17
+ },
18
+ "peerDependencies": {
19
+ "typescript": "^5"
20
+ },
21
+ "dependencies": {
22
+ "commander": "^14.0.3"
23
+ }
24
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env bun
2
+ import { Command } from "commander";
3
+ import { translate } from "./translate";
4
+ import type { Provider, ProviderConfig } from "./types";
5
+
6
+ const program = new Command();
7
+
8
+ program
9
+ .name("clippy-graph")
10
+ .description("Translate natural language to Neo4j Cypher queries")
11
+ .version("1.0.0");
12
+
13
+ program
14
+ .option(
15
+ "-p, --provider <provider>",
16
+ "LLM provider: ollama or openrouter",
17
+ process.env.CLIPPY_PROVIDER ?? "openrouter"
18
+ )
19
+ .option(
20
+ "-m, --model <model>",
21
+ "Model name to use",
22
+ process.env.CLIPPY_MODEL
23
+ )
24
+ .option(
25
+ "--ollama-host <host>",
26
+ "Ollama API host",
27
+ process.env.OLLAMA_HOST ?? "http://localhost:11434"
28
+ )
29
+ .option(
30
+ "--openrouter-key <key>",
31
+ "OpenRouter API key",
32
+ process.env.OPENROUTER_API_KEY
33
+ )
34
+ .option(
35
+ "-s, --schema <schema>",
36
+ "Graph schema description for better results (e.g. '(:Person {name, age})-[:KNOWS]->(:Person)')"
37
+ );
38
+
39
+ // Default command: translate
40
+ program
41
+ .argument("[question...]", "Natural language to translate to Cypher")
42
+ .action(async (questionParts: string[], opts) => {
43
+ if (questionParts.length === 0) {
44
+ program.help();
45
+ return;
46
+ }
47
+
48
+ const question = questionParts.join(" ");
49
+ const config = resolveConfig(opts);
50
+
51
+ try {
52
+ const result = await translate(question, config, opts.schema);
53
+ // Output just the cypher to stdout so it can be piped
54
+ console.log(result.cypher);
55
+
56
+ if (result.explanation) {
57
+ console.error(`\n-- ${result.explanation}`);
58
+ }
59
+ } catch (err) {
60
+ console.error(
61
+ `error: ${err instanceof Error ? err.message : String(err)}`
62
+ );
63
+ process.exit(1);
64
+ }
65
+ });
66
+
67
+ // Interactive REPL
68
+ program
69
+ .command("repl")
70
+ .description("Interactive mode — type questions, get Cypher")
71
+ .action(async () => {
72
+ const opts = program.opts();
73
+ const config = resolveConfig(opts);
74
+
75
+ console.error("clippy-graph repl — type a question, get Cypher. Ctrl+C to exit.\n");
76
+
77
+ const prompt = "> ";
78
+ process.stdout.write(prompt);
79
+
80
+ for await (const line of readLines()) {
81
+ const input = line.trim();
82
+ if (!input) {
83
+ process.stdout.write(prompt);
84
+ continue;
85
+ }
86
+
87
+ try {
88
+ const result = await translate(input, config, opts.schema);
89
+ console.log(`\n${result.cypher}`);
90
+ if (result.explanation) {
91
+ console.error(`-- ${result.explanation}`);
92
+ }
93
+ console.log("");
94
+ } catch (err) {
95
+ console.error(
96
+ `error: ${err instanceof Error ? err.message : String(err)}`
97
+ );
98
+ }
99
+
100
+ process.stdout.write(prompt);
101
+ }
102
+ });
103
+
104
+ function resolveConfig(opts: {
105
+ provider: string;
106
+ model?: string;
107
+ ollamaHost?: string;
108
+ openrouterKey?: string;
109
+ }): ProviderConfig {
110
+ const provider = opts.provider as Provider;
111
+
112
+ if (provider !== "ollama" && provider !== "openrouter") {
113
+ console.error(`error: unknown provider "${provider}". Use ollama or openrouter.`);
114
+ process.exit(1);
115
+ }
116
+
117
+ const defaultModel =
118
+ provider === "ollama"
119
+ ? "llama3.1"
120
+ : "nvidia/nemotron-3-nano-30b-a3b:free";
121
+
122
+ return {
123
+ provider,
124
+ model: opts.model ?? defaultModel,
125
+ ollamaHost: opts.ollamaHost,
126
+ openrouterApiKey: opts.openrouterKey ?? process.env.OPENROUTER_API_KEY,
127
+ };
128
+ }
129
+
130
+ async function* readLines(): AsyncGenerator<string> {
131
+ const decoder = new TextDecoder();
132
+ const reader = Bun.stdin.stream().getReader();
133
+ let buffer = "";
134
+
135
+ while (true) {
136
+ const { done, value } = await reader.read();
137
+ if (done) break;
138
+ buffer += decoder.decode(value, { stream: true });
139
+ const lines = buffer.split("\n");
140
+ buffer = lines.pop() ?? "";
141
+ for (const line of lines) {
142
+ yield line;
143
+ }
144
+ }
145
+ if (buffer) yield buffer;
146
+ }
147
+
148
+ program.parse();
@@ -0,0 +1,78 @@
1
+ import type { ChatMessage, ProviderConfig } from "./types";
2
+
3
+ export async function chatCompletion(
4
+ config: ProviderConfig,
5
+ messages: ChatMessage[]
6
+ ): Promise<string> {
7
+ if (config.provider === "ollama") {
8
+ return ollamaChat(config, messages);
9
+ }
10
+ return openrouterChat(config, messages);
11
+ }
12
+
13
+ async function ollamaChat(
14
+ config: ProviderConfig,
15
+ messages: ChatMessage[]
16
+ ): Promise<string> {
17
+ const host = config.ollamaHost ?? "http://localhost:11434";
18
+ const url = `${host.replace(/\/$/, "")}/api/chat`;
19
+
20
+ const res = await fetch(url, {
21
+ method: "POST",
22
+ headers: { "Content-Type": "application/json" },
23
+ body: JSON.stringify({
24
+ model: config.model,
25
+ messages,
26
+ stream: false,
27
+ options: { temperature: 0.1 },
28
+ }),
29
+ });
30
+
31
+ if (!res.ok) {
32
+ const body = await res.text();
33
+ throw new Error(`Ollama error (${res.status}): ${body}`);
34
+ }
35
+
36
+ const data = (await res.json()) as { message: { content: string } };
37
+ return data.message.content;
38
+ }
39
+
40
+ async function openrouterChat(
41
+ config: ProviderConfig,
42
+ messages: ChatMessage[]
43
+ ): Promise<string> {
44
+ const apiKey = config.openrouterApiKey;
45
+ if (!apiKey) {
46
+ throw new Error(
47
+ "OPENROUTER_API_KEY is required when using the openrouter provider."
48
+ );
49
+ }
50
+
51
+ const res = await fetch("https://openrouter.ai/api/v1/chat/completions", {
52
+ method: "POST",
53
+ headers: {
54
+ "Content-Type": "application/json",
55
+ Authorization: `Bearer ${apiKey}`,
56
+ },
57
+ body: JSON.stringify({
58
+ model: config.model,
59
+ messages,
60
+ temperature: 0.1,
61
+ }),
62
+ });
63
+
64
+ if (!res.ok) {
65
+ const body = await res.text();
66
+ throw new Error(`OpenRouter error (${res.status}): ${body}`);
67
+ }
68
+
69
+ const data = (await res.json()) as {
70
+ choices: { message: { content: string } }[];
71
+ };
72
+
73
+ if (!data.choices?.[0]?.message?.content) {
74
+ throw new Error("OpenRouter returned an empty response.");
75
+ }
76
+
77
+ return data.choices[0].message.content;
78
+ }
@@ -0,0 +1,77 @@
1
+ import type { ChatMessage, ProviderConfig, TranslationResult } from "./types";
2
+ import { chatCompletion } from "./provider";
3
+
4
+ const SYSTEM_PROMPT = `You are a Cypher query expert for Neo4j graph databases.
5
+ Your sole job is to translate natural language into valid Cypher queries.
6
+
7
+ RULES:
8
+ 1. Output ONLY valid Cypher. No markdown fences, no explanations before or after.
9
+ 2. Use standard Neo4j Cypher syntax.
10
+ 3. For string matching, prefer case-insensitive matching with toLower() or CONTAINS.
11
+ 4. Use parameters ($param) when the user provides specific values.
12
+ 5. Unless the user specifies a limit, add LIMIT 25.
13
+ 6. For aggregations, include ORDER BY when it makes sense.
14
+ 7. Use descriptive variable names (person, company, movie — not n, m, r).
15
+
16
+ RESPONSE FORMAT — strictly follow this:
17
+
18
+ CYPHER:
19
+ <the cypher query, raw, no backticks>
20
+
21
+ EXPLANATION:
22
+ <one sentence explaining the query>`;
23
+
24
+ export async function translate(
25
+ naturalLanguage: string,
26
+ config: ProviderConfig,
27
+ schema?: string
28
+ ): Promise<TranslationResult> {
29
+ const messages: ChatMessage[] = [
30
+ { role: "system", content: buildPrompt(schema) },
31
+ { role: "user", content: naturalLanguage },
32
+ ];
33
+
34
+ const raw = await chatCompletion(config, messages);
35
+ return parseResponse(raw);
36
+ }
37
+
38
+ function buildPrompt(schema?: string): string {
39
+ if (!schema) return SYSTEM_PROMPT;
40
+
41
+ return `${SYSTEM_PROMPT}
42
+
43
+ DATABASE SCHEMA (use these exact labels, types and properties):
44
+ ${schema}`;
45
+ }
46
+
47
+ function parseResponse(content: string): TranslationResult {
48
+ // Try structured format first
49
+ const cypherMatch = content.match(/CYPHER:\s*\n([\s\S]*?)(?:\nEXPLANATION:|\n*$)/i);
50
+ const explanationMatch = content.match(/EXPLANATION:\s*\n?([\s\S]*?)$/i);
51
+
52
+ if (cypherMatch) {
53
+ return {
54
+ cypher: cleanCypher(cypherMatch[1]),
55
+ explanation: explanationMatch?.[1]?.trim() ?? "",
56
+ };
57
+ }
58
+
59
+ // Fallback: extract any Cypher-looking statement
60
+ const fallback = content.match(
61
+ /((?:MATCH|CREATE|MERGE|DELETE|RETURN|WITH|CALL|UNWIND|OPTIONAL\s+MATCH)[\s\S]*)/i
62
+ );
63
+
64
+ return {
65
+ cypher: cleanCypher(fallback?.[1] ?? content),
66
+ explanation: "",
67
+ };
68
+ }
69
+
70
+ function cleanCypher(raw: string): string {
71
+ return raw
72
+ .replace(/```cypher\s*/g, "")
73
+ .replace(/```\s*/g, "")
74
+ .replace(/^CYPHER:\s*/im, "")
75
+ .trim()
76
+ .replace(/;\s*$/, "");
77
+ }
package/src/types.ts ADDED
@@ -0,0 +1,18 @@
1
+ export type Provider = "ollama" | "openrouter";
2
+
3
+ export interface ProviderConfig {
4
+ provider: Provider;
5
+ model: string;
6
+ ollamaHost?: string;
7
+ openrouterApiKey?: string;
8
+ }
9
+
10
+ export interface ChatMessage {
11
+ role: "system" | "user" | "assistant";
12
+ content: string;
13
+ }
14
+
15
+ export interface TranslationResult {
16
+ cypher: string;
17
+ explanation: string;
18
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "compilerOptions": {
3
+ // Environment setup & latest features
4
+ "lib": ["ESNext"],
5
+ "target": "ESNext",
6
+ "module": "Preserve",
7
+ "moduleDetection": "force",
8
+ "jsx": "react-jsx",
9
+ "allowJs": true,
10
+
11
+ // Bundler mode
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "verbatimModuleSyntax": true,
15
+ "noEmit": true,
16
+
17
+ // Best practices
18
+ "strict": true,
19
+ "skipLibCheck": true,
20
+ "noFallthroughCasesInSwitch": true,
21
+ "noUncheckedIndexedAccess": true,
22
+ "noImplicitOverride": true,
23
+
24
+ // Some stricter flags (disabled by default)
25
+ "noUnusedLocals": false,
26
+ "noUnusedParameters": false,
27
+ "noPropertyAccessFromIndexSignature": false
28
+ }
29
+ }