pi-widget-host 0.2.0 → 0.3.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/CHANGELOG.md CHANGED
@@ -4,6 +4,30 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  This project follows semantic versioning.
6
6
 
7
+ ## [0.3.2] - 2026-06-26
8
+
9
+ ### Fixed
10
+
11
+ - npm Trusted Publishing publish retry after registry configuration fix (no functional changes from 0.3.1).
12
+
13
+ ## [0.3.1] - 2026-06-25
14
+
15
+ ### Changed
16
+
17
+ - README install and development guidance now matches the current Pi OSS template baseline, including project-local install, `pi -e npm:pi-widget-host`, and `npm run pack:check` (`npm pack --dry-run`) validation in the public docs flow.
18
+ - `Package contents` now lists the actual shipped paths instead of unused template resource directories.
19
+
20
+ ## [0.3.0] - 2026-06-24
21
+
22
+ ### Added
23
+
24
+ - Package-readiness hardening for the first publishable Host-only MVP release boundary.
25
+ - Explicit acceptance coverage for `matchday` event boost, known host tags, and silent empty slot behavior.
26
+
27
+ ### Changed
28
+
29
+ - Minor semver bump marking the completed Host-only MVP: event boost, known tags, registry protocol, and demo-provider dogfooding path.
30
+
7
31
  ## [0.2.0] - 2026-06-15
8
32
 
9
33
  ### Added
package/README.md CHANGED
@@ -32,19 +32,31 @@ v1 is a **Host-only MVP**:
32
32
 
33
33
  ## Install
34
34
 
35
- Published package:
35
+ Install the published npm package with Pi:
36
36
 
37
37
  ```bash
38
38
  pi install npm:pi-widget-host
39
39
  ```
40
40
 
41
- GitHub install:
41
+ Install into the current project instead of your user Pi settings:
42
+
43
+ ```bash
44
+ pi install npm:pi-widget-host -l
45
+ ```
46
+
47
+ Or install from GitHub:
42
48
 
43
49
  ```bash
44
50
  pi install git:github.com/eiei114/pi-widget-host
45
51
  ```
46
52
 
47
- Local dev:
53
+ Try it without permanently installing:
54
+
55
+ ```bash
56
+ pi -e npm:pi-widget-host
57
+ ```
58
+
59
+ Local development from this repository:
48
60
 
49
61
  ```bash
50
62
  pi -e .
@@ -97,17 +109,21 @@ The built-in demo provider exists to prove the host loop first:
97
109
 
98
110
  | Path | Purpose |
99
111
  |---|---|
100
- | `extensions/` | Pi extension entrypoint and command registration |
101
- | `lib/` | config store, registry protocol, policy evaluation, demo provider |
102
- | `docs/` | supporting docs such as release and protocol notes |
112
+ | `extensions/index.ts` | Pi extension entrypoint and `/widget-host:*` command registration |
113
+ | `lib/` | config store, registry protocol, policy evaluation, and demo provider |
114
+ | `docs/protocol.md` | registry protocol reference for future provider packages |
115
+ | `docs/release.md` | Trusted Publishing release notes |
103
116
 
104
117
  ## Development
105
118
 
106
119
  ```bash
107
120
  npm install
108
121
  npm run ci
122
+ npm run pack:check
109
123
  ```
110
124
 
125
+ `npm run ci` runs typecheck, tests, and pack validation. `npm run pack:check` runs `npm pack --dry-run` to confirm the shipped tarball contents.
126
+
111
127
  ## Release
112
128
 
113
129
  This package uses npm Trusted Publishing.
@@ -2,7 +2,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { readHostConfig, updateHostConfig, writeHostConfig } from "../lib/config.ts";
3
3
  import { stopDemoProviderHeartbeat, syncBuiltInDemoProvider } from "../lib/demo-provider.ts";
4
4
  import { DEFAULT_PRESET_ID, describePreset, evaluateProviderEntries, getPreset, getRemainingTtlMs, PRESET_OPTIONS } from "../lib/policy.ts";
5
- import { getWidgetHostRegistry, listProviderEntries, removeProviderEntry } from "../lib/registry.ts";
5
+ import { clearHostPresent, getWidgetHostRegistry, listProviderEntries, markHostPresent, removeProviderEntry } from "../lib/registry.ts";
6
6
  import { DEMO_PROVIDER_ID, WIDGET_ID, type HostConfig, type PolicyEvaluationResult, type ProviderState } from "../lib/types.ts";
7
7
 
8
8
  type NotifyLevel = "info" | "warning" | "error";
@@ -103,6 +103,7 @@ export default function widgetHostExtension(pi: ExtensionAPI) {
103
103
  pi.on("session_start", async (_event, ctx) => {
104
104
  if (!ctx.hasUI) return;
105
105
  bindUi(ctx as { hasUI: boolean; ui: HostUiBridge });
106
+ markHostPresent();
106
107
  ensureRegistryWatcher();
107
108
  await refreshHostWidget(ctx as { hasUI: boolean; ui: HostUiBridge });
108
109
  });
@@ -113,6 +114,7 @@ export default function widgetHostExtension(pi: ExtensionAPI) {
113
114
  registryDispose = undefined;
114
115
  stopDemoProviderHeartbeat();
115
116
  removeProviderEntry(DEMO_PROVIDER_ID);
117
+ clearHostPresent();
116
118
  uiBridge = undefined;
117
119
  });
118
120
 
@@ -1,12 +1,18 @@
1
+ import { createProviderRuntime, type ProviderRuntime } from "pi-widget-core/provider";
1
2
  import { readHostConfig } from "./config.ts";
2
3
  import { detectTimeBlock } from "./policy.ts";
3
- import { publishProviderEntry, removeProviderEntry } from "./registry.ts";
4
4
  import { DEMO_PROVIDER_ID, type HostConfig, type ProviderEntry } from "./types.ts";
5
5
 
6
6
  const HEARTBEAT_MS = 30_000;
7
7
  const ENTRY_TTL_MS = 90_000;
8
8
 
9
9
  let heartbeat: NodeJS.Timeout | undefined;
10
+ let runtime: ProviderRuntime | undefined;
11
+
12
+ function ensureRuntime(): ProviderRuntime {
13
+ runtime ??= createProviderRuntime({ providerId: DEMO_PROVIDER_ID });
14
+ return runtime;
15
+ }
10
16
 
11
17
  function baseTagsForBlock(block: ReturnType<typeof detectTimeBlock>): string[] {
12
18
  switch (block) {
@@ -52,11 +58,10 @@ export function buildDemoProviderEntry(config: HostConfig, now = new Date()): Pr
52
58
  async function heartbeatTick(): Promise<void> {
53
59
  const config = await readHostConfig();
54
60
  if (!config.demoProviderEnabled) {
55
- removeProviderEntry(DEMO_PROVIDER_ID);
56
61
  stopDemoProviderHeartbeat();
57
62
  return;
58
63
  }
59
- publishProviderEntry(buildDemoProviderEntry(config));
64
+ ensureRuntime().update(buildDemoProviderEntry(config));
60
65
  }
61
66
 
62
67
  function ensureHeartbeat(): void {
@@ -68,17 +73,19 @@ function ensureHeartbeat(): void {
68
73
 
69
74
  export async function syncBuiltInDemoProvider(config: HostConfig): Promise<void> {
70
75
  if (!config.demoProviderEnabled) {
71
- removeProviderEntry(DEMO_PROVIDER_ID);
72
76
  stopDemoProviderHeartbeat();
73
77
  return;
74
78
  }
75
79
 
76
- publishProviderEntry(buildDemoProviderEntry(config));
80
+ ensureRuntime().update(buildDemoProviderEntry(config));
77
81
  ensureHeartbeat();
78
82
  }
79
83
 
80
84
  export function stopDemoProviderHeartbeat(): void {
81
- if (!heartbeat) return;
82
- clearInterval(heartbeat);
83
- heartbeat = undefined;
85
+ if (heartbeat) {
86
+ clearInterval(heartbeat);
87
+ heartbeat = undefined;
88
+ }
89
+ runtime?.stop();
90
+ runtime = undefined;
84
91
  }
package/lib/registry.ts CHANGED
@@ -1,89 +1,11 @@
1
- import { REGISTRY_SYMBOL, type ProviderEntry } from "./types.ts";
2
-
3
- type Listener = () => void;
4
-
5
- export interface WidgetHostRegistry {
6
- version: 1;
7
- set: (entry: ProviderEntry) => void;
8
- remove: (providerId: string) => void;
9
- list: () => ProviderEntry[];
10
- subscribe: (listener: Listener) => () => void;
11
- clear: () => void;
12
- }
13
-
14
- function normalizeEntry(entry: ProviderEntry): ProviderEntry {
15
- return {
16
- providerId: entry.providerId.trim(),
17
- available: entry.available === true,
18
- lines: Array.isArray(entry.lines) ? entry.lines.map((line) => String(line)) : [],
19
- updatedAt: new Date(entry.updatedAt).toISOString(),
20
- priority: typeof entry.priority === "number" ? entry.priority : 0,
21
- tags: Array.isArray(entry.tags) ? [...new Set(entry.tags.map((tag) => String(tag).trim()).filter(Boolean))] : [],
22
- mode: typeof entry.mode === "string" && entry.mode.trim().length > 0 ? entry.mode.trim() : undefined,
23
- ttlMs: typeof entry.ttlMs === "number" && entry.ttlMs > 0 ? entry.ttlMs : undefined,
24
- };
25
- }
26
-
27
- function sameEntry(left: ProviderEntry | undefined, right: ProviderEntry): boolean {
28
- return JSON.stringify(left) === JSON.stringify(right);
29
- }
30
-
31
- function createRegistry(): WidgetHostRegistry {
32
- const entries = new Map<string, ProviderEntry>();
33
- const listeners = new Set<Listener>();
34
- const notify = () => {
35
- for (const listener of listeners) {
36
- listener();
37
- }
38
- };
39
-
40
- return {
41
- version: 1,
42
- set(entry) {
43
- const normalized = normalizeEntry(entry);
44
- const previous = entries.get(normalized.providerId);
45
- if (sameEntry(previous, normalized)) return;
46
- entries.set(normalized.providerId, normalized);
47
- notify();
48
- },
49
- remove(providerId) {
50
- if (!entries.delete(providerId)) return;
51
- notify();
52
- },
53
- list() {
54
- return [...entries.values()].map((entry) => ({ ...entry, lines: [...entry.lines], tags: [...(entry.tags ?? [])] }));
55
- },
56
- subscribe(listener) {
57
- listeners.add(listener);
58
- return () => {
59
- listeners.delete(listener);
60
- };
61
- },
62
- clear() {
63
- if (entries.size === 0) return;
64
- entries.clear();
65
- notify();
66
- },
67
- };
68
- }
69
-
70
- export function getWidgetHostRegistry(): WidgetHostRegistry {
71
- const root = globalThis as typeof globalThis & Record<symbol, WidgetHostRegistry | undefined>;
72
- const existing = root[REGISTRY_SYMBOL];
73
- if (existing) return existing;
74
- const registry = createRegistry();
75
- root[REGISTRY_SYMBOL] = registry;
76
- return registry;
77
- }
78
-
79
- export function publishProviderEntry(entry: ProviderEntry): void {
80
- getWidgetHostRegistry().set(entry);
81
- }
82
-
83
- export function removeProviderEntry(providerId: string): void {
84
- getWidgetHostRegistry().remove(providerId);
85
- }
86
-
87
- export function listProviderEntries(): ProviderEntry[] {
88
- return getWidgetHostRegistry().list();
89
- }
1
+ export {
2
+ clearHostPresent,
3
+ getWidgetHostRegistry,
4
+ listProviderEntries,
5
+ markHostPresent,
6
+ publishProviderEntry,
7
+ removeProviderEntry,
8
+ subscribeToProviderEntries,
9
+ type ProviderEntry,
10
+ type WidgetHostRegistry,
11
+ } from "pi-widget-core/host";
package/lib/types.ts CHANGED
@@ -1,25 +1,15 @@
1
1
  import { homedir } from "node:os";
2
2
  import { join } from "node:path";
3
+ import { REGISTRY_SYMBOL, type ProviderEntry } from "pi-widget-core/protocol";
3
4
 
4
5
  export const PACKAGE_NAME = "pi-widget-host";
5
6
  export const WIDGET_ID = PACKAGE_NAME;
6
7
  export const DEMO_PROVIDER_ID = `${PACKAGE_NAME}.demo`;
7
- export const REGISTRY_SYMBOL = Symbol.for("pi-widget-host.registry.v1");
8
+ export { REGISTRY_SYMBOL, type ProviderEntry };
8
9
 
9
10
  export type HostTimeBlock = "morning" | "day" | "evening" | "night";
10
11
  export type KnownHostTag = "music" | "sports" | "playing-now" | "matchday" | "idle";
11
12
 
12
- export interface ProviderEntry {
13
- providerId: string;
14
- available: boolean;
15
- lines: string[];
16
- updatedAt: string;
17
- priority?: number;
18
- tags?: string[];
19
- mode?: string;
20
- ttlMs?: number;
21
- }
22
-
23
13
  export interface BlockPolicy {
24
14
  allowedProviderIds?: string[];
25
15
  preferredTags?: KnownHostTag[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-widget-host",
3
- "version": "0.2.0",
3
+ "version": "0.3.2",
4
4
  "description": "Host package for managing one shared Pi widget slot across multiple providers with preset policies and a built-in demo provider.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -45,6 +45,9 @@
45
45
  "publishConfig": {
46
46
  "access": "public"
47
47
  },
48
+ "dependencies": {
49
+ "pi-widget-core": "^0.1.0"
50
+ },
48
51
  "peerDependencies": {
49
52
  "@earendil-works/pi-coding-agent": "*"
50
53
  },
@@ -55,7 +58,7 @@
55
58
  },
56
59
  "devDependencies": {
57
60
  "@earendil-works/pi-coding-agent": "latest",
58
- "@types/node": "^25.9.1",
61
+ "@types/node": "^26.0.0",
59
62
  "tsx": "^4.20.6",
60
63
  "typescript": "^6.0.3"
61
64
  }