@projectservan8n/cnapse 0.7.0 → 0.8.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,6 @@
1
+ import {
2
+ ProviderSelector
3
+ } from "./chunk-OPX7FFL6.js";
4
+ export {
5
+ ProviderSelector
6
+ };
@@ -0,0 +1,391 @@
1
+ // src/components/ProviderSelector.tsx
2
+ import { useState, useEffect } from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import TextInput from "ink-text-input";
5
+ import Spinner from "ink-spinner";
6
+
7
+ // src/lib/config.ts
8
+ import Conf from "conf";
9
+ var config = new Conf({
10
+ projectName: "cnapse",
11
+ defaults: {
12
+ provider: "ollama",
13
+ model: "qwen2.5:0.5b",
14
+ apiKeys: {},
15
+ ollamaHost: "http://localhost:11434",
16
+ openrouter: {
17
+ siteUrl: "https://github.com/projectservan8n/C-napse",
18
+ appName: "C-napse"
19
+ },
20
+ telegram: {
21
+ enabled: false
22
+ }
23
+ }
24
+ });
25
+ function getConfig() {
26
+ return {
27
+ provider: config.get("provider"),
28
+ model: config.get("model"),
29
+ apiKeys: config.get("apiKeys"),
30
+ ollamaHost: config.get("ollamaHost"),
31
+ openrouter: config.get("openrouter"),
32
+ telegram: config.get("telegram")
33
+ };
34
+ }
35
+ function setProvider(provider) {
36
+ config.set("provider", provider);
37
+ }
38
+ function setModel(model) {
39
+ config.set("model", model);
40
+ }
41
+ function setApiKey(provider, key) {
42
+ const keys = config.get("apiKeys");
43
+ keys[provider] = key;
44
+ config.set("apiKeys", keys);
45
+ }
46
+ function getApiKey(provider) {
47
+ return config.get("apiKeys")[provider];
48
+ }
49
+
50
+ // src/lib/ollama.ts
51
+ import { exec } from "child_process";
52
+ import { promisify } from "util";
53
+ var execAsync = promisify(exec);
54
+ async function checkOllamaStatus() {
55
+ try {
56
+ const { stdout } = await execAsync("ollama list", { timeout: 1e4 });
57
+ const lines = stdout.trim().split("\n");
58
+ const models = [];
59
+ for (let i = 1; i < lines.length; i++) {
60
+ const line = lines[i];
61
+ if (!line?.trim()) continue;
62
+ const parts = line.split(/\s{2,}/);
63
+ if (parts.length >= 3) {
64
+ models.push({
65
+ name: parts[0]?.trim() || "",
66
+ size: parts[2]?.trim() || "",
67
+ modified: parts[3]?.trim() || ""
68
+ });
69
+ }
70
+ }
71
+ return {
72
+ installed: true,
73
+ running: true,
74
+ models
75
+ };
76
+ } catch (err) {
77
+ const errorMsg = err instanceof Error ? err.message : "Unknown error";
78
+ if (errorMsg.includes("connect") || errorMsg.includes("refused")) {
79
+ return {
80
+ installed: true,
81
+ running: false,
82
+ models: [],
83
+ error: "Ollama is not running. Start it with: ollama serve"
84
+ };
85
+ }
86
+ if (errorMsg.includes("not found") || errorMsg.includes("not recognized")) {
87
+ return {
88
+ installed: false,
89
+ running: false,
90
+ models: [],
91
+ error: "Ollama not installed. Get it at: https://ollama.ai"
92
+ };
93
+ }
94
+ return {
95
+ installed: false,
96
+ running: false,
97
+ models: [],
98
+ error: errorMsg
99
+ };
100
+ }
101
+ }
102
+ function hasModel(status, modelId) {
103
+ const modelName = modelId.split(":")[0]?.toLowerCase() || "";
104
+ return status.models.some((m) => m.name.toLowerCase().startsWith(modelName));
105
+ }
106
+
107
+ // src/components/ProviderSelector.tsx
108
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
109
+ var PROVIDERS = [
110
+ {
111
+ id: "ollama",
112
+ name: "Ollama",
113
+ description: "Local AI - Free, private",
114
+ needsApiKey: false,
115
+ models: [
116
+ { id: "qwen2.5:0.5b", name: "Qwen 2.5 0.5B (fast)", recommended: true },
117
+ { id: "qwen2.5:1.5b", name: "Qwen 2.5 1.5B" },
118
+ { id: "qwen2.5:7b", name: "Qwen 2.5 7B (quality)" },
119
+ { id: "llama3.2:1b", name: "Llama 3.2 1B" },
120
+ { id: "llama3.2:3b", name: "Llama 3.2 3B" },
121
+ { id: "codellama:7b", name: "Code Llama 7B" },
122
+ { id: "llava:7b", name: "LLaVA 7B (vision)" }
123
+ ]
124
+ },
125
+ {
126
+ id: "openrouter",
127
+ name: "OpenRouter",
128
+ description: "Many models, budget-friendly",
129
+ needsApiKey: true,
130
+ models: [
131
+ { id: "openai/gpt-5-nano", name: "GPT-5 Nano ($0.05/1M) + Vision", recommended: true },
132
+ { id: "google/gemini-2.5-flash-lite", name: "Gemini 2.5 Flash Lite ($0.10/1M)" },
133
+ { id: "qwen/qwen-2.5-coder-32b-instruct", name: "Qwen Coder 32B ($0.07/1M)" },
134
+ { id: "meta-llama/llama-3.3-70b-instruct", name: "Llama 3.3 70B ($0.10/1M)" },
135
+ { id: "deepseek/deepseek-chat", name: "DeepSeek V3 ($0.14/1M)" }
136
+ ]
137
+ },
138
+ {
139
+ id: "anthropic",
140
+ name: "Anthropic",
141
+ description: "Claude - Best reasoning",
142
+ needsApiKey: true,
143
+ models: [
144
+ { id: "claude-3-5-sonnet-20241022", name: "Claude 3.5 Sonnet", recommended: true },
145
+ { id: "claude-3-opus-20240229", name: "Claude 3 Opus" },
146
+ { id: "claude-3-haiku-20240307", name: "Claude 3 Haiku" }
147
+ ]
148
+ },
149
+ {
150
+ id: "openai",
151
+ name: "OpenAI",
152
+ description: "GPT models",
153
+ needsApiKey: true,
154
+ models: [
155
+ { id: "gpt-4o", name: "GPT-4o", recommended: true },
156
+ { id: "gpt-4o-mini", name: "GPT-4o Mini" },
157
+ { id: "gpt-4-turbo", name: "GPT-4 Turbo" }
158
+ ]
159
+ }
160
+ ];
161
+ function ProviderSelector({ onClose, onSelect }) {
162
+ const config2 = getConfig();
163
+ const [step, setStep] = useState("provider");
164
+ const [providerIndex, setProviderIndex] = useState(() => {
165
+ const idx = PROVIDERS.findIndex((p) => p.id === config2.provider);
166
+ return idx >= 0 ? idx : 0;
167
+ });
168
+ const [modelIndex, setModelIndex] = useState(0);
169
+ const [apiKeyInput, setApiKeyInput] = useState("");
170
+ const [selectedProvider, setSelectedProvider] = useState(null);
171
+ const [ollamaStatus, setOllamaStatus] = useState(null);
172
+ const [checkingOllama, setCheckingOllama] = useState(false);
173
+ useEffect(() => {
174
+ if (step === "model" && selectedProvider?.id === "ollama" && !ollamaStatus) {
175
+ setCheckingOllama(true);
176
+ checkOllamaStatus().then((status) => {
177
+ setOllamaStatus(status);
178
+ setCheckingOllama(false);
179
+ if (!status.running) {
180
+ setStep("ollamaError");
181
+ }
182
+ });
183
+ }
184
+ }, [step, selectedProvider, ollamaStatus]);
185
+ useInput((input, key) => {
186
+ if (key.escape) {
187
+ onClose();
188
+ return;
189
+ }
190
+ if (step === "provider") {
191
+ if (key.upArrow) {
192
+ setProviderIndex((prev) => prev > 0 ? prev - 1 : PROVIDERS.length - 1);
193
+ } else if (key.downArrow) {
194
+ setProviderIndex((prev) => prev < PROVIDERS.length - 1 ? prev + 1 : 0);
195
+ } else if (key.return) {
196
+ const provider = PROVIDERS[providerIndex];
197
+ setSelectedProvider(provider);
198
+ const currentIdx = provider.models.findIndex((m) => m.id === config2.model);
199
+ const recommendedIdx = provider.models.findIndex((m) => m.recommended);
200
+ setModelIndex(currentIdx >= 0 ? currentIdx : recommendedIdx >= 0 ? recommendedIdx : 0);
201
+ if (provider.needsApiKey) {
202
+ const apiKeyProvider = provider.id;
203
+ if (!config2.apiKeys[apiKeyProvider]) {
204
+ setStep("apiKey");
205
+ } else {
206
+ setStep("model");
207
+ }
208
+ } else {
209
+ setStep("model");
210
+ }
211
+ }
212
+ } else if (step === "model" && selectedProvider) {
213
+ if (key.upArrow) {
214
+ setModelIndex((prev) => prev > 0 ? prev - 1 : selectedProvider.models.length - 1);
215
+ } else if (key.downArrow) {
216
+ setModelIndex((prev) => prev < selectedProvider.models.length - 1 ? prev + 1 : 0);
217
+ } else if (key.return) {
218
+ const model = selectedProvider.models[modelIndex];
219
+ if (selectedProvider.id === "ollama" && ollamaStatus && !hasModel(ollamaStatus, model.id)) {
220
+ }
221
+ setProvider(selectedProvider.id);
222
+ setModel(model.id);
223
+ setStep("done");
224
+ onSelect(selectedProvider.id, model.id);
225
+ setTimeout(() => onClose(), 1500);
226
+ } else if (key.leftArrow || input === "b") {
227
+ setStep("provider");
228
+ setOllamaStatus(null);
229
+ }
230
+ } else if (step === "ollamaError") {
231
+ if (key.return || input === "b") {
232
+ setStep("provider");
233
+ setOllamaStatus(null);
234
+ }
235
+ }
236
+ });
237
+ const handleApiKeySubmit = (value) => {
238
+ if (value.trim() && selectedProvider) {
239
+ setApiKey(selectedProvider.id, value.trim());
240
+ setStep("model");
241
+ }
242
+ };
243
+ if (step === "provider") {
244
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", padding: 1, width: 60, children: [
245
+ /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "Select Provider" }) }),
246
+ /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: "Arrows to navigate, Enter to select" }) }),
247
+ PROVIDERS.map((provider, index) => {
248
+ const isSelected = index === providerIndex;
249
+ const isCurrent = provider.id === config2.provider;
250
+ const hasKey = provider.needsApiKey && provider.id !== "ollama" ? !!config2.apiKeys[provider.id] : true;
251
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
252
+ /* @__PURE__ */ jsxs(Text, { color: isSelected ? "cyan" : "white", children: [
253
+ isSelected ? "\u276F " : " ",
254
+ provider.name,
255
+ isCurrent && /* @__PURE__ */ jsx(Text, { color: "green", children: " (current)" }),
256
+ provider.needsApiKey && !hasKey && /* @__PURE__ */ jsx(Text, { color: "red", children: " (needs key)" }),
257
+ provider.needsApiKey && hasKey && !isCurrent && /* @__PURE__ */ jsx(Text, { color: "yellow", children: " (key saved)" })
258
+ ] }),
259
+ isSelected && /* @__PURE__ */ jsxs(Text, { color: "gray", children: [
260
+ " ",
261
+ provider.description
262
+ ] })
263
+ ] }, provider.id);
264
+ }),
265
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: "Press Esc to cancel" }) })
266
+ ] });
267
+ }
268
+ if (step === "apiKey" && selectedProvider) {
269
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", padding: 1, width: 60, children: [
270
+ /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "Enter API Key" }) }),
271
+ /* @__PURE__ */ jsxs(Text, { children: [
272
+ /* @__PURE__ */ jsx(Text, { color: "green", children: "\u2713" }),
273
+ " Provider: ",
274
+ selectedProvider.name
275
+ ] }),
276
+ /* @__PURE__ */ jsxs(Box, { marginTop: 1, flexDirection: "column", children: [
277
+ /* @__PURE__ */ jsxs(Text, { color: "gray", dimColor: true, children: [
278
+ selectedProvider.id === "openrouter" && "Get key: openrouter.ai/keys",
279
+ selectedProvider.id === "anthropic" && "Get key: console.anthropic.com",
280
+ selectedProvider.id === "openai" && "Get key: platform.openai.com/api-keys"
281
+ ] }),
282
+ /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
283
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: "\u276F " }),
284
+ /* @__PURE__ */ jsx(
285
+ TextInput,
286
+ {
287
+ value: apiKeyInput,
288
+ onChange: setApiKeyInput,
289
+ onSubmit: handleApiKeySubmit,
290
+ mask: "*"
291
+ }
292
+ )
293
+ ] })
294
+ ] }),
295
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: "Press Esc to cancel" }) })
296
+ ] });
297
+ }
298
+ if (step === "ollamaError" && ollamaStatus) {
299
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "red", padding: 1, width: 60, children: [
300
+ /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(Text, { bold: true, color: "red", children: "Ollama Not Available" }) }),
301
+ /* @__PURE__ */ jsx(Text, { color: "red", children: ollamaStatus.error }),
302
+ /* @__PURE__ */ jsxs(Box, { marginTop: 1, flexDirection: "column", children: [
303
+ !ollamaStatus.installed && /* @__PURE__ */ jsxs(Fragment, { children: [
304
+ /* @__PURE__ */ jsx(Text, { children: "1. Install Ollama from https://ollama.ai" }),
305
+ /* @__PURE__ */ jsx(Text, { children: "2. Run: ollama pull qwen2.5:0.5b" }),
306
+ /* @__PURE__ */ jsx(Text, { children: "3. Try again" })
307
+ ] }),
308
+ ollamaStatus.installed && !ollamaStatus.running && /* @__PURE__ */ jsxs(Fragment, { children: [
309
+ /* @__PURE__ */ jsx(Text, { children: "1. Start Ollama: ollama serve" }),
310
+ /* @__PURE__ */ jsx(Text, { children: "2. Or run any model: ollama run qwen2.5:0.5b" }),
311
+ /* @__PURE__ */ jsx(Text, { children: "3. Try again" })
312
+ ] })
313
+ ] }),
314
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: "Press Enter or B to go back" }) })
315
+ ] });
316
+ }
317
+ if (step === "model" && selectedProvider) {
318
+ const isOllama = selectedProvider.id === "ollama";
319
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", padding: 1, width: 60, children: [
320
+ /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "Select Model" }) }),
321
+ /* @__PURE__ */ jsxs(Text, { children: [
322
+ /* @__PURE__ */ jsx(Text, { color: "green", children: "\u2713" }),
323
+ " Provider: ",
324
+ selectedProvider.name
325
+ ] }),
326
+ isOllama && checkingOllama && /* @__PURE__ */ jsxs(Box, { marginY: 1, children: [
327
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: /* @__PURE__ */ jsx(Spinner, { type: "dots" }) }),
328
+ /* @__PURE__ */ jsx(Text, { children: " Checking Ollama status..." })
329
+ ] }),
330
+ isOllama && ollamaStatus && ollamaStatus.running && /* @__PURE__ */ jsxs(Text, { color: "green", children: [
331
+ "\u2713 Ollama running (",
332
+ ollamaStatus.models.length,
333
+ " models installed)"
334
+ ] }),
335
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, marginBottom: 1, children: /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: "Arrows to navigate, Enter to select, B to go back" }) }),
336
+ selectedProvider.models.map((model, index) => {
337
+ const isSelected = index === modelIndex;
338
+ const isCurrent = model.id === config2.model && selectedProvider.id === config2.provider;
339
+ let modelStatus = "";
340
+ if (isOllama && ollamaStatus) {
341
+ const available = hasModel(ollamaStatus, model.id);
342
+ modelStatus = available ? " (installed)" : " (not installed)";
343
+ }
344
+ return /* @__PURE__ */ jsxs(Text, { color: isSelected ? "cyan" : "white", children: [
345
+ isSelected ? "\u276F " : " ",
346
+ model.name,
347
+ model.recommended && /* @__PURE__ */ jsx(Text, { color: "yellow", children: " *" }),
348
+ isCurrent && /* @__PURE__ */ jsx(Text, { color: "green", children: " (current)" }),
349
+ isOllama && ollamaStatus && (hasModel(ollamaStatus, model.id) ? /* @__PURE__ */ jsx(Text, { color: "green", children: modelStatus }) : /* @__PURE__ */ jsx(Text, { color: "red", children: modelStatus }))
350
+ ] }, model.id);
351
+ }),
352
+ isOllama && /* @__PURE__ */ jsxs(Box, { marginTop: 1, flexDirection: "column", children: [
353
+ /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: "* = Recommended" }),
354
+ ollamaStatus && !hasModel(ollamaStatus, selectedProvider.models[modelIndex]?.id || "") && /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
355
+ "Run: ollama pull ",
356
+ selectedProvider.models[modelIndex]?.id
357
+ ] })
358
+ ] }),
359
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: "Press Esc to cancel" }) })
360
+ ] });
361
+ }
362
+ if (step === "done" && selectedProvider) {
363
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "green", padding: 1, width: 60, children: [
364
+ /* @__PURE__ */ jsx(Text, { color: "green", bold: true, children: "Configuration Updated!" }),
365
+ /* @__PURE__ */ jsxs(Text, { children: [
366
+ /* @__PURE__ */ jsx(Text, { color: "green", children: "\u2713" }),
367
+ " Provider: ",
368
+ selectedProvider.name
369
+ ] }),
370
+ /* @__PURE__ */ jsxs(Text, { children: [
371
+ /* @__PURE__ */ jsx(Text, { color: "green", children: "\u2713" }),
372
+ " Model: ",
373
+ selectedProvider.models[modelIndex]?.name
374
+ ] }),
375
+ selectedProvider.id === "ollama" && ollamaStatus && !hasModel(ollamaStatus, selectedProvider.models[modelIndex]?.id || "") && /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
376
+ "Remember to run: ollama pull ",
377
+ selectedProvider.models[modelIndex]?.id
378
+ ] })
379
+ ] });
380
+ }
381
+ return null;
382
+ }
383
+
384
+ export {
385
+ getConfig,
386
+ setProvider,
387
+ setModel,
388
+ setApiKey,
389
+ getApiKey,
390
+ ProviderSelector
391
+ };