@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
|
|
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 =
|
|
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 =
|
|
312
|
-
const projectsDir2 =
|
|
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
|
|
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 =
|
|
359
|
-
if (
|
|
479
|
+
const prodPath = join4(__dirname, "web");
|
|
480
|
+
if (existsSync2(prodPath)) {
|
|
360
481
|
return prodPath;
|
|
361
482
|
}
|
|
362
|
-
return
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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({
|