pi-soly 2.5.2 → 2.5.4

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/index.ts CHANGED
@@ -353,7 +353,17 @@ export default function solyExtension(pi: ExtensionAPI) {
353
353
  d.home = os.homedir();
354
354
  const model = ctx.model as { id?: string; provider?: string; reasoning?: boolean } | undefined;
355
355
  d.modelId = model?.id ?? null;
356
+ const prevProvider = d.modelProvider;
356
357
  d.modelProvider = model?.provider ?? null;
358
+ // TEMP DEBUG
359
+ if (prevProvider !== d.modelProvider) {
360
+ try {
361
+ const fs = require("node:fs");
362
+ const os = require("node:os");
363
+ const p = require("node:path");
364
+ fs.appendFileSync(p.join(os.tmpdir(), "pi-soly-quota-debug.log"), `[${new Date().toISOString()}] updateChromeData: modelProvider changed ${prevProvider} -> ${d.modelProvider}, modelId=${d.modelId}\n`);
365
+ } catch {}
366
+ }
357
367
  d.reasoning = Boolean(model?.reasoning);
358
368
  try {
359
369
  d.thinkingLevel = pi.getThinkingLevel();
@@ -552,6 +562,14 @@ export default function solyExtension(pi: ExtensionAPI) {
552
562
  // quotaPercent/quotaResetsLabel back for the footer to render).
553
563
  if (quotaPoller) quotaPoller.stop();
554
564
  quotaPoller = startQuotaPoller(chrome.data, () => getActiveConfig().chrome.enabled, () => chrome.poke());
565
+ // TEMP DEBUG
566
+ try {
567
+ const fs = await import("node:fs");
568
+ const os = await import("node:os");
569
+ const p = await import("node:path");
570
+ const dbgLog = p.join(os.tmpdir(), "pi-soly-quota-debug.log");
571
+ fs.appendFileSync(dbgLog, `[${new Date().toISOString()}] session_start: poller started, chrome.enabled=${getActiveConfig().chrome.enabled}, modelProvider=${chrome.data.modelProvider}\n`);
572
+ } catch {}
555
573
  // Editors save in bursts (write to .tmp, rename, touch). Coalesce
556
574
  // those rapid reload events into a single sub-line event under the
557
575
  // Working indicator (└─ reloaded 47 rules). Errors here are real
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-soly",
3
- "version": "2.5.2",
3
+ "version": "2.5.4",
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/quota/poller.ts CHANGED
@@ -21,10 +21,21 @@
21
21
  // It only runs while a session is active.
22
22
  // =============================================================================
23
23
 
24
+ import * as fs from "node:fs";
25
+ import * as os from "node:os";
26
+ import * as path from "node:path";
24
27
  import type { ChromeData } from "../visual/data.ts";
25
28
  import { resolveQuotaProvider } from "./registry.ts";
26
29
  import { formatReset } from "./format.ts";
27
30
 
31
+ /** TEMP DEBUG — append a line to the quota debug log. Remove after diagnosis. */
32
+ const DEBUG_LOG = path.join(os.tmpdir(), "pi-soly-quota-debug.log");
33
+ function dbg(msg: string): void {
34
+ try {
35
+ fs.appendFileSync(DEBUG_LOG, `[${new Date().toISOString()}] ${msg}\n`);
36
+ } catch { /* best effort */ }
37
+ }
38
+
28
39
  /** Default poll interval: 60 seconds. */
29
40
  const POLL_INTERVAL_MS = 60_000;
30
41
 
@@ -59,33 +70,44 @@ export function startQuotaPoller(
59
70
 
60
71
  const tick = async (): Promise<void> => {
61
72
  if (stopped) return;
73
+ dbg(`tick: enabled=${isEnabled()} provider=${data.modelProvider}`);
62
74
  if (!isEnabled()) {
75
+ dbg("disabled, skipping");
63
76
  scheduleNext();
64
77
  return;
65
78
  }
66
79
 
67
80
  const providerId = data.modelProvider;
68
81
  if (!providerId) {
82
+ dbg("no modelProvider");
69
83
  scheduleNext();
70
84
  return;
71
85
  }
72
86
 
73
87
  const provider = resolveQuotaProvider(providerId);
74
88
  if (!provider) {
75
- // No adapter for this provider — clear and skip.
89
+ dbg(`no adapter for ${providerId}`);
76
90
  data.quotaPercent = null;
77
91
  data.quotaResetsLabel = null;
78
92
  scheduleNext();
79
93
  return;
80
94
  }
81
95
 
82
- const snapshot = await provider.fetch();
96
+ let snapshot;
97
+ try {
98
+ snapshot = await provider.fetch();
99
+ dbg(`fetch result: ${JSON.stringify(snapshot)}`);
100
+ } catch (e) {
101
+ dbg(`fetch threw: ${e instanceof Error ? e.message : String(e)}`);
102
+ snapshot = null;
103
+ }
83
104
  if (snapshot) {
84
105
  data.quotaPercent = snapshot.remainingPercent;
85
106
  data.quotaResetsLabel = snapshot.resetsInMs !== null ? formatReset(snapshot.resetsInMs) : null;
107
+ dbg(`wrote data: pct=${data.quotaPercent} label=${data.quotaResetsLabel}`);
86
108
  onUpdate();
109
+ dbg("called onUpdate (poke)");
87
110
  }
88
- // On null (fetch failed), keep the previous snapshot — don't clear.
89
111
  scheduleNext();
90
112
  };
91
113