pi-soly 2.5.3 → 2.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-soly",
3
- "version": "2.5.3",
3
+ "version": "2.5.5",
4
4
  "description": "Workflow + project management for pi-coding-agent. Plans, state, MANDATORY rules, self-review, multi-question picker. One npm install, zero config. LLM drives the workflow inline via the soly_workflow tool — no external subagent plugin.",
5
5
  "type": "module",
6
6
  "main": "index.ts",
package/quota/minimax.ts CHANGED
@@ -26,8 +26,21 @@
26
26
  // =============================================================================
27
27
 
28
28
  import { execFile } from "node:child_process";
29
+ import * as fs from "node:fs";
30
+ import * as os from "node:os";
31
+ import * as path from "node:path";
29
32
  import type { QuotaProvider, QuotaSnapshot } from "./types.ts";
30
33
 
34
+ /** TEMP DEBUG. */
35
+ const DEBUG_LOG = path.join(os.tmpdir(), "pi-soly-quota-debug.log");
36
+ function dbg(msg: string): void {
37
+ try { fs.appendFileSync(DEBUG_LOG, `[${new Date().toISOString()}] [minimax] ${msg}\n`); } catch { /* best effort */ }
38
+ }
39
+
40
+ /** Detect Windows (where npm-installed CLIs are `.cmd` shims that execFile
41
+ * can't spawn directly without a shell). */
42
+ const IS_WIN = process.platform === "win32";
43
+
31
44
  /** Minimal shape we read from `mmx quota show --output json`. */
32
45
  type MinimaxQuotaResponse = {
33
46
  model_remains?: Array<{
@@ -39,12 +52,28 @@ type MinimaxQuotaResponse = {
39
52
  };
40
53
 
41
54
  /** Run mmx and capture stdout. Resolves to null on any failure
42
- * (mmx missing, non-zero exit, timeout, bad JSON). Never throws. */
55
+ * (mmx missing, non-zero exit, timeout, bad JSON). Never throws.
56
+ *
57
+ * On Windows, npm-global CLIs are `.cmd` shims (e.g. `mmx.cmd`). Node's
58
+ * `execFile` spawns the executable directly without a shell, so it can't
59
+ * resolve `mmx` → `mmx.cmd` and fails with ENOENT. `shell: true` lets the
60
+ * shell do that resolution. Safe here: args are fixed (no user input). */
43
61
  function runMmx(args: string[], timeoutMs: number): Promise<string | null> {
44
62
  return new Promise((resolve) => {
45
- execFile("mmx", args, { encoding: "utf-8", timeout: timeoutMs, maxBuffer: 1024 * 1024 }, (err, stdout) => {
46
- if (err) resolve(null);
47
- else resolve(stdout ?? "");
63
+ const opts = {
64
+ encoding: "utf-8" as const,
65
+ timeout: timeoutMs,
66
+ maxBuffer: 1024 * 1024,
67
+ shell: IS_WIN, // resolve .cmd shims on Windows
68
+ };
69
+ execFile("mmx", args, opts, (err, stdout, stderr) => {
70
+ if (err) {
71
+ dbg(`runMmx error: ${err instanceof Error ? err.message : String(err)} (code=${(err as { code?: string }).code ?? "?"}) stderr=${(stderr ?? "").slice(0, 200)}`);
72
+ resolve(null);
73
+ } else {
74
+ dbg(`runMmx ok: ${String(stdout).length} bytes`);
75
+ resolve(stdout ?? "");
76
+ }
48
77
  });
49
78
  });
50
79
  }
@@ -54,16 +83,26 @@ function parseGeneralQuota(raw: string): QuotaSnapshot | null {
54
83
  let parsed: unknown;
55
84
  try {
56
85
  parsed = JSON.parse(raw);
57
- } catch {
86
+ } catch (e) {
87
+ dbg(`parse: JSON.parse failed: ${e instanceof Error ? e.message : String(e)}; raw[0..100]=${raw.slice(0, 100)}`);
58
88
  return null; // not valid JSON — mmx printed an error to stdout
59
89
  }
60
90
  // Cast at the boundary (single documented place, per code-style rules).
61
91
  const data = parsed as MinimaxQuotaResponse;
62
- if (data.base_resp?.status_code !== 0) return null;
92
+ if (data.base_resp?.status_code !== 0) {
93
+ dbg(`parse: status_code=${data.base_resp?.status_code} (expected 0)`);
94
+ return null;
95
+ }
63
96
  const general = data.model_remains?.find((m) => m.model_name === "general");
64
- if (!general) return null;
97
+ if (!general) {
98
+ dbg(`parse: no 'general' model entry; have: ${(data.model_remains ?? []).map((m) => m.model_name).join(",")}`);
99
+ return null;
100
+ }
65
101
  const pct = general.current_interval_remaining_percent;
66
- if (typeof pct !== "number") return null;
102
+ if (typeof pct !== "number") {
103
+ dbg(`parse: pct is ${typeof pct}`);
104
+ return null;
105
+ }
67
106
  return {
68
107
  remainingPercent: pct,
69
108
  resetsInMs: typeof general.remains_time === "number" ? general.remains_time : null,
package/visual/footer.ts CHANGED
@@ -75,6 +75,21 @@ export function buildFooterLine(data: ChromeData, fd: FooterData, width: number,
75
75
  const quotaText = ascii ? `${data.quotaPercent}%` : `⬢ ${data.quotaPercent}%`;
76
76
  const label = data.quotaResetsLabel ? `${quotaText} · ${data.quotaResetsLabel}` : quotaText;
77
77
  left.push({ id: "quota", text: styler.fg("muted", label), priority: 6 });
78
+ // TEMP DEBUG
79
+ try {
80
+ const fs = require("node:fs");
81
+ const os = require("node:os");
82
+ const p = require("node:path");
83
+ fs.appendFileSync(p.join(os.tmpdir(), "pi-soly-quota-debug.log"), `[${new Date().toISOString()}] [footer] render: pushed quota segment pct=${data.quotaPercent}\n`);
84
+ } catch {}
85
+ } else {
86
+ // TEMP DEBUG
87
+ try {
88
+ const fs = require("node:fs");
89
+ const os = require("node:os");
90
+ const p = require("node:path");
91
+ fs.appendFileSync(p.join(os.tmpdir(), "pi-soly-quota-debug.log"), `[${new Date().toISOString()}] [footer] render: quotaPercent=null (segment skipped)\n`);
92
+ } catch {}
78
93
  }
79
94
 
80
95
  if (data.rulesActive > 0) {