kritya 0.8.51-beta → 0.8.53-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.
- package/dist/config/config.js +47 -1
- package/dist/index.js +10 -1
- package/dist/mcp/client.js +8 -1
- package/dist/tools/grep.js +10 -0
- package/dist/tools/read.js +10 -2
- package/dist/ui/App.js +17 -3
- package/package.json +1 -1
package/dist/config/config.js
CHANGED
|
@@ -101,6 +101,44 @@ export function sanitizeMcpServersRecord(servers, label = "mcpServers") {
|
|
|
101
101
|
}
|
|
102
102
|
return out;
|
|
103
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Top-level keys `~/.kritya/config.json` understands — keep in sync with
|
|
106
|
+
* CliConfig. Anything else in the file is silently ignored (the parsed
|
|
107
|
+
* object is only ever consumed through the typed interface), so a typo like
|
|
108
|
+
* `maxStep` or `contextwindow` looks like "my config isn't taking effect"
|
|
109
|
+
* with no clue why. loadConfig warns about those instead.
|
|
110
|
+
*/
|
|
111
|
+
const KNOWN_CONFIG_KEYS = new Set([
|
|
112
|
+
"apiKey",
|
|
113
|
+
"model",
|
|
114
|
+
"baseUrl",
|
|
115
|
+
"provider",
|
|
116
|
+
"providers",
|
|
117
|
+
"customModels",
|
|
118
|
+
"pricing",
|
|
119
|
+
"contextWindow",
|
|
120
|
+
"maxSteps",
|
|
121
|
+
"tokenBudget",
|
|
122
|
+
"toolTimeoutSeconds",
|
|
123
|
+
"subagentConcurrency",
|
|
124
|
+
"subagentTimeoutSeconds",
|
|
125
|
+
"subagentMaxSteps",
|
|
126
|
+
"subagentWriteMaxSteps",
|
|
127
|
+
"mcpServers",
|
|
128
|
+
"sandboxExec",
|
|
129
|
+
"retentionDays",
|
|
130
|
+
"audit",
|
|
131
|
+
"otel",
|
|
132
|
+
"commitAttribution",
|
|
133
|
+
"privacyMode",
|
|
134
|
+
]);
|
|
135
|
+
/**
|
|
136
|
+
* Keys in a parsed config object that CliConfig doesn't know — all ignored
|
|
137
|
+
* by the typed code that consumes it. Exported for tests.
|
|
138
|
+
*/
|
|
139
|
+
export function unknownConfigKeys(parsed) {
|
|
140
|
+
return Object.keys(parsed).filter((key) => !KNOWN_CONFIG_KEYS.has(key));
|
|
141
|
+
}
|
|
104
142
|
function sanitizeCliConfig(config) {
|
|
105
143
|
if (!config.mcpServers)
|
|
106
144
|
return config;
|
|
@@ -218,7 +256,15 @@ export function loadConfig() {
|
|
|
218
256
|
}
|
|
219
257
|
try {
|
|
220
258
|
assertJsonWithinLimits(raw, "config.json");
|
|
221
|
-
|
|
259
|
+
const parsed = JSON.parse(raw);
|
|
260
|
+
const unknown = unknownConfigKeys(parsed);
|
|
261
|
+
if (unknown.length > 0) {
|
|
262
|
+
// warnUser dedupes per context, so this prints once per process even
|
|
263
|
+
// though loadConfig runs often.
|
|
264
|
+
warnUser(`loadConfig(${CONFIG_FILE})`, new Error(`unknown key(s) ${unknown.map((key) => `"${key}"`).join(", ")} ignored — ` +
|
|
265
|
+
`check the spelling against the documented config keys`));
|
|
266
|
+
}
|
|
267
|
+
return sanitizeCliConfig(parsed);
|
|
222
268
|
}
|
|
223
269
|
catch (err) {
|
|
224
270
|
// Malformed or oversized — worth being able to see when someone reports
|
package/dist/index.js
CHANGED
|
@@ -258,7 +258,16 @@ if (args.prompt) {
|
|
|
258
258
|
trust: args.trust,
|
|
259
259
|
timeoutSeconds: args.timeoutSeconds,
|
|
260
260
|
privacy: args.privacy,
|
|
261
|
-
})
|
|
261
|
+
})
|
|
262
|
+
.then((code) => process.exit(code))
|
|
263
|
+
// runHeadless is documented to resolve with an exit code rather than
|
|
264
|
+
// throw, so reaching here means a bug in that path. Fail loudly with a
|
|
265
|
+
// non-zero code instead of letting Node print an unhandled-rejection
|
|
266
|
+
// trace — a CI job reading `$?` deserves a real exit status either way.
|
|
267
|
+
.catch((err) => {
|
|
268
|
+
console.error(err);
|
|
269
|
+
process.exit(1);
|
|
270
|
+
});
|
|
262
271
|
}
|
|
263
272
|
else {
|
|
264
273
|
runInteractive();
|
package/dist/mcp/client.js
CHANGED
|
@@ -216,7 +216,14 @@ class McpConnection {
|
|
|
216
216
|
* server can tell the difference between an unsupported client and a hung one.
|
|
217
217
|
*/
|
|
218
218
|
onServerRequest(id, method, params) {
|
|
219
|
-
|
|
219
|
+
// Typed void on purpose: every reply is best-effort, its own `.catch`
|
|
220
|
+
// already keeps a failed send from becoming an unhandled rejection, and no
|
|
221
|
+
// caller of `reply` has anything to do with the promise. The explicit
|
|
222
|
+
// return type is what tells that to callers — and to no-floating-promises,
|
|
223
|
+
// which cannot see through the `.catch` above.
|
|
224
|
+
const reply = (body) => {
|
|
225
|
+
void this.transport.send({ jsonrpc: "2.0", id, ...body }, CONNECT_TIMEOUT_MS).catch(() => { });
|
|
226
|
+
};
|
|
220
227
|
if (method === "roots/list") {
|
|
221
228
|
reply({
|
|
222
229
|
result: {
|
package/dist/tools/grep.js
CHANGED
|
@@ -5,6 +5,14 @@ import { isPathSafe, resolveSafe, safeCompileRegex, truncateResult, meaningfulLi
|
|
|
5
5
|
import { loadIgnorePatterns } from "./ignore.js";
|
|
6
6
|
const MAX_MATCHES = 200;
|
|
7
7
|
const MAX_FILE_BYTES = 1024 * 1024;
|
|
8
|
+
/**
|
|
9
|
+
* Longest line regex-tested. A minified file is a single ~1MB line, and
|
|
10
|
+
* safeCompileRegex only blocks nested-quantifier shapes — any other
|
|
11
|
+
* ReDoS-shaped pattern tested against that one line spins the event loop,
|
|
12
|
+
* where the tool timeout can't fire. Skipping the line closes it; a real
|
|
13
|
+
* match on a line this long is unreadable in the result anyway.
|
|
14
|
+
*/
|
|
15
|
+
const MAX_LINE_CHARS = 10_000;
|
|
8
16
|
export const grepTool = {
|
|
9
17
|
name: "grep",
|
|
10
18
|
description: "Search file contents with a regular expression. Returns 'file:line: text' matches. " +
|
|
@@ -74,6 +82,8 @@ export const grepTool = {
|
|
|
74
82
|
const rel = path.relative(ctx.workspace, abs);
|
|
75
83
|
const lines = content.split("\n");
|
|
76
84
|
for (let i = 0; i < lines.length && matches.length < MAX_MATCHES; i++) {
|
|
85
|
+
if (lines[i].length > MAX_LINE_CHARS)
|
|
86
|
+
continue;
|
|
77
87
|
if (regex.test(lines[i])) {
|
|
78
88
|
matches.push(`${rel}:${i + 1}: ${lines[i].slice(0, 250)}`);
|
|
79
89
|
}
|
package/dist/tools/read.js
CHANGED
|
@@ -3,7 +3,7 @@ import { resolveSafe, truncateResult, countLines } from "./common.js";
|
|
|
3
3
|
export const readFileTool = {
|
|
4
4
|
name: "read_file",
|
|
5
5
|
description: "Read a text file from the workspace. Returns the content with line numbers. " +
|
|
6
|
-
"Use offset/limit for large files.",
|
|
6
|
+
"Use offset/limit for large files. Refuses binary files — use read_document for .pdf/.docx/.xlsx/.pptx.",
|
|
7
7
|
parameters: {
|
|
8
8
|
type: "object",
|
|
9
9
|
properties: {
|
|
@@ -18,7 +18,15 @@ export const readFileTool = {
|
|
|
18
18
|
resultSummary: (output) => countLines(output, "line", "lines"),
|
|
19
19
|
async execute(args, ctx) {
|
|
20
20
|
const abs = resolveSafe(ctx.workspace, String(args.path));
|
|
21
|
-
const
|
|
21
|
+
const buffer = await fs.readFile(abs);
|
|
22
|
+
// Binary sniff, same as grep: a NUL byte in the first 8KB means this is
|
|
23
|
+
// not text. Decoding it as UTF-8 anyway would dump garbage into the
|
|
24
|
+
// context and burn tokens on bytes no one can read.
|
|
25
|
+
if (buffer.subarray(0, 8000).includes(0)) {
|
|
26
|
+
throw new Error(`"${args.path}" looks like a binary file — read_file only reads text. ` +
|
|
27
|
+
`For .pdf, .docx, .xlsx, or .pptx use read_document instead.`);
|
|
28
|
+
}
|
|
29
|
+
const content = buffer.toString("utf8");
|
|
22
30
|
const lines = content.split("\n");
|
|
23
31
|
const offset = Math.max(1, Number(args.offset) || 1);
|
|
24
32
|
const limit = Number(args.limit) || 2000;
|
package/dist/ui/App.js
CHANGED
|
@@ -5,6 +5,7 @@ import fs from "node:fs";
|
|
|
5
5
|
import { glob } from "tinyglobby";
|
|
6
6
|
import stringWidth from "string-width";
|
|
7
7
|
import { gitDiffStat } from "../git/git.js";
|
|
8
|
+
import { debugLog } from "../config/debug.js";
|
|
8
9
|
import { modelDisplaySlug } from "../config/models.js";
|
|
9
10
|
import { setStderrSink } from "../stderr.js";
|
|
10
11
|
import { openInEditor } from "./openInEditor.js";
|
|
@@ -145,7 +146,12 @@ export function App({ agent, workspace, modelRef, providerRef, config, resumedCo
|
|
|
145
146
|
ignore: ["**/node_modules/**", "**/.git/**", "**/dist/**", ...loadIgnorePatterns(workspace)],
|
|
146
147
|
})
|
|
147
148
|
.then((files) => setFileList(files.sort().slice(0, 2000)))
|
|
148
|
-
.catch(() => {
|
|
149
|
+
.catch((err) => {
|
|
150
|
+
// A failed refresh must not leave the previous workspace's files
|
|
151
|
+
// behind for @-mentions: clear, and leave a trail in the debug log.
|
|
152
|
+
debugLog("refreshFileList", err);
|
|
153
|
+
setFileList([]);
|
|
154
|
+
});
|
|
149
155
|
}, [workspace]);
|
|
150
156
|
useEffect(refreshFileList, [refreshFileList]);
|
|
151
157
|
const { items, addItem, clearItems, phase, setPhase, stream, thinking, activity, setActivity, runningPhase, setRunningPhase, setChained, workflow, refreshWorkflow, prefill, setPrefill, permission, elicitation, inFlight, model, provider, servedModel, totalUsage, totalCost, tasks, setTasks, ctxPct, setCtxPct, tokenBudget, budgetPct, budgetUsed, budgetStopped, resetBudget, setBudgetLimit, branch, planMode, setPlanMode, dryRunMode, killed, killReason, engageKill, releaseKill, acceptEdits, setAcceptEdits, bypassMode, setBypassMode, autoApprovedCount, cycleMode, onAcceptEditsConfirm, onBypassModeConfirm, abortRef, setModelEverywhere, setProviderEverywhere, costReport, runAgent, runWebSearch, onPermissionDecision, onElicitationDecision, onResumeSelect, requestPermission, requestElicitation, persistenceWarningCount, } = useAgent({
|
|
@@ -576,7 +582,11 @@ export function App({ agent, workspace, modelRef, providerRef, config, resumedCo
|
|
|
576
582
|
costReport,
|
|
577
583
|
statusReport,
|
|
578
584
|
gitDiffStat,
|
|
579
|
-
|
|
585
|
+
// exitAfterFlush is async because it waits for Ink to flush the final
|
|
586
|
+
// render, but `exit` is typed void: no caller can do anything with the
|
|
587
|
+
// promise, so the detach is marked here rather than by widening the
|
|
588
|
+
// interface to `void | Promise<void>` to silence the lint rule.
|
|
589
|
+
exit: () => void exitAfterFlush(),
|
|
580
590
|
};
|
|
581
591
|
void runCommand(cmd, ctx);
|
|
582
592
|
};
|
|
@@ -739,7 +749,11 @@ export function App({ agent, workspace, modelRef, providerRef, config, resumedCo
|
|
|
739
749
|
setInput(v.replace(/\t/g, ""));
|
|
740
750
|
setCmdIndex(0);
|
|
741
751
|
setFileIndex(0);
|
|
742
|
-
},
|
|
752
|
+
},
|
|
753
|
+
// handleSubmit is async only because it awaits runAgent, which
|
|
754
|
+
// catches its own errors and renders them as transcript items —
|
|
755
|
+
// so there is no rejection left for this callback to surface.
|
|
756
|
+
onSubmit: (value) => void handleSubmit(value) }, inputKey)] }), suggestions.length > 0 && (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, children: [suggestionWindow.visible.map((line, i) => {
|
|
743
757
|
if (line.kind === "header") {
|
|
744
758
|
return (_jsx(Text, { dimColor: true, wrap: "truncate", children: line.label }, `header:${line.label}`));
|
|
745
759
|
}
|