@llblab/pi-kit 0.38.0 → 0.38.1

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/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  All notable changes to `@llblab/pi-kit` are documented here.
4
4
 
5
+ ## 0.38.1: Bundled Package Updates (2026-10-29)
6
+
7
+ - `Exact pins`: `@llblab/pi-claude-usage` 0.2.3 → 0.2.4; `@llblab/pi-clean-room` 0.3.0 → 0.3.1.
8
+ - `Packaging`: Synchronizes the lockfile, bundle aliases and README inventory. Package membership and resource order remain unchanged; packed validation does not certify extension safety or installed-client behavior.
9
+
5
10
  ## 0.38.0: Bundled Package Updates (2026-10-26)
6
11
 
7
12
  - `Exact pins`: `@llblab/pi-state-flow` 0.26.5 → 0.27.1.
package/README.md CHANGED
@@ -13,8 +13,8 @@ Package links lead to the owning repositories for usage, documentation, issues,
13
13
  | Package | Version | Purpose |
14
14
  | --- | ---: | --- |
15
15
  | [`@llblab/pi-actors`](https://github.com/llblab/pi-actors) | `0.55.0` | Inspectable local Runs, reusable Recipes, persistent tools, and delegation Skills |
16
- | [`@llblab/pi-claude-usage`](https://github.com/llblab/pi-claude-usage) | `0.2.3` | Claude subscription quota status and shared per-model Fast toggle for Opus, mirrored in Telegram |
17
- | [`@llblab/pi-clean-room`](https://github.com/llblab/pi-clean-room) | `0.3.0` | Isolated nested Pi TUI with named npm extensions and compatible model selection |
16
+ | [`@llblab/pi-claude-usage`](https://github.com/llblab/pi-claude-usage) | `0.2.4` | Claude subscription quota status and shared per-model Fast toggle for Opus, mirrored in Telegram |
17
+ | [`@llblab/pi-clean-room`](https://github.com/llblab/pi-clean-room) | `0.3.1` | Isolated nested Pi TUI with named npm extensions and compatible model selection |
18
18
  | [`@llblab/pi-codex-usage`](https://github.com/llblab/pi-codex-usage) | `0.13.2` | OpenAI/legacy Codex subscription quota and Business credit status, with persistent priority Fast toggle mirrored in Telegram |
19
19
  | [`@llblab/pi-grow-loop`](https://github.com/llblab/pi-grow-loop) | `0.9.0` | Visible continuation scheduling and bounded worker Skills through compiled, manifest-owned resources |
20
20
  | [`@llblab/pi-state-flow`](https://github.com/llblab/pi-state-flow) | `0.27.1` | Scoped context/memory compiler with step-independent current memory, stable Passive/Active modes and visible blocked-state status |
@@ -20,4 +20,4 @@
20
20
  - Action: Do not add fallbacks, CLI probes, or API-key paths; API keys have no subscription quota, so report `n/a`. `utilization` is a used percent.
21
21
  - `Rate-limit discipline`: Quota polling must be shared across instances; HTTP 429 triggers shared backoff.
22
22
  - Trigger: Changing refresh cadence, retries, locking, or adding fetch paths.
23
- - Action: Keep the single shared-state protocol in `lib/usage-store.ts`, orchestrated by `lib/status.ts` (leader refreshes every minute, takeover after 90 seconds by claiming leadership before fetching, a non-waiting OS-backed SQLite mutex around claiming and fenced publication, atomic writes, shared failure backoff). Always re-read the file for request authorization (`owner` + `claimId` + lease) and publication; do not use an in-memory ownership fallback. Lock failures and failed claim writes must deny requests. `mutex.sqlite` stores no quota or leadership data: never unlink/replace it while instances run or evict a paused holder; close or process death releases the mutex. Keep network calls outside critical sections. Instances otherwise only read the file; never add per-instance polling or probing requests.
23
+ - Action: Keep the single shared-state protocol in `lib/usage-store.ts`, orchestrated by `lib/status.ts` (leader refreshes every 3 minutes, takeover after 210 seconds by claiming leadership before fetching, a non-waiting OS-backed SQLite mutex around claiming and fenced publication, atomic writes, shared failure backoff). Always re-read the file for request authorization (`owner` + `claimId` + lease) and publication; do not use an in-memory ownership fallback. Lock failures and failed claim writes must deny requests. `mutex.sqlite` stores no quota or leadership data: never unlink/replace it while instances run or evict a paused holder; close or process death releases the mutex. Keep network calls outside critical sections. Instances otherwise only read the file; never add per-instance polling or probing requests.
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.4: Quota Polling Hotfix
4
+
5
+ - Reduced shared quota polling from once a minute to once every three minutes to lower request pressure on Anthropic's usage endpoint. Follower takeover now waits three minutes and thirty seconds so multiple instances preserve the slower cadence. Shared HTTP 429 backoff and local reset countdown updates remain in place.
6
+
3
7
  ## 0.2.3: Empty Quota Bar Hotfix
4
8
 
5
9
  - Aligned quota bars with rounded remaining percentages: when either window rounds to `0%`, its final segment disappears and the whole dual bar uses the error background, while the other half retains its quota. The 5-hour reset countdown follows the same zero boundary; near-full quota still rounds to the nearest 5% step.
@@ -124,10 +124,10 @@ The extension ships an export-only [`index.ts`](./index.ts) and a flat domain DA
124
124
 
125
125
  To avoid independent polling, instances coordinate through `~/.pi/agent/tmp/pi-claude-usage/usage.json` (percentages and timestamps only, no tokens):
126
126
 
127
- - The instance that last updated the file is the leader and refreshes it every minute
127
+ - The instance that last updated the file is the leader and refreshes it every 3 minutes
128
128
  - Every other instance only reads the file (re-checking every ≤30s) and redraws when it changes
129
- - Leadership is never cached in memory: before each usage request, the instance re-reads the file and checks its `owner`, unique `claimId`, and 90s lease. Publication rechecks that claim under the lock; a superseded success or failure is discarded. Failed locks or unwritten claims never authorize a request
130
- - If the file is 90 seconds old (leader is closed, busy, or asleep), the first follower that obtains the mutex takes over: it re-reads the JSON, writes itself as the leader with a fresh timestamp, releases the mutex, then fetches from the server and publishes under the mutex after rechecking its claim. Its next refresh is one minute later; JSON writes remain atomic renames
129
+ - Leadership is never cached in memory: before each usage request, the instance re-reads the file and checks its `owner`, unique `claimId`, and 210s lease. Publication rechecks that claim under the lock; a superseded success or failure is discarded. Failed locks or unwritten claims never authorize a request
130
+ - If the file is 210 seconds old (leader is closed, busy, or asleep), the first follower that obtains the mutex takes over: it re-reads the JSON, writes itself as the leader with a fresh timestamp, releases the mutex, then fetches from the server and publishes under the mutex after rechecking its claim. Its next refresh is three minutes later; JSON writes remain atomic renames
131
131
  - Claiming and publication use a non-waiting transaction in `mutex.sqlite`, through Node's built-in `node:sqlite` (Node ≥22.19.0, the existing package minimum). It stores no quota or leadership records and needs no extra package or service. The OS releases the lock when the connection closes or the process dies; there is no timeout-based lock stealing
132
132
  - Failures are written to the file with an exponential backoff (1 to 5 minutes; 5 to 30 minutes for HTTP 429) that applies to all instances
133
133
  - The last report stays visible for up to an hour; after that `error` is shown
@@ -210,7 +210,7 @@ export function createClaudeUsageStatus(pi: ExtensionAPI) {
210
210
  };
211
211
 
212
212
  /**
213
- * When this instance is due (leader after 1 minute, anyone after 90 seconds) it
213
+ * When this instance is due (leader after 3 minutes, anyone after 210 seconds) it
214
214
  * claims leadership under the lock (owner and timestamp, so no other
215
215
  * instance is due), then fetches, then stamps the outcome. Failures are
216
216
  * published with a backoff so other instances do not pile on.
@@ -17,11 +17,11 @@ import type { ClaudeUsageReport } from "./usage.ts";
17
17
  * atomic renames; short critical sections use an OS-backed SQLite mutex.
18
18
  */
19
19
 
20
- export const LEADER_INTERVAL_MS = 60_000;
20
+ export const LEADER_INTERVAL_MS = 3 * 60_000;
21
21
  /** The leader is considered gone after missing its slot by 30 seconds. */
22
22
  export const TAKEOVER_AFTER_MS = LEADER_INTERVAL_MS + 30_000;
23
23
  /** Minimum pause between two fetch attempts of the same instance. */
24
- export const MIN_ATTEMPT_GAP_MS = 60_000;
24
+ export const MIN_ATTEMPT_GAP_MS = LEADER_INTERVAL_MS;
25
25
 
26
26
  const STATE_FILE = "usage.json";
27
27
  const LOCK_FILE = "mutex.sqlite";
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-claude-usage",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "private": false,
5
5
  "description": "Minimal Pi extension that shows Anthropic Claude subscription usage limits using Pi OAuth auth",
6
6
  "keywords": [
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.1: Extension-owned skill selection
4
+
5
+ - Accept a bundled skill when its owning extension is also allowlisted, regardless of argument order, and prefer it over a same-named standalone skill. When the owner is missing, suggest its `/clean-room` command without enabling it implicitly or loading unrelated skills.
6
+ - Accept equivalent separators for explicit skill selection while keeping completion unchanged.
7
+
3
8
  ## 0.3.0: Pi 1.0 Baseline and Package Licensing
4
9
 
5
10
  - `Licensing`: Includes the MIT LICENSE in source checkouts and npm packages, preserving existing author attribution.
@@ -1,12 +1,13 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import { existsSync, readFileSync, readdirSync } from "node:fs";
2
+ import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
- import { isAbsolute, join, resolve } from "node:path";
4
+ import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
5
5
  import {
6
6
  CONFIG_DIR_NAME,
7
7
  getAgentDir,
8
8
  type ExtensionAPI,
9
9
  type ExtensionCommandContext,
10
+ type SlashCommandInfo,
10
11
  } from "@earendil-works/pi-coding-agent";
11
12
 
12
13
  const EMPTY_COMPONENT = { render: () => [], invalidate: () => {} };
@@ -94,6 +95,27 @@ export function resolveSkill(name: string, cwd: string, agentDir = getAgentDir()
94
95
  return undefined;
95
96
  }
96
97
 
98
+ function isWithin(path: string, root: string): boolean {
99
+ return path === root || path.startsWith(`${root}${sep}`);
100
+ }
101
+
102
+ function resolveBundledSkill(name: string, extensions: string[], commands: SlashCommandInfo[]): string | undefined {
103
+ const skill = commands.find((command) => command.source === "skill" && command.name === `skill:${name}`);
104
+ if (!skill || !existsSync(skill.sourceInfo.path)) return undefined;
105
+ const skillPath = realpathSync(skill.sourceInfo.path);
106
+ const baseDir = skill.sourceInfo.baseDir;
107
+ const owner = baseDir && existsSync(baseDir) ? realpathSync(baseDir) : undefined;
108
+ const allowed = extensions.some((extension) => {
109
+ const path = realpathSync(extension);
110
+ if (statSync(path).isDirectory()) return isWithin(skillPath, path) || (owner !== undefined && isWithin(owner, path));
111
+ if (!owner) return false;
112
+ if (skill.sourceInfo.origin === "package") return isWithin(path, owner);
113
+ const source = `extension:${basename(extension).replace(/\.(ts|js)$/, "")}`;
114
+ return skill.sourceInfo.source === source && owner === realpathSync(dirname(extension));
115
+ });
116
+ return allowed ? skill.sourceInfo.path : undefined;
117
+ }
118
+
97
119
  export const RESOURCE_ARGUMENTS = ["skills", "agents", "system", "append-system"] as const;
98
120
  type ResourceArgument = (typeof RESOURCE_ARGUMENTS)[number];
99
121
 
@@ -126,7 +148,7 @@ export function buildCleanRoomArgs(
126
148
  }
127
149
 
128
150
  function parseNames(args: string): string[] {
129
- return args.trim().split(/\s+/).filter(Boolean);
151
+ return args.trim().split(/\s+/).filter(Boolean).map((name) => name.replace(/^skills::?/, "skills/"));
130
152
  }
131
153
 
132
154
  function launchCleanRoom(
@@ -177,19 +199,27 @@ export default function cleanRoomExtension(pi: ExtensionAPI) {
177
199
  ));
178
200
  const skillNames = names.filter((name) => name.startsWith("skills/")).map((name) => name.slice("skills/".length));
179
201
  const extensionNames = names.filter((name) => !resources.has(name as ResourceArgument) && !name.startsWith("skills/"));
180
- const missingSkills: string[] = [];
181
- const skills = skillNames.map((name) => {
182
- const path = resolveSkill(name, ctx.cwd);
183
- if (!path) missingSkills.push(name);
184
- return path;
185
- }).filter((path): path is string => path !== undefined);
186
202
  const missingExtensions: string[] = [];
187
203
  const paths = extensionNames.map((name) => {
188
204
  const path = resolveExtension(name, ctx.cwd);
189
205
  if (!path) missingExtensions.push(name);
190
206
  return path;
191
207
  }).filter((path): path is string => path !== undefined);
208
+ const commands = skillNames.length > 0 ? pi.getCommands() : [];
209
+ const missingSkills: string[] = [];
210
+ const skills = skillNames.map((name) => {
211
+ const path = resolveBundledSkill(name, paths, commands) ?? resolveSkill(name, ctx.cwd);
212
+ if (!path) missingSkills.push(name);
213
+ return path;
214
+ }).filter((path): path is string => path !== undefined);
192
215
  if (missingSkills.length > 0) {
216
+ const candidates = discoverExtensionNames(ctx.cwd).map((name) => ({ name, path: resolveExtension(name, ctx.cwd) }));
217
+ for (const name of missingSkills) {
218
+ const owner = candidates.find(({ path }) => path && resolveBundledSkill(name, [path], commands));
219
+ if (!owner) continue;
220
+ ctx.ui.notify(`Skill ${name} belongs to ${owner.name}; use /clean-room ${owner.name}`, "error");
221
+ return;
222
+ }
193
223
  ctx.ui.notify(`Unknown skill${missingSkills.length > 1 ? "s" : ""}: ${missingSkills.join(", ")}`, "error");
194
224
  return;
195
225
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-clean-room",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "private": false,
5
5
  "description": "Launch an isolated Pi TUI with only explicitly selected extensions",
6
6
  "keywords": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-kit",
3
- "version": "0.38.0",
3
+ "version": "0.38.1",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -45,8 +45,8 @@
45
45
  ],
46
46
  "dependencies": {
47
47
  "@llblab/pi-actors": "0.55.0",
48
- "@llblab/pi-claude-usage": "0.2.3",
49
- "@llblab/pi-clean-room": "0.3.0",
48
+ "@llblab/pi-claude-usage": "0.2.4",
49
+ "@llblab/pi-clean-room": "0.3.1",
50
50
  "@llblab/pi-codex-usage": "0.13.2",
51
51
  "@llblab/pi-grow-loop": "0.9.0",
52
52
  "@llblab/pi-state-flow": "0.27.1",