@teddysc/claude-run 0.5.0 → 0.6.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.
package/README.md CHANGED
@@ -25,6 +25,12 @@ The browser will open automatically at http://localhost:12001.
25
25
 
26
26
  ## Changelog
27
27
 
28
+ ### 0.6.0
29
+ - Add full-text search using ripgrep
30
+ - Search across all conversation content including tool calls
31
+ - Include debug command in search response for troubleshooting
32
+ - Always search ignored files for comprehensive results
33
+
28
34
  ### 0.5.0
29
35
  - Add conversation export to Markdown
30
36
  - Export modes: Full conversation or tools only
@@ -36,12 +42,12 @@ The browser will open automatically at http://localhost:12001.
36
42
 
37
43
  ## Features
38
44
 
45
+ - **Full-text search** - Search across all conversation content including tool calls using ripgrep
39
46
  - **Export conversations** - Download conversations as Markdown with customizable options
40
47
  - **Export modes** - Choose between full conversation or tools-only export
41
48
  - **Truncation options** - Limit long tool outputs by line count or character count
42
49
  - **Batch export** - Export multiple conversations at once
43
50
  - **Real-time streaming** - Watch conversations update live as Claude responds
44
- - **Search** - Find sessions by prompt text or project name
45
51
  - **Filter by project** - Focus on specific projects
46
52
  - **Resume sessions** - Copy the resume command to continue any conversation in your terminal
47
53
  - **Copy messages** - Click the copy button on any message to copy its text content
package/dist/index.js CHANGED
@@ -269,9 +269,130 @@ async function getConversationStream(sessionId, fromOffset = 0) {
269
269
  }
270
270
  }
271
271
 
272
+ // api/fulltext-search.ts
273
+ import { existsSync } from "fs";
274
+ import { spawn } from "child_process";
275
+ import { join as join2, basename as basename2 } from "path";
276
+ var DEFAULT_RG_PATH = "/opt/homebrew/bin/rg";
277
+ var DEFAULT_TIMEOUT_MS = 8e3;
278
+ function getRipgrepPath() {
279
+ return process.env.CLAUDE_RUN_RG_PATH || DEFAULT_RG_PATH;
280
+ }
281
+ function encodeProjectPath2(projectPath) {
282
+ return projectPath.replace(/[/.]/g, "-");
283
+ }
284
+ function buildArgs(query, options) {
285
+ const args = [];
286
+ args.push("--no-ignore");
287
+ args.push("--glob", "*.jsonl");
288
+ args.push("--count-matches");
289
+ const maxCount = Math.max(1, Math.min(options.maxMatchesPerFile ?? 200, 2e3));
290
+ args.push("--max-count", String(maxCount));
291
+ if (options.caseSensitive) {
292
+ args.push("-s");
293
+ } else {
294
+ args.push("-i");
295
+ }
296
+ if (options.mode === "literal") {
297
+ args.push("-F");
298
+ }
299
+ if (options.wordRegexp) {
300
+ args.push("-w");
301
+ }
302
+ args.push("--", query);
303
+ return args;
304
+ }
305
+ function formatCommand(rgPath, args) {
306
+ return [rgPath, ...args].map((a) => JSON.stringify(a)).join(" ");
307
+ }
308
+ function parseCountMatchesOutput(stdout) {
309
+ const results = [];
310
+ for (const rawLine of stdout.split("\n")) {
311
+ const line = rawLine.trim();
312
+ if (!line) {
313
+ continue;
314
+ }
315
+ const lastColon = line.lastIndexOf(":");
316
+ if (lastColon <= 0) {
317
+ continue;
318
+ }
319
+ const filePath = line.slice(0, lastColon);
320
+ const countText = line.slice(lastColon + 1);
321
+ const count = Number.parseInt(countText, 10);
322
+ if (!Number.isFinite(count)) {
323
+ continue;
324
+ }
325
+ results.push({ filePath, count });
326
+ }
327
+ return results;
328
+ }
329
+ async function searchConversationsWithRipgrep(input) {
330
+ const { claudeDir: claudeDir3, query, options, project } = input;
331
+ const trimmedQuery = query.trim();
332
+ if (!trimmedQuery) {
333
+ return { matches: [], debugCommand: "" };
334
+ }
335
+ if (trimmedQuery.length > 500) {
336
+ throw new Error("Query too long (max 500 characters). ");
337
+ }
338
+ const rgPath = getRipgrepPath();
339
+ if (!existsSync(rgPath)) {
340
+ throw new Error(
341
+ `ripgrep not found at ${rgPath}. Set CLAUDE_RUN_RG_PATH to your rg binary path.`
342
+ );
343
+ }
344
+ const projectsRoot = join2(claudeDir3, "projects");
345
+ const searchRoot = project ? join2(projectsRoot, encodeProjectPath2(project)) : projectsRoot;
346
+ if (!existsSync(searchRoot)) {
347
+ return { matches: [], debugCommand: "" };
348
+ }
349
+ const args = [...buildArgs(trimmedQuery, options), searchRoot];
350
+ const debugCommand = formatCommand(rgPath, args);
351
+ console.log("[claude-run] rg:", debugCommand);
352
+ const timeoutMs = Math.max(1e3, Math.min(options.timeoutMs ?? DEFAULT_TIMEOUT_MS, 3e4));
353
+ const result = await new Promise((resolve, reject) => {
354
+ const child = spawn(rgPath, args, {
355
+ stdio: ["ignore", "pipe", "pipe"]
356
+ });
357
+ let stdout = "";
358
+ let stderr = "";
359
+ const timer = setTimeout(() => {
360
+ child.kill("SIGKILL");
361
+ reject(new Error(`ripgrep timed out after ${timeoutMs}ms`));
362
+ }, timeoutMs);
363
+ child.stdout.setEncoding("utf-8");
364
+ child.stderr.setEncoding("utf-8");
365
+ child.stdout.on("data", (chunk) => {
366
+ stdout += chunk;
367
+ });
368
+ child.stderr.on("data", (chunk) => {
369
+ stderr += chunk;
370
+ });
371
+ child.on("error", (err) => {
372
+ clearTimeout(timer);
373
+ reject(err);
374
+ });
375
+ child.on("close", (code) => {
376
+ clearTimeout(timer);
377
+ if (code === 2) {
378
+ reject(new Error(stderr || "ripgrep failed"));
379
+ return;
380
+ }
381
+ const parsed = parseCountMatchesOutput(stdout);
382
+ const matches = parsed.map(({ filePath, count }) => ({
383
+ sessionId: basename2(filePath, ".jsonl"),
384
+ matchCount: count
385
+ })).sort((a, b) => b.matchCount - a.matchCount);
386
+ resolve({ matches });
387
+ });
388
+ });
389
+ const maxSessions = Math.max(1, Math.min(options.maxSessions ?? 500, 5e3));
390
+ return { matches: result.matches.slice(0, maxSessions), debugCommand };
391
+ }
392
+
272
393
  // api/watcher.ts
273
394
  import { watch } from "chokidar";
274
- import { basename as basename2, join as join2 } from "path";
395
+ import { basename as basename3, join as join3 } from "path";
275
396
  var watcher = null;
276
397
  var claudeDir2 = "";
277
398
  var debounceTimers = /* @__PURE__ */ new Map();
@@ -287,7 +408,7 @@ function emitChange(filePath) {
287
408
  callback();
288
409
  }
289
410
  } else if (filePath.endsWith(".jsonl")) {
290
- const sessionId = basename2(filePath, ".jsonl");
411
+ const sessionId = basename3(filePath, ".jsonl");
291
412
  for (const callback of sessionChangeListeners) {
292
413
  callback(sessionId, filePath);
293
414
  }
@@ -308,8 +429,8 @@ function startWatcher() {
308
429
  if (watcher) {
309
430
  return;
310
431
  }
311
- const historyPath = join2(claudeDir2, "history.jsonl");
312
- const projectsDir2 = join2(claudeDir2, "projects");
432
+ const historyPath = join3(claudeDir2, "history.jsonl");
433
+ const projectsDir2 = join3(claudeDir2, "projects");
313
434
  const usePolling = process.env.CLAUDE_RUN_USE_POLLING === "1";
314
435
  watcher = watch([historyPath, projectsDir2], {
315
436
  persistent: true,
@@ -348,18 +469,18 @@ function offSessionChange(callback) {
348
469
  }
349
470
 
350
471
  // api/server.ts
351
- import { join as join3, dirname } from "path";
472
+ import { join as join4, dirname } from "path";
352
473
  import { fileURLToPath } from "url";
353
- import { readFileSync, existsSync } from "fs";
474
+ import { readFileSync, existsSync as existsSync2 } from "fs";
354
475
  import open2 from "open";
355
476
  var __filename = fileURLToPath(import.meta.url);
356
477
  var __dirname = dirname(__filename);
357
478
  function getWebDistPath() {
358
- const prodPath = join3(__dirname, "web");
359
- if (existsSync(prodPath)) {
479
+ const prodPath = join4(__dirname, "web");
480
+ if (existsSync2(prodPath)) {
360
481
  return prodPath;
361
482
  }
362
- return join3(__dirname, "..", "dist", "web");
483
+ return join4(__dirname, "..", "dist", "web");
363
484
  }
364
485
  function createServer(options) {
365
486
  const {
@@ -390,6 +511,32 @@ function createServer(options) {
390
511
  const projects = await getProjects();
391
512
  return c.json(projects);
392
513
  });
514
+ app.post("/api/search", async (c) => {
515
+ let body;
516
+ try {
517
+ body = await c.req.json();
518
+ } catch {
519
+ return c.json({ error: "Invalid JSON body" }, 400);
520
+ }
521
+ if (!body || typeof body.query !== "string" || !body.options) {
522
+ return c.json({ error: "Missing query/options" }, 400);
523
+ }
524
+ try {
525
+ const result = await searchConversationsWithRipgrep({
526
+ claudeDir: getClaudeDir(),
527
+ query: body.query,
528
+ options: body.options,
529
+ project: body.project ?? null
530
+ });
531
+ if (result.debugCommand) {
532
+ c.header("X-Claude-Run-Rg-Command", result.debugCommand);
533
+ }
534
+ return c.json({ matches: result.matches });
535
+ } catch (err) {
536
+ const message = err instanceof Error ? err.message : "Search failed";
537
+ return c.json({ error: message }, 500);
538
+ }
539
+ });
393
540
  app.get("/api/sessions/stream", async (c) => {
394
541
  return streamSSE(c, async (stream) => {
395
542
  let isConnected = true;
@@ -505,7 +652,7 @@ function createServer(options) {
505
652
  const webDistPath = getWebDistPath();
506
653
  app.use("/*", serveStatic({ root: webDistPath }));
507
654
  app.get("/*", async (c) => {
508
- const indexPath = join3(webDistPath, "index.html");
655
+ const indexPath = join4(webDistPath, "index.html");
509
656
  try {
510
657
  const html = readFileSync(indexPath, "utf-8");
511
658
  return c.html(html);
@@ -551,7 +698,7 @@ function createServer(options) {
551
698
 
552
699
  // api/index.ts
553
700
  import { homedir as homedir2 } from "os";
554
- import { join as join4 } from "path";
701
+ import { join as join5 } from "path";
555
702
  import { readFileSync as readFileSync2 } from "fs";
556
703
  import { fileURLToPath as fileURLToPath2 } from "url";
557
704
  import { dirname as dirname2 } from "path";
@@ -559,7 +706,7 @@ var __filename2 = fileURLToPath2(import.meta.url);
559
706
  var __dirname2 = dirname2(__filename2);
560
707
  function getVersion() {
561
708
  try {
562
- const pkgPath = join4(__dirname2, "..", "package.json");
709
+ const pkgPath = join5(__dirname2, "..", "package.json");
563
710
  const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
564
711
  return pkg.version;
565
712
  } catch {
@@ -571,7 +718,7 @@ program.name("claude-run").description(
571
718
  ).version(getVersion()).option("-p, --port <number>", "Port to listen on", "12001").option("-H, --host <address>", "Host address to listen on", "127.0.0.1").option(
572
719
  "-d, --dir <path>",
573
720
  "Claude directory path",
574
- join4(homedir2(), ".claude")
721
+ join5(homedir2(), ".claude")
575
722
  ).option("--dev", "Enable CORS for development").option("--no-open", "Do not open browser automatically").parse();
576
723
  var opts = program.opts();
577
724
  var server = createServer({