pi-soly 2.5.5 → 2.6.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.
- package/index.ts +0 -18
- package/package.json +1 -1
- package/quota/minimax.ts +23 -39
- package/quota/poller.ts +5 -20
- package/visual/colors.ts +9 -0
- package/visual/data.ts +7 -1
- package/visual/footer.ts +14 -19
package/index.ts
CHANGED
|
@@ -353,17 +353,7 @@ 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;
|
|
357
356
|
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
|
-
}
|
|
367
357
|
d.reasoning = Boolean(model?.reasoning);
|
|
368
358
|
try {
|
|
369
359
|
d.thinkingLevel = pi.getThinkingLevel();
|
|
@@ -562,14 +552,6 @@ export default function solyExtension(pi: ExtensionAPI) {
|
|
|
562
552
|
// quotaPercent/quotaResetsLabel back for the footer to render).
|
|
563
553
|
if (quotaPoller) quotaPoller.stop();
|
|
564
554
|
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 {}
|
|
573
555
|
// Editors save in bursts (write to .tmp, rename, touch). Coalesce
|
|
574
556
|
// those rapid reload events into a single sub-line event under the
|
|
575
557
|
// 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.
|
|
3
|
+
"version": "2.6.0",
|
|
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
|
@@ -23,25 +23,19 @@
|
|
|
23
23
|
//
|
|
24
24
|
// We pick the "general" entry — that's the quota bucket MiniMax-M3 (and all
|
|
25
25
|
// text/chat models) draw from. Video/music have separate buckets.
|
|
26
|
+
//
|
|
27
|
+
// The API returns `current_interval_remaining_percent` (what's left). We
|
|
28
|
+
// invert it to **used** (100 - remaining) to match the MiniMax web dashboard
|
|
29
|
+
// semantics — "used" grows as you spend, which reads more intuitively in a
|
|
30
|
+
// status bar than "remaining" which shrinks.
|
|
26
31
|
// =============================================================================
|
|
27
32
|
|
|
28
33
|
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";
|
|
32
34
|
import type { QuotaProvider, QuotaSnapshot } from "./types.ts";
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
44
|
-
/** Minimal shape we read from `mmx quota show --output json`. */
|
|
36
|
+
// Minimal shape we read from `mmx quota show --output json`. We track
|
|
37
|
+
// `remaining` (what the API calls `current_interval_remaining_percent`) and
|
|
38
|
+
// invert it to `used` for display.
|
|
45
39
|
type MinimaxQuotaResponse = {
|
|
46
40
|
model_remains?: Array<{
|
|
47
41
|
model_name?: string;
|
|
@@ -51,6 +45,10 @@ type MinimaxQuotaResponse = {
|
|
|
51
45
|
base_resp?: { status_code?: number };
|
|
52
46
|
};
|
|
53
47
|
|
|
48
|
+
/** Detect Windows (where npm-installed CLIs are `.cmd` shims that execFile
|
|
49
|
+
* can't spawn directly without a shell). */
|
|
50
|
+
const IS_WIN = process.platform === "win32";
|
|
51
|
+
|
|
54
52
|
/** Run mmx and capture stdout. Resolves to null on any failure
|
|
55
53
|
* (mmx missing, non-zero exit, timeout, bad JSON). Never throws.
|
|
56
54
|
*
|
|
@@ -66,45 +64,31 @@ function runMmx(args: string[], timeoutMs: number): Promise<string | null> {
|
|
|
66
64
|
maxBuffer: 1024 * 1024,
|
|
67
65
|
shell: IS_WIN, // resolve .cmd shims on Windows
|
|
68
66
|
};
|
|
69
|
-
execFile("mmx", args, opts, (err, stdout
|
|
70
|
-
if (err)
|
|
71
|
-
|
|
72
|
-
resolve(null);
|
|
73
|
-
} else {
|
|
74
|
-
dbg(`runMmx ok: ${String(stdout).length} bytes`);
|
|
75
|
-
resolve(stdout ?? "");
|
|
76
|
-
}
|
|
67
|
+
execFile("mmx", args, opts, (err, stdout) => {
|
|
68
|
+
if (err) resolve(null);
|
|
69
|
+
else resolve(stdout ?? "");
|
|
77
70
|
});
|
|
78
71
|
});
|
|
79
72
|
}
|
|
80
73
|
|
|
81
|
-
/** Parse the mmx quota response, returning the "general" model snapshot
|
|
74
|
+
/** Parse the mmx quota response, returning the "general" model snapshot
|
|
75
|
+
* with `remainingPercent` expressed as **used** (100 - remaining). */
|
|
82
76
|
function parseGeneralQuota(raw: string): QuotaSnapshot | null {
|
|
83
77
|
let parsed: unknown;
|
|
84
78
|
try {
|
|
85
79
|
parsed = JSON.parse(raw);
|
|
86
|
-
} catch
|
|
87
|
-
dbg(`parse: JSON.parse failed: ${e instanceof Error ? e.message : String(e)}; raw[0..100]=${raw.slice(0, 100)}`);
|
|
80
|
+
} catch {
|
|
88
81
|
return null; // not valid JSON — mmx printed an error to stdout
|
|
89
82
|
}
|
|
90
83
|
// Cast at the boundary (single documented place, per code-style rules).
|
|
91
84
|
const data = parsed as MinimaxQuotaResponse;
|
|
92
|
-
if (data.base_resp?.status_code !== 0)
|
|
93
|
-
dbg(`parse: status_code=${data.base_resp?.status_code} (expected 0)`);
|
|
94
|
-
return null;
|
|
95
|
-
}
|
|
85
|
+
if (data.base_resp?.status_code !== 0) return null;
|
|
96
86
|
const general = data.model_remains?.find((m) => m.model_name === "general");
|
|
97
|
-
if (!general)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
const pct = general.current_interval_remaining_percent;
|
|
102
|
-
if (typeof pct !== "number") {
|
|
103
|
-
dbg(`parse: pct is ${typeof pct}`);
|
|
104
|
-
return null;
|
|
105
|
-
}
|
|
87
|
+
if (!general) return null;
|
|
88
|
+
const remaining = general.current_interval_remaining_percent;
|
|
89
|
+
if (typeof remaining !== "number") return null;
|
|
106
90
|
return {
|
|
107
|
-
remainingPercent:
|
|
91
|
+
remainingPercent: Math.max(0, Math.min(100, 100 - remaining)),
|
|
108
92
|
resetsInMs: typeof general.remains_time === "number" ? general.remains_time : null,
|
|
109
93
|
};
|
|
110
94
|
}
|
package/quota/poller.ts
CHANGED
|
@@ -21,21 +21,10 @@
|
|
|
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";
|
|
27
24
|
import type { ChromeData } from "../visual/data.ts";
|
|
28
25
|
import { resolveQuotaProvider } from "./registry.ts";
|
|
29
26
|
import { formatReset } from "./format.ts";
|
|
30
27
|
|
|
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
|
-
|
|
39
28
|
/** Default poll interval: 60 seconds. */
|
|
40
29
|
const POLL_INTERVAL_MS = 60_000;
|
|
41
30
|
|
|
@@ -70,25 +59,23 @@ export function startQuotaPoller(
|
|
|
70
59
|
|
|
71
60
|
const tick = async (): Promise<void> => {
|
|
72
61
|
if (stopped) return;
|
|
73
|
-
dbg(`tick: enabled=${isEnabled()} provider=${data.modelProvider}`);
|
|
74
62
|
if (!isEnabled()) {
|
|
75
|
-
dbg("disabled, skipping");
|
|
76
63
|
scheduleNext();
|
|
77
64
|
return;
|
|
78
65
|
}
|
|
79
66
|
|
|
80
67
|
const providerId = data.modelProvider;
|
|
81
68
|
if (!providerId) {
|
|
82
|
-
dbg("no modelProvider");
|
|
83
69
|
scheduleNext();
|
|
84
70
|
return;
|
|
85
71
|
}
|
|
86
72
|
|
|
87
73
|
const provider = resolveQuotaProvider(providerId);
|
|
88
74
|
if (!provider) {
|
|
89
|
-
|
|
75
|
+
// No adapter for this provider — clear and skip.
|
|
90
76
|
data.quotaPercent = null;
|
|
91
77
|
data.quotaResetsLabel = null;
|
|
78
|
+
data.quotaResetsMs = null;
|
|
92
79
|
scheduleNext();
|
|
93
80
|
return;
|
|
94
81
|
}
|
|
@@ -96,18 +83,16 @@ export function startQuotaPoller(
|
|
|
96
83
|
let snapshot;
|
|
97
84
|
try {
|
|
98
85
|
snapshot = await provider.fetch();
|
|
99
|
-
|
|
100
|
-
} catch (e) {
|
|
101
|
-
dbg(`fetch threw: ${e instanceof Error ? e.message : String(e)}`);
|
|
86
|
+
} catch {
|
|
102
87
|
snapshot = null;
|
|
103
88
|
}
|
|
104
89
|
if (snapshot) {
|
|
105
90
|
data.quotaPercent = snapshot.remainingPercent;
|
|
91
|
+
data.quotaResetsMs = snapshot.resetsInMs;
|
|
106
92
|
data.quotaResetsLabel = snapshot.resetsInMs !== null ? formatReset(snapshot.resetsInMs) : null;
|
|
107
|
-
dbg(`wrote data: pct=${data.quotaPercent} label=${data.quotaResetsLabel}`);
|
|
108
93
|
onUpdate();
|
|
109
|
-
dbg("called onUpdate (poke)");
|
|
110
94
|
}
|
|
95
|
+
// On null (fetch failed), keep the previous snapshot — don't clear.
|
|
111
96
|
scheduleNext();
|
|
112
97
|
};
|
|
113
98
|
|
package/visual/colors.ts
CHANGED
|
@@ -22,3 +22,12 @@ export function ctxColor(percent: number | null): ChromeColor {
|
|
|
22
22
|
if (percent > 70) return "warning";
|
|
23
23
|
return "muted";
|
|
24
24
|
}
|
|
25
|
+
|
|
26
|
+
/** Quota color by **used** percentage. Conservative threshold: only yellow when
|
|
27
|
+
* > 90% of the interval is spent (no red — quota resets, it's not a hard
|
|
28
|
+
* error). Muted otherwise. */
|
|
29
|
+
export function quotaColor(usedPercent: number | null): ChromeColor {
|
|
30
|
+
if (usedPercent === null || !Number.isFinite(usedPercent)) return "muted";
|
|
31
|
+
if (usedPercent > 90) return "warning";
|
|
32
|
+
return "muted";
|
|
33
|
+
}
|
package/visual/data.ts
CHANGED
|
@@ -46,10 +46,15 @@ export type ChromeData = {
|
|
|
46
46
|
/** Level of the recent event (used for glyph/color). */
|
|
47
47
|
recentEventLevel: "info" | "warning" | "error" | null;
|
|
48
48
|
/** Remaining quota percent (0–100) for the active provider, polled in
|
|
49
|
-
* the background by quota/poller.ts. null = no adapter / not polled.
|
|
49
|
+
* the background by quota/poller.ts. null = no adapter / not polled.
|
|
50
|
+
* Semantic: **used** (inverted from API's remaining) — grows as you
|
|
51
|
+
* spend, matching the MiniMax dashboard. */
|
|
50
52
|
quotaPercent: number | null;
|
|
51
53
|
/** Human-readable reset time label (e.g. "in 20m"), or null. */
|
|
52
54
|
quotaResetsLabel: string | null;
|
|
55
|
+
/** Raw ms until the quota window resets, or null. Used by the footer to
|
|
56
|
+
* color the time yellow when < 30 minutes remain. */
|
|
57
|
+
quotaResetsMs: number | null;
|
|
53
58
|
};
|
|
54
59
|
|
|
55
60
|
/** A fresh ChromeData with everything empty/idle. */
|
|
@@ -73,5 +78,6 @@ export function emptyChromeData(): ChromeData {
|
|
|
73
78
|
recentEventLevel: null,
|
|
74
79
|
quotaPercent: null,
|
|
75
80
|
quotaResetsLabel: null,
|
|
81
|
+
quotaResetsMs: null,
|
|
76
82
|
};
|
|
77
83
|
}
|
package/visual/footer.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import type { Component } from "@earendil-works/pi-tui";
|
|
19
19
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
20
20
|
import { composeBar, type Segment } from "./segments.ts";
|
|
21
|
-
import { ctxColor } from "./colors.ts";
|
|
21
|
+
import { ctxColor, quotaColor } from "./colors.ts";
|
|
22
22
|
import { fitPath, formatTokens } from "./format.ts";
|
|
23
23
|
import { withGlyph } from "./glyphs.ts";
|
|
24
24
|
import type { ChromeData } from "./data.ts";
|
|
@@ -72,24 +72,19 @@ export function buildFooterLine(data: ChromeData, fd: FooterData, width: number,
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
if (data.quotaPercent !== null) {
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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 {}
|
|
75
|
+
const used = data.quotaPercent;
|
|
76
|
+
const glyph = ascii ? "" : "⬢ ";
|
|
77
|
+
const pctPart = styler.fg(quotaColor(used), `${glyph}${used}%`);
|
|
78
|
+
let label: string;
|
|
79
|
+
if (data.quotaResetsLabel) {
|
|
80
|
+
// Yellow time when < 30 minutes until reset; muted otherwise.
|
|
81
|
+
const soon = data.quotaResetsMs !== null && data.quotaResetsMs <= 30 * 60_000;
|
|
82
|
+
const timePart = soon ? styler.fg("warning", data.quotaResetsLabel) : styler.dim(data.quotaResetsLabel);
|
|
83
|
+
label = `${pctPart} · ${timePart}`;
|
|
84
|
+
} else {
|
|
85
|
+
label = pctPart;
|
|
86
|
+
}
|
|
87
|
+
left.push({ id: "quota", text: label, priority: 8 });
|
|
93
88
|
}
|
|
94
89
|
|
|
95
90
|
if (data.rulesActive > 0) {
|