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,218 @@
1
+ import { lookup as dnsLookup } from "node:dns/promises";
2
+ import { truncateResult } from "./common.js";
3
+ import { isPrivateOrLoopbackHost } from "../net/urlSafety.js";
4
+ /** Cap on how much text a single fetch returns, unless the caller asks for less. */
5
+ const DEFAULT_MAX_CHARS = 20_000;
6
+ /** Give up on a slow or hanging server rather than blocking the whole turn. */
7
+ const FETCH_TIMEOUT_MS = 20_000;
8
+ /**
9
+ * Hard cap on raw response bytes read off the wire, independent of max_chars.
10
+ * Enforced while streaming (not after buffering the full body) so a server
11
+ * that lies about content-length or sends gigabytes of text can't blow up
12
+ * process memory before truncation ever gets a chance to run.
13
+ */
14
+ export const MAX_RESPONSE_BYTES = 10 * 1024 * 1024;
15
+ /**
16
+ * Reject URLs that point at the local machine or a private/internal network.
17
+ * fetch_url can reach any host the process can, so without this guard a prompt
18
+ * injection could aim it at localhost services or a cloud metadata endpoint
19
+ * (169.254.169.254) to read credentials. This is a literal-host/IP check, not
20
+ * DNS-rebinding-proof, but it blocks the obvious SSRF targets.
21
+ */
22
+ function assertPublicUrl(raw) {
23
+ let url;
24
+ try {
25
+ url = new URL(raw);
26
+ }
27
+ catch {
28
+ throw new Error(`Not a valid URL: ${raw}`);
29
+ }
30
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
31
+ throw new Error(`Only http and https URLs are allowed (got ${url.protocol})`);
32
+ }
33
+ const host = url.hostname.toLowerCase().replace(/^\[|\]$/g, "");
34
+ if (host === "localhost" ||
35
+ host.endsWith(".localhost") ||
36
+ host === "0.0.0.0" ||
37
+ host === "::1" ||
38
+ host === "::") {
39
+ throw new Error(`Refusing to fetch a local address: ${host}`);
40
+ }
41
+ if (isPrivateOrLoopbackHost(host)) {
42
+ throw new Error(`Refusing to fetch a private/internal address: ${host}`);
43
+ }
44
+ return url;
45
+ }
46
+ /**
47
+ * Resolve `hostname` and check whether any answer is private/loopback.
48
+ * assertPublicUrl only checks the literal hostname string, so a
49
+ * public-looking name that resolves (now, or on a later request) to an
50
+ * internal IP — DNS rebinding, or simply attacker-controlled DNS — slips
51
+ * past it. This closes that gap for the initial request; lookupFn is
52
+ * injectable for tests, and defaults to a real DNS resolution.
53
+ */
54
+ export async function hostResolvesToPrivateAddress(hostname, lookupFn = dnsLookup) {
55
+ try {
56
+ const addresses = await lookupFn(hostname, { all: true });
57
+ return addresses.some((a) => isPrivateOrLoopbackHost(a.address));
58
+ }
59
+ catch {
60
+ // Unresolvable host — let the actual fetch fail naturally with its own error.
61
+ return false;
62
+ }
63
+ }
64
+ /** Refuse to keep chasing redirects forever. */
65
+ const MAX_REDIRECTS = 5;
66
+ /**
67
+ * Fetch `url`, following redirects manually so every hop — not just the
68
+ * original URL — is checked against assertPublicUrl. A public URL that
69
+ * 302s to 169.254.169.254 or localhost would otherwise sail straight past
70
+ * the initial check, since `redirect: "follow"` never re-validates.
71
+ */
72
+ async function assertNotDnsRebound(url) {
73
+ if (await hostResolvesToPrivateAddress(url.hostname)) {
74
+ throw new Error(`Refusing to fetch ${url.hostname}: resolves to a private/internal address`);
75
+ }
76
+ }
77
+ async function fetchFollowingRedirects(url, signal, headers) {
78
+ let current = url;
79
+ await assertNotDnsRebound(current);
80
+ for (let hop = 0;; hop++) {
81
+ const res = await fetch(current, { redirect: "manual", signal, headers });
82
+ const isRedirect = res.status >= 300 && res.status < 400;
83
+ const location = res.headers.get("location");
84
+ if (!isRedirect || !location) {
85
+ return { res, finalUrl: current };
86
+ }
87
+ if (hop >= MAX_REDIRECTS) {
88
+ throw new Error(`Too many redirects fetching ${url.href} (stopped at ${current.href})`);
89
+ }
90
+ current = assertPublicUrl(new URL(location, current).href);
91
+ await assertNotDnsRebound(current);
92
+ }
93
+ }
94
+ /**
95
+ * Read a response body up to `maxBytes`, stopping the underlying stream as
96
+ * soon as the cap is hit instead of buffering the whole thing first. A
97
+ * server that ignores Content-Length (or lies about it) can otherwise be
98
+ * used to exhaust process memory before `truncateResult` ever runs.
99
+ */
100
+ async function readBodyCapped(res, maxBytes) {
101
+ if (!res.body)
102
+ return res.text();
103
+ const reader = res.body.getReader();
104
+ const chunks = [];
105
+ let total = 0;
106
+ for (;;) {
107
+ const { done, value } = await reader.read();
108
+ if (done)
109
+ break;
110
+ if (!value)
111
+ continue;
112
+ total += value.length;
113
+ if (total > maxBytes) {
114
+ const keep = value.length - (total - maxBytes);
115
+ if (keep > 0)
116
+ chunks.push(value.subarray(0, keep));
117
+ await reader.cancel();
118
+ break;
119
+ }
120
+ chunks.push(value);
121
+ }
122
+ return Buffer.concat(chunks.map((c) => Buffer.from(c))).toString("utf-8");
123
+ }
124
+ /** Collapse an HTML document down to readable plain text (best-effort, no deps). */
125
+ function htmlToText(html) {
126
+ return html
127
+ .replace(/<script[\s\S]*?<\/script>/gi, " ")
128
+ .replace(/<style[\s\S]*?<\/style>/gi, " ")
129
+ .replace(/<noscript[\s\S]*?<\/noscript>/gi, " ")
130
+ .replace(/<!--[\s\S]*?-->/g, " ")
131
+ .replace(/<\/(p|div|li|tr|h[1-6]|section|article|br)>/gi, "\n")
132
+ .replace(/<br\s*\/?>/gi, "\n")
133
+ .replace(/<[^>]+>/g, " ")
134
+ .replace(/&nbsp;/gi, " ")
135
+ .replace(/&amp;/gi, "&")
136
+ .replace(/&lt;/gi, "<")
137
+ .replace(/&gt;/gi, ">")
138
+ .replace(/&quot;/gi, '"')
139
+ .replace(/&#39;/gi, "'")
140
+ .replace(/[ \t]+/g, " ")
141
+ .replace(/\n{3,}/g, "\n\n")
142
+ .trim();
143
+ }
144
+ /**
145
+ * Fetch one URL and return its text content. HTML is reduced to plain text;
146
+ * JSON and plain-text responses are returned as-is. Used by both the fetch_url
147
+ * tool and deep_research. The result is untrusted web content — callers that
148
+ * feed it to the model must fence it (the agent loop does this for tools with
149
+ * `external: true`).
150
+ */
151
+ export async function fetchUrlText(raw, maxChars = DEFAULT_MAX_CHARS) {
152
+ const url = assertPublicUrl(raw);
153
+ const controller = new AbortController();
154
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
155
+ let res;
156
+ let finalUrl;
157
+ try {
158
+ ({ res, finalUrl } = await fetchFollowingRedirects(url, controller.signal, {
159
+ // Some servers 403 an unknown agent; identify honestly.
160
+ "User-Agent": "kritya-agent/1.0 (+https://github.com/) fetch_url",
161
+ Accept: "text/html,application/xhtml+xml,application/json,text/plain,*/*",
162
+ }));
163
+ }
164
+ catch (err) {
165
+ if (err instanceof Error && err.name === "AbortError") {
166
+ throw new Error(`Timed out after ${FETCH_TIMEOUT_MS / 1000}s fetching ${url.href}`, {
167
+ cause: err,
168
+ });
169
+ }
170
+ throw new Error(`Could not fetch ${url.href}: ${err instanceof Error ? err.message : err}`, {
171
+ cause: err,
172
+ });
173
+ }
174
+ finally {
175
+ clearTimeout(timer);
176
+ }
177
+ if (!res.ok) {
178
+ throw new Error(`HTTP ${res.status} ${res.statusText} for ${finalUrl.href}`);
179
+ }
180
+ const contentType = res.headers.get("content-type") ?? "";
181
+ if (!/text\/|application\/(json|xml|xhtml)|\+xml|\+json/i.test(contentType) && contentType) {
182
+ throw new Error(`Refusing to fetch non-text content (${contentType}) from ${finalUrl.href}. ` +
183
+ `fetch_url only reads web pages, docs, and APIs — not binaries.`);
184
+ }
185
+ const body = await readBodyCapped(res, MAX_RESPONSE_BYTES);
186
+ const text = /html/i.test(contentType) ? htmlToText(body) : body.trim();
187
+ const header = `URL: ${finalUrl.href}\nContent-Type: ${contentType || "unknown"}\n\n`;
188
+ return truncateResult(header + (text || "(empty response body)"), maxChars);
189
+ }
190
+ export const fetchUrlTool = {
191
+ name: "fetch_url",
192
+ description: "Fetch the full text of a single known web page, documentation URL, raw file, or JSON/API " +
193
+ "endpoint. Use this when you already have a specific URL to read — it complements web_search, " +
194
+ "which only returns short result snippets. HTML is reduced to readable text. Local and " +
195
+ "private-network addresses are refused.",
196
+ parameters: {
197
+ type: "object",
198
+ properties: {
199
+ url: { type: "string", description: "The http(s) URL to fetch" },
200
+ max_chars: {
201
+ type: "number",
202
+ description: `Max characters to return (default ${DEFAULT_MAX_CHARS})`,
203
+ },
204
+ },
205
+ required: ["url"],
206
+ },
207
+ // Prompts before reaching out: the URL is visible so an injected exfiltration
208
+ // target (data smuggled into a query string) can be caught before it's sent.
209
+ requiresPermission: true,
210
+ external: true,
211
+ summarize: (args) => `Fetch URL: ${args.url}`,
212
+ async execute(args) {
213
+ const maxChars = args.max_chars === undefined || args.max_chars === null
214
+ ? DEFAULT_MAX_CHARS
215
+ : Math.min(Math.max(Number(args.max_chars), 500), 50_000);
216
+ return fetchUrlText(String(args.url), maxChars);
217
+ },
218
+ };
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Edit matching for edit_file. Weaker models frequently reproduce a target
3
+ * snippet with slightly different indentation or trailing whitespace, which
4
+ * makes strict exact-string matching fail a lot. applyEdit tries an exact
5
+ * match first, then a line-trimmed fallback that ignores per-line leading and
6
+ * trailing whitespace while still requiring the lines to be contiguous and to
7
+ * match in order — enough tolerance to be useful without matching the wrong code.
8
+ */
9
+ function countOccurrences(haystack, needle) {
10
+ if (!needle)
11
+ return 0;
12
+ return haystack.split(needle).length - 1;
13
+ }
14
+ /** Drop a single trailing empty line produced by a trailing newline in `s`. */
15
+ function toLines(s) {
16
+ const lines = s.split("\n");
17
+ if (lines.length > 1 && lines[lines.length - 1] === "")
18
+ lines.pop();
19
+ return lines;
20
+ }
21
+ function lineTrimmedMatch(content, oldStr, newStr, replaceAll) {
22
+ const contentLines = content.split("\n");
23
+ const oldLines = toLines(oldStr);
24
+ if (oldLines.length === 0)
25
+ return null;
26
+ const trimmedOld = oldLines.map((l) => l.trim());
27
+ const starts = [];
28
+ for (let i = 0; i + oldLines.length <= contentLines.length; i++) {
29
+ let ok = true;
30
+ for (let j = 0; j < oldLines.length; j++) {
31
+ if (contentLines[i + j].trim() !== trimmedOld[j]) {
32
+ ok = false;
33
+ break;
34
+ }
35
+ }
36
+ if (ok) {
37
+ starts.push(i);
38
+ i += oldLines.length - 1;
39
+ }
40
+ }
41
+ if (starts.length === 0)
42
+ return null;
43
+ if (starts.length > 1 && !replaceAll) {
44
+ return { matched: true, count: starts.length, strategy: "line-trimmed" };
45
+ }
46
+ const newLines = toLines(newStr);
47
+ const targets = replaceAll ? [...starts].reverse() : [starts[0]];
48
+ for (const s of targets) {
49
+ contentLines.splice(s, oldLines.length, ...newLines);
50
+ }
51
+ return {
52
+ matched: true,
53
+ count: starts.length,
54
+ result: contentLines.join("\n"),
55
+ strategy: "line-trimmed",
56
+ };
57
+ }
58
+ export function applyEdit(content, oldStr, newStr, replaceAll) {
59
+ const exactCount = countOccurrences(content, oldStr);
60
+ if (exactCount > 0) {
61
+ if (exactCount > 1 && !replaceAll) {
62
+ return { matched: true, count: exactCount, strategy: "exact" };
63
+ }
64
+ const result = replaceAll
65
+ ? content.split(oldStr).join(newStr)
66
+ : content.replace(oldStr, newStr);
67
+ return { matched: true, count: exactCount, result, strategy: "exact" };
68
+ }
69
+ return (lineTrimmedMatch(content, oldStr, newStr, replaceAll) ?? {
70
+ matched: false,
71
+ count: 0,
72
+ strategy: "none",
73
+ });
74
+ }
@@ -0,0 +1,34 @@
1
+ import path from "node:path";
2
+ import fg from "fast-glob";
3
+ import { isPathSafe, truncateResult, countLines } from "./common.js";
4
+ import { loadIgnorePatterns } from "./ignore.js";
5
+ export const globTool = {
6
+ name: "glob",
7
+ description: "Find files matching a glob pattern (e.g. 'src/**/*.ts'). Ignores node_modules, .git, " +
8
+ "and anything matched by a .krityaignore file in the workspace root.",
9
+ parameters: {
10
+ type: "object",
11
+ properties: {
12
+ pattern: { type: "string", description: "Glob pattern relative to the workspace root" },
13
+ },
14
+ required: ["pattern"],
15
+ },
16
+ requiresPermission: false,
17
+ summarize: (args) => `Glob ${args.pattern}`,
18
+ resultSummary: (output) => countLines(output, "match", "matches"),
19
+ async execute(args, ctx) {
20
+ // Models on Windows sometimes emit backslash paths; fast-glob needs forward slashes.
21
+ const files = await fg(String(args.pattern).replaceAll("\\", "/"), {
22
+ cwd: ctx.workspace,
23
+ dot: false,
24
+ onlyFiles: true,
25
+ followSymbolicLinks: false,
26
+ ignore: ["**/node_modules/**", "**/.git/**", ...loadIgnorePatterns(ctx.workspace)],
27
+ suppressErrors: true,
28
+ });
29
+ const safeFiles = files.filter((f) => isPathSafe(ctx.workspace, path.join(ctx.workspace, f)));
30
+ const capped = safeFiles.sort().slice(0, 200);
31
+ const suffix = safeFiles.length > 200 ? `\n... [${safeFiles.length - 200} more]` : "";
32
+ return truncateResult(capped.join("\n") + suffix || "(no matches)");
33
+ },
34
+ };
@@ -0,0 +1,85 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import fg from "fast-glob";
4
+ import { isPathSafe, resolveSafe, safeCompileRegex, truncateResult, meaningfulLines, } from "./common.js";
5
+ import { loadIgnorePatterns } from "./ignore.js";
6
+ const MAX_MATCHES = 200;
7
+ const MAX_FILE_BYTES = 1024 * 1024;
8
+ export const grepTool = {
9
+ name: "grep",
10
+ description: "Search file contents with a regular expression. Returns 'file:line: text' matches. " +
11
+ "Ignores node_modules, .git, binary files, and anything matched by a .krityaignore " +
12
+ "file in the workspace root.",
13
+ parameters: {
14
+ type: "object",
15
+ properties: {
16
+ pattern: { type: "string", description: "JavaScript regular expression to search for" },
17
+ path: { type: "string", description: "Directory to search in (default: workspace root)" },
18
+ include: {
19
+ type: "string",
20
+ description: "Glob filter for files to search, e.g. '**/*.ts' (default: all files)",
21
+ },
22
+ },
23
+ required: ["pattern"],
24
+ },
25
+ requiresPermission: false,
26
+ summarize: (args) => `Grep /${args.pattern}/ in ${args.path ?? "."}`,
27
+ resultSummary(output) {
28
+ const lines = meaningfulLines(output);
29
+ if (!lines)
30
+ return "no matches";
31
+ const files = new Set(lines.map((l) => l.split(":")[0])).size;
32
+ return `${lines.length} match${lines.length === 1 ? "" : "es"} in ${files} file${files === 1 ? "" : "s"}`;
33
+ },
34
+ async execute(args, ctx) {
35
+ const regex = safeCompileRegex(String(args.pattern));
36
+ const searchRoot = resolveSafe(ctx.workspace, String(args.path ?? "."));
37
+ // Models on Windows sometimes emit backslash paths; fast-glob needs forward slashes.
38
+ const files = await fg(String(args.include ?? "**/*").replaceAll("\\", "/"), {
39
+ cwd: searchRoot,
40
+ dot: false,
41
+ onlyFiles: true,
42
+ followSymbolicLinks: false,
43
+ ignore: ["**/node_modules/**", "**/.git/**", ...loadIgnorePatterns(ctx.workspace)],
44
+ suppressErrors: true,
45
+ });
46
+ const matches = [];
47
+ for (const file of files) {
48
+ if (matches.length >= MAX_MATCHES)
49
+ break;
50
+ const abs = path.join(searchRoot, file);
51
+ // Same secret-file and symlink-escape guard as read_file, applied per
52
+ // matched file (the root-only check above isn't enough for a recursive
53
+ // glob that can traverse into a sensitive path or a symlink).
54
+ if (!isPathSafe(ctx.workspace, abs))
55
+ continue;
56
+ let stat;
57
+ try {
58
+ stat = await fs.stat(abs);
59
+ }
60
+ catch {
61
+ continue;
62
+ }
63
+ if (stat.size > MAX_FILE_BYTES)
64
+ continue;
65
+ let content;
66
+ try {
67
+ content = await fs.readFile(abs, "utf8");
68
+ }
69
+ catch {
70
+ continue;
71
+ }
72
+ if (content.slice(0, 8000).includes("\0"))
73
+ continue;
74
+ const rel = path.relative(ctx.workspace, abs);
75
+ const lines = content.split("\n");
76
+ for (let i = 0; i < lines.length && matches.length < MAX_MATCHES; i++) {
77
+ if (regex.test(lines[i])) {
78
+ matches.push(`${rel}:${i + 1}: ${lines[i].slice(0, 250)}`);
79
+ }
80
+ }
81
+ }
82
+ const suffix = matches.length >= MAX_MATCHES ? `\n... [match limit reached]` : "";
83
+ return truncateResult(matches.join("\n") + suffix || "(no matches)");
84
+ },
85
+ };
@@ -0,0 +1,28 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ const IGNORE_FILE = ".krityaignore";
4
+ /** Converts one gitignore-style line into fast-glob ignore pattern(s). */
5
+ function toGlobPatterns(line) {
6
+ let pattern = line.trim();
7
+ if (!pattern || pattern.startsWith("#") || pattern.startsWith("!"))
8
+ return [];
9
+ const isDir = pattern.endsWith("/");
10
+ if (isDir)
11
+ pattern = pattern.slice(0, -1);
12
+ const anchored = pattern.startsWith("/");
13
+ if (anchored)
14
+ pattern = pattern.slice(1);
15
+ const base = anchored ? pattern : `**/${pattern}`;
16
+ return isDir ? [`${base}/**`] : [base, `${base}/**`];
17
+ }
18
+ /** Reads `<workspace>/.krityaignore` (gitignore-style, one pattern per line) as fast-glob ignore patterns. */
19
+ export function loadIgnorePatterns(workspace) {
20
+ let raw;
21
+ try {
22
+ raw = fs.readFileSync(path.join(workspace, IGNORE_FILE), "utf8");
23
+ }
24
+ catch {
25
+ return [];
26
+ }
27
+ return raw.split("\n").flatMap(toGlobPatterns);
28
+ }
@@ -0,0 +1,66 @@
1
+ import { readFileTool } from "./read.js";
2
+ import { writeFileTool } from "./write.js";
3
+ import { editFileTool } from "./edit.js";
4
+ import { shellTool } from "./shell.js";
5
+ import { listDirTool } from "./ls.js";
6
+ import { globTool } from "./glob.js";
7
+ import { grepTool } from "./grep.js";
8
+ import { updateTasksTool } from "./tasks.js";
9
+ import { webSearchTool } from "./webSearch.js";
10
+ import { fetchUrlTool } from "./fetchUrl.js";
11
+ import { deepResearchTool } from "./deepResearch.js";
12
+ import { bgOutputTool, bgKillTool } from "./bg.js";
13
+ import { spawnAgentTool } from "./subagent.js";
14
+ import { spawnWriteAgentTool } from "./writeAgent.js";
15
+ import { readDocumentTool, writeDocumentTool, editSpreadsheetTool, editPdfTool, } from "./document.js";
16
+ import { readNotebookTool, editNotebookTool } from "./notebook.js";
17
+ import { lspDefinitionTool, lspReferencesTool, lspDiagnosticsTool, lspHoverTool, lspRenameTool, } from "./lsp.js";
18
+ import { repoMapTool } from "./repoMap.js";
19
+ import { loadSkillTool } from "./skills.js";
20
+ import { askUserTool } from "./askUser.js";
21
+ export const ALL_TOOLS = [
22
+ readFileTool,
23
+ writeFileTool,
24
+ editFileTool,
25
+ shellTool,
26
+ bgOutputTool,
27
+ bgKillTool,
28
+ listDirTool,
29
+ globTool,
30
+ grepTool,
31
+ repoMapTool,
32
+ lspDefinitionTool,
33
+ lspReferencesTool,
34
+ lspDiagnosticsTool,
35
+ lspHoverTool,
36
+ lspRenameTool,
37
+ updateTasksTool,
38
+ webSearchTool,
39
+ fetchUrlTool,
40
+ deepResearchTool,
41
+ spawnAgentTool,
42
+ spawnWriteAgentTool,
43
+ readDocumentTool,
44
+ writeDocumentTool,
45
+ editSpreadsheetTool,
46
+ editPdfTool,
47
+ readNotebookTool,
48
+ editNotebookTool,
49
+ loadSkillTool,
50
+ askUserTool,
51
+ ];
52
+ /** Read-only tools a subagent is allowed to use (no writes, edits, or shell). */
53
+ export const READONLY_TOOLS = [
54
+ readFileTool,
55
+ listDirTool,
56
+ globTool,
57
+ grepTool,
58
+ repoMapTool,
59
+ lspDefinitionTool,
60
+ lspReferencesTool,
61
+ lspDiagnosticsTool,
62
+ lspHoverTool,
63
+ readDocumentTool,
64
+ readNotebookTool,
65
+ loadSkillTool,
66
+ ];
@@ -0,0 +1,27 @@
1
+ import fs from "node:fs/promises";
2
+ import { resolveSafe, truncateResult, countLines } from "./common.js";
3
+ export const listDirTool = {
4
+ name: "list_dir",
5
+ description: "List the entries of a directory in the workspace. Directories end with '/'.",
6
+ parameters: {
7
+ type: "object",
8
+ properties: {
9
+ path: {
10
+ type: "string",
11
+ description: "Directory path relative to the workspace root (default: '.')",
12
+ },
13
+ },
14
+ },
15
+ requiresPermission: false,
16
+ summarize: (args) => `List ${args.path ?? "."}`,
17
+ resultSummary: (output) => countLines(output, "entry", "entries"),
18
+ async execute(args, ctx) {
19
+ const abs = resolveSafe(ctx.workspace, String(args.path ?? "."));
20
+ const entries = await fs.readdir(abs, { withFileTypes: true });
21
+ const names = entries
22
+ .map((e) => (e.isDirectory() ? `${e.name}/` : e.name))
23
+ .sort()
24
+ .slice(0, 500);
25
+ return truncateResult(names.join("\n") || "(empty)");
26
+ },
27
+ };