kritya 0.8.52-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.
@@ -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
- return sanitizeCliConfig(JSON.parse(raw));
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
@@ -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
  }
@@ -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 content = await fs.readFile(abs, "utf8");
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({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kritya",
3
- "version": "0.8.52-beta",
3
+ "version": "0.8.53-beta",
4
4
  "description": "Kritya — Superintelligence at your command line",
5
5
  "type": "module",
6
6
  "publishConfig": {