@scira/cli 0.1.4 → 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.
Files changed (57) hide show
  1. package/dist/agent/harness-agent.js +206 -0
  2. package/dist/agent/{research-agent.js → main-agent.js} +20 -1
  3. package/dist/cli/commands/init.js +7 -5
  4. package/dist/cli/index.js +52 -11
  5. package/dist/cli/shell/shell.js +4 -5
  6. package/dist/cli/shell/tui.js +5 -2
  7. package/dist/config/env-guide.js +24 -0
  8. package/dist/config/env-store.js +5 -3
  9. package/dist/config/load-config.js +9 -14
  10. package/dist/providers/harness/local-sandbox.js +143 -0
  11. package/dist/providers/llm/gateway.js +5 -2
  12. package/dist/providers/llm/models.js +13 -0
  13. package/dist/providers/llm/readiness.js +5 -1
  14. package/dist/providers/llm/registry.js +24 -3
  15. package/dist/storage/jsonl.js +2 -2
  16. package/dist/storage/run-store.js +15 -15
  17. package/dist/tools/agent-tools.js +7 -7
  18. package/dist/tools/background-tasks.js +4 -5
  19. package/dist/tools/mcp-oauth.js +29 -25
  20. package/dist/tools/open-url.js +1 -2
  21. package/dist/tools/todos.js +3 -3
  22. package/dist/types/index.js +13 -1
  23. package/dist/ui/ink/SciraApp.js +53 -12
  24. package/dist/ui/ink/components/home-screen.js +2 -2
  25. package/dist/ui/ink/components/overlays.js +73 -15
  26. package/dist/ui/ink/constants.js +37 -7
  27. package/dist/ui/ink/hooks/use-agent-turn.js +17 -6
  28. package/dist/ui/ink/hooks/use-feed-lines.js +34 -7
  29. package/dist/ui/ink/hooks/use-keyboard.js +28 -5
  30. package/dist/ui/ink/hooks/use-session.js +7 -5
  31. package/dist/ui/ink/hooks/use-settings.js +20 -0
  32. package/dist/ui/ink/hooks/use-submit.js +15 -8
  33. package/dist/ui/ink/lib/file-mentions.js +1 -2
  34. package/dist/ui/ink/lib/tool-result.js +205 -2
  35. package/dist/ui/ink/lib/utils.js +52 -28
  36. package/dist/ui/ink/theme.js +5 -10
  37. package/dist/watch/runner.js +2 -2
  38. package/package.json +15 -13
  39. package/dist/agent/background-tasks.js +0 -173
  40. package/dist/agent/todos.js +0 -140
  41. package/dist/agent/tools.js +0 -432
  42. package/dist/agent/tools.test.js +0 -60
  43. package/dist/agent/workspace.js +0 -85
  44. package/dist/config/env-guide.test.js +0 -18
  45. package/dist/config/env-store.test.js +0 -60
  46. package/dist/storage/jsonl.test.js +0 -38
  47. package/dist/storage/run-store.test.js +0 -65
  48. package/dist/tools/bash-policy.test.js +0 -38
  49. package/dist/tools/search-web.test.js +0 -24
  50. package/dist/tools/workspace.test.js +0 -75
  51. package/dist/types/schema.test.js +0 -61
  52. package/dist/ui/ink/hooks/use-feed-lines.test.js +0 -16
  53. package/dist/ui/ink/lib/tool-result.test.js +0 -60
  54. package/dist/ui/ink/lib/utils.test.js +0 -48
  55. package/dist/ui/ink/session-manager.test.js +0 -31
  56. package/dist/ui/ink/terminal-probe.test.js +0 -12
  57. package/dist/ui/ink/theme.test.js +0 -68
@@ -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
- }
@@ -1,140 +0,0 @@
1
- import { readFile, writeFile, mkdir } from "node:fs/promises";
2
- import { dirname, join } from "node:path";
3
- import { tool } from "ai";
4
- import { z } from "zod";
5
- import { logEvent } from "../storage/run-store.js";
6
- const TodoStatusSchema = z.enum(["pending", "in_progress", "completed", "cancelled"]);
7
- function nextTodoId(existing) {
8
- const nums = existing
9
- .map((t) => /^todo_(\d+)$/u.exec(t.id)?.[1])
10
- .filter((n) => Boolean(n))
11
- .map((n) => Number.parseInt(n, 10));
12
- const next = nums.length > 0 ? Math.max(...nums) + 1 : 1;
13
- return `todo_${String(next).padStart(3, "0")}`;
14
- }
15
- async function loadTodos(path) {
16
- try {
17
- const raw = await readFile(path, "utf8");
18
- const parsed = JSON.parse(raw);
19
- if (!Array.isArray(parsed))
20
- return [];
21
- return parsed.filter((t) => typeof t === "object" && t !== null && typeof t.id === "string");
22
- }
23
- catch {
24
- return [];
25
- }
26
- }
27
- async function saveTodos(path, items) {
28
- await mkdir(dirname(path), { recursive: true });
29
- await writeFile(path, JSON.stringify(items, null, 2) + "\n");
30
- }
31
- function formatTodoList(items) {
32
- if (items.length === 0)
33
- return "No todos.";
34
- const icon = {
35
- pending: "[ ]",
36
- in_progress: "[~]",
37
- completed: "[x]",
38
- cancelled: "[-]"
39
- };
40
- return items
41
- .map((t) => `${icon[t.status]} ${t.id}: ${t.content} (${t.status})`)
42
- .join("\n");
43
- }
44
- export function createTodoTool(runPath) {
45
- const todosPath = join(runPath, "todos.json");
46
- return tool({
47
- description: "Manage structured task todos for the current session. " +
48
- "Actions: create (add items), edit (change content), mark (set status), remove (delete one), rewrite (replace entire list), list (show all). " +
49
- "Statuses: pending, in_progress, completed, cancelled.",
50
- inputSchema: z.object({
51
- action: z.enum(["create", "edit", "mark", "remove", "rewrite", "list"]),
52
- id: z.string().optional().describe("Todo id for edit, mark, or remove."),
53
- content: z.string().optional().describe("Todo text for create, edit, or rewrite items."),
54
- status: TodoStatusSchema.optional().describe("Status for mark action or rewrite items."),
55
- items: z
56
- .array(z.object({
57
- id: z.string().optional(),
58
- content: z.string(),
59
- status: TodoStatusSchema.optional()
60
- }))
61
- .optional()
62
- .describe("Items for create or rewrite.")
63
- }),
64
- execute: async ({ action, id, content, status, items }) => {
65
- const now = new Date().toISOString();
66
- let todos = await loadTodos(todosPath);
67
- switch (action) {
68
- case "list":
69
- return formatTodoList(todos);
70
- case "create": {
71
- const toAdd = items ?? (content ? [{ content, status: status ?? "pending" }] : []);
72
- if (toAdd.length === 0)
73
- return "create requires content or items.";
74
- for (const item of toAdd) {
75
- const todoId = item.id ?? nextTodoId(todos);
76
- todos.push({
77
- id: todoId,
78
- content: item.content,
79
- status: item.status ?? "pending",
80
- createdAt: now,
81
- updatedAt: now
82
- });
83
- }
84
- await saveTodos(todosPath, todos);
85
- await logEvent(runPath, "todo.created", { count: toAdd.length });
86
- return `Created ${toAdd.length} todo(s).\n\n${formatTodoList(todos)}`;
87
- }
88
- case "edit": {
89
- if (!id || !content)
90
- return "edit requires id and content.";
91
- const idx = todos.findIndex((t) => t.id === id);
92
- if (idx === -1)
93
- return `Todo "${id}" not found.`;
94
- todos[idx] = { ...todos[idx], content, updatedAt: now };
95
- await saveTodos(todosPath, todos);
96
- await logEvent(runPath, "todo.edited", { id });
97
- return `Updated ${id}.\n\n${formatTodoList(todos)}`;
98
- }
99
- case "mark": {
100
- if (!id || !status)
101
- return "mark requires id and status.";
102
- const idx = todos.findIndex((t) => t.id === id);
103
- if (idx === -1)
104
- return `Todo "${id}" not found.`;
105
- todos[idx] = { ...todos[idx], status, updatedAt: now };
106
- await saveTodos(todosPath, todos);
107
- await logEvent(runPath, "todo.marked", { id, status });
108
- return `Marked ${id} as ${status}.\n\n${formatTodoList(todos)}`;
109
- }
110
- case "remove": {
111
- if (!id)
112
- return "remove requires id.";
113
- const before = todos.length;
114
- todos = todos.filter((t) => t.id !== id);
115
- if (todos.length === before)
116
- return `Todo "${id}" not found.`;
117
- await saveTodos(todosPath, todos);
118
- await logEvent(runPath, "todo.removed", { id });
119
- return `Removed ${id}.\n\n${formatTodoList(todos)}`;
120
- }
121
- case "rewrite": {
122
- if (!items || items.length === 0)
123
- return "rewrite requires a non-empty items array.";
124
- todos = items.map((item, i) => ({
125
- id: item.id ?? `todo_${String(i + 1).padStart(3, "0")}`,
126
- content: item.content,
127
- status: item.status ?? "pending",
128
- createdAt: now,
129
- updatedAt: now
130
- }));
131
- await saveTodos(todosPath, todos);
132
- await logEvent(runPath, "todo.rewritten", { count: todos.length });
133
- return `Rewrote todo list (${todos.length} items).\n\n${formatTodoList(todos)}`;
134
- }
135
- default:
136
- return `Unknown action: ${action}`;
137
- }
138
- }
139
- });
140
- }