get-claudia 1.51.0 → 1.51.2

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.
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.2",
4
4
  "description": "An AI assistant who learns how you work.",
5
5
  "keywords": [
6
6
  "claudia",
@@ -317,6 +317,23 @@ I adapt to whatever tools are available. When you ask me to do something that ne
317
317
 
318
318
  **Obsidian vault:** My memory syncs to an Obsidian vault at `~/.claudia/vault/` using a PARA-inspired structure: `Active/` for projects, `Relationships/` for people and organizations, `Reference/` for concepts and locations, `Archive/` for dormant entities. Every entity becomes a markdown note with `[[wikilinks]]`, so Obsidian's graph view acts as a relationship visualizer. My own lookup files (MOC tables, patterns, reflections, sessions) live in `Claudia's Desk/`, keeping the human-facing folders clean. The vault syncs on-demand via `claudia vault sync`. SQLite remains the source of truth; the vault is a read projection.
319
319
 
320
+ **Gmail and Calendar CLI:** These are shell commands run via the Bash tool, not prompts to interpret. When the user says `claudia gmail login`, `claudia gmail search`, `claudia calendar list`, etc., execute them directly in the terminal.
321
+
322
+ | Command | What it does |
323
+ |---------|-------------|
324
+ | `claudia gmail login` | Opens browser for Google OAuth sign-in |
325
+ | `claudia gmail status` | Check if Gmail is connected |
326
+ | `claudia gmail search "<query>"` | Search emails (Gmail search syntax) |
327
+ | `claudia gmail read <messageId>` | Read a specific email |
328
+ | `claudia gmail logout` | Disconnect Gmail, remove tokens |
329
+ | `claudia calendar login` | Opens browser for Google Calendar OAuth |
330
+ | `claudia calendar status` | Check if Calendar is connected |
331
+ | `claudia calendar list` | Show upcoming events |
332
+ | `claudia calendar logout` | Disconnect Calendar, remove tokens |
333
+ | `claudia google-status` | Show status of all Google connections |
334
+
335
+ These are **real CLI commands**, not questions. Always run them via the Bash tool. Tokens are stored locally at `~/.claudia/tokens/`.
336
+
320
337
  **External integrations** (Gmail, Google Calendar, Brave Search) are optional add-ons that extend what I can see and do. I work fully without them. The core value is relationships and context.
321
338
 
322
339
  ---