reasonix 0.27.0 → 0.27.2
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/cli/index.js +66 -27
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +53 -19
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -5282,6 +5282,7 @@ function escapeAttr(s) {
|
|
|
5282
5282
|
// src/tools/filesystem.ts
|
|
5283
5283
|
import { promises as fs } from "fs";
|
|
5284
5284
|
import * as pathMod from "path";
|
|
5285
|
+
import picomatch2 from "picomatch";
|
|
5285
5286
|
var DEFAULT_MAX_READ_BYTES = 2 * 1024 * 1024;
|
|
5286
5287
|
var DEFAULT_MAX_LIST_BYTES = 256 * 1024;
|
|
5287
5288
|
var DEFAULT_AUTO_PREVIEW_LINES = 200;
|
|
@@ -5289,6 +5290,20 @@ var AUTO_PREVIEW_HEAD_LINES = 80;
|
|
|
5289
5290
|
var AUTO_PREVIEW_TAIL_LINES = 40;
|
|
5290
5291
|
var SKIP_DIR_NAMES = new Set(DEFAULT_INDEX_EXCLUDES.dirs);
|
|
5291
5292
|
var BINARY_EXTENSIONS = new Set(DEFAULT_INDEX_EXCLUDES.exts);
|
|
5293
|
+
function displayRel(rootDir, full) {
|
|
5294
|
+
return pathMod.relative(rootDir, full).replaceAll("\\", "/");
|
|
5295
|
+
}
|
|
5296
|
+
var GLOB_METACHARS = /[*?{[]/;
|
|
5297
|
+
function compileNameFilter(filter) {
|
|
5298
|
+
if (!filter) return null;
|
|
5299
|
+
if (!GLOB_METACHARS.test(filter)) {
|
|
5300
|
+
const needle = filter.toLowerCase();
|
|
5301
|
+
return (name) => name.toLowerCase().includes(needle);
|
|
5302
|
+
}
|
|
5303
|
+
const matchPath = filter.includes("/");
|
|
5304
|
+
const isMatch = picomatch2(filter, { dot: true, nocase: true });
|
|
5305
|
+
return matchPath ? (_n, rel) => isMatch(rel) : (name) => isMatch(name);
|
|
5306
|
+
}
|
|
5292
5307
|
function isLikelyBinaryByName(name) {
|
|
5293
5308
|
const dot2 = name.lastIndexOf(".");
|
|
5294
5309
|
if (dot2 < 0) return false;
|
|
@@ -5493,7 +5508,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
5493
5508
|
});
|
|
5494
5509
|
registry.register({
|
|
5495
5510
|
name: "search_files",
|
|
5496
|
-
description: "Find files whose NAME matches a substring or regex. Case-insensitive. Walks the directory recursively under the sandbox root. Returns one path per line.",
|
|
5511
|
+
description: "Find files whose NAME matches a substring or regex. Case-insensitive. Walks the directory recursively under the sandbox root. Returns one path per line. Skips dependency / VCS / build directories (node_modules, .git, dist, build, .next, target, .venv) by default.",
|
|
5497
5512
|
readOnly: true,
|
|
5498
5513
|
parameters: {
|
|
5499
5514
|
type: "object",
|
|
@@ -5502,6 +5517,10 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
5502
5517
|
pattern: {
|
|
5503
5518
|
type: "string",
|
|
5504
5519
|
description: "Substring (or regex) to match against filenames."
|
|
5520
|
+
},
|
|
5521
|
+
include_deps: {
|
|
5522
|
+
type: "boolean",
|
|
5523
|
+
description: "When true, also walk node_modules / .git / dist / build / etc. Off by default \u2014 most filename searches are about the user's own code."
|
|
5505
5524
|
}
|
|
5506
5525
|
},
|
|
5507
5526
|
required: ["pattern"]
|
|
@@ -5509,6 +5528,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
5509
5528
|
fn: async (args) => {
|
|
5510
5529
|
const startAbs = safePath(args.path ?? ".");
|
|
5511
5530
|
const needle = args.pattern.toLowerCase();
|
|
5531
|
+
const includeDeps = args.include_deps === true;
|
|
5512
5532
|
let re = null;
|
|
5513
5533
|
try {
|
|
5514
5534
|
re = new RegExp(args.pattern, "i");
|
|
@@ -5529,7 +5549,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
5529
5549
|
const lower = e.name.toLowerCase();
|
|
5530
5550
|
const hit = re ? re.test(e.name) : lower.includes(needle);
|
|
5531
5551
|
if (hit) {
|
|
5532
|
-
const rel =
|
|
5552
|
+
const rel = displayRel(rootDir, full);
|
|
5533
5553
|
if (totalBytes + rel.length + 1 > maxListBytes) {
|
|
5534
5554
|
matches.push("[\u2026 search truncated \u2014 refine pattern \u2026]");
|
|
5535
5555
|
return;
|
|
@@ -5537,7 +5557,10 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
5537
5557
|
matches.push(rel);
|
|
5538
5558
|
totalBytes += rel.length + 1;
|
|
5539
5559
|
}
|
|
5540
|
-
if (e.isDirectory())
|
|
5560
|
+
if (e.isDirectory()) {
|
|
5561
|
+
if (!includeDeps && SKIP_DIR_NAMES.has(e.name)) continue;
|
|
5562
|
+
await walk4(full);
|
|
5563
|
+
}
|
|
5541
5564
|
}
|
|
5542
5565
|
};
|
|
5543
5566
|
await walk4(startAbs);
|
|
@@ -5561,7 +5584,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
5561
5584
|
},
|
|
5562
5585
|
glob: {
|
|
5563
5586
|
type: "string",
|
|
5564
|
-
description: "Optional
|
|
5587
|
+
description: "Optional filename filter. Real glob when the value contains `*`, `?`, `{`, or `[` \u2014 e.g. '*.ts', '**/*.tsx', 'src/**/*.{ts,tsx}'. Plain substring otherwise \u2014 e.g. '.ts' (suffix), 'test' (anywhere in the name). Patterns containing `/` match against the path relative to the search root; otherwise just the basename."
|
|
5565
5588
|
},
|
|
5566
5589
|
case_sensitive: {
|
|
5567
5590
|
type: "boolean",
|
|
@@ -5578,7 +5601,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
5578
5601
|
const startAbs = safePath(args.path ?? ".");
|
|
5579
5602
|
const caseSensitive = args.case_sensitive === true;
|
|
5580
5603
|
const includeDeps = args.include_deps === true;
|
|
5581
|
-
const
|
|
5604
|
+
const nameMatch = compileNameFilter(typeof args.glob === "string" ? args.glob : null);
|
|
5582
5605
|
let re = null;
|
|
5583
5606
|
try {
|
|
5584
5607
|
re = new RegExp(args.pattern, caseSensitive ? "" : "i");
|
|
@@ -5606,9 +5629,9 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
5606
5629
|
continue;
|
|
5607
5630
|
}
|
|
5608
5631
|
if (!e.isFile()) continue;
|
|
5609
|
-
if (nameFilter && !e.name.toLowerCase().includes(nameFilter)) continue;
|
|
5610
|
-
if (isLikelyBinaryByName(e.name)) continue;
|
|
5611
5632
|
const full = pathMod.join(dir, e.name);
|
|
5633
|
+
if (nameMatch && !nameMatch(e.name, displayRel(rootDir, full))) continue;
|
|
5634
|
+
if (isLikelyBinaryByName(e.name)) continue;
|
|
5612
5635
|
let stat2;
|
|
5613
5636
|
try {
|
|
5614
5637
|
stat2 = await fs.stat(full);
|
|
@@ -5625,7 +5648,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
5625
5648
|
const firstNul = raw.indexOf(0);
|
|
5626
5649
|
if (firstNul !== -1 && firstNul < 8 * 1024) continue;
|
|
5627
5650
|
const text = raw.toString("utf8");
|
|
5628
|
-
const rel =
|
|
5651
|
+
const rel = displayRel(rootDir, full);
|
|
5629
5652
|
const lines = text.split(/\r?\n/);
|
|
5630
5653
|
for (let li = 0; li < lines.length; li++) {
|
|
5631
5654
|
const line = lines[li];
|
|
@@ -5690,7 +5713,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
5690
5713
|
const abs = safePath(args.path);
|
|
5691
5714
|
await fs.mkdir(pathMod.dirname(abs), { recursive: true });
|
|
5692
5715
|
await fs.writeFile(abs, args.content, "utf8");
|
|
5693
|
-
return `wrote ${args.content.length} chars to ${
|
|
5716
|
+
return `wrote ${args.content.length} chars to ${displayRel(rootDir, abs)}`;
|
|
5694
5717
|
}
|
|
5695
5718
|
});
|
|
5696
5719
|
registry.register({
|
|
@@ -5716,17 +5739,17 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
5716
5739
|
const adaptedReplace = args.replace.replace(/\r?\n/g, le);
|
|
5717
5740
|
const firstIdx = before.indexOf(adaptedSearch);
|
|
5718
5741
|
if (firstIdx < 0) {
|
|
5719
|
-
throw new Error(`edit_file: search text not found in ${
|
|
5742
|
+
throw new Error(`edit_file: search text not found in ${displayRel(rootDir, abs)}`);
|
|
5720
5743
|
}
|
|
5721
5744
|
const nextIdx = before.indexOf(adaptedSearch, firstIdx + 1);
|
|
5722
5745
|
if (nextIdx >= 0) {
|
|
5723
5746
|
throw new Error(
|
|
5724
|
-
`edit_file: search text appears multiple times in ${
|
|
5747
|
+
`edit_file: search text appears multiple times in ${displayRel(rootDir, abs)} \u2014 include more context to disambiguate`
|
|
5725
5748
|
);
|
|
5726
5749
|
}
|
|
5727
5750
|
const after = before.slice(0, firstIdx) + adaptedReplace + before.slice(firstIdx + adaptedSearch.length);
|
|
5728
5751
|
await fs.writeFile(abs, after, "utf8");
|
|
5729
|
-
const rel =
|
|
5752
|
+
const rel = displayRel(rootDir, abs);
|
|
5730
5753
|
const header2 = `edited ${rel} (${adaptedSearch.length}\u2192${adaptedReplace.length} chars)`;
|
|
5731
5754
|
const startLine = before.slice(0, firstIdx).split(/\r?\n/).length;
|
|
5732
5755
|
const diff = renderEditDiff(adaptedSearch, adaptedReplace, startLine);
|
|
@@ -5745,7 +5768,7 @@ ${diff}`;
|
|
|
5745
5768
|
fn: async (args) => {
|
|
5746
5769
|
const abs = safePath(args.path);
|
|
5747
5770
|
await fs.mkdir(abs, { recursive: true });
|
|
5748
|
-
return `created ${
|
|
5771
|
+
return `created ${displayRel(rootDir, abs)}/`;
|
|
5749
5772
|
}
|
|
5750
5773
|
});
|
|
5751
5774
|
registry.register({
|
|
@@ -5764,7 +5787,7 @@ ${diff}`;
|
|
|
5764
5787
|
const dst = safePath(args.destination);
|
|
5765
5788
|
await fs.mkdir(pathMod.dirname(dst), { recursive: true });
|
|
5766
5789
|
await fs.rename(src, dst);
|
|
5767
|
-
return `moved ${
|
|
5790
|
+
return `moved ${displayRel(rootDir, src)} \u2192 ${displayRel(rootDir, dst)}`;
|
|
5768
5791
|
}
|
|
5769
5792
|
});
|
|
5770
5793
|
return registry;
|
|
@@ -6720,7 +6743,7 @@ function splitOnChainOps(cmd) {
|
|
|
6720
6743
|
const ch = cmd[i];
|
|
6721
6744
|
if (quote2) {
|
|
6722
6745
|
if (ch === quote2) quote2 = null;
|
|
6723
|
-
else if (
|
|
6746
|
+
else if (quote2 === '"' && isDqEscape(ch, cmd[i + 1])) i++;
|
|
6724
6747
|
i++;
|
|
6725
6748
|
atTokenStart = false;
|
|
6726
6749
|
continue;
|
|
@@ -6792,7 +6815,7 @@ function parseSegment(segStr) {
|
|
|
6792
6815
|
if (quote2) {
|
|
6793
6816
|
if (ch === quote2) {
|
|
6794
6817
|
quote2 = null;
|
|
6795
|
-
} else if (
|
|
6818
|
+
} else if (quote2 === '"' && isDqEscape(ch, segStr[i + 1])) {
|
|
6796
6819
|
cur += segStr[++i] ?? "";
|
|
6797
6820
|
curHasContent = true;
|
|
6798
6821
|
} else {
|
|
@@ -6898,6 +6921,14 @@ function parseCommandChain(cmd) {
|
|
|
6898
6921
|
}
|
|
6899
6922
|
segments.push(parseSegment(trimmed));
|
|
6900
6923
|
}
|
|
6924
|
+
for (const seg of segments) {
|
|
6925
|
+
const cmdName = seg.argv[0] ?? "";
|
|
6926
|
+
if (cmdName.toLowerCase() === "cd") {
|
|
6927
|
+
throw new UnsupportedSyntaxError(
|
|
6928
|
+
"cd in parsed command chains does not change cwd for later segments. Use a command-native cwd flag instead, such as `npm --prefix <dir> run <script>`, `git -C <dir> ...`, or `cargo -C <dir> ...`."
|
|
6929
|
+
);
|
|
6930
|
+
}
|
|
6931
|
+
}
|
|
6901
6932
|
if (ops.length === 0 && segments[0].redirects.length === 0) return null;
|
|
6902
6933
|
return { segments, ops };
|
|
6903
6934
|
}
|
|
@@ -7195,6 +7226,9 @@ var BUILTIN_ALLOWLIST = [
|
|
|
7195
7226
|
"ruff",
|
|
7196
7227
|
"mypy"
|
|
7197
7228
|
];
|
|
7229
|
+
function isDqEscape(prev, next) {
|
|
7230
|
+
return prev === "\\" && (next === '"' || next === "\\");
|
|
7231
|
+
}
|
|
7198
7232
|
function tokenizeCommand(cmd) {
|
|
7199
7233
|
const out = [];
|
|
7200
7234
|
let cur = "";
|
|
@@ -7204,7 +7238,7 @@ function tokenizeCommand(cmd) {
|
|
|
7204
7238
|
if (quote2) {
|
|
7205
7239
|
if (ch === quote2) {
|
|
7206
7240
|
quote2 = null;
|
|
7207
|
-
} else if (
|
|
7241
|
+
} else if (quote2 === '"' && isDqEscape(ch, cmd[i + 1])) {
|
|
7208
7242
|
cur += cmd[++i];
|
|
7209
7243
|
} else {
|
|
7210
7244
|
cur += ch;
|
|
@@ -7246,7 +7280,7 @@ function detectShellOperator(cmd) {
|
|
|
7246
7280
|
if (quote2) {
|
|
7247
7281
|
if (ch === quote2) {
|
|
7248
7282
|
quote2 = null;
|
|
7249
|
-
} else if (
|
|
7283
|
+
} else if (quote2 === '"' && isDqEscape(ch, cmd[i + 1])) {
|
|
7250
7284
|
cur += cmd[++i];
|
|
7251
7285
|
curQuoted = true;
|
|
7252
7286
|
} else {
|
|
@@ -7552,7 +7586,7 @@ function registerShellTools(registry, opts) {
|
|
|
7552
7586
|
const isAllowAll = typeof opts.allowAll === "function" ? opts.allowAll : () => opts.allowAll === true;
|
|
7553
7587
|
registry.register({
|
|
7554
7588
|
name: "run_command",
|
|
7555
|
-
description: "Run a shell command in the project root and return its combined stdout+stderr.\n\nConstraints (read these before the first call):\n\u2022 Chain operators `|`, `||`, `&&`, `;` ARE supported \u2014 parsed natively, no shell invoked, so semantics are identical on Windows / macOS / Linux. Each chain segment is allowlist-checked individually: `git status | grep main` runs if both halves are allowed.\n\u2022 File redirects ARE supported: `>` truncate, `>>` append, `<` stdin from file, `2>` / `2>>` stderr to file, `2>&1` merge stderr\u2192stdout, `&>` both to file. Targets resolve relative to the project root. At most one redirect per fd per segment.\n\u2022 Background `&`, heredoc `<<`, command substitution `$(\u2026)`, subshells `(\u2026)`, and process substitution `<(\u2026)` are NOT supported. Wrap a literal `&` arg in quotes; for input use a `<` file or the binary's own --input flag.\n\u2022 Env-var expansion `$VAR` is NOT performed \u2014 `$VAR` is passed as a literal string. Use the binary's own --env flag or substitute the value yourself.\n\u2022 `cd` DOES NOT PERSIST between calls \u2014 each call spawns a fresh process rooted at the project.
|
|
7589
|
+
description: "Run a shell command in the project root and return its combined stdout+stderr.\n\nConstraints (read these before the first call):\n\u2022 Chain operators `|`, `||`, `&&`, `;` ARE supported \u2014 parsed natively, no shell invoked, so semantics are identical on Windows / macOS / Linux. Each chain segment is allowlist-checked individually: `git status | grep main` runs if both halves are allowed.\n\u2022 File redirects ARE supported: `>` truncate, `>>` append, `<` stdin from file, `2>` / `2>>` stderr to file, `2>&1` merge stderr\u2192stdout, `&>` both to file. Targets resolve relative to the project root. At most one redirect per fd per segment.\n\u2022 Background `&`, heredoc `<<`, command substitution `$(\u2026)`, subshells `(\u2026)`, and process substitution `<(\u2026)` are NOT supported. Wrap a literal `&` arg in quotes; for input use a `<` file or the binary's own --input flag.\n\u2022 Env-var expansion `$VAR` is NOT performed \u2014 `$VAR` is passed as a literal string. Use the binary's own --env flag or substitute the value yourself.\n\u2022 `cd` DOES NOT PERSIST between calls \u2014 each call spawns a fresh process rooted at the project. `cd` also does not persist within parsed chains like `cd dir && command`. Use a command-native cwd flag instead: `npm --prefix <dir> run <script>`, `npm --prefix <dir> exec -- <bin>`, `git -C <dir> ...`, `cargo -C <dir> ...`, `pytest <dir>/tests`.\n\u2022 Glob patterns (`*.ts`) are passed through as literal arguments \u2014 no shell expansion. Use `grep -r`, `rg`, `find -name`, etc.\n\u2022 Avoid commands with unbounded output (`netstat -ano`, `find /`, etc.) \u2014 they waste tokens. Filter at source: `netstat -ano -p TCP`, `find src -name '*.ts'`, `grep -c`, `wc -l`.\n\nCommon read-only inspection and test/lint/typecheck commands run immediately; anything that could mutate state, install dependencies, or touch the network is refused until the user confirms it in the TUI. Prefer this over asking the user to run a command manually \u2014 after edits, run the project's tests to verify.",
|
|
7556
7590
|
// Plan-mode gate: allow allowlisted commands through (git status,
|
|
7557
7591
|
// cargo check, ls, grep …) so the model can actually investigate
|
|
7558
7592
|
// during planning. Anything that would otherwise trigger a
|
|
@@ -15560,6 +15594,9 @@ async function handleSettings(method, _rest, body, ctx) {
|
|
|
15560
15594
|
const fields = parseBody8(body);
|
|
15561
15595
|
const cfg = readConfig(ctx.configPath);
|
|
15562
15596
|
const changed = [];
|
|
15597
|
+
let langPending = null;
|
|
15598
|
+
let presetPendingLive = null;
|
|
15599
|
+
let effortPendingLive = null;
|
|
15563
15600
|
if (fields.lang !== void 0) {
|
|
15564
15601
|
const raw = String(fields.lang);
|
|
15565
15602
|
const supported = getSupportedLanguages();
|
|
@@ -15567,14 +15604,15 @@ async function handleSettings(method, _rest, body, ctx) {
|
|
|
15567
15604
|
if (!langCode) {
|
|
15568
15605
|
return { status: 400, body: { error: `lang must be one of: ${supported.join(", ")}` } };
|
|
15569
15606
|
}
|
|
15570
|
-
|
|
15607
|
+
cfg.lang = langCode;
|
|
15608
|
+
langPending = langCode;
|
|
15571
15609
|
changed.push("lang");
|
|
15572
15610
|
}
|
|
15573
15611
|
if (fields.apiKey !== void 0) {
|
|
15574
15612
|
if (typeof fields.apiKey !== "string" || !isPlausibleKey(fields.apiKey)) {
|
|
15575
15613
|
return { status: 400, body: { error: "apiKey must be a plausible sk- token" } };
|
|
15576
15614
|
}
|
|
15577
|
-
|
|
15615
|
+
cfg.apiKey = fields.apiKey.trim();
|
|
15578
15616
|
changed.push("apiKey");
|
|
15579
15617
|
}
|
|
15580
15618
|
if (fields.baseUrl !== void 0) {
|
|
@@ -15582,7 +15620,6 @@ async function handleSettings(method, _rest, body, ctx) {
|
|
|
15582
15620
|
return { status: 400, body: { error: "baseUrl must be a non-empty string" } };
|
|
15583
15621
|
}
|
|
15584
15622
|
cfg.baseUrl = fields.baseUrl.trim();
|
|
15585
|
-
writeConfig(cfg, ctx.configPath);
|
|
15586
15623
|
changed.push("baseUrl");
|
|
15587
15624
|
}
|
|
15588
15625
|
if (fields.preset !== void 0) {
|
|
@@ -15590,16 +15627,15 @@ async function handleSettings(method, _rest, body, ctx) {
|
|
|
15590
15627
|
return { status: 400, body: { error: "preset must be auto | flash | pro" } };
|
|
15591
15628
|
}
|
|
15592
15629
|
cfg.preset = fields.preset;
|
|
15593
|
-
|
|
15594
|
-
ctx.applyPresetLive?.(fields.preset);
|
|
15630
|
+
presetPendingLive = fields.preset;
|
|
15595
15631
|
changed.push("preset");
|
|
15596
15632
|
}
|
|
15597
15633
|
if (fields.reasoningEffort !== void 0) {
|
|
15598
15634
|
if (typeof fields.reasoningEffort !== "string" || !VALID_EFFORTS.has(fields.reasoningEffort)) {
|
|
15599
15635
|
return { status: 400, body: { error: "reasoningEffort must be high | max" } };
|
|
15600
15636
|
}
|
|
15601
|
-
|
|
15602
|
-
|
|
15637
|
+
cfg.reasoningEffort = fields.reasoningEffort;
|
|
15638
|
+
effortPendingLive = fields.reasoningEffort;
|
|
15603
15639
|
changed.push("reasoningEffort");
|
|
15604
15640
|
}
|
|
15605
15641
|
if (fields.search !== void 0) {
|
|
@@ -15607,10 +15643,13 @@ async function handleSettings(method, _rest, body, ctx) {
|
|
|
15607
15643
|
return { status: 400, body: { error: "search must be a boolean" } };
|
|
15608
15644
|
}
|
|
15609
15645
|
cfg.search = fields.search;
|
|
15610
|
-
writeConfig(cfg, ctx.configPath);
|
|
15611
15646
|
changed.push("search");
|
|
15612
15647
|
}
|
|
15613
15648
|
if (changed.length > 0) {
|
|
15649
|
+
writeConfig(cfg, ctx.configPath);
|
|
15650
|
+
if (langPending) setLanguage(langPending);
|
|
15651
|
+
if (presetPendingLive) ctx.applyPresetLive?.(presetPendingLive);
|
|
15652
|
+
if (effortPendingLive) ctx.applyEffortLive?.(effortPendingLive);
|
|
15614
15653
|
ctx.audit?.({ ts: Date.now(), action: "set-settings", payload: { fields: changed } });
|
|
15615
15654
|
}
|
|
15616
15655
|
return { status: 200, body: { changed } };
|