@pi-unipi/unipi 2.1.0 → 2.1.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/unipi",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
4
4
  "description": "All-in-one extension suite for Pi coding agent",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -65,26 +65,26 @@
65
65
  "typebox": "^1.1.38"
66
66
  },
67
67
  "dependencies": {
68
- "@pi-unipi/ask-user": "2.1.0",
69
- "@pi-unipi/btw": "2.1.0",
70
- "@pi-unipi/compactor": "2.1.0",
71
- "@pi-unipi/notify": "2.1.0",
72
- "@pi-unipi/command-enchantment": "2.1.0",
73
- "@pi-unipi/core": "2.1.0",
74
- "@pi-unipi/info-screen": "2.1.0",
75
- "@pi-unipi/mcp": "2.1.0",
76
- "@pi-unipi/memory": "2.1.0",
77
- "@pi-unipi/ralph": "2.1.0",
78
- "@pi-unipi/subagents": "2.1.0",
79
- "@pi-unipi/utility": "2.1.0",
80
- "@pi-unipi/milestone": "2.1.0",
81
- "@pi-unipi/kanboard": "2.1.0",
82
- "@pi-unipi/web-api": "2.1.0",
83
- "@pi-unipi/workflow": "2.1.0",
84
- "@pi-unipi/footer": "2.1.0",
85
- "@pi-unipi/updater": "2.1.0",
86
- "@pi-unipi/input-shortcuts": "2.1.0",
87
- "@pi-unipi/cocoindex": "2.1.0"
68
+ "@pi-unipi/ask-user": "2.1.2",
69
+ "@pi-unipi/btw": "2.1.2",
70
+ "@pi-unipi/compactor": "2.1.2",
71
+ "@pi-unipi/notify": "2.1.2",
72
+ "@pi-unipi/command-enchantment": "2.1.2",
73
+ "@pi-unipi/core": "2.1.2",
74
+ "@pi-unipi/info-screen": "2.1.2",
75
+ "@pi-unipi/mcp": "2.1.2",
76
+ "@pi-unipi/memory": "2.1.2",
77
+ "@pi-unipi/ralph": "2.1.2",
78
+ "@pi-unipi/subagents": "2.1.2",
79
+ "@pi-unipi/utility": "2.1.2",
80
+ "@pi-unipi/milestone": "2.1.2",
81
+ "@pi-unipi/kanboard": "2.1.2",
82
+ "@pi-unipi/web-api": "2.1.2",
83
+ "@pi-unipi/workflow": "2.1.2",
84
+ "@pi-unipi/footer": "2.1.2",
85
+ "@pi-unipi/updater": "2.1.2",
86
+ "@pi-unipi/input-shortcuts": "2.1.2",
87
+ "@pi-unipi/cocoindex": "2.1.2"
88
88
  },
89
89
  "devDependencies": {
90
90
  "@earendil-works/pi-agent-core": "^0.80.0",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pi-unipi/ask-user",
3
- "version": "2.1.0",
4
- "description": "Structured user input tool for Pi coding agent \u2014 single-select, multi-select, freeform",
3
+ "version": "2.1.2",
4
+ "description": "Structured user input tool for Pi coding agent single-select, multi-select, freeform",
5
5
  "type": "module",
6
6
  "main": "index.ts",
7
7
  "license": "MIT",
@@ -40,7 +40,7 @@
40
40
  "access": "public"
41
41
  },
42
42
  "dependencies": {
43
- "@pi-unipi/core": "2.1.0"
43
+ "@pi-unipi/core": "2.1.2"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "@earendil-works/pi-coding-agent": "^0.80.0",
@@ -11,6 +11,7 @@ import {
11
11
  COMPACTOR_INSTRUCTION,
12
12
  UNIPI_EVENTS,
13
13
  emitEvent,
14
+ withHerdrBlocked,
14
15
  } from "@pi-unipi/core";
15
16
  import type { NormalizedOption, AskUserResponse, SessionLauncherResult } from "./types.js";
16
17
  import { renderAskUI, createRenderCall, createRenderResult } from "./ask-ui.js";
@@ -272,15 +273,19 @@ export function registerAskUserTools(pi: ExtensionAPI): void {
272
273
  }
273
274
 
274
275
  // Render interactive UI
275
- const result = await ctx.ui.custom<{ response: AskUserResponse } | null>(
276
- renderAskUI({
277
- question,
278
- context,
279
- options: normalizedOptions,
280
- allowMultiple,
281
- allowFreeform,
282
- timeout,
283
- }),
276
+ const result = await withHerdrBlocked(
277
+ pi,
278
+ "ask_user",
279
+ () => ctx.ui.custom<{ response: AskUserResponse } | null>(
280
+ renderAskUI({
281
+ question,
282
+ context,
283
+ options: normalizedOptions,
284
+ allowMultiple,
285
+ allowFreeform,
286
+ timeout,
287
+ }),
288
+ ),
284
289
  );
285
290
 
286
291
  // Handle cancel
@@ -346,8 +351,12 @@ export function registerAskUserTools(pi: ExtensionAPI): void {
346
351
  // Session launcher intercept: when user selects new_session, offer compact/direct/cancel
347
352
  if (response.kind === "new_session") {
348
353
  const prefill = response.prefill || "";
349
- const launcherResult = await ctx.ui.custom<SessionLauncherResult | null>(
350
- renderLauncherUI({ prefill }),
354
+ const launcherResult = await withHerdrBlocked(
355
+ pi,
356
+ "ask_user: launch",
357
+ () => ctx.ui.custom<SessionLauncherResult | null>(
358
+ renderLauncherUI({ prefill }),
359
+ ),
351
360
  );
352
361
 
353
362
  if (!launcherResult || launcherResult.action === "cancel") {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pi-unipi/command-enchantment",
3
- "version": "2.1.0",
4
- "description": "Enhanced TUI autocomplete for /unipi:* commands \u2014 colored, sorted, and grouped by package",
3
+ "version": "2.1.2",
4
+ "description": "Enhanced TUI autocomplete for /unipi:* commands colored, sorted, and grouped by package",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
7
7
  "license": "MIT",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pi-unipi/btw",
3
- "version": "2.1.0",
4
- "description": "A pi extension for parallel side conversations with /unipi:btw \u2014 part of the Unipi suite",
3
+ "version": "2.1.2",
4
+ "description": "A pi extension for parallel side conversations with /unipi:btw part of the Unipi suite",
5
5
  "type": "module",
6
6
  "main": "extensions/btw.ts",
7
7
  "license": "MIT",
@@ -37,7 +37,7 @@
37
37
  "@earendil-works/pi-tui": "^0.80.0"
38
38
  },
39
39
  "dependencies": {
40
- "@pi-unipi/core": "2.1.0"
40
+ "@pi-unipi/core": "2.1.2"
41
41
  },
42
42
  "pi": {
43
43
  "extensions": [],
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pi-unipi/cocoindex",
3
- "version": "2.1.0",
4
- "description": "CocoIndex integration for Pi \u2014 AST-aware content indexing, semantic vector search, and incremental pipeline management",
3
+ "version": "2.1.2",
4
+ "description": "CocoIndex integration for Pi AST-aware content indexing, semantic vector search, and incremental pipeline management",
5
5
  "type": "module",
6
6
  "main": "index.ts",
7
7
  "types": "index.ts",
@@ -25,7 +25,7 @@
25
25
  "typebox": "^1.1.38"
26
26
  },
27
27
  "dependencies": {
28
- "@pi-unipi/core": "2.1.0"
28
+ "@pi-unipi/core": "2.1.2"
29
29
  },
30
30
  "optionalDependencies": {
31
31
  "@lancedb/lancedb": "^0.21.0"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pi-unipi/compactor",
3
- "version": "2.1.0",
4
- "description": "Context engine for Pi \u2014 zero-LLM compaction, session continuity, sandbox execution, and tool display optimization",
3
+ "version": "2.1.2",
4
+ "description": "Context engine for Pi zero-LLM compaction, session continuity, sandbox execution, and tool display optimization",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
7
7
  "license": "MIT",
@@ -34,8 +34,8 @@
34
34
  "access": "public"
35
35
  },
36
36
  "dependencies": {
37
- "@pi-unipi/core": "2.1.0",
38
- "@pi-unipi/info-screen": "2.1.0",
37
+ "@pi-unipi/core": "2.1.2",
38
+ "@pi-unipi/info-screen": "2.1.2",
39
39
  "@earendil-works/pi-agent-core": "^0.80.0"
40
40
  },
41
41
  "peerDependencies": {
@@ -51,8 +51,12 @@
51
51
  "better-sqlite3": ">=11.0.0"
52
52
  },
53
53
  "pi": {
54
- "extensions": [],
55
- "skills": [],
54
+ "extensions": [
55
+ "./index.ts"
56
+ ],
57
+ "skills": [
58
+ "./skills"
59
+ ],
56
60
  "prompts": [],
57
61
  "themes": []
58
62
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/core",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
4
4
  "description": "Shared utilities, event types, and constants for Unipi extension suite",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -240,3 +240,29 @@ export function emitEvent(
240
240
  return false;
241
241
  }
242
242
  }
243
+
244
+ /**
245
+ * Wrap an awaitable blocking-UI operation with herdr `blocked` state reporting.
246
+ *
247
+ * Emits `herdr:blocked` active (with a label) before awaiting `fn`, and
248
+ * inactive (with matching label) afterwards — including on throw. This lets
249
+ * the herdr integration surface `blocked` agent status for ask_user and other
250
+ * stop-and-continue UIs instead of `working`.
251
+ *
252
+ * The label should match what's shown to the user so herdr's sidebar reads
253
+ * meaningfully (e.g. "ask_user", "helper viewer").
254
+ */
255
+ export async function withHerdrBlocked<T>(
256
+ pi: { events: { emit: (name: string, payload: unknown) => void } },
257
+ label: string,
258
+ fn: () => Promise<T>,
259
+ ): Promise<T> {
260
+ const event = (active: boolean) =>
261
+ emitEvent(pi, "herdr:blocked", { active, label });
262
+ event(true);
263
+ try {
264
+ return await fn();
265
+ } finally {
266
+ event(false);
267
+ }
268
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pi-unipi/footer",
3
- "version": "2.1.0",
4
- "description": "Persistent status bar for Unipi \u2014 subscribes to UNIPI_EVENTS and renders key stats from all unipi packages",
3
+ "version": "2.1.2",
4
+ "description": "Persistent status bar for Unipi subscribes to UNIPI_EVENTS and renders key stats from all unipi packages",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
7
7
  "license": "MIT",
@@ -32,7 +32,7 @@
32
32
  "access": "public"
33
33
  },
34
34
  "dependencies": {
35
- "@pi-unipi/core": "2.1.0"
35
+ "@pi-unipi/core": "2.1.2"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@earendil-works/pi-coding-agent": "^0.80.0",
@@ -96,6 +96,14 @@ export default function footerExtension(pi: ExtensionAPI): void {
96
96
  state.registry.registerGroup(group);
97
97
  }
98
98
 
99
+ // ─── TPS streaming-event hooks (registered once) ────────────────────────
100
+ // pi.on() has no unsubscribe, so we register these exactly once at factory
101
+ // time (not per session_start) to avoid duplicate handlers accumulating
102
+ // across session restarts. The streamingIndex counter is reset on each
103
+ // session_shutdown. These hooks feed the TPS tracker in real time; the
104
+ // 1s branch-scan in the refresh timer only reconciles persisted messages.
105
+ wireTpsStreamingEvents(pi);
106
+
99
107
  // ─── Session lifecycle ──────────────────────────────────────────────────
100
108
 
101
109
  pi.on("session_start", async (_event, ctx) => {
@@ -127,6 +135,7 @@ export default function footerExtension(pi: ExtensionAPI): void {
127
135
  }
128
136
  state.tuiRef = null;
129
137
  tpsTracker.reset();
138
+ resetTpsStreamingIndex();
130
139
  });
131
140
 
132
141
  // ─── Register commands ──────────────────────────────────────────────────
@@ -152,10 +161,14 @@ function setupFooterUI(pi: ExtensionAPI, ctx: ExtensionContext, state: FooterSta
152
161
  ctx.ui.setFooter((tui, _theme, footerData) => {
153
162
  state.tuiRef = tui;
154
163
 
155
- // Start periodic refresh for time-sensitive segments (e.g. clock)
164
+ // Start periodic refresh for time-sensitive segments (e.g. clock, TPS)
156
165
  if (!state.refreshTimer) {
157
166
  state.refreshTimer = setInterval(() => {
158
- // Feed TPS tracker with per-message data
167
+ // Re-seed TPS tracker from the session branch on each tick.
168
+ // Streaming events (message_start/update/end) handle live updates
169
+ // in real time; this scan reconciles the tracker with persisted
170
+ // messages after compactions, branch switches, or session reloads
171
+ // where in-flight streaming state may have been lost.
159
172
  try {
160
173
  const piCtx = state.piContext as Record<string, unknown> | undefined;
161
174
  if (piCtx?.sessionManager) {
@@ -168,9 +181,10 @@ function setupFooterUI(pi: ExtensionAPI, ctx: ExtensionContext, state: FooterSta
168
181
  const m = e.message;
169
182
  if (!m || m.role !== "assistant") continue;
170
183
  if (m.stopReason === "error" || m.stopReason === "aborted") continue;
171
- const output = m.usage?.output ?? 0;
172
184
  const hasStop = !!m.stopReason;
173
- tpsTracker.onMessageUpdate(msgIndex, output, hasStop);
185
+ // Pass the whole message: TPS tracker counts tokens from content,
186
+ // not from usage.output (which is 0 during streaming).
187
+ tpsTracker.onMessageUpdate(msgIndex, m, hasStop);
174
188
  msgIndex++;
175
189
  }
176
190
  }
@@ -251,3 +265,56 @@ function setupFooterUI(pi: ExtensionAPI, ctx: ExtensionContext, state: FooterSta
251
265
  };
252
266
  }, { placement: "belowEditor" });
253
267
  }
268
+
269
+ // ─── TPS streaming-event hooks ──────────────────────────────────────────────
270
+
271
+ /**
272
+ * Sequential index of the currently-streaming assistant message. Tracked
273
+ * locally because pi does not expose a stable message index on streaming
274
+ * events, and the TPS tracker keys records off this index.
275
+ */
276
+ let tpsStreamingIndex = -1;
277
+
278
+ /** Reset the streaming index (called on session_shutdown). */
279
+ function resetTpsStreamingIndex(): void {
280
+ tpsStreamingIndex = -1;
281
+ }
282
+
283
+ /**
284
+ * Subscribe to pi's message streaming events and feed the TPS tracker in real
285
+ * time. This complements the 1s branch-scan in the refresh timer, which only
286
+ * sees persisted (completed) messages. Without these hooks the tracker would
287
+ * never observe an in-flight assistant message, so live TPS would stay frozen
288
+ * at the last completed message's value.
289
+ *
290
+ * Registered once at extension-factory time (pi.on has no unsubscribe, so we
291
+ * must not re-register per session_start or handlers would accumulate).
292
+ */
293
+ function wireTpsStreamingEvents(pi: ExtensionAPI): void {
294
+ const safe = (fn: () => void) => {
295
+ try { fn(); } catch { /* TPS is best-effort */ }
296
+ };
297
+
298
+ pi.on("message_start", ((event: { message: unknown }) => safe(() => {
299
+ const m = event.message as Record<string, unknown> | undefined;
300
+ if (!m || m.role !== "assistant") return;
301
+ if (m.stopReason === "error" || m.stopReason === "aborted") return;
302
+ tpsStreamingIndex++;
303
+ tpsTracker.onMessageUpdate(tpsStreamingIndex, m, false);
304
+ })) as (event: unknown) => void);
305
+
306
+ pi.on("message_update", ((event: { message: unknown }) => safe(() => {
307
+ if (tpsStreamingIndex < 0) return;
308
+ const m = event.message as Record<string, unknown> | undefined;
309
+ if (!m || m.role !== "assistant") return;
310
+ tpsTracker.onMessageUpdate(tpsStreamingIndex, m, false);
311
+ })) as (event: unknown) => void);
312
+
313
+ pi.on("message_end", ((event: { message: unknown }) => safe(() => {
314
+ if (tpsStreamingIndex < 0) return;
315
+ const m = event.message as Record<string, unknown> | undefined;
316
+ if (!m || m.role !== "assistant") return;
317
+ if (m.stopReason === "error" || m.stopReason === "aborted") return;
318
+ tpsTracker.onMessageUpdate(tpsStreamingIndex, m, true);
319
+ })) as (event: unknown) => void);
320
+ }
@@ -4,37 +4,112 @@
4
4
  * Per-message TPS calculation for live generation rate display.
5
5
  * Tracks individual assistant messages with start/stop timestamps
6
6
  * to measure generation rate excluding idle/tool-execution time.
7
+ *
8
+ * ## Token counting
9
+ *
10
+ * Output tokens are counted from the assistant message *content* (text +
11
+ * thinking blocks) via a lightweight word/char heuristic, NOT from
12
+ * `usage.output`. Rationale:
13
+ * - `usage.output` is only populated by the provider at stream end (or in
14
+ * a final `message_delta`), so it is 0 throughout generation and useless
15
+ * for a live rate.
16
+ * - Counting the streamed text ourselves gives a real, monotonically
17
+ * increasing numerator that reflects how much the model has actually
18
+ * produced so far.
19
+ * - This matches what users perceive as "tokens per second" (visible
20
+ * output rate) and avoids the previous bug where the displayed TPS was
21
+ * always inflated to 90+ t/s (caused by dividing a post-hoc `usage.output`
22
+ * total by a too-short, incorrectly measured elapsed window).
23
+ *
24
+ * The heuristic intentionally over-counts slightly (whitespace + punctuation
25
+ * are treated as tokens). That is acceptable for a live speed indicator and
26
+ * keeps the estimate in the right order of magnitude (within ~10-20% of the
27
+ * provider's reported output tokens for typical English/code output).
7
28
  */
8
29
 
9
30
  /** Per-message TPS record */
10
31
  interface MessageTpsRecord {
11
- /** Message index in the session */
32
+ /** Message index in the session (sequential, assistant-only) */
12
33
  messageIndex: number;
13
- /** Output tokens for this message */
34
+ /** Estimated output tokens produced so far for this message */
14
35
  outputTokens: number;
15
- /** When generation started (Date.now()) */
36
+ /** When generation started (Date.now(), ms) */
16
37
  startedAt: number;
17
- /** When generation completed (Date.now()), 0 if still generating */
38
+ /** When generation completed (Date.now(), ms), 0 if still generating */
18
39
  completedAt: number;
19
- /** Computed TPS for this message */
40
+ /** Computed TPS for this message (final, once completed) */
20
41
  tps: number;
21
42
  }
22
43
 
44
+ /**
45
+ * Estimate the number of tokens in a string.
46
+ *
47
+ * Uses a cheap word + punctuation split. Good enough for a live TPS display;
48
+ * we deliberately avoid pulling in a full BPE tokenizer for performance and
49
+ * footprint reasons.
50
+ */
51
+ function estimateTokens(text: string): number {
52
+ if (!text) return 0;
53
+ // Count word-ish tokens and standalone punctuation.
54
+ // \p{L} / \p{N} keep this Unicode-aware for non-ASCII content.
55
+ const wordish = text.match(/[\p{L}\p{N}]+/gu);
56
+ const punct = text.match(/[^\s\p{L}\p{N}]+/gu);
57
+ const wordCount = wordish ? wordish.length : 0;
58
+ const punctCount = punct ? punct.length : 0;
59
+ return wordCount + punctCount;
60
+ }
61
+
62
+ /**
63
+ * Estimate output tokens from an assistant message by summing text and
64
+ * thinking content lengths. Tool-call argument JSON is also counted since
65
+ * it represents model output.
66
+ *
67
+ * Accepts the raw message shape from pi's session entries (loosely typed).
68
+ */
69
+ function estimateOutputTokens(message: unknown): number {
70
+ if (!message || typeof message !== "object") return 0;
71
+ const m = message as Record<string, unknown>;
72
+ if (m.role !== "assistant") return 0;
73
+ const content = m.content;
74
+ if (!Array.isArray(content)) return 0;
75
+
76
+ let total = 0;
77
+ for (const block of content) {
78
+ if (!block || typeof block !== "object") continue;
79
+ const b = block as Record<string, unknown>;
80
+ const type = b.type;
81
+ if (type === "text" && typeof b.text === "string") {
82
+ total += estimateTokens(b.text);
83
+ } else if (type === "thinking" && typeof b.thinking === "string") {
84
+ total += estimateTokens(b.thinking);
85
+ } else if (type === "tool_use" || type === "toolcall") {
86
+ // Count tool name + serialized arguments as model output.
87
+ const name = typeof b.name === "string" ? b.name : "";
88
+ const input = b.input ?? b.arguments;
89
+ let inputStr: string;
90
+ try {
91
+ inputStr = typeof input === "string" ? input : JSON.stringify(input ?? "");
92
+ } catch {
93
+ inputStr = "";
94
+ }
95
+ total += estimateTokens(name) + estimateTokens(inputStr);
96
+ }
97
+ }
98
+ return total;
99
+ }
100
+
23
101
  /**
24
102
  * Tracks per-message TPS and computes live/session metrics.
25
103
  *
26
- * Usage: Call `onMessageUpdate()` whenever output tokens change.
27
- * The tracker records generation start/stop per message and computes
28
- * live TPS from the current message and session averages excluding idle time.
104
+ * Usage: Call `onMessageUpdate()` on every tick (e.g. 1s interval) with the
105
+ * current assistant message state. The tracker records generation start/stop
106
+ * per message and computes live TPS from elapsed wall-clock time.
29
107
  */
30
108
  export class TpsTracker {
31
- /** Per-message records */
109
+ /** Per-message records (one per assistant message, in order) */
32
110
  private records: MessageTpsRecord[] = [];
33
111
 
34
- /** Highest message index seen so far (for dedup) */
35
- private lastSeenMessageCount = 0;
36
-
37
- /** Total output tokens across all completed messages */
112
+ /** Total estimated output tokens across all messages */
38
113
  private totalOutput = 0;
39
114
 
40
115
  /**
@@ -42,15 +117,16 @@ export class TpsTracker {
42
117
  * Call this on every tick (e.g. 1s interval) with the current state.
43
118
  *
44
119
  * @param messageIndex - Index of the assistant message (0-based, sequential)
45
- * @param outputTokens - Output tokens for this message
120
+ * @param message - The assistant message object (content used to count tokens)
46
121
  * @param hasStopReason - Whether this message has completed (has stopReason)
47
122
  */
48
- onMessageUpdate(messageIndex: number, outputTokens: number, hasStopReason: boolean): void {
123
+ onMessageUpdate(messageIndex: number, message: unknown, hasStopReason: boolean): void {
49
124
  const now = Date.now();
125
+ const outputTokens = estimateOutputTokens(message);
50
126
 
51
127
  // New message — create a record
52
128
  if (messageIndex >= this.records.length) {
53
- // Fill gaps if indices jump
129
+ // Fill gaps if indices jump (shouldn't normally happen)
54
130
  while (this.records.length < messageIndex) {
55
131
  this.records.push({
56
132
  messageIndex: this.records.length,
@@ -62,11 +138,12 @@ export class TpsTracker {
62
138
  }
63
139
 
64
140
  if (hasStopReason && outputTokens > 0) {
65
- // Fast message: already completed on first sighting
66
- // Estimate duration: floor of 1 second, or outputTokens/100, whichever is smaller
67
- const estimatedDuration = Math.max(1, outputTokens / 100);
141
+ // Fast message: already completed on first sighting.
142
+ // We never observed it streaming, so we can't measure real duration.
143
+ // Fall back to a conservative estimate instead of inventing a 1s
144
+ // window (which previously produced inflated TPS values).
145
+ const estimatedDuration = Math.max(0.5, outputTokens / 60);
68
146
  const tps = outputTokens / estimatedDuration;
69
-
70
147
  this.records.push({
71
148
  messageIndex,
72
149
  outputTokens,
@@ -85,7 +162,6 @@ export class TpsTracker {
85
162
  tps: 0,
86
163
  });
87
164
  }
88
- this.lastSeenMessageCount = messageIndex + 1;
89
165
  return;
90
166
  }
91
167
 
@@ -93,23 +169,24 @@ export class TpsTracker {
93
169
  const record = this.records[messageIndex];
94
170
  if (!record) return;
95
171
 
96
- record.outputTokens = outputTokens;
172
+ if (record.completedAt === 0) {
173
+ // Still generating — update token count (live TPS computed on demand)
174
+ record.outputTokens = outputTokens;
97
175
 
98
- if (record.completedAt === 0 && hasStopReason) {
99
- // Message just completed
100
- record.completedAt = now;
101
- const durationSec = (record.completedAt - record.startedAt) / 1000;
102
- record.tps = durationSec > 0 ? outputTokens / durationSec : outputTokens;
103
- this.totalOutput += outputTokens;
104
- } else if (record.completedAt === 0) {
105
- // Still generating — update output tokens (live TPS computed on demand)
176
+ if (hasStopReason) {
177
+ // Message just completed
178
+ record.completedAt = now;
179
+ const durationSec = (record.completedAt - record.startedAt) / 1000;
180
+ record.tps = durationSec > 0 ? record.outputTokens / durationSec : 0;
181
+ this.totalOutput += record.outputTokens;
182
+ }
106
183
  }
107
184
  }
108
185
 
109
186
  /**
110
187
  * Get the live TPS from the currently generating message.
111
188
  * Returns the instantaneous rate based on tokens generated so far
112
- * in the current message divided by elapsed time.
189
+ * in the current message divided by elapsed wall-clock time.
113
190
  */
114
191
  getLiveTps(): number {
115
192
  // Find the last record that's still generating
@@ -177,7 +254,7 @@ export class TpsTracker {
177
254
  }
178
255
 
179
256
  /**
180
- * Get total output tokens for the session.
257
+ * Get total output tokens for the session (estimated).
181
258
  */
182
259
  getTotalOutput(): number {
183
260
  // Include tokens from incomplete messages too
@@ -195,7 +272,6 @@ export class TpsTracker {
195
272
  */
196
273
  reset(): void {
197
274
  this.records = [];
198
- this.lastSeenMessageCount = 0;
199
275
  this.totalOutput = 0;
200
276
  }
201
277
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pi-unipi/info-screen",
3
- "version": "2.1.0",
4
- "description": "Dashboard and module registry for Unipi \u2014 configurable info overlay with tabbed groups",
3
+ "version": "2.1.2",
4
+ "description": "Dashboard and module registry for Unipi configurable info overlay with tabbed groups",
5
5
  "type": "module",
6
6
  "main": "index.ts",
7
7
  "license": "MIT",
@@ -33,7 +33,7 @@
33
33
  "access": "public"
34
34
  },
35
35
  "dependencies": {
36
- "@pi-unipi/core": "2.1.0"
36
+ "@pi-unipi/core": "2.1.2"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@earendil-works/pi-coding-agent": "^0.80.0",
@@ -94,15 +94,24 @@ export class InfoOverlay implements Component {
94
94
 
95
95
  /**
96
96
  * Fetch all groups in background. Each resolves independently.
97
+ *
98
+ * Each fetch is deferred to a macrotask (setTimeout 0) so the constructor
99
+ * returns immediately. Without this, getGroupData() runs each group's
100
+ * dataProvider synchronously up to its first `await` before yielding —
101
+ * heavy providers (usage stats parse 1GB+ of session files, memory scans)
102
+ * blocked the session_start handler for seconds.
97
103
  */
98
- private async fetchAllBackground(): Promise<void> {
104
+ private fetchAllBackground(): void {
99
105
  for (const group of this.groups) {
100
- // Fire each fetch independently don't await sequentially
101
- infoRegistry.getGroupData(group.id).then(() => {
102
- this.groupLoading.set(group.id, false);
103
- }).catch(() => {
104
- this.groupLoading.set(group.id, false);
105
- });
106
+ // Defer each fetch to a macrotask so the overlay constructs instantly.
107
+ setTimeout(() => {
108
+ if (this._destroyed) return;
109
+ infoRegistry.getGroupData(group.id).then(() => {
110
+ this.groupLoading.set(group.id, false);
111
+ }).catch(() => {
112
+ this.groupLoading.set(group.id, false);
113
+ });
114
+ }, 0);
106
115
  }
107
116
  }
108
117
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pi-unipi/input-shortcuts",
3
- "version": "2.1.0",
4
- "description": "Keyboard shortcuts for stash/restore, undo/redo, clipboard, and thinking toggle \u2014 chord-based overlay system",
3
+ "version": "2.1.2",
4
+ "description": "Keyboard shortcuts for stash/restore, undo/redo, clipboard, and thinking toggle chord-based overlay system",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
7
7
  "license": "MIT",
@@ -33,7 +33,7 @@
33
33
  "access": "public"
34
34
  },
35
35
  "dependencies": {
36
- "@pi-unipi/core": "2.1.0"
36
+ "@pi-unipi/core": "2.1.2"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@earendil-works/pi-coding-agent": "^0.80.0",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pi-unipi/kanboard",
3
- "version": "2.1.0",
4
- "description": "Visualization layer for unipi workflow \u2014 HTTP server with htmx/Alpine.js UI, modular parsers, TUI overlay, and kanban board",
3
+ "version": "2.1.2",
4
+ "description": "Visualization layer for unipi workflow HTTP server with htmx/Alpine.js UI, modular parsers, TUI overlay, and kanban board",
5
5
  "type": "module",
6
6
  "main": "index.ts",
7
7
  "license": "MIT",
@@ -39,7 +39,7 @@
39
39
  "access": "public"
40
40
  },
41
41
  "dependencies": {
42
- "@pi-unipi/core": "2.1.0"
42
+ "@pi-unipi/core": "2.1.2"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "@earendil-works/pi-coding-agent": "^0.80.0",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pi-unipi/mcp",
3
- "version": "2.1.0",
4
- "description": "MCP server management extension for Pi coding agent \u2014 browse, add, configure, and use MCP servers",
3
+ "version": "2.1.2",
4
+ "description": "MCP server management extension for Pi coding agent browse, add, configure, and use MCP servers",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
7
7
  "license": "MIT",
@@ -27,7 +27,7 @@
27
27
  "README.md"
28
28
  ],
29
29
  "dependencies": {
30
- "@pi-unipi/core": "2.1.0"
30
+ "@pi-unipi/core": "2.1.2"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "@earendil-works/pi-coding-agent": "^0.80.0",
@@ -22,6 +22,10 @@ export const DEFAULT_PALACE = path.join(os.homedir(), ".mempalace", "palace");
22
22
 
23
23
  const INSTALL_FLAG = path.join(os.homedir(), ".unipi", "memory", ".mempalace-install");
24
24
  const MIGRATED_FLAG = path.join(os.homedir(), ".unipi", "memory", ".mempalace-migrated");
25
+ /** Flag written after a successful ping, so subsequent sessions can skip
26
+ * the ~0.5s Python cold-start sanity check. Stale after PING_VERIFIED_TTL_MS. */
27
+ const PING_VERIFIED_FLAG = path.join(os.homedir(), ".unipi", "memory", ".mempalace-ping-verified");
28
+ const PING_VERIFIED_TTL_MS = 24 * 60 * 60 * 1000; // 24h
25
29
 
26
30
  /** Path to the bundled bridge script. */
27
31
  const BRIDGE_PATH = path.join(dirname(fileURLToPath(import.meta.url)), "bridge", "mempalace_bridge.py");
@@ -166,6 +170,30 @@ export function ensureMempalace(): MempalaceInstall | null {
166
170
  /** Drop the cached install record (forces re-detection next session). */
167
171
  export function invalidateInstallCache(): void {
168
172
  try { if (fs.existsSync(INSTALL_FLAG)) fs.unlinkSync(INSTALL_FLAG); } catch { /* ignore */ }
173
+ invalidatePingVerified();
174
+ }
175
+
176
+ /** Was the palace ping-verified recently enough to trust without re-pinging? */
177
+ export function isPingVerified(): boolean {
178
+ try {
179
+ if (!fs.existsSync(PING_VERIFIED_FLAG)) return false;
180
+ const ts = Number.parseInt(fs.readFileSync(PING_VERIFIED_FLAG, "utf-8").trim(), 10);
181
+ if (!Number.isFinite(ts)) return false;
182
+ return Date.now() - ts < PING_VERIFIED_TTL_MS;
183
+ } catch { return false; }
184
+ }
185
+
186
+ /** Mark the palace as ping-verified (written after a successful ping). */
187
+ export function markPingVerified(): void {
188
+ try {
189
+ fs.mkdirSync(path.dirname(PING_VERIFIED_FLAG), { recursive: true });
190
+ fs.writeFileSync(PING_VERIFIED_FLAG, String(Date.now()), "utf-8");
191
+ } catch { /* ignore */ }
192
+ }
193
+
194
+ /** Drop the ping-verified flag (forces a real ping next session). */
195
+ export function invalidatePingVerified(): void {
196
+ try { if (fs.existsSync(PING_VERIFIED_FLAG)) fs.unlinkSync(PING_VERIFIED_FLAG); } catch { /* ignore */ }
169
197
  }
170
198
 
171
199
  /** Has the one-way legacy migration been completed? */
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/memory",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
4
4
  "description": "Persistent cross-session memory with MemPalace backend (auto-installed) and SQLite fallback for Pi coding agent",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -43,8 +43,8 @@
43
43
  "better-sqlite3": "^12.9.0",
44
44
  "sqlite-vec": "^0.1.9",
45
45
  "js-yaml": "^4.1.0",
46
- "@pi-unipi/core": "2.1.0",
47
- "@pi-unipi/info-screen": "2.1.0"
46
+ "@pi-unipi/core": "2.1.2",
47
+ "@pi-unipi/info-screen": "2.1.2"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "@earendil-works/pi-coding-agent": "^0.80.0",
@@ -19,6 +19,9 @@ import {
19
19
  runBridge,
20
20
  isMigrated,
21
21
  markMigrated,
22
+ isPingVerified,
23
+ markPingVerified,
24
+ invalidatePingVerified,
22
25
  DEFAULT_PALACE,
23
26
  type MempalaceInstall,
24
27
  type MempalaceRecord,
@@ -262,6 +265,22 @@ export class MemoryStorage {
262
265
  return this.backend === "mempalace" && this.mempalaceInstall !== null;
263
266
  }
264
267
 
268
+ /**
269
+ * Run a MemPalace bridge command, invalidating the ping-verified flag
270
+ * when the call fails. This ensures a palace that breaks after the
271
+ * startup ping-skip gets re-verified on the next session.
272
+ */
273
+ private memPalaceCall<T>(cmd: string, args: Record<string, unknown> = {}): T | null {
274
+ const install = this.mempalaceInstall;
275
+ if (!install) return null;
276
+ const result = runBridge<T>(install, this.palacePath, cmd, args);
277
+ if (result === null) {
278
+ // Backend didn't respond — force a real ping next session.
279
+ invalidatePingVerified();
280
+ }
281
+ return result;
282
+ }
283
+
265
284
  /**
266
285
  * Initialize storage. Tries MemPalace first (auto-install + one-way
267
286
  * auto-migration of legacy memories); falls back to SQLite if MemPalace
@@ -325,8 +344,14 @@ export class MemoryStorage {
325
344
  if (!install) return false;
326
345
 
327
346
  // Sanity ping — if the palace/bridge is broken, fall back.
328
- const ok = runBridge<string>(install, this.palacePath, "ping");
329
- if (ok !== "pong") return false;
347
+ // Skip the ~0.5s Python cold-start when we ping-verified recently;
348
+ // the flag is invalidated on any backend failure so a broken palace
349
+ // is re-checked on the next session.
350
+ if (!isPingVerified()) {
351
+ const ok = runBridge<string>(install, this.palacePath, "ping");
352
+ if (ok !== "pong") return false;
353
+ markPingVerified();
354
+ }
330
355
 
331
356
  this.mempalaceInstall = install;
332
357
  this.backend = "mempalace";
@@ -539,8 +564,7 @@ export class MemoryStorage {
539
564
  * consistent and durable as a fallback source.
540
565
  */
541
566
  private storeMempalace(record: MemoryRecord): void {
542
- const install = this.mempalaceInstall!;
543
- runBridge(install, this.palacePath, "store", {
567
+ this.memPalaceCall("store", {
544
568
  record: {
545
569
  id: record.id,
546
570
  title: record.title,
@@ -572,8 +596,7 @@ export class MemoryStorage {
572
596
  */
573
597
  syncOrphanedFiles(): number {
574
598
  if (this.isMempalace()) {
575
- const install = this.mempalaceInstall!;
576
- const synced = runBridge<number>(install, this.palacePath, "sync_orphaned", {
599
+ const synced = this.memPalaceCall<number>("sync_orphaned", {
577
600
  project_dir: this.scopeDir,
578
601
  wing: this.projectName,
579
602
  });
@@ -636,8 +659,7 @@ export class MemoryStorage {
636
659
  */
637
660
  hasByTitle(title: string): boolean {
638
661
  if (this.isMempalace()) {
639
- const install = this.mempalaceInstall!;
640
- return runBridge<boolean>(install, this.palacePath, "has_title", {
662
+ return this.memPalaceCall<boolean>("has_title", {
641
663
  wing: this.projectName,
642
664
  title,
643
665
  }) ?? false;
@@ -654,9 +676,8 @@ export class MemoryStorage {
654
676
  */
655
677
  findSimilarByTitle(title: string, threshold = 0.6): Array<{ record: MemoryRecord; similarity: number }> {
656
678
  if (this.isMempalace()) {
657
- const install = this.mempalaceInstall!;
658
- const rows = runBridge<Array<{ record: MempalaceRecord; similarity: number }>>(
659
- install, this.palacePath, "find_similar",
679
+ const rows = this.memPalaceCall<Array<{ record: MempalaceRecord; similarity: number }>>(
680
+ "find_similar",
660
681
  { wing: this.projectName, title, threshold },
661
682
  ) ?? [];
662
683
  return rows.map((r) => ({ record: toMemoryRecord(r.record), similarity: r.similarity }));
@@ -694,8 +715,7 @@ export class MemoryStorage {
694
715
  */
695
716
  getById(id: string): MemoryRecord | null {
696
717
  if (this.isMempalace()) {
697
- const install = this.mempalaceInstall!;
698
- const rec = runBridge<MempalaceRecord | null>(install, this.palacePath, "get", { id });
718
+ const rec = this.memPalaceCall<MempalaceRecord | null>("get", { id });
699
719
  return rec ? toMemoryRecord(rec) : null;
700
720
  }
701
721
  if (!this.db) throw new Error("Storage not initialized");
@@ -721,8 +741,7 @@ export class MemoryStorage {
721
741
  */
722
742
  getByTitle(title: string): MemoryRecord | null {
723
743
  if (this.isMempalace()) {
724
- const install = this.mempalaceInstall!;
725
- const rec = runBridge<MempalaceRecord | null>(install, this.palacePath, "get_by_title", {
744
+ const rec = this.memPalaceCall<MempalaceRecord | null>("get_by_title", {
726
745
  wing: this.projectName,
727
746
  title,
728
747
  });
@@ -768,8 +787,7 @@ export class MemoryStorage {
768
787
  */
769
788
  listAll(): Array<{ id: string; title: string; type: string }> {
770
789
  if (this.isMempalace()) {
771
- const install = this.mempalaceInstall!;
772
- const items = runBridge<MempalaceListItem[]>(install, this.palacePath, "list", {
790
+ const items = this.memPalaceCall<MempalaceListItem[]>("list", {
773
791
  wing: this.projectName,
774
792
  }) ?? [];
775
793
  return items;
@@ -785,8 +803,7 @@ export class MemoryStorage {
785
803
  */
786
804
  delete(id: string): boolean {
787
805
  if (this.isMempalace()) {
788
- const install = this.mempalaceInstall!;
789
- const ok = runBridge<boolean>(install, this.palacePath, "delete", { id }) ?? false;
806
+ const ok = this.memPalaceCall<boolean>("delete", { id }) ?? false;
790
807
  // Also remove the markdown tier if present.
791
808
  try {
792
809
  const mdPath = path.join(this.scopeDir, `${id}.md`);
@@ -833,8 +850,7 @@ export class MemoryStorage {
833
850
  */
834
851
  search(query: string, limit = 10, embedding?: Float32Array | null): SearchResult[] {
835
852
  if (this.isMempalace()) {
836
- const install = this.mempalaceInstall!;
837
- const rows = runBridge<MempalaceSearchResult[]>(install, this.palacePath, "search", {
853
+ const rows = this.memPalaceCall<MempalaceSearchResult[]>("search", {
838
854
  query,
839
855
  wing: this.projectName,
840
856
  limit,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pi-unipi/milestone",
3
- "version": "2.1.0",
4
- "description": "Lifecycle layer for project-level goals \u2014 MILESTONES.md tracking, session hooks, auto-sync",
3
+ "version": "2.1.2",
4
+ "description": "Lifecycle layer for project-level goals MILESTONES.md tracking, session hooks, auto-sync",
5
5
  "type": "module",
6
6
  "main": "index.ts",
7
7
  "license": "MIT",
@@ -29,7 +29,7 @@
29
29
  "access": "public"
30
30
  },
31
31
  "dependencies": {
32
- "@pi-unipi/core": "2.1.0"
32
+ "@pi-unipi/core": "2.1.2"
33
33
  },
34
34
  "peerDependencies": {
35
35
  "@earendil-works/pi-coding-agent": "^0.80.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/notify",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
4
4
  "description": "Cross-platform notification extension for Pi — native OS, Gotify, and Telegram notifications for agent lifecycle events",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/ralph",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
4
4
  "description": "Long-running iterative development loops for Pi coding agent",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -27,8 +27,8 @@
27
27
  "access": "public"
28
28
  },
29
29
  "dependencies": {
30
- "@pi-unipi/core": "2.1.0",
31
- "@pi-unipi/info-screen": "2.1.0"
30
+ "@pi-unipi/core": "2.1.2",
31
+ "@pi-unipi/info-screen": "2.1.2"
32
32
  },
33
33
  "peerDependencies": {
34
34
  "@earendil-works/pi-ai": "^0.80.0",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pi-unipi/subagents",
3
- "version": "2.1.0",
4
- "description": "Subagents for UniPi \u2014 parallel execution, file locking, workflow integration",
3
+ "version": "2.1.2",
4
+ "description": "Subagents for UniPi parallel execution, file locking, workflow integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -17,9 +17,9 @@
17
17
  "test": "node --test src/__tests__/*.test.ts"
18
18
  },
19
19
  "dependencies": {
20
- "@pi-unipi/core": "2.1.0",
21
- "@pi-unipi/workflow": "2.1.0",
22
- "@pi-unipi/info-screen": "2.1.0",
20
+ "@pi-unipi/core": "2.1.2",
21
+ "@pi-unipi/workflow": "2.1.2",
22
+ "@pi-unipi/info-screen": "2.1.2",
23
23
  "@earendil-works/pi-agent-core": "^0.80.0"
24
24
  },
25
25
  "devDependencies": {
@@ -12,7 +12,7 @@ import { Type } from "typebox";
12
12
  import { existsSync, readdirSync } from "node:fs";
13
13
  import { join } from "node:path";
14
14
  import { homedir } from "node:os";
15
- import { emitEvent, MODULES, UNIPI_EVENTS, type UnipiBadgeGenerateRequestEvent } from "@pi-unipi/core";
15
+ import { emitEvent, MODULES, UNIPI_EVENTS, withHerdrBlocked, type UnipiBadgeGenerateRequestEvent } from "@pi-unipi/core";
16
16
  import { AgentManager } from "./agent-manager.js";
17
17
  import { initConfig } from "./config.js";
18
18
  import { type AgentActivity, type NotificationDetails, BUILTIN_TYPES } from "./types.js";
@@ -758,28 +758,32 @@ Guidelines:
758
758
  // Open conversation viewer overlay if requested
759
759
  if (params.view && record.session) {
760
760
  const activity = agentActivity.get(record.id);
761
- await ctx.ui.custom<undefined>(
762
- (tui, theme, _keybindings, done) => {
763
- return new ConversationViewer(
764
- tui,
765
- record.session!,
766
- {
767
- type: record.type,
768
- description: record.description,
769
- status: record.status,
770
- toolUses: record.toolUses,
771
- startedAt: record.startedAt,
772
- completedAt: record.completedAt,
773
- },
774
- activity,
775
- theme,
776
- done,
777
- );
778
- },
779
- {
780
- overlay: true,
781
- overlayOptions: { anchor: "center", width: "90%" },
782
- },
761
+ await withHerdrBlocked(
762
+ pi,
763
+ "helper viewer",
764
+ () => ctx.ui.custom<undefined>(
765
+ (tui, theme, _keybindings, done) => {
766
+ return new ConversationViewer(
767
+ tui,
768
+ record.session!,
769
+ {
770
+ type: record.type,
771
+ description: record.description,
772
+ status: record.status,
773
+ toolUses: record.toolUses,
774
+ startedAt: record.startedAt,
775
+ completedAt: record.completedAt,
776
+ },
777
+ activity,
778
+ theme,
779
+ done,
780
+ );
781
+ },
782
+ {
783
+ overlay: true,
784
+ overlayOptions: { anchor: "center", width: "90%" },
785
+ },
786
+ ),
783
787
  );
784
788
  }
785
789
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pi-unipi/updater",
3
- "version": "2.1.0",
4
- "description": "Auto-updater, changelog browser, and readme browser for Unipi \u2014 checks npm registry, renders CHANGELOG.md and README.md files in TUI overlays",
3
+ "version": "2.1.2",
4
+ "description": "Auto-updater, changelog browser, and readme browser for Unipi checks npm registry, renders CHANGELOG.md and README.md files in TUI overlays",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
7
7
  "license": "MIT",
@@ -33,7 +33,7 @@
33
33
  "access": "public"
34
34
  },
35
35
  "dependencies": {
36
- "@pi-unipi/core": "2.1.0"
36
+ "@pi-unipi/core": "2.1.2"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@earendil-works/pi-coding-agent": "^0.80.0",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pi-unipi/utility",
3
- "version": "2.1.0",
4
- "description": "Utility commands and tools for Pi coding agent \u2014 lifecycle, diagnostics, cache, analytics, display, batch execution",
3
+ "version": "2.1.2",
4
+ "description": "Utility commands and tools for Pi coding agent lifecycle, diagnostics, cache, analytics, display, batch execution",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
7
7
  "license": "MIT",
@@ -34,7 +34,7 @@
34
34
  "access": "public"
35
35
  },
36
36
  "dependencies": {
37
- "@pi-unipi/core": "2.1.0",
37
+ "@pi-unipi/core": "2.1.2",
38
38
  "diff": "^7.0.0",
39
39
  "@shikijs/cli": "^4.0.2"
40
40
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/web-api",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
4
4
  "description": "Web search, read, and summarize tools with provider-based backend selection for Pi coding agent",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -31,7 +31,7 @@
31
31
  "README.md"
32
32
  ],
33
33
  "dependencies": {
34
- "@pi-unipi/core": "2.1.0",
34
+ "@pi-unipi/core": "2.1.2",
35
35
  "defuddle": "^0.18.1",
36
36
  "linkedom": "^0.18.12",
37
37
  "lodash": "^4.17.21",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/workflow",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
4
4
  "description": "Structured development workflow commands for Pi coding agent",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -26,7 +26,7 @@
26
26
  "access": "public"
27
27
  },
28
28
  "dependencies": {
29
- "@pi-unipi/core": "2.1.0"
29
+ "@pi-unipi/core": "2.1.2"
30
30
  },
31
31
  "peerDependencies": {
32
32
  "@earendil-works/pi-ai": "^0.80.0",