@critiquedotsh/harness 0.1.4 → 0.1.5
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.js +129 -16
- package/package.json +4 -2
- package/scripts/postinstall.mjs +25 -0
package/dist/cli.js
CHANGED
|
@@ -12884,6 +12884,16 @@ var init_critique_code_runtime_config = __esm({
|
|
|
12884
12884
|
});
|
|
12885
12885
|
|
|
12886
12886
|
// lib/finish/critique-code-model-defaults.ts
|
|
12887
|
+
function modelFromInferenceSelection(id4) {
|
|
12888
|
+
const raw = id4?.trim();
|
|
12889
|
+
if (!raw || raw === "auto") return DEFAULT_INFERENCE_MODEL;
|
|
12890
|
+
try {
|
|
12891
|
+
const parsed = parseCritiqueCodeModels(raw.startsWith("critique/") ? raw : `critique/${raw}`)[0];
|
|
12892
|
+
if (parsed?.provider === "critique") return parsed;
|
|
12893
|
+
} catch {
|
|
12894
|
+
}
|
|
12895
|
+
return DEFAULT_INFERENCE_MODEL;
|
|
12896
|
+
}
|
|
12887
12897
|
function defaultModelForRoute(route) {
|
|
12888
12898
|
if (route === "critique-inference") return DEFAULT_INFERENCE_MODEL;
|
|
12889
12899
|
if (route === "anthropic") return DEFAULT_ANTHROPIC_MODEL;
|
|
@@ -20290,16 +20300,17 @@ async function beginCritiqueInferenceLogin(input) {
|
|
|
20290
20300
|
if (token?.status === "denied") throw new Error("Website approval was denied.");
|
|
20291
20301
|
const apiKey = token?.api_key?.trim() || token?.apiKey?.trim();
|
|
20292
20302
|
if (token?.status === "authorized" && apiKey) {
|
|
20303
|
+
const author = modelFromInferenceSelection(token.model);
|
|
20293
20304
|
await saveCritiqueCodeCredential({ home: input.home, kind: "critique", key: apiKey });
|
|
20294
20305
|
await saveCritiqueCodeRuntimeSettings({
|
|
20295
20306
|
home: input.home,
|
|
20296
20307
|
settings: settingsFromSelection({
|
|
20297
20308
|
route: "critique-inference",
|
|
20298
|
-
author
|
|
20299
|
-
review: [
|
|
20309
|
+
author,
|
|
20310
|
+
review: [author]
|
|
20300
20311
|
})
|
|
20301
20312
|
});
|
|
20302
|
-
input.io.note(
|
|
20313
|
+
input.io.note(`Critique Inference is connected. Default model is ${piModelId(author)}.`);
|
|
20303
20314
|
return { apiKey, verificationUri: verifyUrl };
|
|
20304
20315
|
}
|
|
20305
20316
|
}
|
|
@@ -20769,14 +20780,16 @@ async function runCritiqueCodeSettingsSession(input) {
|
|
|
20769
20780
|
}
|
|
20770
20781
|
});
|
|
20771
20782
|
config = await loadCritiqueCodeRuntimeConfig(env);
|
|
20772
|
-
route = "critique-inference";
|
|
20773
|
-
author = DEFAULT_INFERENCE_MODEL;
|
|
20774
|
-
review = [author];
|
|
20783
|
+
route = config.settings?.route ?? "critique-inference";
|
|
20784
|
+
author = config.settings?.author_model ?? DEFAULT_INFERENCE_MODEL;
|
|
20785
|
+
review = config.settings?.review_models ?? [author];
|
|
20786
|
+
screen = "models";
|
|
20787
|
+
modelRole = "author";
|
|
20775
20788
|
} catch (error) {
|
|
20776
20789
|
input.ui.note(error instanceof Error ? error.message : String(error));
|
|
20790
|
+
if (exitNested()) break;
|
|
20791
|
+
screen = "home";
|
|
20777
20792
|
}
|
|
20778
|
-
if (input.start === "login") break;
|
|
20779
|
-
screen = "home";
|
|
20780
20793
|
continue;
|
|
20781
20794
|
}
|
|
20782
20795
|
if (screen === "keys") {
|
|
@@ -20852,8 +20865,12 @@ async function runCritiqueCodeSettingsSession(input) {
|
|
|
20852
20865
|
continue;
|
|
20853
20866
|
}
|
|
20854
20867
|
if (screen === "models") {
|
|
20868
|
+
const current = piModelId(modelRole === "author" ? author : review[0] ?? author);
|
|
20855
20869
|
const items = [
|
|
20856
|
-
...modelsForRoute(route)
|
|
20870
|
+
...modelsForRoute(route).map((item) => ({
|
|
20871
|
+
...item,
|
|
20872
|
+
description: item.value === current || item.value === current.replace(/^critique\/critique\//, "critique/") ? "current" : item.description
|
|
20873
|
+
})),
|
|
20857
20874
|
{ value: "custom", label: "Enter a model id" },
|
|
20858
20875
|
{ value: "back", label: "Back" }
|
|
20859
20876
|
];
|
|
@@ -20874,7 +20891,7 @@ async function runCritiqueCodeSettingsSession(input) {
|
|
|
20874
20891
|
await persistSettings();
|
|
20875
20892
|
input.ui.note(`Using ${piModelId(parsed)}.`);
|
|
20876
20893
|
}
|
|
20877
|
-
if (
|
|
20894
|
+
if (exitNested()) break;
|
|
20878
20895
|
screen = "home";
|
|
20879
20896
|
continue;
|
|
20880
20897
|
}
|
|
@@ -21131,7 +21148,9 @@ function parseAuthorCommand(line) {
|
|
|
21131
21148
|
if (lower === "login" || lower === "/login") return { kind: "settings", start: "login" };
|
|
21132
21149
|
if (lower === "settings" || lower === "/settings") return { kind: "settings", start: "home" };
|
|
21133
21150
|
if (lower === "keys" || lower === "/keys") return { kind: "settings", start: "keys" };
|
|
21134
|
-
if (lower === "models" || lower === "/models"
|
|
21151
|
+
if (lower === "models" || lower === "/models" || lower === "model" || lower === "/model") {
|
|
21152
|
+
return { kind: "settings", start: "models" };
|
|
21153
|
+
}
|
|
21135
21154
|
if (lower === "skills" || lower === "/skills") return { kind: "skills" };
|
|
21136
21155
|
if (lower === "review" || lower === "/review" || lower === "review since" || lower === "/review since") {
|
|
21137
21156
|
return { kind: "review", mode: "since" };
|
|
@@ -22694,6 +22713,7 @@ var CritiqueCodeLineHub = class {
|
|
|
22694
22713
|
};
|
|
22695
22714
|
|
|
22696
22715
|
// lib/finish/critique-code-web-server.ts
|
|
22716
|
+
init_critique_code_kernel();
|
|
22697
22717
|
init_critique_code_model_defaults();
|
|
22698
22718
|
init_critique_code_runtime_config();
|
|
22699
22719
|
|
|
@@ -22716,7 +22736,13 @@ function critiqueCodeWebPage(input) {
|
|
|
22716
22736
|
const branch = escapeHtml(input.branch);
|
|
22717
22737
|
const model = escapeHtml(input.model);
|
|
22718
22738
|
const listen = escapeHtml(input.listen);
|
|
22719
|
-
const modelShort = escapeHtml(input.model.replace(/^openrouter\//, "").replace(/^critique\//, "critique/"));
|
|
22739
|
+
const modelShort = escapeHtml(input.model.replace(/^openrouter\//, "").replace(/^critique\/critique\//, "critique/").replace(/^critique\//, "critique/"));
|
|
22740
|
+
const models = input.models && input.models.length > 0 ? input.models : [{ id: input.model, label: modelShort }];
|
|
22741
|
+
const currentId = input.model.replace(/^critique\/critique\//, "critique/");
|
|
22742
|
+
const options = models.map((entry) => {
|
|
22743
|
+
const selected = entry.id === input.model || entry.id === currentId ? " selected" : "";
|
|
22744
|
+
return `<option value="${escapeHtml(entry.id)}"${selected}>${escapeHtml(entry.label)}</option>`;
|
|
22745
|
+
}).join("");
|
|
22720
22746
|
return `<!DOCTYPE html>
|
|
22721
22747
|
<html lang="en">
|
|
22722
22748
|
<head>
|
|
@@ -22789,6 +22815,8 @@ textarea::placeholder { color: #6a6a6a; }
|
|
|
22789
22815
|
.cmds button:hover { background: rgba(255,255,255,.05); color: var(--fg); }
|
|
22790
22816
|
.bar-right { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
|
|
22791
22817
|
.model-chip { color: var(--muted); font-size: 12px; max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
22818
|
+
#model { max-width: 220px; background: transparent; border: 0; color: var(--muted); font-size: 12px; padding: 4px 0; outline: none; }
|
|
22819
|
+
#model:hover, #model:focus { color: var(--fg); }
|
|
22792
22820
|
#send { width: 34px; height: 34px; border: 0; border-radius: 10px; background: #ececec; color: #111; display: grid; place-items: center; transition: transform .18s var(--ease), opacity .18s var(--ease); }
|
|
22793
22821
|
#send:hover { transform: translateY(-1px); }
|
|
22794
22822
|
#send:disabled, textarea:disabled { opacity: .4; cursor: not-allowed; }
|
|
@@ -22827,7 +22855,7 @@ textarea::placeholder { color: #6a6a6a; }
|
|
|
22827
22855
|
<div class="badges">
|
|
22828
22856
|
<span class="badge"><span class="dot"></span>${listen}</span>
|
|
22829
22857
|
<span class="badge" id="files-badge" hidden></span>
|
|
22830
|
-
<span class="badge model" title="${model}">${modelShort}</span>
|
|
22858
|
+
<span class="badge model" id="model-badge" title="${model}">${modelShort}</span>
|
|
22831
22859
|
</div>
|
|
22832
22860
|
</header>
|
|
22833
22861
|
<main>
|
|
@@ -22864,7 +22892,7 @@ textarea::placeholder { color: #6a6a6a; }
|
|
|
22864
22892
|
<button type="button" data-cmd="/exit">Exit</button>
|
|
22865
22893
|
</div>
|
|
22866
22894
|
<div class="bar-right">
|
|
22867
|
-
<
|
|
22895
|
+
<select id="model" aria-label="Author model">${options}</select>
|
|
22868
22896
|
<button type="button" id="send" aria-label="Send">
|
|
22869
22897
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M7 11V3M3.5 6.5 7 3l3.5 3.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
22870
22898
|
</button>
|
|
@@ -22892,6 +22920,8 @@ window.CRITIQUE_CODE_TOKEN = "__CRITIQUE_CODE_TOKEN__";
|
|
|
22892
22920
|
var emptyEl = document.getElementById("empty");
|
|
22893
22921
|
var feedEl = document.getElementById("feed");
|
|
22894
22922
|
var filesBadge = document.getElementById("files-badge");
|
|
22923
|
+
var modelEl = document.getElementById("model");
|
|
22924
|
+
var modelBadge = document.getElementById("model-badge");
|
|
22895
22925
|
var inputEl = document.getElementById("input");
|
|
22896
22926
|
var sendEl = document.getElementById("send");
|
|
22897
22927
|
var modalEl = document.getElementById("modal");
|
|
@@ -22921,10 +22951,32 @@ window.CRITIQUE_CODE_TOKEN = "__CRITIQUE_CODE_TOKEN__";
|
|
|
22921
22951
|
feedEl.className = "on";
|
|
22922
22952
|
}
|
|
22923
22953
|
|
|
22954
|
+
function setModel(id) {
|
|
22955
|
+
if (!id) return;
|
|
22956
|
+
if (modelEl) {
|
|
22957
|
+
modelEl.value = id;
|
|
22958
|
+
if (modelEl.value !== id) {
|
|
22959
|
+
for (var i = 0; i < modelEl.options.length; i++) {
|
|
22960
|
+
if (modelEl.options[i].value === id || "critique/" + modelEl.options[i].value === id) {
|
|
22961
|
+
modelEl.selectedIndex = i;
|
|
22962
|
+
break;
|
|
22963
|
+
}
|
|
22964
|
+
}
|
|
22965
|
+
}
|
|
22966
|
+
}
|
|
22967
|
+
if (modelBadge) {
|
|
22968
|
+
var option = modelEl && modelEl.options[modelEl.selectedIndex];
|
|
22969
|
+
var label = option && option.text ? option.text : String(id).replace(/^openrouter//, "").replace(/^critique/critique//, "critique/");
|
|
22970
|
+
modelBadge.textContent = label;
|
|
22971
|
+
modelBadge.title = id;
|
|
22972
|
+
}
|
|
22973
|
+
}
|
|
22974
|
+
|
|
22924
22975
|
function setBusy(next) {
|
|
22925
22976
|
busy = !!next;
|
|
22926
22977
|
sendEl.disabled = busy;
|
|
22927
22978
|
inputEl.disabled = busy;
|
|
22979
|
+
if (modelEl) modelEl.disabled = busy;
|
|
22928
22980
|
}
|
|
22929
22981
|
|
|
22930
22982
|
function scrollMain() {
|
|
@@ -23053,6 +23105,9 @@ window.CRITIQUE_CODE_TOKEN = "__CRITIQUE_CODE_TOKEN__";
|
|
|
23053
23105
|
case "review":
|
|
23054
23106
|
setReview((ev.text || "") + (ev.conclusion ? "\\n" + ev.conclusion : ""));
|
|
23055
23107
|
break;
|
|
23108
|
+
case "model":
|
|
23109
|
+
setModel(ev.text);
|
|
23110
|
+
break;
|
|
23056
23111
|
case "busy":
|
|
23057
23112
|
setBusy(ev.busy);
|
|
23058
23113
|
break;
|
|
@@ -23068,6 +23123,7 @@ window.CRITIQUE_CODE_TOKEN = "__CRITIQUE_CODE_TOKEN__";
|
|
|
23068
23123
|
|
|
23069
23124
|
function applySnapshot(s) {
|
|
23070
23125
|
if (!s) return;
|
|
23126
|
+
if (s.model) setModel(s.model);
|
|
23071
23127
|
setBusy(s.busy);
|
|
23072
23128
|
setPaths(s.changed_paths);
|
|
23073
23129
|
if (s.last_review_text) setReview(s.last_review_text);
|
|
@@ -23149,6 +23205,20 @@ window.CRITIQUE_CODE_TOKEN = "__CRITIQUE_CODE_TOKEN__";
|
|
|
23149
23205
|
});
|
|
23150
23206
|
document.getElementById("approve").addEventListener("click", function () { decide("approve"); });
|
|
23151
23207
|
document.getElementById("deny").addEventListener("click", function () { decide("deny"); });
|
|
23208
|
+
if (modelEl) {
|
|
23209
|
+
modelEl.addEventListener("change", function () {
|
|
23210
|
+
var id = modelEl.value;
|
|
23211
|
+
if (!id || busy) return;
|
|
23212
|
+
api("POST", "/api/model", { id: id }).then(function (res) {
|
|
23213
|
+
return res.json().then(function (body) {
|
|
23214
|
+
if (!res.ok) addBlock("alert", body.error || "Could not change model.");
|
|
23215
|
+
else setModel(body.model || id);
|
|
23216
|
+
});
|
|
23217
|
+
}).catch(function () {
|
|
23218
|
+
addBlock("alert", "Could not change model.");
|
|
23219
|
+
});
|
|
23220
|
+
});
|
|
23221
|
+
}
|
|
23152
23222
|
|
|
23153
23223
|
api("GET", "/api/snapshot").then(function (res) { return res.json(); }).then(applySnapshot).catch(function () {});
|
|
23154
23224
|
connect();
|
|
@@ -23179,7 +23249,9 @@ async function startCritiqueCodeWebServer(input) {
|
|
|
23179
23249
|
}
|
|
23180
23250
|
const bindHost = "127.0.0.1";
|
|
23181
23251
|
const env = input.env ?? process.env;
|
|
23252
|
+
let pendingModelChoice;
|
|
23182
23253
|
let model = "critique/auto";
|
|
23254
|
+
let modelChoices = [];
|
|
23183
23255
|
try {
|
|
23184
23256
|
const config = await loadCritiqueCodeRuntimeConfig(env);
|
|
23185
23257
|
const authorModels = input.author_models && input.author_models.length > 0 ? [...input.author_models] : await resolveHarnessModels({ role: "author", env, config });
|
|
@@ -23188,6 +23260,8 @@ async function startCritiqueCodeWebServer(input) {
|
|
|
23188
23260
|
return { kind: "kernel_required", limitation: KERNEL_REQUIRED3 };
|
|
23189
23261
|
}
|
|
23190
23262
|
model = piModelId(authorModels[0]);
|
|
23263
|
+
const route = config.settings?.route ?? "critique-inference";
|
|
23264
|
+
modelChoices = modelsForRoute(route).map((item) => ({ id: item.value, label: item.label }));
|
|
23191
23265
|
} catch (error) {
|
|
23192
23266
|
return { kind: "kernel_required", limitation: error instanceof Error ? error.message : String(error) };
|
|
23193
23267
|
}
|
|
@@ -23242,6 +23316,7 @@ async function startCritiqueCodeWebServer(input) {
|
|
|
23242
23316
|
const snapshot = () => ({
|
|
23243
23317
|
repository_root: repositoryRoot,
|
|
23244
23318
|
model,
|
|
23319
|
+
models: modelChoices,
|
|
23245
23320
|
busy,
|
|
23246
23321
|
changed_paths: [...changedPaths],
|
|
23247
23322
|
last_review_text: lastReviewText,
|
|
@@ -23288,6 +23363,7 @@ async function startCritiqueCodeWebServer(input) {
|
|
|
23288
23363
|
repository_root: repositoryRoot,
|
|
23289
23364
|
branch: gitBranch(repositoryRoot),
|
|
23290
23365
|
model,
|
|
23366
|
+
models: modelChoices,
|
|
23291
23367
|
listen: `127.0.0.1:${port}`
|
|
23292
23368
|
}).replaceAll("__CRITIQUE_CODE_TOKEN__", token);
|
|
23293
23369
|
send(res, 200, html, "text/html; charset=utf-8");
|
|
@@ -23367,7 +23443,7 @@ async function startCritiqueCodeWebServer(input) {
|
|
|
23367
23443
|
});
|
|
23368
23444
|
send(res, 202, { ok: true, verification_uri: started.verificationUri });
|
|
23369
23445
|
void started.finished.then(() => {
|
|
23370
|
-
emit({ type: "note", text: "Inference is connected. Restart critique-code web if
|
|
23446
|
+
emit({ type: "note", text: "Inference is connected. The author model is the one you picked on the website. Restart critique-code web if this session still shows the previous model." });
|
|
23371
23447
|
}).catch((error) => {
|
|
23372
23448
|
const text = error instanceof Error ? error.message : String(error);
|
|
23373
23449
|
emit({ type: "error", text: humanizeCritiqueCodeWebError(text) });
|
|
@@ -23379,6 +23455,38 @@ async function startCritiqueCodeWebServer(input) {
|
|
|
23379
23455
|
}
|
|
23380
23456
|
return;
|
|
23381
23457
|
}
|
|
23458
|
+
if (req.method === "GET" && path2 === "/api/models") {
|
|
23459
|
+
send(res, 200, { model, models: modelChoices });
|
|
23460
|
+
return;
|
|
23461
|
+
}
|
|
23462
|
+
if (req.method === "POST" && path2 === "/api/model") {
|
|
23463
|
+
const body = await readJson(req);
|
|
23464
|
+
const id4 = String(body.id ?? body.model ?? "").trim();
|
|
23465
|
+
const parsed = parseCritiqueCodeModels(id4)[0];
|
|
23466
|
+
if (!parsed) {
|
|
23467
|
+
send(res, 400, { error: "Unknown model." });
|
|
23468
|
+
return;
|
|
23469
|
+
}
|
|
23470
|
+
const config = await loadCritiqueCodeRuntimeConfig(env);
|
|
23471
|
+
const route = config.settings?.route ?? "critique-inference";
|
|
23472
|
+
await saveCritiqueCodeRuntimeSettings({
|
|
23473
|
+
home: critiqueCodeHome(env),
|
|
23474
|
+
settings: settingsFromSelection({
|
|
23475
|
+
route,
|
|
23476
|
+
author: parsed,
|
|
23477
|
+
review: [parsed],
|
|
23478
|
+
...config.settings?.custom_openai ? { custom_openai: config.settings.custom_openai } : {}
|
|
23479
|
+
})
|
|
23480
|
+
});
|
|
23481
|
+
model = piModelId(parsed);
|
|
23482
|
+
emit({ type: "model", text: model });
|
|
23483
|
+
if (!busy && !ended) {
|
|
23484
|
+
pendingModelChoice = id4;
|
|
23485
|
+
hub.push("/models");
|
|
23486
|
+
}
|
|
23487
|
+
send(res, 200, { ok: true, model });
|
|
23488
|
+
return;
|
|
23489
|
+
}
|
|
23382
23490
|
send(res, 404, { error: "not found" });
|
|
23383
23491
|
});
|
|
23384
23492
|
let portValue = 0;
|
|
@@ -23427,7 +23535,12 @@ Loopback only. The author session stays in this process.
|
|
|
23427
23535
|
approve_exec: approveExec,
|
|
23428
23536
|
on_kernel_event: mapKernel,
|
|
23429
23537
|
settings_ui: {
|
|
23430
|
-
async select() {
|
|
23538
|
+
async select(_title, items) {
|
|
23539
|
+
if (pendingModelChoice) {
|
|
23540
|
+
const choice = pendingModelChoice;
|
|
23541
|
+
pendingModelChoice = void 0;
|
|
23542
|
+
if (items.some((item) => item.value === choice)) return choice;
|
|
23543
|
+
}
|
|
23431
23544
|
return "back";
|
|
23432
23545
|
},
|
|
23433
23546
|
async prompt() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@critiquedotsh/harness",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "CritiqueCode author agent — implement, then forced review and verified repair. Same org as @critiquedotsh/cli; not the sidecar.",
|
|
5
5
|
"homepage": "https://critique.sh/docs/platform/critique-code",
|
|
6
6
|
"repository": {
|
|
@@ -27,10 +27,12 @@
|
|
|
27
27
|
},
|
|
28
28
|
"files": [
|
|
29
29
|
"dist",
|
|
30
|
-
"README.md"
|
|
30
|
+
"README.md",
|
|
31
|
+
"scripts/postinstall.mjs"
|
|
31
32
|
],
|
|
32
33
|
"scripts": {
|
|
33
34
|
"build": "node scripts/build-cli.mjs",
|
|
35
|
+
"postinstall": "node scripts/postinstall.mjs",
|
|
34
36
|
"start": "node --import ../../scripts/register-test-path-alias.mjs --experimental-strip-types src/cli.ts",
|
|
35
37
|
"install:pi": "npm install --prefix .vendor",
|
|
36
38
|
"prepublishOnly": "npm run build",
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const silent =
|
|
4
|
+
process.env.CI === 'true'
|
|
5
|
+
|| process.env.npm_config_loglevel === 'silent'
|
|
6
|
+
|| process.env.npm_config_loglevel === 'error'
|
|
7
|
+
|
|
8
|
+
if (silent) process.exit(0)
|
|
9
|
+
|
|
10
|
+
process.stderr.write(`
|
|
11
|
+
CritiqueCode installed. Binary: critique-code
|
|
12
|
+
|
|
13
|
+
cd into a git repo, then:
|
|
14
|
+
|
|
15
|
+
critique-code login connect Critique Inference in the browser (no key paste)
|
|
16
|
+
critique-code terminal author session
|
|
17
|
+
critique-code web local browser UI on 127.0.0.1
|
|
18
|
+
critique-code help commands, /review /repair /ship, voice
|
|
19
|
+
|
|
20
|
+
Voice needs the ffmpeg-static install script:
|
|
21
|
+
npm install -g @critiquedotsh/harness --allow-scripts=ffmpeg-static
|
|
22
|
+
|
|
23
|
+
Docs: https://critique.sh/docs/platform/critique-code
|
|
24
|
+
|
|
25
|
+
`)
|