get-claudia 1.51.0 → 1.51.1

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.
Files changed (2) hide show
  1. package/bin/index.js +185 -7
  2. package/package.json +1 -1
package/bin/index.js CHANGED
@@ -202,6 +202,145 @@ class ProgressRenderer {
202
202
  }
203
203
  }
204
204
 
205
+ // ─── Ollama helpers ──────────────────────────────────────────────────────
206
+
207
+ /** Check if Ollama CLI is installed (on PATH or in common locations). */
208
+ async function isOllamaInstalled() {
209
+ // Check PATH
210
+ const which = isWindows ? 'where' : 'which';
211
+ const found = await new Promise((resolve) => {
212
+ const proc = spawn(which, ['ollama'], { stdio: 'pipe', timeout: 5000 });
213
+ proc.on('close', (code) => resolve(code === 0));
214
+ proc.on('error', () => resolve(false));
215
+ });
216
+ if (found) return true;
217
+
218
+ // Check common install locations
219
+ if (process.platform === 'darwin') {
220
+ return existsSync('/usr/local/bin/ollama') || existsSync('/opt/homebrew/bin/ollama');
221
+ } else if (!isWindows) {
222
+ return existsSync('/usr/local/bin/ollama') || existsSync('/usr/bin/ollama');
223
+ }
224
+ return existsSync(join(process.env.LOCALAPPDATA || '', 'Ollama', 'ollama.exe'));
225
+ }
226
+
227
+ /**
228
+ * Install Ollama automatically.
229
+ * macOS: uses brew if available, otherwise curl installer
230
+ * Linux: uses official curl installer
231
+ * Windows: skip (requires manual download from ollama.com)
232
+ */
233
+ async function installOllama() {
234
+ if (isWindows) return false; // Windows needs manual install from ollama.com
235
+
236
+ if (process.platform === 'darwin') {
237
+ // Try Homebrew first
238
+ const hasBrew = await new Promise((resolve) => {
239
+ const proc = spawn('which', ['brew'], { stdio: 'pipe', timeout: 5000 });
240
+ proc.on('close', (code) => resolve(code === 0));
241
+ proc.on('error', () => resolve(false));
242
+ });
243
+
244
+ if (hasBrew) {
245
+ return new Promise((resolve) => {
246
+ const proc = spawn('brew', ['install', 'ollama'], { stdio: 'pipe', timeout: 120000 });
247
+ proc.on('close', (code) => resolve(code === 0));
248
+ proc.on('error', () => resolve(false));
249
+ });
250
+ }
251
+ }
252
+
253
+ // Linux and macOS fallback: official install script
254
+ return new Promise((resolve) => {
255
+ const proc = spawn('sh', ['-c', 'curl -fsSL https://ollama.com/install.sh | sh'], {
256
+ stdio: 'pipe',
257
+ timeout: 120000
258
+ });
259
+ proc.on('close', (code) => resolve(code === 0));
260
+ proc.on('error', () => resolve(false));
261
+ });
262
+ }
263
+
264
+ /**
265
+ * Start the Ollama service and wait for it to respond.
266
+ * On macOS: open the Ollama app or run `ollama serve` in background.
267
+ * On Linux: run `ollama serve` in background.
268
+ * Returns true if Ollama API responds within ~15 seconds.
269
+ */
270
+ async function startOllama() {
271
+ try {
272
+ if (process.platform === 'darwin') {
273
+ // Try macOS app first (installed by brew cask or .dmg), fall back to serve
274
+ const appExists = existsSync('/Applications/Ollama.app');
275
+ if (appExists) {
276
+ spawn('open', ['-a', 'Ollama'], { stdio: 'pipe', detached: true }).unref();
277
+ } else {
278
+ spawn('ollama', ['serve'], { stdio: 'pipe', detached: true }).unref();
279
+ }
280
+ } else if (!isWindows) {
281
+ spawn('ollama', ['serve'], { stdio: 'pipe', detached: true }).unref();
282
+ } else {
283
+ return false;
284
+ }
285
+ } catch {
286
+ return false;
287
+ }
288
+
289
+ // Poll until API responds (up to 15 seconds)
290
+ for (let i = 0; i < 15; i++) {
291
+ await new Promise(r => setTimeout(r, 1000));
292
+ try {
293
+ const resp = await fetch('http://127.0.0.1:11434/api/version');
294
+ if (resp.ok) return true;
295
+ } catch { /* not ready yet */ }
296
+ }
297
+ return false;
298
+ }
299
+
300
+ /**
301
+ * Ensure Ollama's Ed25519 identity key exists at ~/.ollama/id_ed25519.
302
+ * A fresh Ollama install sometimes creates ~/.ollama/ without the key file,
303
+ * causing registry pull requests to fail silently. We generate one with
304
+ * ssh-keygen (available on macOS, Linux, and Windows with Git).
305
+ */
306
+ async function ensureOllamaKey() {
307
+ const ollamaDir = join(homedir(), '.ollama');
308
+ const keyPath = join(ollamaDir, 'id_ed25519');
309
+ if (existsSync(keyPath)) return;
310
+
311
+ mkdirSync(ollamaDir, { recursive: true });
312
+ try {
313
+ await new Promise((resolve, reject) => {
314
+ const proc = spawn('ssh-keygen', ['-t', 'ed25519', '-f', keyPath, '-N', '', '-q'], {
315
+ stdio: 'pipe',
316
+ timeout: 10000
317
+ });
318
+ proc.on('close', (code) => code === 0 ? resolve() : reject(new Error(`ssh-keygen exited ${code}`)));
319
+ proc.on('error', reject);
320
+ });
321
+ } catch {
322
+ // ssh-keygen unavailable or failed; Ollama will need a restart to self-generate.
323
+ }
324
+ }
325
+
326
+ /**
327
+ * Restart Ollama so it regenerates missing config (identity keys, etc.).
328
+ * Kills the running process, waits, then delegates to startOllama().
329
+ */
330
+ async function restartOllama() {
331
+ try {
332
+ const killCmd = isWindows ? 'taskkill' : 'pkill';
333
+ const killArgs = isWindows ? ['/f', '/im', 'ollama.exe'] : ['-f', 'ollama'];
334
+ await new Promise((resolve) => {
335
+ const proc = spawn(killCmd, killArgs, { stdio: 'pipe', timeout: 5000 });
336
+ proc.on('close', () => resolve());
337
+ proc.on('error', () => resolve());
338
+ });
339
+ await new Promise(r => setTimeout(r, 2000));
340
+ } catch { /* ignore */ }
341
+ return startOllama();
342
+ }
343
+
205
344
  // ─── Main ───────────────────────────────────────────────────────────────
206
345
 
207
346
  async function main() {
@@ -314,7 +453,7 @@ async function main() {
314
453
  let memoryOk = false;
315
454
 
316
455
  try {
317
- // Step 1: Environment -- check Node.js version and Ollama
456
+ // Step 1: Environment -- check Node.js version, detect/install/start Ollama
318
457
  renderer.update('environment', 'active', 'checking...');
319
458
  const nodeVersion = process.versions.node;
320
459
  const nodeMajor = parseInt(nodeVersion.split('.')[0], 10);
@@ -324,15 +463,32 @@ async function main() {
324
463
  throw new Error('Node 18+ required');
325
464
  }
326
465
 
327
- // Check Ollama is running
466
+ // Phase 1: Is Ollama running?
328
467
  let ollamaOk = false;
329
468
  try {
330
469
  const resp = await fetch('http://127.0.0.1:11434/api/version');
331
- if (resp.ok) {
332
- ollamaOk = true;
470
+ if (resp.ok) ollamaOk = true;
471
+ } catch { /* not running */ }
472
+
473
+ // Phase 2: If not running, is it installed?
474
+ if (!ollamaOk) {
475
+ const ollamaInstalled = await isOllamaInstalled();
476
+
477
+ if (!ollamaInstalled) {
478
+ // Phase 3: Not installed at all. Install it.
479
+ renderer.update('environment', 'active', 'installing Ollama...');
480
+ const installed = await installOllama();
481
+ if (!installed) {
482
+ renderer.update('environment', 'warn', `Node ${nodeVersion}, no Ollama`);
483
+ if (!supportsInPlace) renderer.appendLine('environment', 'warn', `Node ${nodeVersion}, no Ollama`);
484
+ }
485
+ }
486
+
487
+ // Phase 4: Installed (or just installed). Try starting it.
488
+ if (!ollamaOk) {
489
+ renderer.update('environment', 'active', 'starting Ollama...');
490
+ ollamaOk = await startOllama();
333
491
  }
334
- } catch {
335
- // Ollama not running
336
492
  }
337
493
 
338
494
  if (ollamaOk) {
@@ -358,15 +514,37 @@ async function main() {
358
514
  } catch { /* ignore */ }
359
515
 
360
516
  if (!modelReady) {
517
+ // Ensure Ollama's identity key exists (required for registry pulls).
518
+ // A fresh Ollama install may have ~/.ollama/ but no key file,
519
+ // causing silent pull failures. Generate one with ssh-keygen if missing.
520
+ await ensureOllamaKey();
521
+
361
522
  renderer.update('models', 'active', 'pulling all-minilm:l6-v2...');
523
+ let pullOk = false;
362
524
  try {
363
525
  const pullResp = await fetch('http://127.0.0.1:11434/api/pull', {
364
526
  method: 'POST',
365
527
  headers: { 'Content-Type': 'application/json' },
366
528
  body: JSON.stringify({ name: 'all-minilm:l6-v2', stream: false })
367
529
  });
368
- modelReady = pullResp.ok;
530
+ pullOk = pullResp.ok;
369
531
  } catch { /* ignore */ }
532
+
533
+ // If pull failed, restart Ollama (regenerates keys) and retry once
534
+ if (!pullOk) {
535
+ renderer.update('models', 'active', 'retrying pull...');
536
+ await restartOllama();
537
+ try {
538
+ const retryResp = await fetch('http://127.0.0.1:11434/api/pull', {
539
+ method: 'POST',
540
+ headers: { 'Content-Type': 'application/json' },
541
+ body: JSON.stringify({ name: 'all-minilm:l6-v2', stream: false })
542
+ });
543
+ pullOk = retryResp.ok;
544
+ } catch { /* ignore */ }
545
+ }
546
+
547
+ modelReady = pullOk;
370
548
  }
371
549
 
372
550
  if (modelReady) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "get-claudia",
3
- "version": "1.51.0",
3
+ "version": "1.51.1",
4
4
  "description": "An AI assistant who learns how you work.",
5
5
  "keywords": [
6
6
  "claudia",