@scira/cli 0.1.5 → 0.1.6
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/dist/agent/harness-agent.js +206 -0
- package/dist/agent/{research-agent.js → main-agent.js} +20 -1
- package/dist/cli/commands/init.js +7 -5
- package/dist/cli/index.js +52 -11
- package/dist/cli/shell/shell.js +4 -5
- package/dist/cli/shell/tui.js +5 -2
- package/dist/config/env-guide.js +24 -0
- package/dist/config/env-store.js +5 -3
- package/dist/config/load-config.js +9 -14
- package/dist/providers/harness/local-sandbox.js +143 -0
- package/dist/providers/llm/gateway.js +5 -2
- package/dist/providers/llm/models.js +13 -0
- package/dist/providers/llm/readiness.js +5 -1
- package/dist/providers/llm/registry.js +24 -3
- package/dist/storage/jsonl.js +2 -2
- package/dist/storage/run-store.js +15 -15
- package/dist/tools/agent-tools.js +7 -7
- package/dist/tools/background-tasks.js +4 -5
- package/dist/tools/mcp-oauth.js +29 -25
- package/dist/tools/open-url.js +1 -2
- package/dist/tools/todos.js +3 -3
- package/dist/types/index.js +13 -1
- package/dist/ui/ink/SciraApp.js +10 -6
- package/dist/ui/ink/components/home-screen.js +2 -2
- package/dist/ui/ink/components/overlays.js +73 -15
- package/dist/ui/ink/constants.js +10 -7
- package/dist/ui/ink/hooks/use-agent-turn.js +14 -5
- package/dist/ui/ink/hooks/use-feed-lines.js +31 -6
- package/dist/ui/ink/hooks/use-keyboard.js +28 -5
- package/dist/ui/ink/hooks/use-session.js +7 -5
- package/dist/ui/ink/hooks/use-settings.js +20 -0
- package/dist/ui/ink/hooks/use-submit.js +15 -8
- package/dist/ui/ink/lib/file-mentions.js +1 -2
- package/dist/ui/ink/lib/tool-result.js +201 -2
- package/dist/ui/ink/lib/utils.js +52 -28
- package/dist/ui/ink/theme.js +5 -10
- package/dist/watch/runner.js +2 -2
- package/package.json +13 -11
- package/dist/agent/background-tasks.js +0 -173
- package/dist/agent/todos.js +0 -140
- package/dist/agent/tools.js +0 -432
- package/dist/agent/tools.test.js +0 -60
- package/dist/agent/workspace.js +0 -85
- package/dist/config/env-guide.test.js +0 -18
- package/dist/config/env-store.test.js +0 -60
- package/dist/storage/jsonl.test.js +0 -38
- package/dist/storage/run-store.test.js +0 -65
- package/dist/tools/bash-policy.test.js +0 -38
- package/dist/tools/search-web.test.js +0 -24
- package/dist/tools/workspace.test.js +0 -75
- package/dist/types/schema.test.js +0 -61
- package/dist/ui/ink/hooks/use-feed-lines.test.js +0 -16
- package/dist/ui/ink/lib/tool-result.test.js +0 -60
- package/dist/ui/ink/lib/utils.test.js +0 -48
- package/dist/ui/ink/session-manager.test.js +0 -31
- package/dist/ui/ink/terminal-probe.test.js +0 -12
- package/dist/ui/ink/theme.test.js +0 -68
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { readdirSync, statSync } from "node:fs";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
3
2
|
import { join } from "node:path";
|
|
4
3
|
import { FILE_MENTION_SKIP, FILE_MENTION_MAX_CHARS } from "../constants.js";
|
|
5
4
|
export function listMentionableFiles(root = process.cwd(), max = 300) {
|
|
@@ -52,7 +51,7 @@ export async function promptWithFileMentions(prompt) {
|
|
|
52
51
|
for (const file of files) {
|
|
53
52
|
const abs = join(process.cwd(), file);
|
|
54
53
|
try {
|
|
55
|
-
const content = await
|
|
54
|
+
const content = await Bun.file(abs).text();
|
|
56
55
|
const body = content.length > FILE_MENTION_MAX_CHARS
|
|
57
56
|
? `${content.slice(0, FILE_MENTION_MAX_CHARS)}\n...[truncated ${content.length - FILE_MENTION_MAX_CHARS} chars]`
|
|
58
57
|
: content;
|
|
@@ -1,8 +1,45 @@
|
|
|
1
|
+
import { diffLines } from "diff";
|
|
1
2
|
import { markdownToSegLines } from "./markdown.js";
|
|
2
3
|
import { wrapText } from "./utils.js";
|
|
4
|
+
const HARNESS_TOOL_PREFIX = "mcp__harness-tools__";
|
|
5
|
+
/** Map Claude Code / Codex built-in (and harness host) tool names onto Scira's renderers. */
|
|
6
|
+
const CANONICAL_TOOL = {
|
|
7
|
+
// Scira host tools exposed to the CLI
|
|
8
|
+
multiWebSearch: "webSearch",
|
|
9
|
+
// Claude Code built-ins
|
|
10
|
+
Read: "readFile",
|
|
11
|
+
Write: "writeFile",
|
|
12
|
+
Edit: "editFile",
|
|
13
|
+
MultiEdit: "editFile",
|
|
14
|
+
NotebookEdit: "editFile",
|
|
15
|
+
Bash: "bash",
|
|
16
|
+
BashOutput: "bash",
|
|
17
|
+
Grep: "grepWorkspace",
|
|
18
|
+
Glob: "listWorkspaceDir",
|
|
19
|
+
LS: "listWorkspaceDir",
|
|
20
|
+
TodoWrite: "todo",
|
|
21
|
+
WebFetch: "readUrl",
|
|
22
|
+
WebSearch: "webSearch",
|
|
23
|
+
// Codex built-ins
|
|
24
|
+
shell: "bash",
|
|
25
|
+
};
|
|
26
|
+
/** Strip the harness host-tool MCP prefix so `mcp__harness-tools__readUrl` reads as `readUrl`. */
|
|
27
|
+
export function displayToolName(name) {
|
|
28
|
+
return name.startsWith(HARNESS_TOOL_PREFIX) ? name.slice(HARNESS_TOOL_PREFIX.length) : name;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Resolve a harness/CLI tool name to the Scira renderer key. The harness exposes
|
|
32
|
+
* our host tools as `mcp__harness-tools__*` and the CLIs have their own builtin
|
|
33
|
+
* names (Read, Bash, Grep, …); both should render like Scira's equivalents.
|
|
34
|
+
*/
|
|
35
|
+
export function canonicalToolName(name) {
|
|
36
|
+
const stripped = displayToolName(name);
|
|
37
|
+
return CANONICAL_TOOL[stripped] ?? stripped;
|
|
38
|
+
}
|
|
3
39
|
/** Tools that start collapsed in the timeline (long output). */
|
|
4
40
|
export const DEFAULT_COLLAPSED_TOOLS = new Set([
|
|
5
41
|
"webSearch",
|
|
42
|
+
"multiWebSearch",
|
|
6
43
|
"readUrl",
|
|
7
44
|
"readFile",
|
|
8
45
|
"readWorkspaceFile",
|
|
@@ -291,7 +328,8 @@ function formatBody(name, result, width, theme) {
|
|
|
291
328
|
}
|
|
292
329
|
}
|
|
293
330
|
/** One-line preview for a collapsed tool header. */
|
|
294
|
-
export function formatToolResultPreview(
|
|
331
|
+
export function formatToolResultPreview(rawName, inputSummary, result, status) {
|
|
332
|
+
const name = canonicalToolName(rawName);
|
|
295
333
|
const input = inputSummary.replace(/\s+/gu, " ").trim();
|
|
296
334
|
if (status === "running")
|
|
297
335
|
return input ? `${input} · running…` : "running…";
|
|
@@ -345,10 +383,171 @@ export function formatToolResultPreview(name, inputSummary, result, status) {
|
|
|
345
383
|
const first = result.replace(/\s+/gu, " ").trim();
|
|
346
384
|
return first.length > 140 ? `${first.slice(0, 137)}…` : first;
|
|
347
385
|
}
|
|
386
|
+
// --- Dedicated renderers for Claude Code / Codex built-in tools ---
|
|
387
|
+
function parseObj(s) {
|
|
388
|
+
if (!s)
|
|
389
|
+
return null;
|
|
390
|
+
try {
|
|
391
|
+
const v = JSON.parse(s);
|
|
392
|
+
return v && typeof v === "object" && !Array.isArray(v) ? v : null;
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
/** Unified-ish diff between two strings: removed lines red, added green, a little context dim. */
|
|
399
|
+
function diffSegLines(oldStr, newStr, width, theme) {
|
|
400
|
+
const parts = diffLines(oldStr ?? "", newStr ?? "");
|
|
401
|
+
const out = [];
|
|
402
|
+
const MAX = 60;
|
|
403
|
+
let count = 0;
|
|
404
|
+
for (const part of parts) {
|
|
405
|
+
const sign = part.added ? "+" : part.removed ? "-" : " ";
|
|
406
|
+
const color = part.added ? theme.success : part.removed ? theme.error : theme.textDim;
|
|
407
|
+
const linesIn = part.value.replace(/\n$/u, "").split("\n");
|
|
408
|
+
for (const ln of linesIn) {
|
|
409
|
+
if (count >= MAX) {
|
|
410
|
+
out.push([seg("… diff truncated", { dim: true, color: theme.textDim })]);
|
|
411
|
+
return out;
|
|
412
|
+
}
|
|
413
|
+
for (const wrapped of wrapText(`${sign} ${ln}`, width)) {
|
|
414
|
+
out.push([seg(wrapped, { color, dim: !part.added && !part.removed })]);
|
|
415
|
+
}
|
|
416
|
+
count++;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return out;
|
|
420
|
+
}
|
|
421
|
+
function pathHeader(p, theme) {
|
|
422
|
+
return [seg("path ", { dim: true, color: theme.textDim }), seg(String(p ?? ""), { color: theme.text })];
|
|
423
|
+
}
|
|
424
|
+
/** Edit / MultiEdit → file path + colored diff(s). */
|
|
425
|
+
function formatEditBody(input, width, theme) {
|
|
426
|
+
const lines = [pathHeader(input.file_path ?? input.notebook_path, theme)];
|
|
427
|
+
const edits = Array.isArray(input.edits)
|
|
428
|
+
? input.edits
|
|
429
|
+
: [{ old_string: input.old_string, new_string: input.new_string }];
|
|
430
|
+
edits.forEach((e, i) => {
|
|
431
|
+
if (edits.length > 1)
|
|
432
|
+
lines.push([seg(`edit ${i + 1}`, { dim: true, color: theme.textDim })]);
|
|
433
|
+
lines.push(...diffSegLines(String(e.old_string ?? ""), String(e.new_string ?? input.new_source ?? ""), width, theme));
|
|
434
|
+
});
|
|
435
|
+
return lines;
|
|
436
|
+
}
|
|
437
|
+
/** TodoWrite → checklist with status glyphs. */
|
|
438
|
+
function formatTodoBody(input, width, theme) {
|
|
439
|
+
const todos = Array.isArray(input.todos) ? input.todos : [];
|
|
440
|
+
if (todos.length === 0)
|
|
441
|
+
return [[seg("(no todos)", { dim: true, color: theme.textDim })]];
|
|
442
|
+
return todos.flatMap((t) => {
|
|
443
|
+
const status = String(t.status ?? "pending");
|
|
444
|
+
const glyph = status === "completed" ? "☑" : status === "in_progress" ? "◐" : "☐";
|
|
445
|
+
const color = status === "completed" ? theme.success : status === "in_progress" ? theme.warning : theme.textDim;
|
|
446
|
+
const text = String(t.content ?? t.activeForm ?? "");
|
|
447
|
+
const wrapped = wrapText(text, Math.max(8, width - 2));
|
|
448
|
+
return wrapped.map((w, i) => [seg(i === 0 ? `${glyph} ` : " ", { color }), seg(w, { color: status === "completed" ? theme.textDim : theme.text })]);
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
/** Write → file path + content preview. */
|
|
452
|
+
function formatWriteBody(input, width, theme) {
|
|
453
|
+
const lines = [pathHeader(input.file_path, theme), blank()];
|
|
454
|
+
const allLines = String(input.content ?? "").split("\n");
|
|
455
|
+
const shown = allLines.slice(0, 40);
|
|
456
|
+
for (const ln of shown)
|
|
457
|
+
lines.push(...plainLines(ln, width, { color: theme.text }));
|
|
458
|
+
if (allLines.length > shown.length)
|
|
459
|
+
lines.push([seg(`… +${allLines.length - shown.length} more lines`, { dim: true, color: theme.textDim })]);
|
|
460
|
+
return lines;
|
|
461
|
+
}
|
|
462
|
+
/** WebFetch → url + fetched/answer text. */
|
|
463
|
+
function formatWebFetchBody(input, result, width, theme) {
|
|
464
|
+
const lines = [];
|
|
465
|
+
const url = input?.url;
|
|
466
|
+
if (url)
|
|
467
|
+
lines.push([seg("url ", { dim: true, color: theme.textDim }), seg(String(url), { color: theme.accent, underline: true, url: String(url) })]);
|
|
468
|
+
if (result.trim()) {
|
|
469
|
+
if (lines.length > 0)
|
|
470
|
+
lines.push(blank());
|
|
471
|
+
lines.push(...plainLines(result, width, { color: theme.text }));
|
|
472
|
+
}
|
|
473
|
+
return lines;
|
|
474
|
+
}
|
|
475
|
+
/** Task / Agent (subagent) → description + output. */
|
|
476
|
+
function formatSubagentBody(input, result, width, theme) {
|
|
477
|
+
const lines = [];
|
|
478
|
+
const desc = input?.description ?? input?.subagent_type;
|
|
479
|
+
if (desc)
|
|
480
|
+
lines.push([seg("task ", { dim: true, color: theme.textDim }), seg(String(desc), { color: theme.text })]);
|
|
481
|
+
if (result.trim()) {
|
|
482
|
+
if (lines.length > 0)
|
|
483
|
+
lines.push(blank());
|
|
484
|
+
lines.push(...markdownToSegLines(result, width, theme));
|
|
485
|
+
}
|
|
486
|
+
return lines;
|
|
487
|
+
}
|
|
488
|
+
/** ToolSearch → query + which tool reference it loaded. */
|
|
489
|
+
function formatToolSearchBody(input, result, width, theme) {
|
|
490
|
+
const lines = [];
|
|
491
|
+
if (input?.query)
|
|
492
|
+
lines.push([seg("query ", { dim: true, color: theme.textDim }), seg(String(input.query), { color: theme.text })]);
|
|
493
|
+
const ref = parseObj(result);
|
|
494
|
+
if (ref?.tool_name)
|
|
495
|
+
lines.push([seg("loaded ", { dim: true, color: theme.textDim }), seg(String(ref.tool_name), { color: theme.accent })]);
|
|
496
|
+
else if (result.trim())
|
|
497
|
+
lines.push(...plainLines(result, width, { color: theme.textDim }));
|
|
498
|
+
return lines;
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Dedicated body for a Claude Code / Codex built-in tool, keyed by its real
|
|
502
|
+
* (un-prefixed) name. Returns null to fall through to the generic renderer.
|
|
503
|
+
*/
|
|
504
|
+
function formatBuiltinBody(real, rawInput, result, width, theme) {
|
|
505
|
+
const input = parseObj(rawInput);
|
|
506
|
+
switch (real) {
|
|
507
|
+
case "Edit":
|
|
508
|
+
case "edit":
|
|
509
|
+
case "MultiEdit":
|
|
510
|
+
case "NotebookEdit":
|
|
511
|
+
return input ? formatEditBody(input, width, theme) : null;
|
|
512
|
+
case "TodoWrite":
|
|
513
|
+
return input ? formatTodoBody(input, width, theme) : null;
|
|
514
|
+
case "Write":
|
|
515
|
+
case "write":
|
|
516
|
+
return input ? formatWriteBody(input, width, theme) : null;
|
|
517
|
+
case "WebFetch":
|
|
518
|
+
return formatWebFetchBody(input, result, width, theme);
|
|
519
|
+
case "Task":
|
|
520
|
+
case "Agent":
|
|
521
|
+
return formatSubagentBody(input, result, width, theme);
|
|
522
|
+
case "ToolSearch":
|
|
523
|
+
return formatToolSearchBody(input, result, width, theme);
|
|
524
|
+
default:
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
348
528
|
/** Multi-line formatted tool output for the feed panel. */
|
|
349
|
-
export function formatToolResultLines(
|
|
529
|
+
export function formatToolResultLines(rawName, inputSummary, rawResult, status, contentWidth, theme, expanded = true, rawInput) {
|
|
530
|
+
const name = canonicalToolName(rawName);
|
|
531
|
+
const real = displayToolName(rawName);
|
|
350
532
|
if (!expanded)
|
|
351
533
|
return [];
|
|
534
|
+
// Bound the text we lay out per render — a terminal can't show a 1MB result,
|
|
535
|
+
// and wrapping/parsing that much on every frame is what stalls the renderer.
|
|
536
|
+
// The full result stays in the stored feed; only what we format is capped.
|
|
537
|
+
const MAX_RENDER = 60_000;
|
|
538
|
+
const result = rawResult && rawResult.length > MAX_RENDER
|
|
539
|
+
? `${rawResult.slice(0, MAX_RENDER)}\n\n… [${rawResult.length - MAX_RENDER} more chars not shown]`
|
|
540
|
+
: rawResult;
|
|
541
|
+
// Dedicated built-in tool rendering (diffs, checklists, …). Input-driven ones
|
|
542
|
+
// (Edit, Write, TodoWrite) render even while the tool is still running.
|
|
543
|
+
if (status !== "error") {
|
|
544
|
+
const builtin = formatBuiltinBody(real, rawInput, result ?? "", Math.max(16, contentWidth), theme);
|
|
545
|
+
if (builtin && builtin.length > 0) {
|
|
546
|
+
if (status === "running" && !result?.trim())
|
|
547
|
+
builtin.push([seg("running…", { dim: true, color: theme.textDim })]);
|
|
548
|
+
return builtin;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
352
551
|
const width = Math.max(16, contentWidth);
|
|
353
552
|
const lines = [];
|
|
354
553
|
const input = inputSummary.replace(/\s+/gu, " ").trim();
|
package/dist/ui/ink/lib/utils.js
CHANGED
|
@@ -1,29 +1,31 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
2
|
+
import * as Bun from "bun";
|
|
3
|
+
import { mkdir } from "node:fs/promises";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { dirname, join, resolve } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { FULL_MODE_TRIGGERS } from "../constants.js";
|
|
8
8
|
export const pkgVersion = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "../../../../package.json"), "utf8")).version;
|
|
9
9
|
/** Pipe text to the OS clipboard (pbcopy / clip / xclip). Resolves false when unavailable. */
|
|
10
|
-
export function copyToClipboard(text) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
10
|
+
export async function copyToClipboard(text) {
|
|
11
|
+
const cmd = process.platform === "darwin" ? "pbcopy" : process.platform === "win32" ? "clip" : "xclip";
|
|
12
|
+
const args = cmd === "xclip" ? ["-selection", "clipboard"] : [];
|
|
13
|
+
try {
|
|
14
|
+
const proc = Bun.spawn([cmd, ...args], { stdin: "pipe", stdout: "ignore", stderr: "ignore" });
|
|
15
|
+
proc.stdin.write(text);
|
|
16
|
+
await proc.stdin.end();
|
|
17
|
+
return (await proc.exited) === 0;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
20
22
|
}
|
|
21
23
|
function historyFile(runDirectory) {
|
|
22
24
|
return resolve(process.cwd(), runDirectory, "..", "input-history.json");
|
|
23
25
|
}
|
|
24
26
|
export async function loadInputHistory(runDirectory) {
|
|
25
27
|
try {
|
|
26
|
-
const parsed =
|
|
28
|
+
const parsed = await Bun.file(historyFile(runDirectory)).json();
|
|
27
29
|
return Array.isArray(parsed) ? parsed.filter((x) => typeof x === "string").slice(-50) : [];
|
|
28
30
|
}
|
|
29
31
|
catch {
|
|
@@ -34,7 +36,7 @@ export async function saveInputHistory(runDirectory, history) {
|
|
|
34
36
|
try {
|
|
35
37
|
const file = historyFile(runDirectory);
|
|
36
38
|
await mkdir(dirname(file), { recursive: true });
|
|
37
|
-
await
|
|
39
|
+
await Bun.write(file, JSON.stringify(history.slice(-50), null, 2));
|
|
38
40
|
}
|
|
39
41
|
catch { /* non-fatal */ }
|
|
40
42
|
}
|
|
@@ -200,15 +202,17 @@ export function linkAtMouseColumn(links, x) {
|
|
|
200
202
|
return undefined;
|
|
201
203
|
}
|
|
202
204
|
/** Open a URL in the system browser. */
|
|
203
|
-
export function openExternalUrl(url) {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
205
|
+
export async function openExternalUrl(url) {
|
|
206
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
207
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
208
|
+
try {
|
|
209
|
+
const proc = Bun.spawn([cmd, ...args], { stdout: "ignore", stderr: "ignore" });
|
|
210
|
+
proc.unref();
|
|
211
|
+
return (await proc.exited) === 0;
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
212
216
|
}
|
|
213
217
|
/** True if the prompt clearly asks for full, report-grade research. */
|
|
214
218
|
export function wantsFullResearch(prompt) {
|
|
@@ -279,16 +283,32 @@ function toolOutputText(output) {
|
|
|
279
283
|
return String(output);
|
|
280
284
|
}
|
|
281
285
|
}
|
|
282
|
-
|
|
286
|
+
// Harness/CLI tool names mapped to Scira renderer keys. Kept local to avoid a
|
|
287
|
+
// circular import with tool-result.ts (which imports from this file).
|
|
288
|
+
const HARNESS_TOOL_PREFIX = "mcp__harness-tools__";
|
|
289
|
+
const SUMMARY_CANONICAL = {
|
|
290
|
+
multiWebSearch: "webSearch",
|
|
291
|
+
Read: "readFile", Write: "writeFile", Edit: "editFile", MultiEdit: "editFile", NotebookEdit: "editFile",
|
|
292
|
+
Bash: "bash", BashOutput: "bash", shell: "bash",
|
|
293
|
+
Grep: "grepWorkspace", Glob: "listWorkspaceDir", LS: "listWorkspaceDir",
|
|
294
|
+
TodoWrite: "todo", WebFetch: "readUrl", WebSearch: "webSearch",
|
|
295
|
+
};
|
|
296
|
+
export function summarizeToolInput(rawName, input) {
|
|
297
|
+
const stripped = rawName.startsWith(HARNESS_TOOL_PREFIX) ? rawName.slice(HARNESS_TOOL_PREFIX.length) : rawName;
|
|
298
|
+
const name = SUMMARY_CANONICAL[stripped] ?? stripped;
|
|
283
299
|
const obj = (input ?? {});
|
|
300
|
+
const path = obj.path ?? obj.file_path ?? obj.notebook_path;
|
|
284
301
|
if (name === "bash" || name === "runBash" || name === "runWorkspaceCommand") {
|
|
285
302
|
const action = obj.action;
|
|
286
303
|
if (action && action !== "run")
|
|
287
304
|
return `${action}${obj.taskId ? ` ${obj.taskId}` : ""}`;
|
|
288
305
|
return String(obj.command ?? "");
|
|
289
306
|
}
|
|
290
|
-
if (name === "todo")
|
|
307
|
+
if (name === "todo") {
|
|
308
|
+
if (Array.isArray(obj.todos))
|
|
309
|
+
return `${obj.todos.length} item(s)`;
|
|
291
310
|
return `${String(obj.action ?? "list")}${obj.id ? ` ${obj.id}` : ""}`;
|
|
311
|
+
}
|
|
292
312
|
if (name === "webSearch" || name === "xSearch") {
|
|
293
313
|
const queries = Array.isArray(obj.queries) ? obj.queries : [];
|
|
294
314
|
return queries.length > 0 ? queries.slice(0, 2).join(" · ") + (queries.length > 2 ? ` +${queries.length - 2}` : "") : String(obj.query ?? "");
|
|
@@ -296,14 +316,18 @@ export function summarizeToolInput(name, input) {
|
|
|
296
316
|
if (name === "readUrl")
|
|
297
317
|
return String(obj.url ?? "");
|
|
298
318
|
if (name === "writeFile" || name === "editFile" || name === "readFile" || name === "readWorkspaceFile" || name === "writeWorkspaceFile" || name === "editWorkspaceFile") {
|
|
299
|
-
return String(
|
|
319
|
+
return String(path ?? "");
|
|
300
320
|
}
|
|
301
321
|
if (name === "listWorkspaceDir" || name === "grepWorkspace")
|
|
302
|
-
return String(obj.
|
|
322
|
+
return String(obj.pattern ?? path ?? "");
|
|
303
323
|
if (name === "readSkill" || name === "listSkills")
|
|
304
|
-
return String(obj.name ?? "");
|
|
324
|
+
return String(obj.name ?? obj.skill ?? "");
|
|
305
325
|
if (name === "createClaim" || name === "verifyClaim")
|
|
306
326
|
return String(obj.id ?? "");
|
|
327
|
+
if (stripped === "ToolSearch")
|
|
328
|
+
return String(obj.query ?? "");
|
|
329
|
+
if (stripped === "Task" || stripped === "Agent")
|
|
330
|
+
return String(obj.description ?? obj.subagent_type ?? "");
|
|
307
331
|
try {
|
|
308
332
|
return JSON.stringify(obj).slice(0, 80);
|
|
309
333
|
}
|
package/dist/ui/ink/theme.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { readFileSync, unwatchFile, watchFile } from "node:fs";
|
|
2
|
-
import { execSync } from "node:child_process";
|
|
3
2
|
import { homedir } from "node:os";
|
|
4
3
|
import { join } from "node:path";
|
|
5
4
|
export const DARK_THEME = {
|
|
@@ -124,11 +123,9 @@ function readEditorColorTheme() {
|
|
|
124
123
|
function readSystemAppearance() {
|
|
125
124
|
if (process.platform === "darwin") {
|
|
126
125
|
try {
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}).trim();
|
|
131
|
-
return style === "Dark" ? "dark" : "light";
|
|
126
|
+
const r = Bun.spawnSync(["defaults", "read", "-g", "AppleInterfaceStyle"], { stdout: "pipe", stderr: "ignore" });
|
|
127
|
+
// The key is absent (and `defaults` exits non-zero) in light mode.
|
|
128
|
+
return r.stdout.toString().trim() === "Dark" ? "dark" : "light";
|
|
132
129
|
}
|
|
133
130
|
catch {
|
|
134
131
|
return "light";
|
|
@@ -136,10 +133,8 @@ function readSystemAppearance() {
|
|
|
136
133
|
}
|
|
137
134
|
if (process.platform === "linux") {
|
|
138
135
|
try {
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
142
|
-
}).trim();
|
|
136
|
+
const r = Bun.spawnSync(["gsettings", "get", "org.gnome.desktop.interface", "color-scheme"], { stdout: "pipe", stderr: "ignore" });
|
|
137
|
+
const scheme = r.stdout.toString().trim();
|
|
143
138
|
if (/dark/i.test(scheme))
|
|
144
139
|
return "dark";
|
|
145
140
|
if (/light/i.test(scheme))
|
package/dist/watch/runner.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { diffLines } from "diff";
|
|
3
3
|
import { createRun, listRuns, getRunPaths } from "../storage/run-store.js";
|
|
4
|
-
import { runResearchAgent } from "../agent/
|
|
4
|
+
import { runResearchAgent } from "../agent/main-agent.js";
|
|
5
5
|
/** Compare two report.md texts and return a human-readable diff summary. */
|
|
6
6
|
export function diffReports(prev, next) {
|
|
7
7
|
const changes = diffLines(prev, next);
|
|
@@ -47,7 +47,7 @@ export async function watchLoop(opts, signal) {
|
|
|
47
47
|
opts.onRunStart?.(runPath, tick);
|
|
48
48
|
try {
|
|
49
49
|
await runResearchAgent(runPath, goal, config);
|
|
50
|
-
const nextReport = await
|
|
50
|
+
const nextReport = await Bun.file(getRunPaths(runPath).report).text().catch(() => "");
|
|
51
51
|
const diffText = diffReports(prevReport, nextReport);
|
|
52
52
|
opts.onRunComplete?.(runPath, diffText, tick);
|
|
53
53
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scira/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Scira — terminal-native AI research agent with grounded sources, verified claims, and local run storage.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -35,22 +35,26 @@
|
|
|
35
35
|
"dev": "bun src/cli/index.ts",
|
|
36
36
|
"docs:dev": "bun run --cwd docs dev",
|
|
37
37
|
"docs:build": "NODE_ENV=production bun run --cwd docs build",
|
|
38
|
-
"test": "
|
|
39
|
-
"test:watch": "
|
|
38
|
+
"test": "bun test src",
|
|
39
|
+
"test:watch": "bun test --watch src"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@ai-sdk/
|
|
42
|
+
"@ai-sdk/harness": "^1.0.0-canary.9",
|
|
43
|
+
"@ai-sdk/harness-claude-code": "^1.0.0-canary.5",
|
|
44
|
+
"@ai-sdk/harness-codex": "^1.0.0-canary.5",
|
|
45
|
+
"@ai-sdk/mcp": "^1.0.50",
|
|
43
46
|
"@ai-sdk/openai-compatible": "^2.0.50",
|
|
44
47
|
"@ai-sdk/xai": "^3.0.95",
|
|
45
48
|
"@clack/prompts": "^1.5.1",
|
|
46
|
-
"@mendable/firecrawl-js": "^4.25.
|
|
49
|
+
"@mendable/firecrawl-js": "^4.25.4",
|
|
47
50
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
48
51
|
"@mozilla/readability": "^0.6.0",
|
|
49
|
-
"ai": "^6.0.
|
|
52
|
+
"ai": "^6.0.204",
|
|
53
|
+
"bun": "^1.3.14",
|
|
50
54
|
"diff": "^9.0.0",
|
|
51
55
|
"exa-js": "^2.13.0",
|
|
52
56
|
"files-sdk": "^1.8.0",
|
|
53
|
-
"ink": "^7.0.
|
|
57
|
+
"ink": "^7.0.6",
|
|
54
58
|
"ink-link": "^5.0.0",
|
|
55
59
|
"jsdom": "^29.1.1",
|
|
56
60
|
"parallel-web": "^1.1.0",
|
|
@@ -62,14 +66,12 @@
|
|
|
62
66
|
"zod": "^4.4.3"
|
|
63
67
|
},
|
|
64
68
|
"devDependencies": {
|
|
65
|
-
"bun
|
|
69
|
+
"@types/bun": "^1.3.14",
|
|
66
70
|
"@types/jsdom": "^28.0.3",
|
|
67
71
|
"@types/node": "^25.9.3",
|
|
68
72
|
"@types/react": "^19.2.17",
|
|
69
|
-
"@vitest/coverage-v8": "^4.1.8",
|
|
70
73
|
"tsx": "^4.22.4",
|
|
71
|
-
"typescript": "^6.0.3"
|
|
72
|
-
"vitest": "^4.1.8"
|
|
74
|
+
"typescript": "^6.0.3"
|
|
73
75
|
},
|
|
74
76
|
"engines": {
|
|
75
77
|
"bun": ">=1.2.0"
|
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
|
-
const MAX_OUTPUT_LINES = 500;
|
|
5
|
-
const MAX_TAIL_CHARS = 4000;
|
|
6
|
-
function nextTaskId(existing) {
|
|
7
|
-
const nums = existing
|
|
8
|
-
.map((t) => /^task_(\d+)$/u.exec(t.id)?.[1])
|
|
9
|
-
.filter((n) => Boolean(n))
|
|
10
|
-
.map((n) => Number.parseInt(n, 10));
|
|
11
|
-
const next = nums.length > 0 ? Math.max(...nums) + 1 : 1;
|
|
12
|
-
return `task_${String(next).padStart(3, "0")}`;
|
|
13
|
-
}
|
|
14
|
-
function tailText(lines, maxChars = MAX_TAIL_CHARS) {
|
|
15
|
-
const joined = lines.join("\n");
|
|
16
|
-
if (joined.length <= maxChars)
|
|
17
|
-
return joined;
|
|
18
|
-
return `…[truncated]\n${joined.slice(-maxChars)}`;
|
|
19
|
-
}
|
|
20
|
-
export class BackgroundTaskManager {
|
|
21
|
-
persistPath;
|
|
22
|
-
defaultCwd;
|
|
23
|
-
runtime = new Map();
|
|
24
|
-
records = [];
|
|
25
|
-
loaded = false;
|
|
26
|
-
constructor(persistPath, defaultCwd) {
|
|
27
|
-
this.persistPath = persistPath;
|
|
28
|
-
this.defaultCwd = defaultCwd;
|
|
29
|
-
}
|
|
30
|
-
async ensureLoaded() {
|
|
31
|
-
if (this.loaded)
|
|
32
|
-
return;
|
|
33
|
-
this.loaded = true;
|
|
34
|
-
try {
|
|
35
|
-
const raw = await readFile(this.persistPath, "utf8");
|
|
36
|
-
const parsed = JSON.parse(raw);
|
|
37
|
-
if (Array.isArray(parsed)) {
|
|
38
|
-
this.records = parsed.filter((t) => typeof t === "object" && t !== null && typeof t.id === "string");
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
catch {
|
|
42
|
-
this.records = [];
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
async persist() {
|
|
46
|
-
await mkdir(dirname(this.persistPath), { recursive: true });
|
|
47
|
-
await writeFile(this.persistPath, JSON.stringify(this.records, null, 2) + "\n");
|
|
48
|
-
}
|
|
49
|
-
syncRecord(task) {
|
|
50
|
-
const idx = this.records.findIndex((r) => r.id === task.record.id);
|
|
51
|
-
task.record.outputTail = tailText(task.output);
|
|
52
|
-
if (idx === -1)
|
|
53
|
-
this.records.push({ ...task.record });
|
|
54
|
-
else
|
|
55
|
-
this.records[idx] = { ...task.record };
|
|
56
|
-
}
|
|
57
|
-
async spawn(command, cwd) {
|
|
58
|
-
await this.ensureLoaded();
|
|
59
|
-
const id = nextTaskId(this.records);
|
|
60
|
-
const workDir = cwd ?? this.defaultCwd;
|
|
61
|
-
const proc = spawn(command, {
|
|
62
|
-
cwd: workDir,
|
|
63
|
-
shell: "/bin/bash",
|
|
64
|
-
env: process.env,
|
|
65
|
-
detached: false,
|
|
66
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
67
|
-
});
|
|
68
|
-
const record = {
|
|
69
|
-
id,
|
|
70
|
-
command,
|
|
71
|
-
cwd: workDir,
|
|
72
|
-
pid: proc.pid ?? 0,
|
|
73
|
-
startedAt: new Date().toISOString(),
|
|
74
|
-
status: "running",
|
|
75
|
-
exitCode: null,
|
|
76
|
-
outputTail: ""
|
|
77
|
-
};
|
|
78
|
-
const output = [];
|
|
79
|
-
const append = (chunk) => {
|
|
80
|
-
const text = chunk.toString();
|
|
81
|
-
for (const line of text.split("\n")) {
|
|
82
|
-
if (line.length > 0)
|
|
83
|
-
output.push(line);
|
|
84
|
-
}
|
|
85
|
-
while (output.length > MAX_OUTPUT_LINES)
|
|
86
|
-
output.shift();
|
|
87
|
-
const rt = this.runtime.get(id);
|
|
88
|
-
if (rt) {
|
|
89
|
-
rt.output = output;
|
|
90
|
-
rt.record.outputTail = tailText(output);
|
|
91
|
-
}
|
|
92
|
-
};
|
|
93
|
-
proc.stdout?.on("data", append);
|
|
94
|
-
proc.stderr?.on("data", append);
|
|
95
|
-
const runtime = { record, proc, output };
|
|
96
|
-
this.runtime.set(id, runtime);
|
|
97
|
-
this.records.push({ ...record });
|
|
98
|
-
await this.persist();
|
|
99
|
-
proc.on("close", (code) => {
|
|
100
|
-
record.status = "exited";
|
|
101
|
-
record.exitCode = code;
|
|
102
|
-
record.outputTail = tailText(output);
|
|
103
|
-
this.syncRecord(runtime);
|
|
104
|
-
void this.persist();
|
|
105
|
-
this.runtime.delete(id);
|
|
106
|
-
});
|
|
107
|
-
proc.on("error", (err) => {
|
|
108
|
-
output.push(`[spawn error] ${err.message}`);
|
|
109
|
-
record.status = "exited";
|
|
110
|
-
record.exitCode = 1;
|
|
111
|
-
record.outputTail = tailText(output);
|
|
112
|
-
this.syncRecord(runtime);
|
|
113
|
-
void this.persist();
|
|
114
|
-
this.runtime.delete(id);
|
|
115
|
-
});
|
|
116
|
-
return { ...record };
|
|
117
|
-
}
|
|
118
|
-
async list() {
|
|
119
|
-
await this.ensureLoaded();
|
|
120
|
-
for (const rt of this.runtime.values()) {
|
|
121
|
-
rt.record.outputTail = tailText(rt.output);
|
|
122
|
-
this.syncRecord(rt);
|
|
123
|
-
}
|
|
124
|
-
return this.records.map((r) => {
|
|
125
|
-
const live = this.runtime.get(r.id);
|
|
126
|
-
return live ? { ...live.record } : { ...r };
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
async getOutput(taskId, tailLines = 50) {
|
|
130
|
-
await this.ensureLoaded();
|
|
131
|
-
const live = this.runtime.get(taskId);
|
|
132
|
-
if (live) {
|
|
133
|
-
const lines = live.output.slice(-tailLines);
|
|
134
|
-
return lines.length > 0 ? lines.join("\n") : "(no output yet)";
|
|
135
|
-
}
|
|
136
|
-
const rec = this.records.find((r) => r.id === taskId);
|
|
137
|
-
if (!rec)
|
|
138
|
-
return `Task "${taskId}" not found.`;
|
|
139
|
-
const lines = rec.outputTail.split("\n").slice(-tailLines);
|
|
140
|
-
return lines.length > 0 ? lines.join("\n") : "(no output)";
|
|
141
|
-
}
|
|
142
|
-
async kill(taskId) {
|
|
143
|
-
await this.ensureLoaded();
|
|
144
|
-
const live = this.runtime.get(taskId);
|
|
145
|
-
if (live) {
|
|
146
|
-
live.proc.kill("SIGTERM");
|
|
147
|
-
live.record.status = "killed";
|
|
148
|
-
live.record.exitCode = live.record.exitCode ?? 143;
|
|
149
|
-
this.syncRecord(live);
|
|
150
|
-
await this.persist();
|
|
151
|
-
return `Killed ${taskId} (pid ${live.record.pid}).`;
|
|
152
|
-
}
|
|
153
|
-
const rec = this.records.find((r) => r.id === taskId);
|
|
154
|
-
if (!rec)
|
|
155
|
-
return `Task "${taskId}" not found.`;
|
|
156
|
-
if (rec.status !== "running")
|
|
157
|
-
return `${taskId} is already ${rec.status}.`;
|
|
158
|
-
rec.status = "killed";
|
|
159
|
-
await this.persist();
|
|
160
|
-
return `Marked ${taskId} as killed (process not tracked in this session).`;
|
|
161
|
-
}
|
|
162
|
-
async formatContextForAgent() {
|
|
163
|
-
const tasks = await this.list();
|
|
164
|
-
const active = tasks.filter((t) => t.status === "running");
|
|
165
|
-
if (active.length === 0)
|
|
166
|
-
return "";
|
|
167
|
-
const lines = active.map((t) => ` - ${t.id}: [running pid ${t.pid}] ${t.command} (cwd: ${t.cwd})`);
|
|
168
|
-
return `\nActive background tasks:\n${lines.join("\n")}\nUse bash with action "output" and taskId to read logs, or action "kill" to stop a task.\n`;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
export function createBackgroundTaskManager(runPath, workspacePath) {
|
|
172
|
-
return new BackgroundTaskManager(join(runPath, "background-tasks.json"), workspacePath);
|
|
173
|
-
}
|