kritya 0.8.2-beta

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 (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +434 -0
  3. package/dist/agent/budget.js +25 -0
  4. package/dist/agent/compactor.js +78 -0
  5. package/dist/agent/contextWarning.js +6 -0
  6. package/dist/agent/killSwitch.js +111 -0
  7. package/dist/agent/loop.js +543 -0
  8. package/dist/agent/memory.js +145 -0
  9. package/dist/agent/plugins.js +408 -0
  10. package/dist/agent/skills.js +210 -0
  11. package/dist/agent/skillsCli.js +51 -0
  12. package/dist/agent/systemPrompt.js +122 -0
  13. package/dist/agent/tokens.js +55 -0
  14. package/dist/agent/toolExecutor.js +331 -0
  15. package/dist/agent/workflow.js +496 -0
  16. package/dist/agent/worktree.js +116 -0
  17. package/dist/atomicWrite.js +109 -0
  18. package/dist/audit/audit.js +230 -0
  19. package/dist/audit/cli.js +135 -0
  20. package/dist/commands/custom.js +69 -0
  21. package/dist/commands/mcpCommand.js +433 -0
  22. package/dist/commands/pluginsCommand.js +204 -0
  23. package/dist/commands/registry.js +739 -0
  24. package/dist/config/config.js +214 -0
  25. package/dist/config/debug.js +19 -0
  26. package/dist/config/models.js +82 -0
  27. package/dist/config/retention.js +24 -0
  28. package/dist/config/winAcl.js +38 -0
  29. package/dist/crash.js +102 -0
  30. package/dist/engine.js +113 -0
  31. package/dist/git/git.js +46 -0
  32. package/dist/headless.js +274 -0
  33. package/dist/hooks/hooks.js +122 -0
  34. package/dist/index.js +670 -0
  35. package/dist/lsp/client.js +413 -0
  36. package/dist/lsp/manager.js +0 -0
  37. package/dist/lsp/registry.js +62 -0
  38. package/dist/mcp/callback.js +141 -0
  39. package/dist/mcp/client.js +945 -0
  40. package/dist/mcp/login.js +117 -0
  41. package/dist/mcp/oauth.js +345 -0
  42. package/dist/mcp/servers.js +114 -0
  43. package/dist/mcp/spawnWin.js +86 -0
  44. package/dist/mcp/tokens.js +97 -0
  45. package/dist/mcp/transport.js +295 -0
  46. package/dist/net/urlSafety.js +158 -0
  47. package/dist/permissions/danger.js +96 -0
  48. package/dist/permissions/permissions.js +62 -0
  49. package/dist/permissions/rules.js +69 -0
  50. package/dist/plugins/discover.js +108 -0
  51. package/dist/plugins/mcp.js +80 -0
  52. package/dist/provider/client.js +460 -0
  53. package/dist/provider/switchyardClient.js +46 -0
  54. package/dist/provider/switchyardSidecar.js +245 -0
  55. package/dist/provider/textToolCalls.js +140 -0
  56. package/dist/repomap/repoMap.js +168 -0
  57. package/dist/repomap/symbols.js +190 -0
  58. package/dist/session/store.js +328 -0
  59. package/dist/shell/background.js +131 -0
  60. package/dist/shell/sandbox.js +396 -0
  61. package/dist/telemetry/metrics.js +172 -0
  62. package/dist/telemetry/otlp.js +128 -0
  63. package/dist/telemetry/tracer.js +240 -0
  64. package/dist/tools/askUser.js +64 -0
  65. package/dist/tools/bg.js +59 -0
  66. package/dist/tools/common.js +158 -0
  67. package/dist/tools/deepResearch.js +126 -0
  68. package/dist/tools/diff.js +70 -0
  69. package/dist/tools/document/docx.js +44 -0
  70. package/dist/tools/document/pdf.js +190 -0
  71. package/dist/tools/document/pptx.js +148 -0
  72. package/dist/tools/document/types.js +26 -0
  73. package/dist/tools/document/xlsx.js +87 -0
  74. package/dist/tools/document.js +389 -0
  75. package/dist/tools/edit.js +56 -0
  76. package/dist/tools/fetchUrl.js +218 -0
  77. package/dist/tools/fuzzyMatch.js +74 -0
  78. package/dist/tools/glob.js +34 -0
  79. package/dist/tools/grep.js +85 -0
  80. package/dist/tools/ignore.js +28 -0
  81. package/dist/tools/index.js +66 -0
  82. package/dist/tools/ls.js +27 -0
  83. package/dist/tools/lsp.js +239 -0
  84. package/dist/tools/notebook.js +215 -0
  85. package/dist/tools/read.js +32 -0
  86. package/dist/tools/repoMap.js +26 -0
  87. package/dist/tools/secretScan.js +143 -0
  88. package/dist/tools/shell.js +134 -0
  89. package/dist/tools/skills.js +69 -0
  90. package/dist/tools/subagent.js +57 -0
  91. package/dist/tools/tasks.js +43 -0
  92. package/dist/tools/webSearch.js +97 -0
  93. package/dist/tools/write.js +47 -0
  94. package/dist/tools/writeAgent.js +72 -0
  95. package/dist/trust/aiDisclosure.js +45 -0
  96. package/dist/trust/mcpTrust.js +111 -0
  97. package/dist/trust/trust.js +231 -0
  98. package/dist/types.js +1 -0
  99. package/dist/ui/AiDisclosurePrompt.js +23 -0
  100. package/dist/ui/App.js +449 -0
  101. package/dist/ui/Banner.js +71 -0
  102. package/dist/ui/ElicitationPrompt.js +46 -0
  103. package/dist/ui/Markdown.js +148 -0
  104. package/dist/ui/McpTrustPrompt.js +42 -0
  105. package/dist/ui/ModelPicker.js +19 -0
  106. package/dist/ui/PermissionPrompt.js +27 -0
  107. package/dist/ui/SelectList.js +24 -0
  108. package/dist/ui/Spinner.js +12 -0
  109. package/dist/ui/StatusLine.js +11 -0
  110. package/dist/ui/TranscriptItem.js +13 -0
  111. package/dist/ui/TrustPrompt.js +12 -0
  112. package/dist/ui/highlight.js +137 -0
  113. package/dist/ui/inline.js +241 -0
  114. package/dist/ui/mermaid.js +98 -0
  115. package/dist/ui/table.js +148 -0
  116. package/dist/ui/toolOutputPreview.js +34 -0
  117. package/dist/ui/useAgent.js +534 -0
  118. package/dist/ui/useKillSwitch.js +65 -0
  119. package/dist/ui/useSessionResume.js +39 -0
  120. package/dist/ui/useUsageBudget.js +149 -0
  121. package/dist/ui/viewport.js +71 -0
  122. package/dist/undo/undo.js +293 -0
  123. package/dist/version.js +4 -0
  124. package/package.json +122 -0
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Language-aware symbol extraction for the repo map. Deliberately regex/
3
+ * heuristic-based rather than AST- or LSP-based: it must run over a whole
4
+ * repository in one pass, on any machine, with no language server to spawn and
5
+ * no native parser dependency. The result is a ctags-style skeleton — top-level
6
+ * (and lightly-nested) definitions with their signatures — not a precise parse.
7
+ * It is intended as a cheap map to orient the model, so the tradeoff favors
8
+ * breadth and speed over completeness: an occasional missed or spurious symbol
9
+ * is acceptable, a hung or crashed scan is not.
10
+ */
11
+ /**
12
+ * File extension → language family. Covers the mainstream languages; a file
13
+ * whose extension isn't here is simply skipped by the map (graceful: no crash,
14
+ * no noise), rather than run through a generic pattern that produces junk.
15
+ */
16
+ const EXT_FAMILY = {
17
+ ts: "js",
18
+ tsx: "js",
19
+ mts: "js",
20
+ cts: "js",
21
+ js: "js",
22
+ jsx: "js",
23
+ mjs: "js",
24
+ cjs: "js",
25
+ py: "python",
26
+ pyi: "python",
27
+ go: "go",
28
+ rs: "rust",
29
+ rb: "ruby",
30
+ php: "php",
31
+ java: "clike",
32
+ kt: "clike",
33
+ kts: "clike",
34
+ cs: "clike",
35
+ scala: "clike",
36
+ swift: "clike",
37
+ c: "clike",
38
+ h: "clike",
39
+ cpp: "clike",
40
+ cc: "clike",
41
+ cxx: "clike",
42
+ hpp: "clike",
43
+ hh: "clike",
44
+ hxx: "clike",
45
+ m: "clike",
46
+ mm: "clike",
47
+ };
48
+ /** Source extensions the map will scan (the keys of EXT_FAMILY). */
49
+ export const CODE_EXTENSIONS = new Set(Object.keys(EXT_FAMILY));
50
+ export function familyForExt(ext) {
51
+ return EXT_FAMILY[ext] ?? null;
52
+ }
53
+ /**
54
+ * Control-flow keywords that can precede `(...)` and otherwise look like a
55
+ * method definition (`if (x) {`, `for (…) {`). Used to reject false positives
56
+ * in the brace-language method heuristic.
57
+ */
58
+ const CONTROL_KEYWORDS = new Set([
59
+ "if",
60
+ "for",
61
+ "while",
62
+ "switch",
63
+ "catch",
64
+ "return",
65
+ "do",
66
+ "else",
67
+ "case",
68
+ "with",
69
+ "await",
70
+ "when",
71
+ "elif",
72
+ "except",
73
+ "finally",
74
+ "match",
75
+ "using",
76
+ "lock",
77
+ "synchronized",
78
+ ]);
79
+ /** Keyword-led definition patterns, matched against the trimmed line. */
80
+ const PATTERNS = {
81
+ js: [
82
+ /^(?:export\s+)?(?:default\s+)?(?:declare\s+)?(?:abstract\s+)?class\s+[\w$]/,
83
+ /^(?:export\s+)?(?:declare\s+)?interface\s+[\w$]/,
84
+ /^(?:export\s+)?(?:declare\s+)?enum\s+[\w$]/,
85
+ /^(?:export\s+)?(?:declare\s+)?type\s+[\w$][^=]*=/,
86
+ /^(?:export\s+)?(?:default\s+)?(?:declare\s+)?(?:async\s+)?function\s*\*?\s*[\w$]/,
87
+ // arrow-function assigned to a binding: `export const foo = (…) => …`
88
+ /^(?:export\s+)?(?:declare\s+)?(?:const|let|var)\s+[\w$]+\s*(?::[^=]+)?=\s*(?:async\s*)?(?:\([^)]*\)|[\w$]+)\s*=>/,
89
+ ],
90
+ python: [/^(?:async\s+)?def\s+[\w]/, /^class\s+[\w]/],
91
+ go: [/^func\s+(?:\([^)]*\)\s*)?[\w]/, /^type\s+[\w]/],
92
+ rust: [
93
+ /^(?:pub(?:\([^)]*\))?\s+)?(?:default\s+)?(?:async\s+)?(?:unsafe\s+)?(?:const\s+)?fn\s+[\w]/,
94
+ /^(?:pub(?:\([^)]*\))?\s+)?(?:struct|enum|trait|union|mod|type)\s+[\w]/,
95
+ /^impl(?:\s|<)/,
96
+ /^macro_rules!\s+[\w]/,
97
+ ],
98
+ ruby: [/^(?:def|class|module)\s+[\w]/],
99
+ php: [
100
+ /^(?:abstract\s+|final\s+)?(?:public\s+|private\s+|protected\s+|static\s+)*function\s+[\w]/,
101
+ /^(?:abstract\s+|final\s+)?(?:class|interface|trait|enum)\s+[\w]/,
102
+ ],
103
+ clike: [
104
+ /^(?:(?:public|private|protected|internal|static|final|sealed|abstract|export|open|data)\s+)*(?:class|struct|enum|interface|namespace|record|union|protocol|object|module)\s+[\w]/,
105
+ // Kotlin `fun` / Swift `func`
106
+ /^(?:(?:public|private|protected|internal|static|final|open|override|suspend|operator|inline|external)\s+)*(?:fun|func)\s+[\w]/,
107
+ ],
108
+ };
109
+ /** Families whose class methods are detected by the brace heuristic below. */
110
+ const BRACE_METHOD_FAMILIES = new Set(["js", "clike"]);
111
+ /**
112
+ * Heuristic for a class/struct method written on a single line that opens its
113
+ * body with `{` — e.g. `async runTurn(text): Promise<void> {` or
114
+ * `public void foo(int x) {`. Requires the brace so a bare call (`foo();`) or a
115
+ * multi-line signature isn't mistaken for a definition, and rejects
116
+ * control-flow keywords (`if (x) {`).
117
+ */
118
+ function isBraceMethod(trimmed) {
119
+ if (!trimmed.endsWith("{"))
120
+ return false;
121
+ if (trimmed.startsWith("}") || trimmed.startsWith("@"))
122
+ return false;
123
+ // A closing paren, an optional return type, then the opening brace.
124
+ if (!/\)\s*(?::\s*[^{]+|->\s*[^{]+)?\{$/.test(trimmed))
125
+ return false;
126
+ // The identifier immediately before the parameter list is the method name.
127
+ const m = trimmed.match(/([\w$~]+)\s*(?:<[^>]*>)?\s*\(/);
128
+ if (!m)
129
+ return false;
130
+ if (CONTROL_KEYWORDS.has(m[1]))
131
+ return false;
132
+ return true;
133
+ }
134
+ /** Strip the trailing body-opener (`{`, `{}`) or colon, collapse whitespace, and cap length. */
135
+ function cleanSignature(trimmed) {
136
+ let s = trimmed
137
+ .replace(/\s*\{\s*\}?\s*$/, "")
138
+ .replace(/\s*[:;]\s*$/, "")
139
+ .replace(/\s+/g, " ")
140
+ .trim();
141
+ if (s.length > 120)
142
+ s = s.slice(0, 119) + "…";
143
+ return s;
144
+ }
145
+ function leadingIndent(line) {
146
+ let indent = 0;
147
+ for (const ch of line) {
148
+ if (ch === " ")
149
+ indent++;
150
+ else if (ch === "\t")
151
+ indent += 2;
152
+ else
153
+ break;
154
+ }
155
+ return indent;
156
+ }
157
+ /**
158
+ * Extract up to `maxSymbols` definition signatures from a file's contents.
159
+ * Returns an empty array for files with no recognizable definitions (or an
160
+ * unsupported extension), which the caller drops from the map entirely.
161
+ */
162
+ export function extractSymbols(content, ext, maxSymbols) {
163
+ const family = familyForExt(ext);
164
+ if (!family)
165
+ return [];
166
+ const patterns = PATTERNS[family];
167
+ const braceMethods = BRACE_METHOD_FAMILIES.has(family);
168
+ const out = [];
169
+ for (const raw of content.split("\n")) {
170
+ if (out.length >= maxSymbols)
171
+ break;
172
+ const trimmed = raw.trim();
173
+ if (!trimmed)
174
+ continue;
175
+ // Skip obvious comment lines (also guards `*` block-comment continuations).
176
+ if (trimmed.startsWith("//") ||
177
+ trimmed.startsWith("*") ||
178
+ trimmed.startsWith("/*") ||
179
+ trimmed.startsWith("#")) {
180
+ continue;
181
+ }
182
+ let matched = patterns.some((re) => re.test(trimmed));
183
+ if (!matched && braceMethods)
184
+ matched = isBraceMethod(trimmed);
185
+ if (!matched)
186
+ continue;
187
+ out.push({ indent: leadingIndent(raw), text: cleanSignature(trimmed) });
188
+ }
189
+ return out;
190
+ }
@@ -0,0 +1,328 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { writeFileAtomicSync } from "../atomicWrite.js";
5
+ import { CONFIG_DIR } from "../config/config.js";
6
+ import { hardenWindowsDir } from "../config/winAcl.js";
7
+ import { debugLog } from "../config/debug.js";
8
+ function sessionDir(workspace) {
9
+ const hash = crypto.createHash("sha1").update(workspace).digest("hex").slice(0, 12);
10
+ return path.join(CONFIG_DIR, "sessions", hash);
11
+ }
12
+ /**
13
+ * Transcripts can contain secrets that passed through tool output, so they are
14
+ * always written 0o600 rather than inheriting whatever mode the file had.
15
+ * writeFileAtomicSync is the shared implementation (see src/atomicWrite.ts):
16
+ * a sibling temp file renamed over the target, so a crash mid-write can never
17
+ * leave a half-written transcript behind.
18
+ */
19
+ function writeSessionFile(filePath, data) {
20
+ writeFileAtomicSync(filePath, data, { mode: 0o600 });
21
+ }
22
+ /**
23
+ * Persists the conversation as one JSON message per line. A session maps to
24
+ * one file; --continue reloads the most recent file for the workspace.
25
+ */
26
+ export class SessionStore {
27
+ ephemeral;
28
+ dir;
29
+ file;
30
+ /** When ephemeral, nothing is persisted to disk (used by subagents). */
31
+ constructor(workspace, ephemeral = false) {
32
+ this.ephemeral = ephemeral;
33
+ this.dir = sessionDir(workspace);
34
+ this.file = this.newFilePath();
35
+ }
36
+ newFilePath() {
37
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
38
+ return path.join(this.dir, `${stamp}.jsonl`);
39
+ }
40
+ /**
41
+ * Stable identifier for this session (the transcript file's basename), used
42
+ * to correlate the audit log and telemetry spans with the session. Updates
43
+ * when the session rotates via reset(), so all three stay in lockstep.
44
+ */
45
+ get id() {
46
+ return path.basename(this.file, ".jsonl");
47
+ }
48
+ /**
49
+ * Absolute path of this session's transcript, or undefined when ephemeral
50
+ * (nothing is on disk). Used by the crash handler to tell the user where the
51
+ * conversation survives — a crash is exactly when that matters.
52
+ */
53
+ get path() {
54
+ return this.ephemeral ? undefined : this.file;
55
+ }
56
+ /** Path of the sidecar file that holds this session's task checklist. */
57
+ tasksFilePath() {
58
+ return SessionStore.tasksFilePathFor(this.file);
59
+ }
60
+ static tasksFilePathFor(sessionFile) {
61
+ return sessionFile.replace(/\.jsonl$/, ".tasks.json");
62
+ }
63
+ /**
64
+ * Persists the current task checklist alongside the session, so `-c`/`-r`
65
+ * can restore not just the conversation but what was done vs. pending.
66
+ * Best-effort, same as append() — never let this crash a turn.
67
+ */
68
+ saveTasks(tasks) {
69
+ if (this.ephemeral)
70
+ return;
71
+ try {
72
+ fs.mkdirSync(this.dir, { recursive: true, mode: 0o700 });
73
+ hardenWindowsDir(CONFIG_DIR);
74
+ if (!tasks.length) {
75
+ fs.rmSync(this.tasksFilePath(), { force: true });
76
+ return;
77
+ }
78
+ writeSessionFile(this.tasksFilePath(), JSON.stringify(tasks));
79
+ }
80
+ catch (err) {
81
+ debugLog(`SessionStore.saveTasks(${this.tasksFilePath()})`, err);
82
+ }
83
+ }
84
+ /** Loads the task checklist saved alongside a given session file, if any. */
85
+ static loadTasksForSession(sessionFile) {
86
+ try {
87
+ const raw = fs.readFileSync(SessionStore.tasksFilePathFor(sessionFile), "utf8");
88
+ const parsed = JSON.parse(raw);
89
+ if (!Array.isArray(parsed))
90
+ return [];
91
+ return parsed.filter((t) => !!t &&
92
+ typeof t === "object" &&
93
+ typeof t.text === "string" &&
94
+ ["pending", "in_progress", "done"].includes(t.status));
95
+ }
96
+ catch {
97
+ return [];
98
+ }
99
+ }
100
+ /** Begin a session, optionally seeded with resumed history. */
101
+ start(seed = []) {
102
+ if (this.ephemeral)
103
+ return;
104
+ // Session transcripts can contain secrets that passed through tool
105
+ // output — keep them readable only by the owner.
106
+ fs.mkdirSync(this.dir, { recursive: true, mode: 0o700 });
107
+ hardenWindowsDir(CONFIG_DIR);
108
+ if (seed.length) {
109
+ writeSessionFile(this.file, seed.map((m) => JSON.stringify(m) + "\n").join(""));
110
+ }
111
+ }
112
+ /**
113
+ * Appends one JSON-encoded message per call — a crash mid-write can only
114
+ * ever corrupt the single line currently being written, never anything
115
+ * appended before it (each prior line was already a completed, separate
116
+ * write). loadFile/matchesContent/listSessions all parse line-by-line and
117
+ * skip a line that fails JSON.parse, so a truncated last line loses at
118
+ * most that one message rather than the whole session.
119
+ */
120
+ append(message) {
121
+ if (this.ephemeral)
122
+ return;
123
+ try {
124
+ fs.mkdirSync(this.dir, { recursive: true, mode: 0o700 });
125
+ hardenWindowsDir(CONFIG_DIR);
126
+ fs.appendFileSync(this.file, JSON.stringify(message) + "\n", { mode: 0o600 });
127
+ }
128
+ catch (err) {
129
+ // Persistence is best-effort; never crash the session over it.
130
+ debugLog(`SessionStore.append(${this.file})`, err);
131
+ }
132
+ }
133
+ /** Start over with a fresh session file (used by /clear). */
134
+ rotate() {
135
+ this.file = this.newFilePath();
136
+ }
137
+ /**
138
+ * Rewrite the session file to hold exactly `messages`, atomically. The log
139
+ * is otherwise append-only; this is the one place it's rewound — used by
140
+ * /rewind to drop the messages after a checkpoint. The atomic write means a
141
+ * reader (e.g. a concurrent --continue) never sees a half-written file.
142
+ */
143
+ overwrite(messages) {
144
+ if (this.ephemeral)
145
+ return;
146
+ try {
147
+ fs.mkdirSync(this.dir, { recursive: true, mode: 0o700 });
148
+ hardenWindowsDir(CONFIG_DIR);
149
+ writeSessionFile(this.file, messages.map((m) => JSON.stringify(m) + "\n").join(""));
150
+ }
151
+ catch (err) {
152
+ // Persistence is best-effort; never crash the session over it.
153
+ debugLog(`SessionStore.overwrite(${this.file})`, err);
154
+ }
155
+ }
156
+ /**
157
+ * True if `filePath` is a real session transcript belonging to `workspace`
158
+ * — i.e. resolves inside that workspace's session directory. Callers that
159
+ * accept a session path from outside the process (e.g. the Electron
160
+ * renderer over IPC) must check this before loading it: without it,
161
+ * "load this session" is really "read any file the OS user can read".
162
+ */
163
+ static isSessionFile(workspace, filePath) {
164
+ const dir = sessionDir(workspace);
165
+ const resolved = path.resolve(dir, filePath);
166
+ const relative = path.relative(dir, resolved);
167
+ return (resolved.endsWith(".jsonl") &&
168
+ relative !== "" &&
169
+ !relative.startsWith("..") &&
170
+ !path.isAbsolute(relative));
171
+ }
172
+ static loadFile(filePath) {
173
+ const messages = [];
174
+ let raw;
175
+ try {
176
+ raw = fs.readFileSync(filePath, "utf8");
177
+ }
178
+ catch {
179
+ return messages;
180
+ }
181
+ for (const line of raw.split("\n")) {
182
+ if (!line.trim())
183
+ continue;
184
+ try {
185
+ messages.push(JSON.parse(line));
186
+ }
187
+ catch {
188
+ // Skip corrupt lines rather than failing the resume.
189
+ }
190
+ }
191
+ return messages;
192
+ }
193
+ /** Lines of a session file, one JSON message per non-blank line (unparsed). */
194
+ static readLines(filePath) {
195
+ let raw;
196
+ try {
197
+ raw = fs.readFileSync(filePath, "utf8");
198
+ }
199
+ catch {
200
+ return [];
201
+ }
202
+ return raw.split("\n").filter((line) => line.trim());
203
+ }
204
+ /** True if any message's content in the session file contains query (case-insensitive). Used for --resume search beyond the title preview. */
205
+ static matchesContent(filePath, query) {
206
+ if (!query.trim())
207
+ return true;
208
+ const needle = query.toLowerCase();
209
+ for (const line of SessionStore.readLines(filePath)) {
210
+ let message;
211
+ try {
212
+ message = JSON.parse(line);
213
+ }
214
+ catch {
215
+ continue;
216
+ }
217
+ if (typeof message.content === "string" && message.content.toLowerCase().includes(needle)) {
218
+ return true;
219
+ }
220
+ }
221
+ return false;
222
+ }
223
+ /**
224
+ * Delete session files older than `retentionDays` across all workspaces.
225
+ * Best-effort. 0 or negative means "keep forever" — auto-delete is
226
+ * disabled rather than treated as an immediate-expiry retention window.
227
+ */
228
+ static cleanupOldSessions(retentionDays) {
229
+ if (retentionDays <= 0)
230
+ return;
231
+ const root = path.join(CONFIG_DIR, "sessions");
232
+ const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
233
+ let dirs;
234
+ try {
235
+ dirs = fs.readdirSync(root);
236
+ }
237
+ catch {
238
+ return;
239
+ }
240
+ for (const d of dirs) {
241
+ const dir = path.join(root, d);
242
+ let files;
243
+ try {
244
+ files = fs.readdirSync(dir);
245
+ }
246
+ catch {
247
+ continue;
248
+ }
249
+ for (const f of files) {
250
+ if (!f.endsWith(".jsonl"))
251
+ continue;
252
+ const file = path.join(dir, f);
253
+ try {
254
+ if (fs.statSync(file).mtimeMs < cutoff) {
255
+ fs.unlinkSync(file);
256
+ fs.rmSync(SessionStore.tasksFilePathFor(file), { force: true });
257
+ }
258
+ }
259
+ catch (err) {
260
+ debugLog(`SessionStore.cleanupOldSessions(${file})`, err);
261
+ }
262
+ }
263
+ }
264
+ }
265
+ static loadLatest(workspace) {
266
+ const latest = SessionStore.listSessions(workspace)[0];
267
+ if (!latest)
268
+ return null;
269
+ const messages = SessionStore.loadFile(latest.file);
270
+ return messages.length ? messages : null;
271
+ }
272
+ /** The task checklist saved alongside the most recent session for `workspace`, if any. */
273
+ static loadLatestTasks(workspace) {
274
+ const latest = SessionStore.listSessions(workspace)[0];
275
+ return latest ? SessionStore.loadTasksForSession(latest.file) : [];
276
+ }
277
+ /** Sessions for a workspace, newest first, with a preview of the first user message. */
278
+ static listSessions(workspace) {
279
+ const dir = sessionDir(workspace);
280
+ let files;
281
+ try {
282
+ files = fs
283
+ .readdirSync(dir)
284
+ .filter((f) => f.endsWith(".jsonl"))
285
+ .sort()
286
+ .reverse();
287
+ }
288
+ catch {
289
+ return [];
290
+ }
291
+ const sessions = [];
292
+ for (const f of files.slice(0, 20)) {
293
+ const file = path.join(dir, f);
294
+ const lines = SessionStore.readLines(file);
295
+ if (!lines.length)
296
+ continue;
297
+ let cleaned = "";
298
+ for (const line of lines) {
299
+ let message;
300
+ try {
301
+ message = JSON.parse(line);
302
+ }
303
+ catch {
304
+ continue; // skip corrupt lines rather than failing the preview
305
+ }
306
+ if (message.role === "user" &&
307
+ typeof message.content === "string" &&
308
+ message.content.trim() &&
309
+ !message.content.startsWith("[") // skip synthetic notes (undo, summaries)
310
+ ) {
311
+ cleaned = message.content.replace(/\s+/g, " ").trim();
312
+ break;
313
+ }
314
+ }
315
+ const preview = cleaned ? cleaned.slice(0, 60) : "(no preview)";
316
+ const title = cleaned ? cleaned.slice(0, 48) : "(untitled session)";
317
+ let date = "";
318
+ try {
319
+ date = fs.statSync(file).mtime.toLocaleString();
320
+ }
321
+ catch {
322
+ // leave empty
323
+ }
324
+ sessions.push({ file, date, preview, title, count: lines.length });
325
+ }
326
+ return sessions;
327
+ }
328
+ }
@@ -0,0 +1,131 @@
1
+ import { spawn } from "node:child_process";
2
+ import os from "node:os";
3
+ import { scrubbedShellEnv } from "../config/config.js";
4
+ import { buildSandboxedCommand, requiresSandbox, sandboxUnavailableReason, shouldSandbox, } from "./sandbox.js";
5
+ const MAX_BUFFER_CHARS = 50_000;
6
+ /**
7
+ * Registry of long-running processes the agent started with shell
8
+ * background:true (dev servers, watchers). Captures a rolling tail of
9
+ * interleaved stdout/stderr; everything is killed when kritya exits.
10
+ */
11
+ class BackgroundManager {
12
+ procs = new Map();
13
+ counter = 0;
14
+ start(command, cwd, sandboxMode) {
15
+ const id = `bg_${++this.counter}`;
16
+ const isWindows = os.platform() === "win32";
17
+ // detached on POSIX puts the command in its own process group, so kill()
18
+ // can signal the whole tree (shell + whatever it spawned), not just it.
19
+ // scrubbedShellEnv: background commands must not inherit provider API keys.
20
+ const spawnOpts = {
21
+ cwd,
22
+ env: scrubbedShellEnv(),
23
+ windowsHide: true,
24
+ detached: !isWindows,
25
+ };
26
+ let proc;
27
+ let cleanup;
28
+ if (shouldSandbox(sandboxMode, command)) {
29
+ const wrapped = buildSandboxedCommand(command, cwd);
30
+ if (wrapped) {
31
+ const opts = wrapped.env
32
+ ? { ...spawnOpts, env: { ...spawnOpts.env, ...wrapped.env } }
33
+ : spawnOpts;
34
+ proc = spawn(wrapped.cmd, wrapped.args, opts);
35
+ cleanup = wrapped.cleanup;
36
+ }
37
+ else if (requiresSandbox(sandboxMode)) {
38
+ // Same fail-closed contract as the foreground shell tool: "strict"
39
+ // means the sandbox is a hard requirement, so refuse rather than
40
+ // start an unconfined background process.
41
+ throw new Error(`sandboxExec is "strict" but sandboxing is unavailable here (${sandboxUnavailableReason()}) ` +
42
+ `— refusing to start an unsandboxed background process`);
43
+ }
44
+ else {
45
+ // Sandboxing was requested but no sandbox binary is available here —
46
+ // fall back to a plain run, same as the foreground shell tool does.
47
+ proc = spawn(command, { ...spawnOpts, shell: true });
48
+ }
49
+ }
50
+ else {
51
+ // shell:true lets Node pick the right shell and quoting per OS from a
52
+ // single command string, same as the exec() used by the regular shell
53
+ // tool -- manually building a cmd.exe "/c" args array (the previous
54
+ // approach) mis-parses commands that themselves contain quotes.
55
+ proc = spawn(command, { ...spawnOpts, shell: true });
56
+ }
57
+ const entry = { proc, command, buffer: "", exitCode: null, running: true };
58
+ const append = (chunk) => {
59
+ entry.buffer += chunk.toString();
60
+ if (entry.buffer.length > MAX_BUFFER_CHARS) {
61
+ entry.buffer = entry.buffer.slice(-MAX_BUFFER_CHARS);
62
+ }
63
+ };
64
+ proc.stdout?.on("data", append);
65
+ proc.stderr?.on("data", append);
66
+ // "exit" (not "close"): grandchildren may inherit the pipes and keep them
67
+ // open after the direct child dies; exit still fires then.
68
+ proc.on("exit", (code) => {
69
+ entry.running = false;
70
+ entry.exitCode = code;
71
+ proc.stdout?.destroy();
72
+ proc.stderr?.destroy();
73
+ cleanup?.();
74
+ });
75
+ proc.on("error", (err) => {
76
+ entry.running = false;
77
+ entry.buffer += `\n[failed to start: ${err.message}]`;
78
+ // If the sandbox binary itself fails to spawn, "exit" may never fire, so
79
+ // the macOS temp profile would leak. cleanup is fs.rm({force:true}),
80
+ // which is idempotent, so running it from both handlers is safe.
81
+ cleanup?.();
82
+ });
83
+ this.procs.set(id, entry);
84
+ return { id };
85
+ }
86
+ signal(entry, sig) {
87
+ try {
88
+ if (os.platform() !== "win32" && entry.proc.pid) {
89
+ process.kill(-entry.proc.pid, sig); // whole process group
90
+ }
91
+ else {
92
+ entry.proc.kill(sig);
93
+ }
94
+ return true;
95
+ }
96
+ catch {
97
+ return false;
98
+ }
99
+ }
100
+ read(id) {
101
+ const entry = this.procs.get(id);
102
+ if (!entry)
103
+ return null;
104
+ return {
105
+ output: entry.buffer,
106
+ running: entry.running,
107
+ exitCode: entry.exitCode,
108
+ command: entry.command,
109
+ };
110
+ }
111
+ kill(id) {
112
+ const entry = this.procs.get(id);
113
+ if (!entry || !entry.running)
114
+ return false;
115
+ return this.signal(entry, "SIGTERM");
116
+ }
117
+ list() {
118
+ return [...this.procs.entries()].map(([id, e]) => ({
119
+ id,
120
+ command: e.command,
121
+ running: e.running,
122
+ }));
123
+ }
124
+ killAll() {
125
+ for (const entry of this.procs.values()) {
126
+ if (entry.running)
127
+ this.signal(entry, "SIGKILL");
128
+ }
129
+ }
130
+ }
131
+ export const backgroundManager = new BackgroundManager();