pinsay-cli 0.7.0 → 0.7.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.
Files changed (2) hide show
  1. package/dist/cli.js +129 -59
  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.7.0" : "0.0.0-dev";
18
+ BUILD_CLI_VERSION = true ? "0.7.2" : "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 fs17 } from "node:fs";
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 fs17.readFile(cacheFile, "utf8"));
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 fs17.mkdir(dirname8(cacheFile), { recursive: true });
751
- await fs17.writeFile(cacheFile, JSON.stringify({ token: login.token }), "utf8");
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 join17 } from "node:path";
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 = join17(cwd2, ".pinsay", "manifest.json");
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 = join17(cwd2, ".pinsay", "manifest.prev.json");
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 fs19 } from "node:fs";
4751
- import { join as join19, dirname as dirname9 } from "node:path";
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 fs18 } from "node:fs";
4910
- import { join as join18 } from "node:path";
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 fs18.readFile(join18(ctx.cwd, relPath), "utf8");
4979
+ const stackRaw = await fs19.readFile(join19(ctx.cwd, relPath), "utf8");
4922
4980
  stack = JSON.parse(stackRaw);
4923
4981
  } catch {
4924
4982
  }
@@ -5241,24 +5299,29 @@ function buildApplyPrompt(items, context, opts) {
5241
5299
  '(Separate style: after each item; Single style: run `npx pinsay-cli apply --mark all --reply "..." --models "..." --tool <your-tool-name>`'
5242
5300
  );
5243
5301
  lines.push(
5244
- "once at the end). Never run git push. `--models` MUST list EVERY model that touched the item and its role:"
5302
+ "once at the end). Never run git push. `--models` is MANDATORY and MUST list EVERY model that touched the item and its role:"
5245
5303
  );
5246
5304
  lines.push(
5247
- "the planner/reviewer (your own model id) and each worker model you delegated to (`implementer`). Use exact"
5305
+ "the planner/reviewer (your own model id) and each worker model you delegated to (`implementer`). NEVER guess or hardcode"
5306
+ );
5307
+ lines.push(
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
- "model and no delegation, pass just yours as `<your-model-id>=implementer`. `--tool` (e.g. `claude-code`, `opencode`,"
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."
5254
5318
  );
5255
5319
  lines.push(
5256
- "`cursor`, `windsurf`, `antigravity`) records which agent you are running as. ALWAYS pass both explicitly, even on a"
5320
+ "ALWAYS pass both explicitly, even on a project you ran `init` on \u2014 `--tool` silently falls back to whichever tool happened"
5257
5321
  );
5258
5322
  lines.push(
5259
- "project you ran `init` on \u2014 `--tool` silently falls back to whichever tool happened to run `init`, which is wrong"
5323
+ "to run `init`, which is wrong the moment a different tool applies a comment later."
5260
5324
  );
5261
- lines.push("the moment a different tool applies a comment later.");
5262
5325
  return lines.join("\n") + "\n";
5263
5326
  }
5264
5327
 
@@ -5285,10 +5348,10 @@ function detectAiTool(override) {
5285
5348
  async function ensureToolRegistered(ctx, tool) {
5286
5349
  const config = await readConfig(ctx.cwd).catch(() => ({}));
5287
5350
  const relStackPath = isMultiProject(config) ? stackFileRelPath(ctx.project) : stackFileRelPath();
5288
- const stackPath = join19(ctx.cwd, relStackPath);
5351
+ const stackPath = join20(ctx.cwd, relStackPath);
5289
5352
  let stackData = {};
5290
5353
  try {
5291
- const raw = await fs19.readFile(stackPath, "utf8");
5354
+ const raw = await fs20.readFile(stackPath, "utf8");
5292
5355
  stackData = JSON.parse(raw);
5293
5356
  } catch {
5294
5357
  }
@@ -5307,8 +5370,8 @@ async function ensureToolRegistered(ctx, tool) {
5307
5370
  }
5308
5371
  );
5309
5372
  if (res) {
5310
- await fs19.mkdir(dirname9(stackPath), { recursive: true });
5311
- await fs19.writeFile(stackPath, JSON.stringify(res, null, 2) + "\n", "utf8");
5373
+ await fs20.mkdir(dirname9(stackPath), { recursive: true });
5374
+ await fs20.writeFile(stackPath, JSON.stringify(res, null, 2) + "\n", "utf8");
5312
5375
  return;
5313
5376
  }
5314
5377
  } catch {
@@ -5317,8 +5380,8 @@ async function ensureToolRegistered(ctx, tool) {
5317
5380
  aiTools.push(tool);
5318
5381
  stackData.aiTools = aiTools;
5319
5382
  try {
5320
- await fs19.mkdir(dirname9(stackPath), { recursive: true });
5321
- await fs19.writeFile(stackPath, JSON.stringify(stackData, null, 2) + "\n", "utf8");
5383
+ await fs20.mkdir(dirname9(stackPath), { recursive: true });
5384
+ await fs20.writeFile(stackPath, JSON.stringify(stackData, null, 2) + "\n", "utf8");
5322
5385
  } catch {
5323
5386
  }
5324
5387
  }
@@ -5392,9 +5455,9 @@ async function runApply(options, ctx) {
5392
5455
  console.error(`Failed to spawn opencode: ${res.error.message}`);
5393
5456
  }
5394
5457
  } else if (tool === "cursor") {
5395
- const promptFile = join19(ctx.cwd, ".pinsay/apply-prompt.md");
5396
- await fs19.mkdir(join19(ctx.cwd, ".pinsay"), { recursive: true });
5397
- await fs19.writeFile(promptFile, prompt, "utf8");
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");
5398
5461
  console.log(`Saved apply prompt to ${promptFile}`);
5399
5462
  } else if (tool === "clipboard") {
5400
5463
  const copied = copyToClipboard(prompt);
@@ -5753,6 +5816,18 @@ async function exitIfWorkspaceFrozen(server, token) {
5753
5816
  } catch {
5754
5817
  }
5755
5818
  }
5819
+ async function checkMetaAndNotify(server, config, root, parsed) {
5820
+ try {
5821
+ const meta = await api(server, "/api/meta");
5822
+ const minCli = meta?.minCliVersion || "0.0.0";
5823
+ if (compareSemver(BUILD_CLI_VERSION, minCli) < 0) {
5824
+ console.error(tooOldMessage(BUILD_CLI_VERSION, minCli));
5825
+ process.exit(5);
5826
+ }
5827
+ await checkAndNotifyUpdates({ meta, config, cwd: root, silent: Boolean(parsed["json"]) });
5828
+ } catch {
5829
+ }
5830
+ }
5756
5831
  function parseModelFlags(parsed) {
5757
5832
  try {
5758
5833
  return resolveModels({
@@ -5796,17 +5871,7 @@ async function applyCommand(cwd2, parsed, positionals = []) {
5796
5871
  return;
5797
5872
  }
5798
5873
  const project = resolved.ok ? resolved.project.key : "";
5799
- try {
5800
- const meta = await api(server, "/api/meta");
5801
- const minCli = meta?.minCliVersion || "0.0.0";
5802
- if (compareSemver(BUILD_CLI_VERSION, minCli) < 0) {
5803
- console.error(tooOldMessage(BUILD_CLI_VERSION, minCli));
5804
- process.exit(5);
5805
- }
5806
- } catch (err) {
5807
- if (!(err instanceof ApiError && err.code === 404)) {
5808
- }
5809
- }
5874
+ await checkMetaAndNotify(server, config, root, parsed);
5810
5875
  const explicitKey = typeof parsed["key"] === "string" ? parsed["key"] : void 0;
5811
5876
  const token = await resolveToken(server, root, explicitKey);
5812
5877
  const apiKey = explicitKey || await readApiKey(root, server);
@@ -5841,8 +5906,10 @@ async function applyCommand(cwd2, parsed, positionals = []) {
5841
5906
  markId = parsedNum;
5842
5907
  }
5843
5908
  const reply = typeof parsed["reply"] === "string" ? parsed["reply"] : "";
5844
- if (!reply) {
5845
- console.error('--reply "<text>" is required when using --mark');
5909
+ if (!reply || reply.trim().length < 15 || /^(done|fixed|applied|ok|yes)$/i.test(reply.trim())) {
5910
+ console.error(
5911
+ '--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").'
5912
+ );
5846
5913
  process.exit(2);
5847
5914
  }
5848
5915
  const noCommit = parsed["no-commit"] === true;
@@ -5855,6 +5922,19 @@ async function applyCommand(cwd2, parsed, positionals = []) {
5855
5922
  );
5856
5923
  }
5857
5924
  const models = parseModelFlags(parsed);
5925
+ if (models.length === 0) {
5926
+ console.error(
5927
+ '--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.'
5928
+ );
5929
+ process.exit(2);
5930
+ }
5931
+ const missingRole = models.find((m) => !m.role);
5932
+ if (missingRole) {
5933
+ console.error(
5934
+ `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").`
5935
+ );
5936
+ process.exit(2);
5937
+ }
5858
5938
  const markStatus = typeof parsed["status"] === "string" ? parsed["status"] : void 0;
5859
5939
  const markEnv = typeof parsed["env"] === "string" ? parsed["env"] : void 0;
5860
5940
  const marked = await markApplied(
@@ -5930,17 +6010,7 @@ async function applyAllProjects(root, cwd2, config, server, parsed) {
5930
6010
  console.error("--tool needs a single project \u2014 pass --project <key> (several are configured).");
5931
6011
  process.exit(2);
5932
6012
  }
5933
- try {
5934
- const meta = await api(server, "/api/meta");
5935
- const minCli = meta?.minCliVersion || "0.0.0";
5936
- if (compareSemver(BUILD_CLI_VERSION, minCli) < 0) {
5937
- console.error(tooOldMessage(BUILD_CLI_VERSION, minCli));
5938
- process.exit(5);
5939
- }
5940
- } catch (err) {
5941
- if (!(err instanceof ApiError && err.code === 404)) {
5942
- }
5943
- }
6013
+ await checkMetaAndNotify(server, config, root, parsed);
5944
6014
  const explicitKey = typeof parsed["key"] === "string" ? parsed["key"] : void 0;
5945
6015
  const token = await resolveToken(server, root, explicitKey);
5946
6016
  const apiKey = explicitKey || await readApiKey(root, server);
@@ -5988,7 +6058,7 @@ init_build_constants();
5988
6058
 
5989
6059
  // src/mcp/server.ts
5990
6060
  import { readFileSync as readFileSync3 } from "node:fs";
5991
- import { join as join21 } from "node:path";
6061
+ import { join as join22 } from "node:path";
5992
6062
 
5993
6063
  // node_modules/zod/v3/external.js
5994
6064
  var external_exports = {};
@@ -11560,7 +11630,7 @@ var ALL_TOOLS = [
11560
11630
  // src/mcp/tools.ts
11561
11631
  init_api();
11562
11632
  import { existsSync as existsSync4, readFileSync as readFileSync2 } from "node:fs";
11563
- import { isAbsolute as isAbsolute3, join as join20, relative as relative5, resolve as resolve4 } from "node:path";
11633
+ import { isAbsolute as isAbsolute3, join as join21, relative as relative5, resolve as resolve4 } from "node:path";
11564
11634
  import { spawnSync as spawnSync3 } from "node:child_process";
11565
11635
  init_build_constants();
11566
11636
  init_projection();
@@ -12100,7 +12170,7 @@ async function handleResolveSource(args, ctx) {
12100
12170
  if (!hash || typeof hash !== "string") {
12101
12171
  throw mcpError("forbidden", "hash is required");
12102
12172
  }
12103
- const manifestPath = join20(ctx.cwd, ".pinsay/manifest.json");
12173
+ const manifestPath = join21(ctx.cwd, ".pinsay/manifest.json");
12104
12174
  if (!existsSync4(manifestPath)) {
12105
12175
  return { path: null, reason: "no-manifest" };
12106
12176
  }
@@ -12280,12 +12350,12 @@ function createMcpServer(ctx) {
12280
12350
  let configObj = {};
12281
12351
  let stackObj = {};
12282
12352
  try {
12283
- const configPath = join21(ctx.cwd, ".pinsay/config.json");
12353
+ const configPath = join22(ctx.cwd, ".pinsay/config.json");
12284
12354
  configObj = JSON.parse(readFileSync3(configPath, "utf8"));
12285
12355
  } catch {
12286
12356
  }
12287
12357
  try {
12288
- const stackPath = join21(ctx.cwd, ".pinsay/stack.json");
12358
+ const stackPath = join22(ctx.cwd, ".pinsay/stack.json");
12289
12359
  stackObj = JSON.parse(readFileSync3(stackPath, "utf8"));
12290
12360
  } catch {
12291
12361
  }
@@ -12350,8 +12420,8 @@ function createMcpServer(ctx) {
12350
12420
  async function runMcpServer(ctx, logFile) {
12351
12421
  if (logFile) {
12352
12422
  try {
12353
- const fs20 = await import("node:fs");
12354
- const logStream = fs20.createWriteStream(logFile, { flags: "a" });
12423
+ const fs21 = await import("node:fs");
12424
+ const logStream = fs21.createWriteStream(logFile, { flags: "a" });
12355
12425
  const origWrite = process.stderr.write;
12356
12426
  process.stderr.write = function(chunk, encoding, cb) {
12357
12427
  logStream.write(chunk);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinsay-cli",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
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",