pinsay-cli 0.6.10 → 0.7.0

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.
Files changed (2) hide show
  1. package/dist/cli.js +194 -31
  2. 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.6.10" : "0.0.0-dev";
18
+ BUILD_CLI_VERSION = true ? "0.7.0" : "0.0.0-dev";
19
19
  }
20
20
  });
21
21
 
@@ -5235,23 +5235,30 @@ function buildApplyPrompt(items, context, opts) {
5235
5235
  }
5236
5236
  lines.push("## When you finish an item");
5237
5237
  lines.push(
5238
- 'Run exactly: `npx pinsay-cli apply --mark <id> --reply "<what changed>" --model <your-model-id> --tool <your-tool-name>`'
5238
+ '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>`'
5239
5239
  );
5240
5240
  lines.push(
5241
- '(Separate style: after each item; Single style: run `npx pinsay-cli apply --mark all --reply "..." --model <your-model-id> --tool <your-tool-name>`'
5241
+ '(Separate style: after each item; Single style: run `npx pinsay-cli apply --mark all --reply "..." --models "..." --tool <your-tool-name>`'
5242
5242
  );
5243
5243
  lines.push(
5244
- "once at the end). Never run git push. `--model` (e.g. `claude-sonnet-5`, `gpt-5.2`) and `--tool` (e.g."
5244
+ "once at the end). Never run git push. `--models` MUST list EVERY model that touched the item and its role:"
5245
5245
  );
5246
5246
  lines.push(
5247
- "`claude-code`, `opencode`, `cursor`, `windsurf`, `antigravity`) record which model and agent you are"
5247
+ "the planner/reviewer (your own model id) and each worker model you delegated to (`implementer`). Use exact"
5248
5248
  );
5249
5249
  lines.push(
5250
- "running as. ALWAYS pass both explicitly, even on a project you ran `init` on \u2014 `--tool` silently falls back"
5250
+ 'model ids (e.g. `claude-opus-5-5`, `claude-sonnet-5-5`, `gpt-5.2`), never aliases like "haiku" or "sonnet". With one'
5251
5251
  );
5252
5252
  lines.push(
5253
- "to whichever tool happened to run `init`, which is wrong the moment a different tool applies a comment later."
5253
+ "model and no delegation, pass just yours as `<your-model-id>=implementer`. `--tool` (e.g. `claude-code`, `opencode`,"
5254
5254
  );
5255
+ lines.push(
5256
+ "`cursor`, `windsurf`, `antigravity`) records which agent you are running as. ALWAYS pass both explicitly, even on a"
5257
+ );
5258
+ lines.push(
5259
+ "project you ran `init` on \u2014 `--tool` silently falls back to whichever tool happened to run `init`, which is wrong"
5260
+ );
5261
+ lines.push("the moment a different tool applies a comment later.");
5255
5262
  return lines.join("\n") + "\n";
5256
5263
  }
5257
5264
 
@@ -5475,6 +5482,106 @@ function commitUrlFor(sha, remoteUrl) {
5475
5482
  return null;
5476
5483
  }
5477
5484
 
5485
+ // src/apply/models.ts
5486
+ var MODEL_ROLES = ["planner", "implementer", "reviewer"];
5487
+ var MAX_AI_MODELS = 8;
5488
+ var MODEL_RE = /^[A-Za-z0-9][A-Za-z0-9._:/+-]*$/;
5489
+ var ModelsError = class extends Error {
5490
+ };
5491
+ function normalizeEntry(raw) {
5492
+ const model = typeof raw.model === "string" ? raw.model.trim() : "";
5493
+ if (!model || model.length > 64 || !MODEL_RE.test(model)) {
5494
+ throw new ModelsError(
5495
+ `Invalid model id "${String(raw.model ?? "")}": use the exact model id (1-64 chars, letters/digits and . _ : / + -), e.g. "claude-opus-5-5".`
5496
+ );
5497
+ }
5498
+ let role = null;
5499
+ if (raw.role !== void 0 && raw.role !== null && raw.role !== "") {
5500
+ const r = String(raw.role).trim().toLowerCase();
5501
+ if (!MODEL_ROLES.includes(r)) {
5502
+ throw new ModelsError(
5503
+ `Invalid role "${String(raw.role)}" for model "${model}": expected one of ${MODEL_ROLES.join(", ")}.`
5504
+ );
5505
+ }
5506
+ role = r;
5507
+ }
5508
+ return { model, role };
5509
+ }
5510
+ function parseModelsFlag(value) {
5511
+ const out = [];
5512
+ for (const part of value.split(",")) {
5513
+ const p = part.trim();
5514
+ if (!p)
5515
+ continue;
5516
+ const eq = p.indexOf("=");
5517
+ out.push(
5518
+ eq === -1 ? normalizeEntry({ model: p }) : normalizeEntry({ model: p.slice(0, eq), role: p.slice(eq + 1) })
5519
+ );
5520
+ }
5521
+ return out;
5522
+ }
5523
+ function dedupeModels(entries) {
5524
+ const seen = /* @__PURE__ */ new Set();
5525
+ const out = [];
5526
+ for (const e of entries) {
5527
+ const key = `${e.model.toLowerCase()}|${e.role ?? ""}`;
5528
+ if (seen.has(key))
5529
+ continue;
5530
+ seen.add(key);
5531
+ out.push(e);
5532
+ }
5533
+ if (out.length > MAX_AI_MODELS) {
5534
+ throw new ModelsError(`Too many models (${out.length}); at most ${MAX_AI_MODELS} entries are allowed.`);
5535
+ }
5536
+ return out;
5537
+ }
5538
+ function mergeModels(...sources) {
5539
+ return dedupeModels(sources.flatMap((s) => s ?? []));
5540
+ }
5541
+ function primaryModel(entries) {
5542
+ return (entries.find((e) => e.role === "implementer") ?? entries[0])?.model;
5543
+ }
5544
+ function resolveModels(flags, env = process.env) {
5545
+ const explicit = !!(flags.models || flags.model);
5546
+ const modelsStr = explicit ? flags.models : env["PINSAY_AI_MODELS"];
5547
+ const modelStr = explicit ? flags.model : env["PINSAY_AI_MODEL"];
5548
+ return mergeModels(
5549
+ modelsStr ? parseModelsFlag(modelsStr) : void 0,
5550
+ modelStr ? [normalizeEntry({ model: modelStr })] : void 0
5551
+ );
5552
+ }
5553
+ function resolveModelsFromArgs(args, env = process.env) {
5554
+ const hasArray = args?.models !== void 0 && args.models !== null;
5555
+ const single = typeof args?.model === "string" && args.model ? args.model : void 0;
5556
+ if (!hasArray && !single) {
5557
+ return resolveModels({}, env);
5558
+ }
5559
+ let fromArray;
5560
+ if (hasArray) {
5561
+ if (!Array.isArray(args.models))
5562
+ throw new ModelsError("models must be an array of {model, role?}.");
5563
+ fromArray = args.models.map(
5564
+ (m) => typeof m === "string" ? normalizeEntry({ model: m }) : normalizeEntry(m ?? {})
5565
+ );
5566
+ }
5567
+ return mergeModels(fromArray, single ? [normalizeEntry({ model: single })] : void 0);
5568
+ }
5569
+ function modelsBody(entries) {
5570
+ if (entries.length === 0)
5571
+ return {};
5572
+ return { aiModels: entries, aiModel: primaryModel(entries) };
5573
+ }
5574
+ function distinctModelCount(entries) {
5575
+ return new Set(entries.map((e) => e.model.toLowerCase())).size;
5576
+ }
5577
+ 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".';
5578
+ function delegationWarning(delegation, entries) {
5579
+ return delegation === "auto" && distinctModelCount(entries) < 2 ? DELEGATION_WARNING : null;
5580
+ }
5581
+ function describeModels(entries) {
5582
+ return entries.length === 0 ? "(none)" : entries.map((e) => `${e.model}${e.role ? `=${e.role}` : ""}`).join(", ");
5583
+ }
5584
+
5478
5585
  // src/apply/mark.ts
5479
5586
  async function markApplied(options, ctx) {
5480
5587
  if (!options.noCommit && !isStaged(ctx.cwd)) {
@@ -5488,10 +5595,19 @@ async function markApplied(options, ctx) {
5488
5595
  const projectCtx = await loadProjectContext(ctx);
5489
5596
  const email = getUserEmail(ctx.cwd);
5490
5597
  const appliedByLabel = options.tool ? `${email} via ${options.tool}` : email;
5598
+ const models = mergeModels(
5599
+ options.models,
5600
+ options.model ? [normalizeEntry({ model: options.model })] : void 0
5601
+ );
5602
+ const aiModelFields = modelsBody(models);
5603
+ const warning = delegationWarning(projectCtx.delegation ?? "auto", models);
5604
+ if (warning)
5605
+ console.error(warning);
5491
5606
  if (options.dryRun) {
5492
5607
  console.log(`[dry-run] Would mark comment ${options.id} as applied`);
5493
5608
  console.log(`[dry-run] Reply: ${options.reply}`);
5494
5609
  console.log(`[dry-run] AppliedBy: ${appliedByLabel}`);
5610
+ console.log(`[dry-run] Models: ${describeModels(models)}`);
5495
5611
  return {
5496
5612
  committed: false,
5497
5613
  patchedIds: typeof options.id === "number" ? [options.id] : []
@@ -5535,7 +5651,7 @@ async function markApplied(options, ctx) {
5535
5651
  // Structured AI attribution on the reply itself (Reply.AiTool/AiModel), distinct from the
5536
5652
  // free-text appliedByLabel above — see CommentService.Normalize server-side.
5537
5653
  aiTool: options.tool || void 0,
5538
- aiModel: options.model || void 0
5654
+ ...aiModelFields
5539
5655
  },
5540
5656
  token: ctx.token
5541
5657
  });
@@ -5579,7 +5695,7 @@ async function markApplied(options, ctx) {
5579
5695
  // against a deployed build.
5580
5696
  commitSha: sha || null,
5581
5697
  aiTool: options.tool || void 0,
5582
- aiModel: options.model || void 0
5698
+ ...aiModelFields
5583
5699
  },
5584
5700
  token: ctx.token
5585
5701
  });
@@ -5599,9 +5715,10 @@ async function markApplied(options, ctx) {
5599
5715
  }
5600
5716
  async function markFailed(id, reason, ctx, tool, model) {
5601
5717
  const replyBody = `Could not apply: ${reason}`;
5718
+ const models = typeof model === "string" ? model ? [normalizeEntry({ model })] : [] : model ?? [];
5602
5719
  await api(ctx.server, `/api/comments/${id}/replies`, {
5603
5720
  method: "POST",
5604
- body: { body: replyBody, aiTool: tool || void 0, aiModel: model || void 0 },
5721
+ body: { body: replyBody, aiTool: tool || void 0, ...modelsBody(models) },
5605
5722
  token: ctx.token
5606
5723
  });
5607
5724
  await postEvent(ctx.server, ctx.token, {
@@ -5636,6 +5753,20 @@ async function exitIfWorkspaceFrozen(server, token) {
5636
5753
  } catch {
5637
5754
  }
5638
5755
  }
5756
+ function parseModelFlags(parsed) {
5757
+ try {
5758
+ return resolveModels({
5759
+ models: typeof parsed["models"] === "string" ? parsed["models"] : void 0,
5760
+ model: typeof parsed["model"] === "string" ? parsed["model"] : void 0
5761
+ });
5762
+ } catch (err) {
5763
+ if (err instanceof ModelsError) {
5764
+ console.error(err.message);
5765
+ process.exit(2);
5766
+ }
5767
+ throw err;
5768
+ }
5769
+ }
5639
5770
  async function applyCommand(cwd2, parsed, positionals = []) {
5640
5771
  const root = await findRepoRoot(cwd2);
5641
5772
  const config = await readConfig(root);
@@ -5723,7 +5854,7 @@ async function applyCommand(cwd2, parsed, positionals = []) {
5723
5854
  `\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
5855
  );
5725
5856
  }
5726
- const model = (typeof parsed["model"] === "string" ? parsed["model"] : void 0) || process.env.PINSAY_AI_MODEL;
5857
+ const models = parseModelFlags(parsed);
5727
5858
  const markStatus = typeof parsed["status"] === "string" ? parsed["status"] : void 0;
5728
5859
  const markEnv = typeof parsed["env"] === "string" ? parsed["env"] : void 0;
5729
5860
  const marked = await markApplied(
@@ -5733,7 +5864,7 @@ async function applyCommand(cwd2, parsed, positionals = []) {
5733
5864
  noCommit,
5734
5865
  dryRun,
5735
5866
  tool: tool2,
5736
- model,
5867
+ models,
5737
5868
  filter: markStatus !== void 0 || markEnv !== void 0 ? { status: markStatus, environment: markEnv } : void 0
5738
5869
  },
5739
5870
  clientCtx
@@ -5763,8 +5894,8 @@ async function applyCommand(cwd2, parsed, positionals = []) {
5763
5894
  `\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
5895
  );
5765
5896
  }
5766
- const failModel = (typeof parsed["model"] === "string" ? parsed["model"] : void 0) || process.env.PINSAY_AI_MODEL;
5767
- await markFailed(failId, reason, clientCtx, failTool, failModel);
5897
+ const failModels = parseModelFlags(parsed);
5898
+ await markFailed(failId, reason, clientCtx, failTool, failModels);
5768
5899
  process.exit(0);
5769
5900
  }
5770
5901
  if (parsed["json"] === true && !parsed["plan"] && !parsed["tool"]) {
@@ -11242,9 +11373,24 @@ var TOOL_PINSAY_GET_COMMENT = {
11242
11373
  additionalProperties: false
11243
11374
  }
11244
11375
  };
11376
+ 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.";
11377
+ var MODELS_PROPERTY = {
11378
+ type: "array",
11379
+ maxItems: 8,
11380
+ 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.',
11381
+ items: {
11382
+ type: "object",
11383
+ properties: {
11384
+ model: { type: "string", description: 'Exact model id, e.g. "claude-opus-5-5"' },
11385
+ role: { type: "string", enum: ["planner", "implementer", "reviewer"] }
11386
+ },
11387
+ required: ["model"],
11388
+ additionalProperties: false
11389
+ }
11390
+ };
11245
11391
  var TOOL_PINSAY_MARK_APPLIED = {
11246
11392
  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}`,
11393
+ 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
11394
  inputSchema: {
11249
11395
  type: "object",
11250
11396
  properties: {
@@ -11266,8 +11412,9 @@ var TOOL_PINSAY_MARK_APPLIED = {
11266
11412
  },
11267
11413
  model: {
11268
11414
  type: "string",
11269
- description: 'The model id you are running as (e.g. "claude-sonnet-5", "gpt-5.2"). Always pass this too.'
11270
- }
11415
+ description: 'Legacy single model id (role unset); prefer `models`. Exact id, not an alias (e.g. "claude-sonnet-5-5", "gpt-5.2").'
11416
+ },
11417
+ models: MODELS_PROPERTY
11271
11418
  },
11272
11419
  required: ["id", "reply"],
11273
11420
  additionalProperties: false
@@ -11275,7 +11422,7 @@ var TOOL_PINSAY_MARK_APPLIED = {
11275
11422
  };
11276
11423
  var TOOL_PINSAY_COMMIT_AND_MARK = {
11277
11424
  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}`,
11425
+ 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
11426
  inputSchema: {
11280
11427
  type: "object",
11281
11428
  properties: {
@@ -11303,8 +11450,9 @@ var TOOL_PINSAY_COMMIT_AND_MARK = {
11303
11450
  },
11304
11451
  model: {
11305
11452
  type: "string",
11306
- description: 'The model id you are running as (e.g. "claude-sonnet-5", "gpt-5.2"). Always pass this too.'
11307
- }
11453
+ description: 'Legacy single model id (role unset); prefer `models`. Exact id, not an alias (e.g. "claude-sonnet-5-5", "gpt-5.2").'
11454
+ },
11455
+ models: MODELS_PROPERTY
11308
11456
  },
11309
11457
  required: ["ids", "reply"],
11310
11458
  additionalProperties: false
@@ -11312,7 +11460,7 @@ var TOOL_PINSAY_COMMIT_AND_MARK = {
11312
11460
  };
11313
11461
  var TOOL_PINSAY_REPLY = {
11314
11462
  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}`,
11463
+ 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
11464
  inputSchema: {
11317
11465
  type: "object",
11318
11466
  properties: {
@@ -11330,8 +11478,9 @@ var TOOL_PINSAY_REPLY = {
11330
11478
  },
11331
11479
  model: {
11332
11480
  type: "string",
11333
- description: 'The model id you are running as (e.g. "claude-sonnet-5", "gpt-5.2"). Always pass this too.'
11334
- }
11481
+ description: 'Legacy single model id (role unset); prefer `models`. Exact id, not an alias (e.g. "claude-sonnet-5-5", "gpt-5.2").'
11482
+ },
11483
+ models: MODELS_PROPERTY
11335
11484
  },
11336
11485
  required: ["id", "body"],
11337
11486
  additionalProperties: false
@@ -11670,6 +11819,15 @@ async function handleGetComment(args, ctx) {
11670
11819
  throw err;
11671
11820
  }
11672
11821
  }
11822
+ function parseModelsArgs(args) {
11823
+ try {
11824
+ return resolveModelsFromArgs(args);
11825
+ } catch (err) {
11826
+ if (err instanceof ModelsError)
11827
+ throw mcpError("forbidden", err.message);
11828
+ throw err;
11829
+ }
11830
+ }
11673
11831
  async function handleMarkApplied(args, ctx) {
11674
11832
  const id = Number(args?.id);
11675
11833
  const reply = args?.reply;
@@ -11680,7 +11838,7 @@ async function handleMarkApplied(args, ctx) {
11680
11838
  const appliedByLabel = email;
11681
11839
  const commitUrl = args.commitUrl || null;
11682
11840
  const tool = args.tool || ctx.configAiTool;
11683
- const model = args.model || process.env.PINSAY_AI_MODEL;
11841
+ const models = parseModelsArgs(args);
11684
11842
  await api(ctx.server, `/api/comments/${id}`, {
11685
11843
  method: "PATCH",
11686
11844
  body: {
@@ -11689,7 +11847,7 @@ async function handleMarkApplied(args, ctx) {
11689
11847
  appliedByLabel,
11690
11848
  commitUrl,
11691
11849
  aiTool: tool || void 0,
11692
- aiModel: model || void 0
11850
+ ...modelsBody(models)
11693
11851
  },
11694
11852
  token: ctx.token
11695
11853
  });
@@ -11709,7 +11867,7 @@ async function handleCommitAndMark(args, ctx) {
11709
11867
  const reply = args?.reply;
11710
11868
  const files = args?.files;
11711
11869
  const tool = args?.tool || ctx.configAiTool;
11712
- const model = args?.model || process.env.PINSAY_AI_MODEL;
11870
+ const models = parseModelsArgs(args);
11713
11871
  if (!Array.isArray(ids) || ids.length === 0 || typeof reply !== "string") {
11714
11872
  throw mcpError("git", "ids (non-empty array) and reply (string) are required");
11715
11873
  }
@@ -11800,7 +11958,7 @@ async function handleCommitAndMark(args, ctx) {
11800
11958
  // so the same fix that shipped for `pinsay apply` has to hold here.
11801
11959
  commitSha: sha,
11802
11960
  aiTool: tool || void 0,
11803
- aiModel: model || void 0
11961
+ ...modelsBody(models)
11804
11962
  },
11805
11963
  token: ctx.token
11806
11964
  });
@@ -11846,7 +12004,7 @@ async function handleCommitAndMark(args, ctx) {
11846
12004
  // Same reason as the single-comment path above.
11847
12005
  commitSha: sha,
11848
12006
  aiTool: tool || void 0,
11849
- aiModel: model || void 0
12007
+ ...modelsBody(models)
11850
12008
  },
11851
12009
  token: ctx.token
11852
12010
  });
@@ -11888,7 +12046,7 @@ async function handleCommitAndMark(args, ctx) {
11888
12046
  // stays "applied" forever even once the fix ships.
11889
12047
  commitSha: sha,
11890
12048
  aiTool: tool || void 0,
11891
- aiModel: model || void 0
12049
+ ...modelsBody(models)
11892
12050
  },
11893
12051
  token: ctx.token
11894
12052
  });
@@ -11909,10 +12067,10 @@ async function handleReply(args, ctx) {
11909
12067
  throw mcpError("forbidden", "id and body are required");
11910
12068
  }
11911
12069
  const tool = args?.tool || ctx.configAiTool;
11912
- const model = args?.model || process.env.PINSAY_AI_MODEL;
12070
+ const models = parseModelsArgs(args);
11913
12071
  const res = await api(ctx.server, `/api/comments/${id}/replies`, {
11914
12072
  method: "POST",
11915
- body: { body, aiTool: tool || void 0, aiModel: model || void 0 },
12073
+ body: { body, aiTool: tool || void 0, ...modelsBody(models) },
11916
12074
  token: ctx.token
11917
12075
  });
11918
12076
  return { replyId: res?.id ?? res?.data?.id ?? null };
@@ -12616,6 +12774,11 @@ Options:
12616
12774
  --reply <text> Reply text for applied comment (required with --mark)
12617
12775
  --no-commit Skip git commit during --mark (PATCH only)
12618
12776
  --dry-run Print what --mark would do without making git/API changes
12777
+ --models <list> Every model that worked on the item, with its role, for --mark/--fail:
12778
+ "<id>=<role>,<id>=<role>" (role: planner|implementer|reviewer, optional;
12779
+ e.g. "claude-opus-5-5=planner,claude-sonnet-5-5=implementer,claude-opus-5-5=reviewer").
12780
+ Env fallback: PINSAY_AI_MODELS
12781
+ --model <id> One model id (role unset); merged after --models. Env: PINSAY_AI_MODEL
12619
12782
  --fail <id> Mark apply failed with a reply
12620
12783
  --reason <text> Failure reason (required with --fail)
12621
12784
  --status <status> Filter queue by status (open, ready, applied, archived)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinsay-cli",
3
- "version": "0.6.10",
3
+ "version": "0.7.0",
4
4
  "description": "Click-to-comment feedback for your web app: install the widget, then turn pending comments into AI apply prompts.",
5
5
  "type": "module",
6
6
  "license": "MIT",