pinsay-cli 0.6.10 → 0.7.1
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 +314 -61
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -15,7 +15,7 @@ var init_build_constants = __esm({
|
|
|
15
15
|
"src/build-constants.ts"() {
|
|
16
16
|
"use strict";
|
|
17
17
|
BUILD_DEFAULT_SERVER = true ? "https://app.pinsay.dev" : "https://app.pinsay.dev";
|
|
18
|
-
BUILD_CLI_VERSION = true ? "0.
|
|
18
|
+
BUILD_CLI_VERSION = true ? "0.7.1" : "0.0.0-dev";
|
|
19
19
|
}
|
|
20
20
|
});
|
|
21
21
|
|
|
@@ -705,7 +705,7 @@ var init_map = __esm({
|
|
|
705
705
|
});
|
|
706
706
|
|
|
707
707
|
// src/auth.ts
|
|
708
|
-
import { promises as
|
|
708
|
+
import { promises as fs18 } from "node:fs";
|
|
709
709
|
import { dirname as dirname8 } from "node:path";
|
|
710
710
|
async function readApiKey(cwd2, server) {
|
|
711
711
|
const { key } = await resolveApiKey(cwd2, server);
|
|
@@ -730,7 +730,7 @@ async function resolveToken(server, cwd2, explicitApiKey) {
|
|
|
730
730
|
return void 0;
|
|
731
731
|
const cacheFile = tokenCacheFile(server, apiKey);
|
|
732
732
|
try {
|
|
733
|
-
const cached = JSON.parse(await
|
|
733
|
+
const cached = JSON.parse(await fs18.readFile(cacheFile, "utf8"));
|
|
734
734
|
const token = typeof cached?.token === "string" ? cached.token.trim() : "";
|
|
735
735
|
if (token && !isJwtExpired(token))
|
|
736
736
|
return token;
|
|
@@ -747,8 +747,8 @@ async function resolveToken(server, cwd2, explicitApiKey) {
|
|
|
747
747
|
);
|
|
748
748
|
if (login?.token) {
|
|
749
749
|
try {
|
|
750
|
-
await
|
|
751
|
-
await
|
|
750
|
+
await fs18.mkdir(dirname8(cacheFile), { recursive: true });
|
|
751
|
+
await fs18.writeFile(cacheFile, JSON.stringify({ token: login.token }), "utf8");
|
|
752
752
|
} catch {
|
|
753
753
|
}
|
|
754
754
|
return login.token;
|
|
@@ -770,7 +770,7 @@ var init_auth = __esm({
|
|
|
770
770
|
|
|
771
771
|
// src/vite/resolve.ts
|
|
772
772
|
import { existsSync as existsSync3, readFileSync } from "node:fs";
|
|
773
|
-
import { join as
|
|
773
|
+
import { join as join18 } from "node:path";
|
|
774
774
|
function entryOf(json, hash) {
|
|
775
775
|
if (!json || typeof json !== "object")
|
|
776
776
|
return void 0;
|
|
@@ -799,11 +799,11 @@ function resolveSource(cwd2, hash) {
|
|
|
799
799
|
const miss = { kind: "unknown", path: null, component: null };
|
|
800
800
|
if (!hash || !/^[0-9a-f]{8}$/.test(hash))
|
|
801
801
|
return miss;
|
|
802
|
-
const manifestPath =
|
|
802
|
+
const manifestPath = join18(cwd2, ".pinsay", "manifest.json");
|
|
803
803
|
const current = normalise(entryOf(readJson2(manifestPath), hash));
|
|
804
804
|
if (current)
|
|
805
805
|
return { kind: "manifest", path: current.path, component: current.component };
|
|
806
|
-
const prevPath =
|
|
806
|
+
const prevPath = join18(cwd2, ".pinsay", "manifest.prev.json");
|
|
807
807
|
const previous = normalise(entryOf(readJson2(prevPath), hash));
|
|
808
808
|
if (previous) {
|
|
809
809
|
return {
|
|
@@ -1103,6 +1103,12 @@ async function statusCommand(cwd2, parsed, positionals = []) {
|
|
|
1103
1103
|
console.error(`Invalid status: ${newStatusStr}. Must be open, ready, applied, or archived.`);
|
|
1104
1104
|
process.exit(2);
|
|
1105
1105
|
}
|
|
1106
|
+
if (statusNum === 3) {
|
|
1107
|
+
console.error(
|
|
1108
|
+
'Marking a comment as \'applied\' is not permitted via `pinsay status`.\nUse `pinsay apply --mark <id> --reply "<explanation>"` (or `pinsay reply <id> "<text>"`) to record a properly structured reply.'
|
|
1109
|
+
);
|
|
1110
|
+
process.exit(2);
|
|
1111
|
+
}
|
|
1106
1112
|
await api(server, `/api/comments/${id}`, {
|
|
1107
1113
|
method: "PATCH",
|
|
1108
1114
|
body: { status: statusNum },
|
|
@@ -4741,14 +4747,66 @@ async function updateCommand(cwd2, options) {
|
|
|
4741
4747
|
// src/commands/apply.ts
|
|
4742
4748
|
init_config();
|
|
4743
4749
|
init_api();
|
|
4750
|
+
|
|
4751
|
+
// src/lib/notify-update.ts
|
|
4752
|
+
import { promises as fs17 } from "node:fs";
|
|
4753
|
+
import { join as join17 } from "node:path";
|
|
4754
|
+
init_build_constants();
|
|
4755
|
+
async function checkAndNotifyUpdates(options) {
|
|
4756
|
+
const result = {
|
|
4757
|
+
cliUpdateAvailable: false,
|
|
4758
|
+
skillsUpdateAvailable: false
|
|
4759
|
+
};
|
|
4760
|
+
if (options.silent || !options.meta)
|
|
4761
|
+
return result;
|
|
4762
|
+
const meta = options.meta;
|
|
4763
|
+
if (meta.latestCliVersion && compareSemver(BUILD_CLI_VERSION, meta.latestCliVersion) < 0) {
|
|
4764
|
+
result.cliUpdateAvailable = true;
|
|
4765
|
+
console.error(
|
|
4766
|
+
`\x1B[33m\u{1F4A1} Update available for pinsay-cli: ${BUILD_CLI_VERSION} \u2192 ${meta.latestCliVersion}\x1B[0m
|
|
4767
|
+
\x1B[33m Run 'npx -y pinsay@latest' to update.\x1B[0m
|
|
4768
|
+
`
|
|
4769
|
+
);
|
|
4770
|
+
}
|
|
4771
|
+
if (meta.skillVersion && options.config && options.cwd) {
|
|
4772
|
+
try {
|
|
4773
|
+
const files = skillFilesFor(options.config);
|
|
4774
|
+
let hasStale = false;
|
|
4775
|
+
for (const rel of files) {
|
|
4776
|
+
const abs = join17(options.cwd, rel);
|
|
4777
|
+
try {
|
|
4778
|
+
await fs17.access(abs);
|
|
4779
|
+
const installed = await readStamp(abs);
|
|
4780
|
+
if (installed && installed !== meta.skillVersion) {
|
|
4781
|
+
hasStale = true;
|
|
4782
|
+
break;
|
|
4783
|
+
}
|
|
4784
|
+
} catch {
|
|
4785
|
+
}
|
|
4786
|
+
}
|
|
4787
|
+
if (hasStale) {
|
|
4788
|
+
result.skillsUpdateAvailable = true;
|
|
4789
|
+
console.error(
|
|
4790
|
+
`\x1B[33m\u{1F4A1} Local AI skills are out of date with the server.\x1B[0m
|
|
4791
|
+
\x1B[33m Run 'npx pinsay-cli update' to refresh your prompt skills.\x1B[0m
|
|
4792
|
+
`
|
|
4793
|
+
);
|
|
4794
|
+
}
|
|
4795
|
+
} catch {
|
|
4796
|
+
}
|
|
4797
|
+
}
|
|
4798
|
+
return result;
|
|
4799
|
+
}
|
|
4800
|
+
|
|
4801
|
+
// src/commands/apply.ts
|
|
4744
4802
|
init_auth();
|
|
4745
4803
|
init_build_constants();
|
|
4746
4804
|
|
|
4747
4805
|
// src/apply/run.ts
|
|
4748
4806
|
init_resolve();
|
|
4749
4807
|
init_api();
|
|
4750
|
-
import { promises as
|
|
4751
|
-
import { join as
|
|
4808
|
+
import { promises as fs20 } from "node:fs";
|
|
4809
|
+
import { join as join20, dirname as dirname9 } from "node:path";
|
|
4752
4810
|
import { spawnSync } from "node:child_process";
|
|
4753
4811
|
|
|
4754
4812
|
// src/apply/queue.ts
|
|
@@ -4906,8 +4964,8 @@ async function fetchQueue(ctx, filter) {
|
|
|
4906
4964
|
|
|
4907
4965
|
// src/apply/context.ts
|
|
4908
4966
|
init_api();
|
|
4909
|
-
import { promises as
|
|
4910
|
-
import { join as
|
|
4967
|
+
import { promises as fs19 } from "node:fs";
|
|
4968
|
+
import { join as join19 } from "node:path";
|
|
4911
4969
|
init_config();
|
|
4912
4970
|
async function loadProjectContext(ctx) {
|
|
4913
4971
|
const branding = await getBranding(ctx.server);
|
|
@@ -4918,7 +4976,7 @@ async function loadProjectContext(ctx) {
|
|
|
4918
4976
|
if (config.delegation === "off")
|
|
4919
4977
|
delegation = "off";
|
|
4920
4978
|
const relPath = isMultiProject(config) ? stackFileRelPath(ctx.project) : stackFileRelPath();
|
|
4921
|
-
const stackRaw = await
|
|
4979
|
+
const stackRaw = await fs19.readFile(join19(ctx.cwd, relPath), "utf8");
|
|
4922
4980
|
stack = JSON.parse(stackRaw);
|
|
4923
4981
|
} catch {
|
|
4924
4982
|
}
|
|
@@ -5235,22 +5293,34 @@ function buildApplyPrompt(items, context, opts) {
|
|
|
5235
5293
|
}
|
|
5236
5294
|
lines.push("## When you finish an item");
|
|
5237
5295
|
lines.push(
|
|
5238
|
-
'Run exactly: `npx pinsay-cli apply --mark <id> --reply "<what changed>" --
|
|
5296
|
+
'Run exactly: `npx pinsay-cli apply --mark <id> --reply "<what changed>" --models "<planner-id>=planner,<worker-id>=implementer,<reviewer-id>=reviewer" --tool <your-tool-name>`'
|
|
5297
|
+
);
|
|
5298
|
+
lines.push(
|
|
5299
|
+
'(Separate style: after each item; Single style: run `npx pinsay-cli apply --mark all --reply "..." --models "..." --tool <your-tool-name>`'
|
|
5239
5300
|
);
|
|
5240
5301
|
lines.push(
|
|
5241
|
-
|
|
5302
|
+
"once at the end). Never run git push. `--models` is MANDATORY and MUST list EVERY model that touched the item and its role:"
|
|
5242
5303
|
);
|
|
5243
5304
|
lines.push(
|
|
5244
|
-
"
|
|
5305
|
+
"the planner/reviewer (your own model id) and each worker model you delegated to (`implementer`). NEVER guess or hardcode"
|
|
5245
5306
|
);
|
|
5246
5307
|
lines.push(
|
|
5247
|
-
"
|
|
5308
|
+
'outdated model names from memory (like "gemini-2.5-pro"). Always inspect your active runtime model name directly. Use exact'
|
|
5248
5309
|
);
|
|
5249
5310
|
lines.push(
|
|
5250
|
-
|
|
5311
|
+
'model ids (e.g. `claude-opus-5-5`, `claude-sonnet-5-5`, `gpt-5.2`), never aliases like "haiku" or "sonnet". With one'
|
|
5251
5312
|
);
|
|
5252
5313
|
lines.push(
|
|
5253
|
-
"
|
|
5314
|
+
"model and no delegation, pass just yours as `<your-model-id>=implementer`. Explicit role `=<role>` is required on each entry."
|
|
5315
|
+
);
|
|
5316
|
+
lines.push(
|
|
5317
|
+
"`--tool` (e.g. `claude-code`, `opencode`, `cursor`, `windsurf`, `antigravity`) records which agent you are running as."
|
|
5318
|
+
);
|
|
5319
|
+
lines.push(
|
|
5320
|
+
"ALWAYS pass both explicitly, even on a project you ran `init` on \u2014 `--tool` silently falls back to whichever tool happened"
|
|
5321
|
+
);
|
|
5322
|
+
lines.push(
|
|
5323
|
+
"to run `init`, which is wrong the moment a different tool applies a comment later."
|
|
5254
5324
|
);
|
|
5255
5325
|
return lines.join("\n") + "\n";
|
|
5256
5326
|
}
|
|
@@ -5278,10 +5348,10 @@ function detectAiTool(override) {
|
|
|
5278
5348
|
async function ensureToolRegistered(ctx, tool) {
|
|
5279
5349
|
const config = await readConfig(ctx.cwd).catch(() => ({}));
|
|
5280
5350
|
const relStackPath = isMultiProject(config) ? stackFileRelPath(ctx.project) : stackFileRelPath();
|
|
5281
|
-
const stackPath =
|
|
5351
|
+
const stackPath = join20(ctx.cwd, relStackPath);
|
|
5282
5352
|
let stackData = {};
|
|
5283
5353
|
try {
|
|
5284
|
-
const raw = await
|
|
5354
|
+
const raw = await fs20.readFile(stackPath, "utf8");
|
|
5285
5355
|
stackData = JSON.parse(raw);
|
|
5286
5356
|
} catch {
|
|
5287
5357
|
}
|
|
@@ -5300,8 +5370,8 @@ async function ensureToolRegistered(ctx, tool) {
|
|
|
5300
5370
|
}
|
|
5301
5371
|
);
|
|
5302
5372
|
if (res) {
|
|
5303
|
-
await
|
|
5304
|
-
await
|
|
5373
|
+
await fs20.mkdir(dirname9(stackPath), { recursive: true });
|
|
5374
|
+
await fs20.writeFile(stackPath, JSON.stringify(res, null, 2) + "\n", "utf8");
|
|
5305
5375
|
return;
|
|
5306
5376
|
}
|
|
5307
5377
|
} catch {
|
|
@@ -5310,8 +5380,8 @@ async function ensureToolRegistered(ctx, tool) {
|
|
|
5310
5380
|
aiTools.push(tool);
|
|
5311
5381
|
stackData.aiTools = aiTools;
|
|
5312
5382
|
try {
|
|
5313
|
-
await
|
|
5314
|
-
await
|
|
5383
|
+
await fs20.mkdir(dirname9(stackPath), { recursive: true });
|
|
5384
|
+
await fs20.writeFile(stackPath, JSON.stringify(stackData, null, 2) + "\n", "utf8");
|
|
5315
5385
|
} catch {
|
|
5316
5386
|
}
|
|
5317
5387
|
}
|
|
@@ -5385,9 +5455,9 @@ async function runApply(options, ctx) {
|
|
|
5385
5455
|
console.error(`Failed to spawn opencode: ${res.error.message}`);
|
|
5386
5456
|
}
|
|
5387
5457
|
} else if (tool === "cursor") {
|
|
5388
|
-
const promptFile =
|
|
5389
|
-
await
|
|
5390
|
-
await
|
|
5458
|
+
const promptFile = join20(ctx.cwd, ".pinsay/apply-prompt.md");
|
|
5459
|
+
await fs20.mkdir(join20(ctx.cwd, ".pinsay"), { recursive: true });
|
|
5460
|
+
await fs20.writeFile(promptFile, prompt, "utf8");
|
|
5391
5461
|
console.log(`Saved apply prompt to ${promptFile}`);
|
|
5392
5462
|
} else if (tool === "clipboard") {
|
|
5393
5463
|
const copied = copyToClipboard(prompt);
|
|
@@ -5475,6 +5545,106 @@ function commitUrlFor(sha, remoteUrl) {
|
|
|
5475
5545
|
return null;
|
|
5476
5546
|
}
|
|
5477
5547
|
|
|
5548
|
+
// src/apply/models.ts
|
|
5549
|
+
var MODEL_ROLES = ["planner", "implementer", "reviewer"];
|
|
5550
|
+
var MAX_AI_MODELS = 8;
|
|
5551
|
+
var MODEL_RE = /^[A-Za-z0-9][A-Za-z0-9._:/+-]*$/;
|
|
5552
|
+
var ModelsError = class extends Error {
|
|
5553
|
+
};
|
|
5554
|
+
function normalizeEntry(raw) {
|
|
5555
|
+
const model = typeof raw.model === "string" ? raw.model.trim() : "";
|
|
5556
|
+
if (!model || model.length > 64 || !MODEL_RE.test(model)) {
|
|
5557
|
+
throw new ModelsError(
|
|
5558
|
+
`Invalid model id "${String(raw.model ?? "")}": use the exact model id (1-64 chars, letters/digits and . _ : / + -), e.g. "claude-opus-5-5".`
|
|
5559
|
+
);
|
|
5560
|
+
}
|
|
5561
|
+
let role = null;
|
|
5562
|
+
if (raw.role !== void 0 && raw.role !== null && raw.role !== "") {
|
|
5563
|
+
const r = String(raw.role).trim().toLowerCase();
|
|
5564
|
+
if (!MODEL_ROLES.includes(r)) {
|
|
5565
|
+
throw new ModelsError(
|
|
5566
|
+
`Invalid role "${String(raw.role)}" for model "${model}": expected one of ${MODEL_ROLES.join(", ")}.`
|
|
5567
|
+
);
|
|
5568
|
+
}
|
|
5569
|
+
role = r;
|
|
5570
|
+
}
|
|
5571
|
+
return { model, role };
|
|
5572
|
+
}
|
|
5573
|
+
function parseModelsFlag(value) {
|
|
5574
|
+
const out = [];
|
|
5575
|
+
for (const part of value.split(",")) {
|
|
5576
|
+
const p = part.trim();
|
|
5577
|
+
if (!p)
|
|
5578
|
+
continue;
|
|
5579
|
+
const eq = p.indexOf("=");
|
|
5580
|
+
out.push(
|
|
5581
|
+
eq === -1 ? normalizeEntry({ model: p }) : normalizeEntry({ model: p.slice(0, eq), role: p.slice(eq + 1) })
|
|
5582
|
+
);
|
|
5583
|
+
}
|
|
5584
|
+
return out;
|
|
5585
|
+
}
|
|
5586
|
+
function dedupeModels(entries) {
|
|
5587
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5588
|
+
const out = [];
|
|
5589
|
+
for (const e of entries) {
|
|
5590
|
+
const key = `${e.model.toLowerCase()}|${e.role ?? ""}`;
|
|
5591
|
+
if (seen.has(key))
|
|
5592
|
+
continue;
|
|
5593
|
+
seen.add(key);
|
|
5594
|
+
out.push(e);
|
|
5595
|
+
}
|
|
5596
|
+
if (out.length > MAX_AI_MODELS) {
|
|
5597
|
+
throw new ModelsError(`Too many models (${out.length}); at most ${MAX_AI_MODELS} entries are allowed.`);
|
|
5598
|
+
}
|
|
5599
|
+
return out;
|
|
5600
|
+
}
|
|
5601
|
+
function mergeModels(...sources) {
|
|
5602
|
+
return dedupeModels(sources.flatMap((s) => s ?? []));
|
|
5603
|
+
}
|
|
5604
|
+
function primaryModel(entries) {
|
|
5605
|
+
return (entries.find((e) => e.role === "implementer") ?? entries[0])?.model;
|
|
5606
|
+
}
|
|
5607
|
+
function resolveModels(flags, env = process.env) {
|
|
5608
|
+
const explicit = !!(flags.models || flags.model);
|
|
5609
|
+
const modelsStr = explicit ? flags.models : env["PINSAY_AI_MODELS"];
|
|
5610
|
+
const modelStr = explicit ? flags.model : env["PINSAY_AI_MODEL"];
|
|
5611
|
+
return mergeModels(
|
|
5612
|
+
modelsStr ? parseModelsFlag(modelsStr) : void 0,
|
|
5613
|
+
modelStr ? [normalizeEntry({ model: modelStr })] : void 0
|
|
5614
|
+
);
|
|
5615
|
+
}
|
|
5616
|
+
function resolveModelsFromArgs(args, env = process.env) {
|
|
5617
|
+
const hasArray = args?.models !== void 0 && args.models !== null;
|
|
5618
|
+
const single = typeof args?.model === "string" && args.model ? args.model : void 0;
|
|
5619
|
+
if (!hasArray && !single) {
|
|
5620
|
+
return resolveModels({}, env);
|
|
5621
|
+
}
|
|
5622
|
+
let fromArray;
|
|
5623
|
+
if (hasArray) {
|
|
5624
|
+
if (!Array.isArray(args.models))
|
|
5625
|
+
throw new ModelsError("models must be an array of {model, role?}.");
|
|
5626
|
+
fromArray = args.models.map(
|
|
5627
|
+
(m) => typeof m === "string" ? normalizeEntry({ model: m }) : normalizeEntry(m ?? {})
|
|
5628
|
+
);
|
|
5629
|
+
}
|
|
5630
|
+
return mergeModels(fromArray, single ? [normalizeEntry({ model: single })] : void 0);
|
|
5631
|
+
}
|
|
5632
|
+
function modelsBody(entries) {
|
|
5633
|
+
if (entries.length === 0)
|
|
5634
|
+
return {};
|
|
5635
|
+
return { aiModels: entries, aiModel: primaryModel(entries) };
|
|
5636
|
+
}
|
|
5637
|
+
function distinctModelCount(entries) {
|
|
5638
|
+
return new Set(entries.map((e) => e.model.toLowerCase())).size;
|
|
5639
|
+
}
|
|
5640
|
+
var DELEGATION_WARNING = '\u26A0 Delegation is on for this project, but fewer than 2 distinct models were reported. Pass every model that worked on the item (planner, worker, reviewer) via --models "<planner>=planner,<worker>=implementer,<reviewer>=reviewer".';
|
|
5641
|
+
function delegationWarning(delegation, entries) {
|
|
5642
|
+
return delegation === "auto" && distinctModelCount(entries) < 2 ? DELEGATION_WARNING : null;
|
|
5643
|
+
}
|
|
5644
|
+
function describeModels(entries) {
|
|
5645
|
+
return entries.length === 0 ? "(none)" : entries.map((e) => `${e.model}${e.role ? `=${e.role}` : ""}`).join(", ");
|
|
5646
|
+
}
|
|
5647
|
+
|
|
5478
5648
|
// src/apply/mark.ts
|
|
5479
5649
|
async function markApplied(options, ctx) {
|
|
5480
5650
|
if (!options.noCommit && !isStaged(ctx.cwd)) {
|
|
@@ -5488,10 +5658,19 @@ async function markApplied(options, ctx) {
|
|
|
5488
5658
|
const projectCtx = await loadProjectContext(ctx);
|
|
5489
5659
|
const email = getUserEmail(ctx.cwd);
|
|
5490
5660
|
const appliedByLabel = options.tool ? `${email} via ${options.tool}` : email;
|
|
5661
|
+
const models = mergeModels(
|
|
5662
|
+
options.models,
|
|
5663
|
+
options.model ? [normalizeEntry({ model: options.model })] : void 0
|
|
5664
|
+
);
|
|
5665
|
+
const aiModelFields = modelsBody(models);
|
|
5666
|
+
const warning = delegationWarning(projectCtx.delegation ?? "auto", models);
|
|
5667
|
+
if (warning)
|
|
5668
|
+
console.error(warning);
|
|
5491
5669
|
if (options.dryRun) {
|
|
5492
5670
|
console.log(`[dry-run] Would mark comment ${options.id} as applied`);
|
|
5493
5671
|
console.log(`[dry-run] Reply: ${options.reply}`);
|
|
5494
5672
|
console.log(`[dry-run] AppliedBy: ${appliedByLabel}`);
|
|
5673
|
+
console.log(`[dry-run] Models: ${describeModels(models)}`);
|
|
5495
5674
|
return {
|
|
5496
5675
|
committed: false,
|
|
5497
5676
|
patchedIds: typeof options.id === "number" ? [options.id] : []
|
|
@@ -5535,7 +5714,7 @@ async function markApplied(options, ctx) {
|
|
|
5535
5714
|
// Structured AI attribution on the reply itself (Reply.AiTool/AiModel), distinct from the
|
|
5536
5715
|
// free-text appliedByLabel above — see CommentService.Normalize server-side.
|
|
5537
5716
|
aiTool: options.tool || void 0,
|
|
5538
|
-
|
|
5717
|
+
...aiModelFields
|
|
5539
5718
|
},
|
|
5540
5719
|
token: ctx.token
|
|
5541
5720
|
});
|
|
@@ -5579,7 +5758,7 @@ async function markApplied(options, ctx) {
|
|
|
5579
5758
|
// against a deployed build.
|
|
5580
5759
|
commitSha: sha || null,
|
|
5581
5760
|
aiTool: options.tool || void 0,
|
|
5582
|
-
|
|
5761
|
+
...aiModelFields
|
|
5583
5762
|
},
|
|
5584
5763
|
token: ctx.token
|
|
5585
5764
|
});
|
|
@@ -5599,9 +5778,10 @@ async function markApplied(options, ctx) {
|
|
|
5599
5778
|
}
|
|
5600
5779
|
async function markFailed(id, reason, ctx, tool, model) {
|
|
5601
5780
|
const replyBody = `Could not apply: ${reason}`;
|
|
5781
|
+
const models = typeof model === "string" ? model ? [normalizeEntry({ model })] : [] : model ?? [];
|
|
5602
5782
|
await api(ctx.server, `/api/comments/${id}/replies`, {
|
|
5603
5783
|
method: "POST",
|
|
5604
|
-
body: { body: replyBody, aiTool: tool || void 0,
|
|
5784
|
+
body: { body: replyBody, aiTool: tool || void 0, ...modelsBody(models) },
|
|
5605
5785
|
token: ctx.token
|
|
5606
5786
|
});
|
|
5607
5787
|
await postEvent(ctx.server, ctx.token, {
|
|
@@ -5636,6 +5816,20 @@ async function exitIfWorkspaceFrozen(server, token) {
|
|
|
5636
5816
|
} catch {
|
|
5637
5817
|
}
|
|
5638
5818
|
}
|
|
5819
|
+
function parseModelFlags(parsed) {
|
|
5820
|
+
try {
|
|
5821
|
+
return resolveModels({
|
|
5822
|
+
models: typeof parsed["models"] === "string" ? parsed["models"] : void 0,
|
|
5823
|
+
model: typeof parsed["model"] === "string" ? parsed["model"] : void 0
|
|
5824
|
+
});
|
|
5825
|
+
} catch (err) {
|
|
5826
|
+
if (err instanceof ModelsError) {
|
|
5827
|
+
console.error(err.message);
|
|
5828
|
+
process.exit(2);
|
|
5829
|
+
}
|
|
5830
|
+
throw err;
|
|
5831
|
+
}
|
|
5832
|
+
}
|
|
5639
5833
|
async function applyCommand(cwd2, parsed, positionals = []) {
|
|
5640
5834
|
const root = await findRepoRoot(cwd2);
|
|
5641
5835
|
const config = await readConfig(root);
|
|
@@ -5672,6 +5866,12 @@ async function applyCommand(cwd2, parsed, positionals = []) {
|
|
|
5672
5866
|
console.error(tooOldMessage(BUILD_CLI_VERSION, minCli));
|
|
5673
5867
|
process.exit(5);
|
|
5674
5868
|
}
|
|
5869
|
+
await checkAndNotifyUpdates({
|
|
5870
|
+
meta,
|
|
5871
|
+
config,
|
|
5872
|
+
cwd: root,
|
|
5873
|
+
silent: Boolean(parsed["json"])
|
|
5874
|
+
});
|
|
5675
5875
|
} catch (err) {
|
|
5676
5876
|
if (!(err instanceof ApiError && err.code === 404)) {
|
|
5677
5877
|
}
|
|
@@ -5710,8 +5910,10 @@ async function applyCommand(cwd2, parsed, positionals = []) {
|
|
|
5710
5910
|
markId = parsedNum;
|
|
5711
5911
|
}
|
|
5712
5912
|
const reply = typeof parsed["reply"] === "string" ? parsed["reply"] : "";
|
|
5713
|
-
if (!reply) {
|
|
5714
|
-
console.error(
|
|
5913
|
+
if (!reply || reply.trim().length < 15 || /^(done|fixed|applied|ok|yes)$/i.test(reply.trim())) {
|
|
5914
|
+
console.error(
|
|
5915
|
+
'--reply "<text>" is required when using --mark and must provide a substantive explanation of the changes (at least 15 characters, e.g. "Applied \u2713 \u2014 updated logo source in header").'
|
|
5916
|
+
);
|
|
5715
5917
|
process.exit(2);
|
|
5716
5918
|
}
|
|
5717
5919
|
const noCommit = parsed["no-commit"] === true;
|
|
@@ -5723,7 +5925,20 @@ async function applyCommand(cwd2, parsed, positionals = []) {
|
|
|
5723
5925
|
`\u26A0 --tool not passed \u2014 defaulting to "${config.aiTool}" (recorded when this project was initialized). Pass --tool <your-tool-name> explicitly if you are not that tool.`
|
|
5724
5926
|
);
|
|
5725
5927
|
}
|
|
5726
|
-
const
|
|
5928
|
+
const models = parseModelFlags(parsed);
|
|
5929
|
+
if (models.length === 0) {
|
|
5930
|
+
console.error(
|
|
5931
|
+
'--models "<model>=<role>" is mandatory when marking a comment as applied.\nSpecify your actual active runtime model and role (e.g. --models "<active-model>=implementer").\nIf multiple models or subagents participated (planner, implementer, reviewer), list all of them separated by commas.\nNever guess or hardcode model names from memory.'
|
|
5932
|
+
);
|
|
5933
|
+
process.exit(2);
|
|
5934
|
+
}
|
|
5935
|
+
const missingRole = models.find((m) => !m.role);
|
|
5936
|
+
if (missingRole) {
|
|
5937
|
+
console.error(
|
|
5938
|
+
`Invalid --models entry "${missingRole.model}": explicit role is required. Expected "<model>=<role>" where role is one of: planner, implementer, reviewer (e.g. "${missingRole.model}=implementer").`
|
|
5939
|
+
);
|
|
5940
|
+
process.exit(2);
|
|
5941
|
+
}
|
|
5727
5942
|
const markStatus = typeof parsed["status"] === "string" ? parsed["status"] : void 0;
|
|
5728
5943
|
const markEnv = typeof parsed["env"] === "string" ? parsed["env"] : void 0;
|
|
5729
5944
|
const marked = await markApplied(
|
|
@@ -5733,7 +5948,7 @@ async function applyCommand(cwd2, parsed, positionals = []) {
|
|
|
5733
5948
|
noCommit,
|
|
5734
5949
|
dryRun,
|
|
5735
5950
|
tool: tool2,
|
|
5736
|
-
|
|
5951
|
+
models,
|
|
5737
5952
|
filter: markStatus !== void 0 || markEnv !== void 0 ? { status: markStatus, environment: markEnv } : void 0
|
|
5738
5953
|
},
|
|
5739
5954
|
clientCtx
|
|
@@ -5763,8 +5978,8 @@ async function applyCommand(cwd2, parsed, positionals = []) {
|
|
|
5763
5978
|
`\u26A0 --tool not passed \u2014 defaulting to "${config.aiTool}" (recorded when this project was initialized). Pass --tool <your-tool-name> explicitly if you are not that tool.`
|
|
5764
5979
|
);
|
|
5765
5980
|
}
|
|
5766
|
-
const
|
|
5767
|
-
await markFailed(failId, reason, clientCtx, failTool,
|
|
5981
|
+
const failModels = parseModelFlags(parsed);
|
|
5982
|
+
await markFailed(failId, reason, clientCtx, failTool, failModels);
|
|
5768
5983
|
process.exit(0);
|
|
5769
5984
|
}
|
|
5770
5985
|
if (parsed["json"] === true && !parsed["plan"] && !parsed["tool"]) {
|
|
@@ -5806,6 +6021,12 @@ async function applyAllProjects(root, cwd2, config, server, parsed) {
|
|
|
5806
6021
|
console.error(tooOldMessage(BUILD_CLI_VERSION, minCli));
|
|
5807
6022
|
process.exit(5);
|
|
5808
6023
|
}
|
|
6024
|
+
await checkAndNotifyUpdates({
|
|
6025
|
+
meta,
|
|
6026
|
+
config,
|
|
6027
|
+
cwd: root,
|
|
6028
|
+
silent: Boolean(parsed["json"])
|
|
6029
|
+
});
|
|
5809
6030
|
} catch (err) {
|
|
5810
6031
|
if (!(err instanceof ApiError && err.code === 404)) {
|
|
5811
6032
|
}
|
|
@@ -5857,7 +6078,7 @@ init_build_constants();
|
|
|
5857
6078
|
|
|
5858
6079
|
// src/mcp/server.ts
|
|
5859
6080
|
import { readFileSync as readFileSync3 } from "node:fs";
|
|
5860
|
-
import { join as
|
|
6081
|
+
import { join as join22 } from "node:path";
|
|
5861
6082
|
|
|
5862
6083
|
// node_modules/zod/v3/external.js
|
|
5863
6084
|
var external_exports = {};
|
|
@@ -11242,9 +11463,24 @@ var TOOL_PINSAY_GET_COMMENT = {
|
|
|
11242
11463
|
additionalProperties: false
|
|
11243
11464
|
}
|
|
11244
11465
|
};
|
|
11466
|
+
var MODELS_NOTICE = "Pass `models` with EVERY model that touched the item and its role (planner/reviewer = your own id, implementer = each worker model you delegated to), using exact model ids, not aliases.";
|
|
11467
|
+
var MODELS_PROPERTY = {
|
|
11468
|
+
type: "array",
|
|
11469
|
+
maxItems: 8,
|
|
11470
|
+
description: 'ALWAYS pass this: EVERY model that worked on the item, each with its role. Include the planner/reviewer (your own model id) and every worker model you delegated to (role "implementer"). Use exact model ids, not aliases like "haiku"/"sonnet". With one model and no delegation, pass just yours as implementer.',
|
|
11471
|
+
items: {
|
|
11472
|
+
type: "object",
|
|
11473
|
+
properties: {
|
|
11474
|
+
model: { type: "string", description: 'Exact model id, e.g. "claude-opus-5-5"' },
|
|
11475
|
+
role: { type: "string", enum: ["planner", "implementer", "reviewer"] }
|
|
11476
|
+
},
|
|
11477
|
+
required: ["model"],
|
|
11478
|
+
additionalProperties: false
|
|
11479
|
+
}
|
|
11480
|
+
};
|
|
11245
11481
|
var TOOL_PINSAY_MARK_APPLIED = {
|
|
11246
11482
|
name: "pinsay_mark_applied",
|
|
11247
|
-
description: `Mark one comment applied and post the reply on it, recording the optional commitUrl, the git user.email as the applier, and the tool/model attribution. Runs no git command and records no commit sha, so a comment marked this way is never flipped to Live by \`npx pinsay-cli status --deployed\`. Use it when a human will commit the change; to commit now, use pinsay_commit_and_mark. Returns {id, status, commitUrl}. ${UNTRUSTED_NOTICE}`,
|
|
11483
|
+
description: `Mark one comment applied and post the reply on it, recording the optional commitUrl, the git user.email as the applier, and the tool/model attribution. Runs no git command and records no commit sha, so a comment marked this way is never flipped to Live by \`npx pinsay-cli status --deployed\`. Use it when a human will commit the change; to commit now, use pinsay_commit_and_mark. Returns {id, status, commitUrl}. ${MODELS_NOTICE} ${UNTRUSTED_NOTICE}`,
|
|
11248
11484
|
inputSchema: {
|
|
11249
11485
|
type: "object",
|
|
11250
11486
|
properties: {
|
|
@@ -11266,8 +11502,9 @@ var TOOL_PINSAY_MARK_APPLIED = {
|
|
|
11266
11502
|
},
|
|
11267
11503
|
model: {
|
|
11268
11504
|
type: "string",
|
|
11269
|
-
description: '
|
|
11270
|
-
}
|
|
11505
|
+
description: 'Legacy single model id (role unset); prefer `models`. Exact id, not an alias (e.g. "claude-sonnet-5-5", "gpt-5.2").'
|
|
11506
|
+
},
|
|
11507
|
+
models: MODELS_PROPERTY
|
|
11271
11508
|
},
|
|
11272
11509
|
required: ["id", "reply"],
|
|
11273
11510
|
additionalProperties: false
|
|
@@ -11275,7 +11512,7 @@ var TOOL_PINSAY_MARK_APPLIED = {
|
|
|
11275
11512
|
};
|
|
11276
11513
|
var TOOL_PINSAY_COMMIT_AND_MARK = {
|
|
11277
11514
|
name: "pinsay_commit_and_mark",
|
|
11278
|
-
description: `Commit the applied changes with git and mark the comments applied, posting the same reply on each and recording its commit URL. Paths in files are relative to the repository root and must exist inside it; they are staged first. Omit files to commit what is already staged (errors when nothing is). The commit takes everything in the index, not only files. The project's commitStyle decides the commits: "single" makes one commit for all ids; "separate" makes one commit per id, and with more than one id files is required and each entry is prefixed "<id>:" to assign it to that comment (e.g. "12:src/Button.tsx"; unprefixed entries go to the first id). Never pushes. Returns [{id, commitUrl}]. ${UNTRUSTED_NOTICE}`,
|
|
11515
|
+
description: `Commit the applied changes with git and mark the comments applied, posting the same reply on each and recording its commit URL. Paths in files are relative to the repository root and must exist inside it; they are staged first. Omit files to commit what is already staged (errors when nothing is). The commit takes everything in the index, not only files. The project's commitStyle decides the commits: "single" makes one commit for all ids; "separate" makes one commit per id, and with more than one id files is required and each entry is prefixed "<id>:" to assign it to that comment (e.g. "12:src/Button.tsx"; unprefixed entries go to the first id). Never pushes. Returns [{id, commitUrl}]. ${MODELS_NOTICE} ${UNTRUSTED_NOTICE}`,
|
|
11279
11516
|
inputSchema: {
|
|
11280
11517
|
type: "object",
|
|
11281
11518
|
properties: {
|
|
@@ -11303,8 +11540,9 @@ var TOOL_PINSAY_COMMIT_AND_MARK = {
|
|
|
11303
11540
|
},
|
|
11304
11541
|
model: {
|
|
11305
11542
|
type: "string",
|
|
11306
|
-
description: '
|
|
11307
|
-
}
|
|
11543
|
+
description: 'Legacy single model id (role unset); prefer `models`. Exact id, not an alias (e.g. "claude-sonnet-5-5", "gpt-5.2").'
|
|
11544
|
+
},
|
|
11545
|
+
models: MODELS_PROPERTY
|
|
11308
11546
|
},
|
|
11309
11547
|
required: ["ids", "reply"],
|
|
11310
11548
|
additionalProperties: false
|
|
@@ -11312,7 +11550,7 @@ var TOOL_PINSAY_COMMIT_AND_MARK = {
|
|
|
11312
11550
|
};
|
|
11313
11551
|
var TOOL_PINSAY_REPLY = {
|
|
11314
11552
|
name: "pinsay_reply",
|
|
11315
|
-
description: `Post a reply on a comment, visible to its author and the other stakeholders, with tool/model attribution. Does not change the comment's status; to close a comment out with a reply, use pinsay_mark_applied or pinsay_commit_and_mark. Returns {replyId}. ${UNTRUSTED_NOTICE}`,
|
|
11553
|
+
description: `Post a reply on a comment, visible to its author and the other stakeholders, with tool/model attribution. Does not change the comment's status; to close a comment out with a reply, use pinsay_mark_applied or pinsay_commit_and_mark. Returns {replyId}. ${MODELS_NOTICE} ${UNTRUSTED_NOTICE}`,
|
|
11316
11554
|
inputSchema: {
|
|
11317
11555
|
type: "object",
|
|
11318
11556
|
properties: {
|
|
@@ -11330,8 +11568,9 @@ var TOOL_PINSAY_REPLY = {
|
|
|
11330
11568
|
},
|
|
11331
11569
|
model: {
|
|
11332
11570
|
type: "string",
|
|
11333
|
-
description: '
|
|
11334
|
-
}
|
|
11571
|
+
description: 'Legacy single model id (role unset); prefer `models`. Exact id, not an alias (e.g. "claude-sonnet-5-5", "gpt-5.2").'
|
|
11572
|
+
},
|
|
11573
|
+
models: MODELS_PROPERTY
|
|
11335
11574
|
},
|
|
11336
11575
|
required: ["id", "body"],
|
|
11337
11576
|
additionalProperties: false
|
|
@@ -11411,7 +11650,7 @@ var ALL_TOOLS = [
|
|
|
11411
11650
|
// src/mcp/tools.ts
|
|
11412
11651
|
init_api();
|
|
11413
11652
|
import { existsSync as existsSync4, readFileSync as readFileSync2 } from "node:fs";
|
|
11414
|
-
import { isAbsolute as isAbsolute3, join as
|
|
11653
|
+
import { isAbsolute as isAbsolute3, join as join21, relative as relative5, resolve as resolve4 } from "node:path";
|
|
11415
11654
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
11416
11655
|
init_build_constants();
|
|
11417
11656
|
init_projection();
|
|
@@ -11670,6 +11909,15 @@ async function handleGetComment(args, ctx) {
|
|
|
11670
11909
|
throw err;
|
|
11671
11910
|
}
|
|
11672
11911
|
}
|
|
11912
|
+
function parseModelsArgs(args) {
|
|
11913
|
+
try {
|
|
11914
|
+
return resolveModelsFromArgs(args);
|
|
11915
|
+
} catch (err) {
|
|
11916
|
+
if (err instanceof ModelsError)
|
|
11917
|
+
throw mcpError("forbidden", err.message);
|
|
11918
|
+
throw err;
|
|
11919
|
+
}
|
|
11920
|
+
}
|
|
11673
11921
|
async function handleMarkApplied(args, ctx) {
|
|
11674
11922
|
const id = Number(args?.id);
|
|
11675
11923
|
const reply = args?.reply;
|
|
@@ -11680,7 +11928,7 @@ async function handleMarkApplied(args, ctx) {
|
|
|
11680
11928
|
const appliedByLabel = email;
|
|
11681
11929
|
const commitUrl = args.commitUrl || null;
|
|
11682
11930
|
const tool = args.tool || ctx.configAiTool;
|
|
11683
|
-
const
|
|
11931
|
+
const models = parseModelsArgs(args);
|
|
11684
11932
|
await api(ctx.server, `/api/comments/${id}`, {
|
|
11685
11933
|
method: "PATCH",
|
|
11686
11934
|
body: {
|
|
@@ -11689,7 +11937,7 @@ async function handleMarkApplied(args, ctx) {
|
|
|
11689
11937
|
appliedByLabel,
|
|
11690
11938
|
commitUrl,
|
|
11691
11939
|
aiTool: tool || void 0,
|
|
11692
|
-
|
|
11940
|
+
...modelsBody(models)
|
|
11693
11941
|
},
|
|
11694
11942
|
token: ctx.token
|
|
11695
11943
|
});
|
|
@@ -11709,7 +11957,7 @@ async function handleCommitAndMark(args, ctx) {
|
|
|
11709
11957
|
const reply = args?.reply;
|
|
11710
11958
|
const files = args?.files;
|
|
11711
11959
|
const tool = args?.tool || ctx.configAiTool;
|
|
11712
|
-
const
|
|
11960
|
+
const models = parseModelsArgs(args);
|
|
11713
11961
|
if (!Array.isArray(ids) || ids.length === 0 || typeof reply !== "string") {
|
|
11714
11962
|
throw mcpError("git", "ids (non-empty array) and reply (string) are required");
|
|
11715
11963
|
}
|
|
@@ -11800,7 +12048,7 @@ async function handleCommitAndMark(args, ctx) {
|
|
|
11800
12048
|
// so the same fix that shipped for `pinsay apply` has to hold here.
|
|
11801
12049
|
commitSha: sha,
|
|
11802
12050
|
aiTool: tool || void 0,
|
|
11803
|
-
|
|
12051
|
+
...modelsBody(models)
|
|
11804
12052
|
},
|
|
11805
12053
|
token: ctx.token
|
|
11806
12054
|
});
|
|
@@ -11846,7 +12094,7 @@ async function handleCommitAndMark(args, ctx) {
|
|
|
11846
12094
|
// Same reason as the single-comment path above.
|
|
11847
12095
|
commitSha: sha,
|
|
11848
12096
|
aiTool: tool || void 0,
|
|
11849
|
-
|
|
12097
|
+
...modelsBody(models)
|
|
11850
12098
|
},
|
|
11851
12099
|
token: ctx.token
|
|
11852
12100
|
});
|
|
@@ -11888,7 +12136,7 @@ async function handleCommitAndMark(args, ctx) {
|
|
|
11888
12136
|
// stays "applied" forever even once the fix ships.
|
|
11889
12137
|
commitSha: sha,
|
|
11890
12138
|
aiTool: tool || void 0,
|
|
11891
|
-
|
|
12139
|
+
...modelsBody(models)
|
|
11892
12140
|
},
|
|
11893
12141
|
token: ctx.token
|
|
11894
12142
|
});
|
|
@@ -11909,10 +12157,10 @@ async function handleReply(args, ctx) {
|
|
|
11909
12157
|
throw mcpError("forbidden", "id and body are required");
|
|
11910
12158
|
}
|
|
11911
12159
|
const tool = args?.tool || ctx.configAiTool;
|
|
11912
|
-
const
|
|
12160
|
+
const models = parseModelsArgs(args);
|
|
11913
12161
|
const res = await api(ctx.server, `/api/comments/${id}/replies`, {
|
|
11914
12162
|
method: "POST",
|
|
11915
|
-
body: { body, aiTool: tool || void 0,
|
|
12163
|
+
body: { body, aiTool: tool || void 0, ...modelsBody(models) },
|
|
11916
12164
|
token: ctx.token
|
|
11917
12165
|
});
|
|
11918
12166
|
return { replyId: res?.id ?? res?.data?.id ?? null };
|
|
@@ -11942,7 +12190,7 @@ async function handleResolveSource(args, ctx) {
|
|
|
11942
12190
|
if (!hash || typeof hash !== "string") {
|
|
11943
12191
|
throw mcpError("forbidden", "hash is required");
|
|
11944
12192
|
}
|
|
11945
|
-
const manifestPath =
|
|
12193
|
+
const manifestPath = join21(ctx.cwd, ".pinsay/manifest.json");
|
|
11946
12194
|
if (!existsSync4(manifestPath)) {
|
|
11947
12195
|
return { path: null, reason: "no-manifest" };
|
|
11948
12196
|
}
|
|
@@ -12122,12 +12370,12 @@ function createMcpServer(ctx) {
|
|
|
12122
12370
|
let configObj = {};
|
|
12123
12371
|
let stackObj = {};
|
|
12124
12372
|
try {
|
|
12125
|
-
const configPath =
|
|
12373
|
+
const configPath = join22(ctx.cwd, ".pinsay/config.json");
|
|
12126
12374
|
configObj = JSON.parse(readFileSync3(configPath, "utf8"));
|
|
12127
12375
|
} catch {
|
|
12128
12376
|
}
|
|
12129
12377
|
try {
|
|
12130
|
-
const stackPath =
|
|
12378
|
+
const stackPath = join22(ctx.cwd, ".pinsay/stack.json");
|
|
12131
12379
|
stackObj = JSON.parse(readFileSync3(stackPath, "utf8"));
|
|
12132
12380
|
} catch {
|
|
12133
12381
|
}
|
|
@@ -12192,8 +12440,8 @@ function createMcpServer(ctx) {
|
|
|
12192
12440
|
async function runMcpServer(ctx, logFile) {
|
|
12193
12441
|
if (logFile) {
|
|
12194
12442
|
try {
|
|
12195
|
-
const
|
|
12196
|
-
const logStream =
|
|
12443
|
+
const fs21 = await import("node:fs");
|
|
12444
|
+
const logStream = fs21.createWriteStream(logFile, { flags: "a" });
|
|
12197
12445
|
const origWrite = process.stderr.write;
|
|
12198
12446
|
process.stderr.write = function(chunk, encoding, cb) {
|
|
12199
12447
|
logStream.write(chunk);
|
|
@@ -12616,6 +12864,11 @@ Options:
|
|
|
12616
12864
|
--reply <text> Reply text for applied comment (required with --mark)
|
|
12617
12865
|
--no-commit Skip git commit during --mark (PATCH only)
|
|
12618
12866
|
--dry-run Print what --mark would do without making git/API changes
|
|
12867
|
+
--models <list> Every model that worked on the item, with its role, for --mark/--fail:
|
|
12868
|
+
"<id>=<role>,<id>=<role>" (role: planner|implementer|reviewer, optional;
|
|
12869
|
+
e.g. "claude-opus-5-5=planner,claude-sonnet-5-5=implementer,claude-opus-5-5=reviewer").
|
|
12870
|
+
Env fallback: PINSAY_AI_MODELS
|
|
12871
|
+
--model <id> One model id (role unset); merged after --models. Env: PINSAY_AI_MODEL
|
|
12619
12872
|
--fail <id> Mark apply failed with a reply
|
|
12620
12873
|
--reason <text> Failure reason (required with --fail)
|
|
12621
12874
|
--status <status> Filter queue by status (open, ready, applied, archived)
|
package/package.json
CHANGED