promptcase 1.0.5 → 1.0.7

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/README.md CHANGED
@@ -40,6 +40,7 @@ The daemon auto-starts on login/boot and runs in the background.
40
40
  | `promptcase show` | Show recent synced prompts |
41
41
  | `promptcase stop` | Stop the running daemon (auto-start is preserved for next boot) |
42
42
  | `promptcase logout` | Stop daemon, revoke token, clear credentials |
43
+ | `promptcase start` | **Internal** — run the daemon in the foreground. Invoked by the auto-start service (LaunchAgent / systemd / Startup folder). You don't normally need to run this directly. |
43
44
 
44
45
  ## How it works
45
46
 
@@ -8,6 +8,9 @@
8
8
  import inquirer from 'inquirer';
9
9
  import { Command } from 'commander';
10
10
  import { exec, spawn } from 'child_process';
11
+ import fs from 'fs';
12
+ import os from 'os';
13
+ import path from 'path';
11
14
  import { APIService } from '../services/api.js';
12
15
  import { DaemonService } from '../services/daemon.js';
13
16
  import { getConfig } from '../lib/config.js';
@@ -89,6 +92,15 @@ async function verifyAndShowUser(api) {
89
92
  }
90
93
  /**
91
94
  * Start daemon in detached background process
95
+ *
96
+ * Polls for the lock dir to appear (the daemon writes the lock as soon as it
97
+ * finishes starting). This avoids the previous 1500ms fixed wait that would
98
+ * give false "Daemon may not have started" warnings on slow machines where
99
+ * Node + module load + lock acquisition took longer than 1500ms — the daemon
100
+ * would actually be running, but the parent had already given up and
101
+ * returned, leaving the user to think the spawn failed. The 1500ms was a
102
+ * too-tight deadline and the real signal is "lock dir exists" which means
103
+ * "daemon is fully initialized".
92
104
  */
93
105
  async function startDaemonDetached(apiUrl) {
94
106
  // Detach the daemon process so it runs independently
@@ -98,12 +110,21 @@ async function startDaemonDetached(apiUrl) {
98
110
  windowsHide: true,
99
111
  });
100
112
  child.unref();
101
- // Give it a moment to start
102
- await new Promise(resolve => setTimeout(resolve, 1500));
103
- // Verify it started
104
- const daemon = new DaemonService(apiUrl);
105
- const isRunning = await daemon.isRunning();
106
- if (isRunning) {
113
+ // Poll for the lock to appear. The daemon writes the lock dir as the
114
+ // first step of its lifecycle, so seeing it is a reliable "running" signal.
115
+ // Cap at 10 seconds — if the lock hasn't appeared by then, the daemon
116
+ // process must have died.
117
+ const lockPath = path.join(os.homedir(), '.promptcase', 'daemon.pid.lock');
118
+ const deadline = Date.now() + 10_000;
119
+ let acquired = false;
120
+ while (Date.now() < deadline) {
121
+ if (fs.existsSync(lockPath)) {
122
+ acquired = true;
123
+ break;
124
+ }
125
+ await new Promise(resolve => setTimeout(resolve, 200));
126
+ }
127
+ if (acquired) {
107
128
  console.log(' ✅ Daemon started in background');
108
129
  }
109
130
  else {
@@ -287,7 +287,23 @@ export class ClaudeCaptureService {
287
287
  }
288
288
  /**
289
289
  * Get all prompts from all sources (project transcripts + history),
290
- * deduplicated by SHA-256 content hash, sorted newest first.
290
+ * deduplicated by SHA-256 content hash.
291
+ *
292
+ * Sort order depends on whether this is a backfill or an incremental sync:
293
+ *
294
+ * - **Backfill** (`since` is undefined): the daemon's first sync, with no
295
+ * prior cursor. We sort **OLDEST FIRST** so that the server inserts
296
+ * prompts in chronological order. Since the server's `created_at`
297
+ * column defaults to insert time and the dashboard sorts `created_at
298
+ * DESC`, the *most recent* prompt in the batch ends up at the top of
299
+ * the dashboard — matching the user's intuition that "latest shows
300
+ * first."
301
+ *
302
+ * - **Incremental** (`since` is a date): the daemon's subsequent syncs
303
+ * pick up only prompts newer than the cursor. We sort **NEWEST FIRST**
304
+ * so that when the cursor advances we always send the freshest
305
+ * prompts first (and the new cursor lands on the actual latest
306
+ * prompt, not a few-batch-behind one).
291
307
  */
292
308
  async getAllPrompts(since, limit = 100) {
293
309
  const sessions = await this.getSessions();
@@ -326,7 +342,14 @@ export class ClaudeCaptureService {
326
342
  seenHashes.add(prompt.hash);
327
343
  allPrompts.push(prompt);
328
344
  }
329
- allPrompts.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
345
+ // Backfill: oldest first (so newest ends up at top of dashboard).
346
+ // Incremental: newest first (so the cursor advances to actual latest).
347
+ if (since) {
348
+ allPrompts.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
349
+ }
350
+ else {
351
+ allPrompts.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
352
+ }
330
353
  return allPrompts.slice(0, limit);
331
354
  }
332
355
  /**
@@ -114,6 +114,14 @@ export class DaemonService {
114
114
  async acquireExclusiveLock() {
115
115
  try {
116
116
  const lockDir = this.pidFile + '.lock';
117
+ // Ensure the parent dir exists before we try to mkdir the lock inside
118
+ // it. Without this, the very first run on a clean machine gets ENOENT
119
+ // because ~/.promptcase/ doesn't exist yet (writePidFile creates it
120
+ // later, but only if it gets to run).
121
+ const parentDir = path.dirname(lockDir);
122
+ if (!fs.existsSync(parentDir)) {
123
+ fs.mkdirSync(parentDir, { recursive: true });
124
+ }
117
125
  try {
118
126
  fs.mkdirSync(lockDir);
119
127
  // We won the race — write our PID into the lock dir for diagnostics
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promptcase",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "CLI daemon to capture and sync AI prompts to PromptCase web app",
5
5
  "main": "dist/index.js",
6
6
  "bin": {