@yunazgr/pi-companion 0.3.1 → 0.3.3

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/README.md CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  Lightweight, local-first remote control for Pi sessions.
4
4
 
5
- **New in 0.3.1:** claymorphic automation workspaces with searchable, status-filtered, paginated lists; Summary/History tabs and calendar-style run cards; sanitized Markdown/ADF/JSON results; a staged job editor with strict validation and optional failed-action retries (up to 5, default 10-second interval). Per-automation history keeps the latest 30 runs by default, with 10 per page. Automated sessions are clearly marked as answers-only. Type `/` in a normal session to discover available commands, skills, and prompt templates. See [automations](docs/automations.md).
5
+ **New in 0.3.3:** Provider usage now shows measured session tokens and cost without pretending they are subscription quotas. Compatible extensions can still supply real weekly/5-hour limits. Mobile sessions, automations and usage use cards; the overview has compact activity filters; automation buttons share a consistent style; pull-to-refresh is gesture-only. No data or provider credentials are collected by the daemon outside the already shared session metadata.
6
+
7
+ **New in 0.3.2:** responsive automation and run-history tables, rounded status chips, matching clay-icon headers, and a single-column automation workspace with full-width mobile controls. **Edit automation** scrolls to and focuses the inline editor. Pull to refresh Overview, Sessions, Automations, or Settings—with a gesture; unsaved settings stay intact. Camera-policy guidance now explains HTTPS proxy configuration. Existing rich results, staged job editing, optional retries, and answers-only automation sessions are retained. See [automations](docs/automations.md).
6
8
 
7
9
  Pi Companion is deliberately not another agent runtime. Pi owns execution and conversation state. A single Rust daemon owns session discovery, pairing, temporary file exchange, browser fan-out, and the embedded web UI.
8
10
 
@@ -151,6 +153,12 @@ Pi's `companion_ask_user` tool asks one to four questions at once. Each question
151
153
 
152
154
  Dialogs from other extensions (`ctx.ui.select`, `ctx.ui.confirm`, `ctx.ui.input`) are relayed to the same sheet while the terminal dialog stays open: whichever side answers first wins and the other closes. Question tools that draw their own `ctx.ui.custom` picker are relayed through a small adapter: pi-jar's `jar_ask` is supported, and a companion answer completes its terminal picker. Other `ctx.ui.custom` components and `ctx.ui.editor` stay terminal-only because they cannot be answered from outside. Pending questions are part of the session snapshot, so a browser that connects later still sees them.
153
155
 
156
+ ## Refreshing your workspace
157
+
158
+ On Overview, Sessions, Automations, and Settings, pull down from the top of the page and release when prompted to refresh current data. Each page also provides a keyboard-accessible **Refresh** button with progress and error feedback. This refreshes data without reloading the app; Settings preserves unsaved edits. Gestures inside inputs and nested scrollable lists are left alone.
159
+
160
+ The Sessions, Devices, and Settings pages now share the automation page’s clay-icon header style. See [device management](docs/devices.webp), [settings](docs/settings.webp), and [mobile settings](docs/settings-mobile.webp).
161
+
154
162
  ## Notifications, camera and catching up
155
163
 
156
164
  Settings → **Notifications and camera** (on every device, not just the console) asks the browser for both permissions from a tap, as browsers require, and shows whether each is allowed, blocked or unavailable. Overview also offers a one-time "Turn on notifications" banner.
@@ -186,7 +194,7 @@ Network loss and daemon restarts reconnect automatically with bounded backoff. H
186
194
 
187
195
  Shared session title, model, effort and working directory refresh on Pi events and once per second while sharing, including while idle. Session detail shows context-window usage and estimated session cost. Native Pi context and recorded usage costs take precedence; independent extension reports fill unavailable fields. Missing usage is shown as unavailable, not zero.
188
196
 
189
- **Settings → Usage** is available on the console and paired devices. It displays the latest snapshot per provider: weekly usage, 5-hour usage when reported, reset times, source and snapshot age. Quota snapshots are not added across sessions or accounts. These are extension-reported limits, not billing totals or locally inferred quotas; providers without a quota report remain unavailable.
197
+ **Settings → Usage** is available on the console and paired devices. It displays the latest snapshot per provider: weekly usage, 5-hour usage when reported, reset times, source and snapshot age. Quota snapshots are not added across sessions or accounts. Those limits are extension-reported, not billing totals or locally inferred quotas. Companion additionally reports actual session token consumption and cost grouped by the model's provider from Pi's recorded assistant messages; this is session-only data, not an account-wide tally. Settings selects the newest consumption independently of quota freshness and displays their timestamps and session-ended indicators separately. When no quota adapter is installed, 5-hour and weekly quotas still show as unavailable rather than fabricated percentages.
190
198
 
191
199
  Extensions can publish a provider-neutral event without importing Companion:
192
200
 
@@ -254,6 +262,18 @@ which destroys one temporary file by opaque id.
254
262
 
255
263
  This means the agent can consume uploaded artifacts using its normal file capabilities while Pi Companion retains ownership of upload placement and cleanup.
256
264
 
265
+ ### Camera access through Cloudflare or another HTTPS proxy
266
+
267
+ Camera scanning works through an HTTPS tunnel; Companion serves `Permissions-Policy: camera=(self)`. A proxy response-header rule that replaces it with `camera=()` blocks the camera before the browser can ask permission. JavaScript cannot override that restriction.
268
+
269
+ For a Cloudflare **Modify Response Header** rule scoped to your Companion hostname (for example, `http.host eq "companion.readynaz.com"`), set `Permissions-Policy` to:
270
+
271
+ ```text
272
+ geolocation=(), camera=(self), microphone=(), payment=(), usb=(), accelerometer=(), gyroscope=(), magnetometer=()
273
+ ```
274
+
275
+ Exclude the Companion hostname from any broader rule that still sets `camera=()`, and check Workers or other proxies for duplicate overrides. Keep Cloudflare Access authentication enabled and tunnel only the paired-device listener. Open Companion directly, not inside an iframe. After signing in, verify the final page response in browser DevTools → Network: its camera directive must be `camera=(self)`. The Access login redirect is not the app response. Reload the app (and restart an older daemon after upgrading), then allow Camera in browser site permissions. Manual pairing-code entry remains available.
276
+
257
277
  ## Workspace and tunnel connection errors
258
278
 
259
279
  If a tunnel closes, the workspace stops, or your device loses its network, Companion shows an actionable **Workspace connection interrupted** notice with **Retry now** and reconnect instructions. On initial connection failure, it shows **Workspace unavailable**, not a misleading empty session list or “session not found.” Gateway errors (including HTTP 502/503/504 and tunnel-provider errors) are translated into readable messages rather than raw HTML or JSON parse errors. A browser cannot always distinguish a closed tunnel from a daemon, DNS or network failure, so these messages describe possible causes rather than claiming certainty.
@@ -354,7 +374,7 @@ Clone the repository, then:
354
374
 
355
375
  `npm run serve` builds the embedded Svelte UI and starts the Rust daemon in the foreground. It prints:
356
376
 
357
- Pi Companion v0.3.1
377
+ Pi Companion v0.3.2
358
378
 
359
379
  Console http://127.0.0.1:43721
360
380
  Paired devices http://127.0.0.1:43722
@@ -1,10 +1,16 @@
1
- # Automations (0.3.1)
1
+ # Automations (0.3.2)
2
2
 
3
3
  Automations are persisted, named JSON scripts run by the Companion daemon. Open **Automations** in the navigation to view definitions, start/stop runs, and inspect timestamped run history. Click a run to view its captured output and Pi summary. Desktop console users can create, edit, delete, and enable/disable definitions; mobile and paired-device users can only inspect and start/stop them.
4
4
 
5
5
  ## Workspace & editor
6
6
 
7
- The claymorphic automation collection supports name search, Enabled/Disabled filters, ten-item pagination, and its own scrollable card region. The detail workspace has **Summary** and **Run history** tabs; history is newest-first with calendar tiles, duration, status filters, and ten-item pagination over each automation’s retained history (latest 30 finished runs by default, configurable 1–1000).
7
+ The claymorphic automation collection uses the same responsive tabular style as overview sessions, with rounded status chips, name search, segmented Enabled/Disabled filters, ten-item pagination, and a scrollable row region. Redundant statistic tiles are removed. On phones, rows stack their labelled fields without horizontal scrolling.
8
+
9
+ The detail workspace is a single-column bento layout with a clay back button and full-width run control below its title. **Summary** and **Run history** tabs fill the container. History is a newest-first responsive table with start time, duration, status filters, and ten-item pagination over each automation’s retained history (latest 30 finished runs by default, configurable 1–1000).
10
+
11
+ Choose **Edit automation** to reveal the editor at the top of Summary, automatically scroll it into view, and focus its name field. This keeps the routine context nearby without navigating to another page. Nothing changes until you save. Desktop console editing remains required.
12
+
13
+ Overview, Sessions, Automations, and Settings support pull-to-refresh from the top of the page and an accessible **Refresh** button. Pull down on non-interactive page content, then release when prompted. Nested scroll regions and form controls keep their own gestures. Settings refresh never discards unsaved edits.
8
14
 
9
15
  The job editor separates name, enablement, UTC cron, optional retry policy, preconditions, actions, and post-actions. Each step is a reorderable JSON DSL object with Command/Pi templates. Saving validates the definition, cron ranges, action types, arguments, absolute directories, timeouts, step count, and retry bounds; the daemon validates again before persistence. Only computer desktop administrators can author definitions.
10
16
 
@@ -15,10 +21,11 @@ Run results have a formatted and raw view. Formatted results split daemon execut
15
21
  Screenshots use only demo data. The same generator refreshes the existing overview, session, and mobile screenshots too.
16
22
 
17
23
  ![Automations on the overview below live sessions](overview-automations.webp)
18
- ![Searchable automation cards](automations.webp)
19
- ![Automation cards on a phone](automations-mobile.webp)
24
+ ![Searchable automation rows](automations.webp)
25
+ ![Automation rows on a phone](automations-mobile.webp)
20
26
  ![Run history on a phone](automation-history-mobile.webp)
21
27
  ![Automation summary and execution steps](automation-detail.webp)
28
+ ![Single-column automation detail on a phone](automation-detail-mobile.webp)
22
29
  ![Filtered, paginated run history](automation-history.webp)
23
30
  ![Friendly staged job editor](automation-editor.webp)
24
31
  ![Rich review summary with linked PR table](automation-result.webp)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yunazgr/pi-companion",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Local-first remote control for Pi sessions: live activity feed, steering, git diff, file drop and phone pairing from a single Rust daemon.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -31,7 +31,7 @@ async function receive(line: string) {
31
31
  case "initialize": {
32
32
  const versions = ["2025-06-18", "2025-03-26", "2024-11-05"];
33
33
  const requested = message.params?.protocolVersion;
34
- reply({ protocolVersion: versions.includes(String(requested)) ? requested : versions[0], capabilities: { tools: {} }, serverInfo: { name: "pi-companion-automations", version: "0.3.1" } });
34
+ reply({ protocolVersion: versions.includes(String(requested)) ? requested : versions[0], capabilities: { tools: {} }, serverInfo: { name: "pi-companion-automations", version: "0.3.2" } });
35
35
  return;
36
36
  }
37
37
  case "ping": reply({}); return;
package/src/protocol.ts CHANGED
@@ -43,6 +43,11 @@ export type ProviderUsage = {
43
43
  updatedAt: string;
44
44
  weekly?: UsageWindow;
45
45
  fiveHour?: UsageWindow;
46
+ /** Observed consumption in this shared Pi session, not account quota utilization. */
47
+ sessionTokens?: number;
48
+ sessionCost?: number;
49
+ /** Native consumption freshness, independent of the account quota report. */
50
+ sessionUpdatedAt?: string;
46
51
  };
47
52
  export type SessionTelemetry = {
48
53
  context?: { tokens?: number; window?: number; percent?: number; source?: string };
package/src/telemetry.ts CHANGED
@@ -51,6 +51,7 @@ export function normalizeTelemetry(value: unknown, source = "extension", now = D
51
51
  /** Independent sources may supply different fields; one broken/missing adapter cannot erase another. */
52
52
  export class TelemetryRelay {
53
53
  private sources = new Map<string, { telemetry: SessionTelemetry; contextAt?: number; costAt?: number }>();
54
+ private nativeUsage = new Map<string, { fingerprint: string; updatedAt: string }>();
54
55
 
55
56
  ingest(value: unknown, fallbackSource = "extension", now = Date.now()) {
56
57
  const raw = record(value);
@@ -140,6 +141,43 @@ export class TelemetryRelay {
140
141
  }
141
142
  if (known && Number.isFinite(total)) result.cost = { amount: total, currency: "USD", source: "native" };
142
143
  } catch { /* Preserve extension estimate when native usage is unavailable. */ }
144
+ // Native session entries are available without another plugin. They report actual
145
+ // consumption, not provider subscription limits, and must never fabricate quotas.
146
+ try {
147
+ const totals = new Map<string, { tokens: number; cost: number; hasTokens: boolean; hasCost: boolean }>();
148
+ for (const entry of ctx?.sessionManager?.getEntries?.() ?? []) {
149
+ const raw = record(entry);
150
+ if (raw?.type !== "message") continue;
151
+ const message = record(raw.message);
152
+ if (message?.role !== "assistant") continue;
153
+ const provider = text(message.provider);
154
+ const usage = record(message.usage);
155
+ if (!provider || !usage) continue;
156
+ const pieces = [usage.input, usage.output, usage.cacheRead, usage.cacheWrite].map(number);
157
+ const tokens = number(usage.totalTokens) ?? (pieces.some(value => value !== undefined)
158
+ ? pieces.reduce<number>((sum, value) => sum + (value ?? 0), 0) : undefined);
159
+ const cost = number(record(usage.cost)?.total);
160
+ if (tokens === undefined && cost === undefined) continue;
161
+ const total = totals.get(provider) ?? { tokens: 0, cost: 0, hasTokens: false, hasCost: false };
162
+ if (tokens !== undefined) { total.tokens += tokens; total.hasTokens = true; }
163
+ if (cost !== undefined) { total.cost += cost; total.hasCost = true; }
164
+ totals.set(provider, total);
165
+ }
166
+ for (const [provider, total] of totals) {
167
+ const fingerprint = JSON.stringify(total);
168
+ const cached = this.nativeUsage.get(provider);
169
+ const updatedAt = cached?.fingerprint === fingerprint ? cached.updatedAt : new Date(now).toISOString();
170
+ this.nativeUsage.set(provider, { fingerprint, updatedAt });
171
+ const old = providers.get(provider);
172
+ providers.set(provider, {
173
+ ...old, provider, source: old?.source ?? "Pi session", updatedAt: old?.updatedAt ?? updatedAt,
174
+ sessionUpdatedAt: updatedAt,
175
+ ...(total.hasTokens ? { sessionTokens: total.tokens } : {}),
176
+ ...(total.hasCost ? { sessionCost: total.cost } : {})
177
+ });
178
+ }
179
+ if (providers.size) result.providers = [...providers.values()];
180
+ } catch { /* Keep independently reported quota snapshots if native entries are unavailable. */ }
143
181
  return result;
144
182
  }
145
183
  }